/** * Guest-side Pear release + seed for /bin/pear (VFS + HDMS Hyperdrive). * Publishes `.pear/stage/` to a writable HDMS mount and emits pear:// links. */ function pearReleaseSanitizeLabel(name) { let base = String(name || 'app') .toLowerCase() .replace(/[^a-z0-9._-]+/g, '-') .replace(/^-+/, '') .replace(/-+$/, '') if (!base) base = 'app' let label = 'pear-' + base if (label.length > 63) label = label.slice(0, 63) if (!/^[a-zA-Z0-9]/.test(label)) label = 'pear-' + label.replace(/^[^a-zA-Z0-9]+/, '') if (label.length > 63) label = label.slice(0, 63) return label } const PEAR_RELEASE_Z32_ALPHABET = 'ybndrfg8ejkmcpqxot1uwisza345h769' function pearReleaseZ32Encode(b4a, buf) { const key = buf instanceof Uint8Array ? buf : b4a.from(buf) if (key.byteLength !== 32) { throw new Error('pear release: drive key must be 32 bytes') } const max = key.byteLength * 8 let s = '' for (let p = 0; p < max; p += 5) { const i = p >>> 3 const j = p & 7 if (j <= 3) { s += PEAR_RELEASE_Z32_ALPHABET[(key[i] >>> (3 - j)) & 0b11111] continue } const of = j - 3 const h = (key[i] << of) & 0b11111 const l = (i >= key.byteLength ? 0 : key[i + 1]) >>> (8 - of) s += PEAR_RELEASE_Z32_ALPHABET[h | l] } return s } function pearReleaseIdEnc(ctx) { const bare = ctx.bare && typeof ctx.bare === 'object' ? ctx.bare : {} const enc = bare.hypercoreIdEncoding if (enc && typeof enc.encode === 'function' && typeof enc.decode === 'function') { return enc } const pear = ctx.pear && typeof ctx.pear === 'object' ? ctx.pear : {} const ref = pear.pearRef if (ref && typeof ref.encode === 'function') return ref return null } function pearReleaseAssertHdms(ctx) { const hdms = ctx.disk && ctx.disk.hdmsController if (!hdms || !hdms.active) { throw new Error( 'pear release: HDMS inactive — log in (unlock identity vault) so Hyperdrive mounts are available' ) } if (ctx.identity?.state !== 'unlocked') { throw new Error('pear release: identity must be unlocked (use login)') } return hdms } async function pearReleaseReadJson(vfs, b4a, path) { try { const buf = await vfs.readFile(path) if (!buf || !buf.byteLength) return null const text = b4a && typeof b4a.toString === 'function' ? b4a.toString(buf, 'utf8') : new TextDecoder().decode(buf instanceof Uint8Array ? buf : new Uint8Array(buf)) return JSON.parse(text) } catch { return null } } async function pearReleaseWriteJson(vfs, b4a, path, obj) { const text = JSON.stringify(obj, null, 2) + '\n' const payload = b4a && typeof b4a.from === 'function' ? b4a.from(text, 'utf8') : new TextEncoder().encode(text) if (typeof vfs.mkdir === 'function') { await vfs.mkdir(pearDirname(path), { recursive: true }).catch(() => {}) } await vfs.writeFile(path, payload) } async function pearReleaseCollectFiles(vfs, root) { /** @type {string[]} */ const out = [] /** @type {string[]} */ const queue = [root] while (queue.length) { const dir = queue.shift() if (!vfs || typeof vfs.readdir !== 'function') continue let names try { names = await vfs.readdir(dir) } catch { continue } if (!Array.isArray(names)) continue for (const name of names) { if (!name || name === '.' || name === '..') continue const abs = pearJoinPath(dir, name) let st try { st = typeof vfs.stat === 'function' ? await vfs.stat(abs) : null } catch { st = null } const isDir = st && (st.isDirectory === true || st.type === 'directory') if (isDir) queue.push(abs) else out.push(abs) } } return out } async function pearReleaseMirrorDir(vfs, b4a, srcDir, dstDir) { const files = await pearReleaseCollectFiles(vfs, srcDir) let copied = 0 for (const src of files) { const rel = src.slice(srcDir.length).replace(/^\//, '') const dst = pearJoinPath(dstDir, rel) if (typeof vfs.mkdir === 'function') { await vfs.mkdir(pearDirname(dst), { recursive: true }).catch(() => {}) } const buf = await vfs.readFile(src) await vfs.writeFile(dst, buf) copied++ } return copied } function pearReleaseDriveKeyZ32(hdms, label, drive, ctx) { const slot = hdms.byLabel.get(label) if (slot?.entry?.key) return String(slot.entry.key) const regEntry = hdms.registry?.drives?.find((d) => d && d.label === label) if (regEntry?.key) return String(regEntry.key) const idEnc = pearReleaseIdEnc(ctx) if (idEnc && drive?.key) return idEnc.encode(drive.key) const b4a = ctx.b4a if (b4a && drive?.key) return pearReleaseZ32Encode(b4a, drive.key) throw new Error('pear release: could not determine drive public key') } function pearReleaseEncodeLinks(hdms, label, drive, ctx) { if (!drive) throw new Error('pear release: drive unavailable') const keyZ32 = pearReleaseDriveKeyZ32(hdms, label, drive, ctx) const version = Number(drive.version) const length = Number.isFinite(version) && version >= 0 ? Math.floor(version) : 0 const pearLink = `pear://${keyZ32}` const versionedLink = length > 0 ? `pear://0.${length}.${keyZ32}` : pearLink return { keyZ32, length, pearLink, versionedLink } } async function pearReleaseEnsureDrive(ctx, hdms, label) { if (hdms.byLabel.has(label)) { const slot = hdms.byLabel.get(label) if (!slot?.drive) throw new Error(`pear release: HDMS mount "${label}" has no drive`) if (!slot.writable) { throw new Error(`pear release: HDMS mount "${label}" is read-only`) } return { label, drive: slot.drive, created: false } } await hdms.create(ctx, label) const slot = hdms.byLabel.get(label) if (!slot?.drive) throw new Error(`pear release: HDMS create failed for "${label}"`) return { label, drive: slot.drive, created: true } } async function pearReleaseFlushDrive(drive) { if (drive && typeof drive.flush === 'function') { try { await drive.flush() } catch { /* best-effort */ } } if (drive?.core && typeof drive.core.update === 'function') { try { await drive.core.update() } catch { /* best-effort */ } } } /** * Release a staged Pear project to HDMS and return pear:// links. * @param {Record} ctx * @param {string} targetDir * @param {{ json?: boolean, label?: string, quiet?: boolean }} [opts] */ async function pearReleaseProject(ctx, targetDir, opts = {}) { const vfs = ctx.vfs const b4a = ctx.b4a if (!vfs || typeof vfs.readFile !== 'function' || typeof vfs.writeFile !== 'function') { throw new Error('pear release: ctx.vfs read/write unavailable') } if (!ctx.b4a) { throw new Error('pear release: ctx.b4a unavailable') } const hdms = pearReleaseAssertHdms(ctx) const projectDir = pearResolveProjectDir(ctx, targetDir) const stageDir = pearJoinPath(projectDir, '.pear/stage') const releaseMetaPath = pearJoinPath(projectDir, '.pear/release.json') const stageMeta = await pearReleaseReadJson(vfs, b4a, pearJoinPath(stageDir, 'stage.json')) if (!stageMeta) { throw new Error(`pear release: no staged tree — run "pear stage" in ${projectDir} first`) } const priorRelease = await pearReleaseReadJson(vfs, b4a, releaseMetaPath) const pkgName = String(stageMeta.name || priorRelease?.name || 'app') let label = String(opts.label || priorRelease?.label || '').trim() if (!label) label = pearReleaseSanitizeLabel(pkgName) const { drive, created } = await pearReleaseEnsureDrive(ctx, hdms, label) const mountRoot = pearJoinPath('/mnt', label) const copied = await pearReleaseMirrorDir(vfs, b4a, stageDir, mountRoot) await pearReleaseFlushDrive(drive) const links = pearReleaseEncodeLinks(hdms, label, drive, ctx) const releaseRecord = { schema: 1, name: pkgName, releasedAtMs: Date.now(), projectDir, stageDir, label, mountRoot, fileCount: copied, driveCreated: created, ...links } const releaseDoc = { schema: 1, name: pkgName, label, key: links.keyZ32, latest: releaseRecord, releases: Array.isArray(priorRelease?.releases) ? priorRelease.releases.slice(-31) : [] } releaseDoc.releases.push(releaseRecord) await pearReleaseWriteJson(vfs, b4a, releaseMetaPath, releaseDoc) if (typeof ctx.bareOsEmitPearStageHint === 'function') { ctx.bareOsEmitPearStageHint({ stage: stageDir, note: `pear release ${links.versionedLink}` }) } return { ok: true, ...releaseRecord, releaseMetaPath, releaseDoc } } /** * Keep an released Pear app replicating on the swarm (HDMS already joins on mount). * @param {Record} ctx * @param {string} targetDir * @param {{ json?: boolean, waitMs?: number }} [opts] */ async function pearSeedProject(ctx, targetDir, opts = {}) { const vfs = ctx.vfs const b4a = ctx.b4a if (!vfs || typeof vfs.readFile !== 'function') { throw new Error('pear seed: ctx.vfs unavailable') } const hdms = pearReleaseAssertHdms(ctx) const projectDir = pearResolveProjectDir(ctx, targetDir) const releaseMetaPath = pearJoinPath(projectDir, '.pear/release.json') const releaseDoc = await pearReleaseReadJson(vfs, b4a, releaseMetaPath) if (!releaseDoc?.latest?.label) { throw new Error(`pear seed: no release metadata — run "pear release" in ${projectDir} first`) } const label = String(releaseDoc.latest.label) let slot = hdms.byLabel.get(label) if (!slot?.drive) { const entry = hdms.registry?.drives?.find((d) => d && d.label === label) if (!entry) { throw new Error(`pear seed: HDMS mount "${label}" not found (was it removed?)`) } await hdms._openEntry(entry) slot = hdms.byLabel.get(label) } if (!slot?.drive) throw new Error(`pear seed: could not open HDMS mount "${label}"`) const drive = slot.drive const swarm = hdms.swarm if (swarm && drive.discoveryKey) { try { swarm.join(drive.discoveryKey) } catch { /* ignore */ } } const waitMs = Math.min(120_000, Math.max(0, Number(opts.waitMs) || 8000)) if (swarm && typeof swarm.flush === 'function' && waitMs > 0) { await Promise.race([ swarm.flush().catch(() => {}), new Promise((resolve) => setTimeout(resolve, waitMs)) ]) } await pearReleaseFlushDrive(drive) const links = pearReleaseEncodeLinks(hdms, label, drive, ctx) return { ok: true, label, mountRoot: pearJoinPath('/mnt', label), pearLink: links?.pearLink || releaseDoc.latest.pearLink || null, versionedLink: links?.versionedLink || releaseDoc.latest.versionedLink || null, length: links?.length ?? releaseDoc.latest.length ?? null, waitMs } }