/* BARE_OS_BIN_API 1.0.0 — bump when staged /bin script semantics change (see developer guide). */ /** Shared helpers for drive-resident /bin scripts (prepended before each command). */ function bareStdin(ctx) { return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : '' } /** @param {number} mode @param {'file' | 'directory' | 'symlink'} type */ function bareFormatModeString(mode, type) { const typeChar = type === 'directory' ? 'd' : type === 'symlink' ? 'l' : '-' const perm = mode & 0o777 const r = (bit) => (perm & bit ? 'r' : '-') const w = (bit) => (perm & bit ? 'w' : '-') const x = (bit) => (perm & bit ? 'x' : '-') return ( typeChar + r(0o400) + w(0o200) + x(0o100) + r(0o040) + w(0o020) + x(0o010) + r(0o004) + w(0o002) + x(0o001) ) } /** @param {number} mtimeMs @param {number} [nowMs] */ function bareFormatLsMtime(mtimeMs, nowMs) { const now = nowMs != null ? nowMs : Date.now() const d = new Date(mtimeMs) const months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ] const mon = months[d.getMonth()] const day = String(d.getDate()).padStart(2, ' ') const sixMo = 180 * 24 * 3600 * 1000 if (Math.abs(now - mtimeMs) > sixMo) { const yr = String(d.getFullYear()).padStart(4, ' ') return mon + ' ' + day + ' ' + yr } const hh = String(d.getHours()).padStart(2, '0') const mm = String(d.getMinutes()).padStart(2, '0') return mon + ' ' + day + ' ' + hh + ':' + mm } /** @param {number} size */ function barePosixBlocks(size) { return Math.ceil(Number(size) / 512) || 0 } /** * Raw stdout for NUL/binary when **`process.stdout.write`** is missing. * If **`ctx.bareOsBinWrite(Uint8Array|string)`** is set (tests / host), use it. * @param {Record} ctx * @param {string | Uint8Array} chunk * @returns {boolean} */ function bareOsEmitRaw(ctx, chunk) { if (typeof ctx.bareOsBinWrite === 'function') { const b4 = ctx.b4a const u8 = typeof chunk === 'string' ? b4 && typeof b4.from === 'function' ? b4.from(chunk) : new TextEncoder().encode(chunk) : chunk ctx.bareOsBinWrite(u8 instanceof Uint8Array ? u8 : new Uint8Array(u8)) return true } const w = globalThis.process?.stdout?.write if (typeof w === 'function') { w.call(globalThis.process.stdout, chunk) return true } return false } /** * Guest-side Pear project staging for /bin/pear (VFS-backed). * Uses ctx.pear / ctx.bare pack+compile tools when available. */ function pearJoinPath(...parts) { const raw = parts .filter((p) => p != null && String(p) !== '') .map((p) => String(p).replace(/\\/g, '/')) .join('/') .replace(/\/+/g, '/') const absolute = raw.startsWith('/') const segs = [] for (const seg of raw.split('/')) { if (!seg || seg === '.') continue if (seg === '..') { if (segs.length) segs.pop() continue } segs.push(seg) } const out = segs.join('/') return absolute ? '/' + out.replace(/^\//, '') : out } function pearDirname(p) { const s = String(p || '').replace(/\\/g, '/') const i = s.lastIndexOf('/') if (i <= 0) return s.startsWith('/') ? '/' : '.' return s.slice(0, i) || '/' } function pearExpandHome(p, home) { const s = String(p || '') if (s === '~') return home || '/home/guest' if (s.startsWith('~/')) return pearJoinPath(home || '/home/guest', s.slice(2)) return s } function pearResolveProjectDir(ctx, target) { let cwd = ctx.env?.PWD || '/home/guest' if (ctx.vfs && typeof ctx.vfs.getcwd === 'function') { try { const g = ctx.vfs.getcwd() if (g) cwd = String(g) } catch { /* ignore */ } } const t = String(target || '.').trim() || '.' const base = pearExpandHome(t === '.' ? cwd : t, ctx.env?.HOME) if (base.startsWith('/')) return pearJoinPath(base) return pearJoinPath(cwd, base) } function pearPathToFileURL(absPath) { const p = pearJoinPath(absPath) return new URL(`file://${p}`) } async function pearReadUtf8(vfs, b4a, path) { const buf = await vfs.readFile(path) if (!buf || !buf.byteLength) return null if (b4a && typeof b4a.toString === 'function') return b4a.toString(buf, 'utf8') return new TextDecoder().decode(buf instanceof Uint8Array ? buf : new Uint8Array(buf)) } async function pearWriteUtf8(vfs, b4a, path, text) { const payload = b4a && typeof b4a.from === 'function' ? b4a.from(String(text), 'utf8') : new TextEncoder().encode(String(text)) if (typeof vfs.mkdir === 'function') { await vfs.mkdir(pearDirname(path), { recursive: true }).catch(() => {}) } await vfs.writeFile(path, payload) } function pearSkipDir(name) { return ( name === 'node_modules' || name === '.git' || name === '.pear' || name === 'dist' || name === 'coverage' ) } async function pearCollectProjectFiles(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 if (pearSkipDir(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 } function pearVfsReadModule(vfs, b4a) { return async function readModule(url) { const href = typeof url === 'string' ? url : url.href let path = href if (href.startsWith('file://')) { try { path = decodeURIComponent(new URL(href).pathname) } catch { path = href.slice('file://'.length) } } try { const buf = await vfs.readFile(path) if (!buf || !buf.byteLength) return null if (b4a && typeof b4a.toString === 'function') return b4a.toString(buf, 'utf8') return new TextDecoder().decode(buf instanceof Uint8Array ? buf : new Uint8Array(buf)) } catch { return null } } } function pearVfsListPrefix(vfs) { return async function* listPrefix(url) { const href = typeof url === 'string' ? url : url.href let path = href if (href.startsWith('file://')) { try { path = decodeURIComponent(new URL(href).pathname) } catch { path = href.slice('file://'.length) } } if (!vfs || typeof vfs.readdir !== 'function') return /** @type {string[]} */ const queue = [path] while (queue.length) { const dir = queue.shift() 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 yield pearPathToFileURL(abs) } } } } /** * Stage a Pear app project directory on the guest VFS. * @param {Record} ctx * @param {string} targetDir * @param {{ json?: boolean, quiet?: boolean }} [opts] */ async function pearStageProject(ctx, targetDir, opts = {}) { const vfs = ctx.vfs const b4a = ctx.b4a const pear = ctx.pear && typeof ctx.pear === 'object' ? ctx.pear : {} const bare = ctx.bare && typeof ctx.bare === 'object' ? ctx.bare : {} if (!vfs || typeof vfs.readFile !== 'function' || typeof vfs.writeFile !== 'function') { throw new Error('pear stage: ctx.vfs read/write unavailable') } const projectDir = pearResolveProjectDir(ctx, targetDir) let pkgRaw try { pkgRaw = await pearReadUtf8(vfs, b4a, pearJoinPath(projectDir, 'package.json')) } catch (err) { throw new Error( `pear stage: cannot read ${pearJoinPath(projectDir, 'package.json')}: ${err?.message || err}` ) } if (!pkgRaw) { throw new Error(`pear stage: missing package.json in ${projectDir}`) } let pkg try { pkg = JSON.parse(pkgRaw) } catch { throw new Error(`pear stage: invalid package.json in ${projectDir}`) } const entryRel = String(pkg.main || 'index.js').replace(/^\.\//, '') const entryPath = pearJoinPath(projectDir, entryRel) try { await vfs.readFile(entryPath) } catch { throw new Error(`pear stage: entry ${entryRel} not found under ${projectDir}`) } const stageDir = pearJoinPath(projectDir, '.pear/stage') const sourcesDir = pearJoinPath(stageDir, 'sources') if (typeof vfs.mkdir === 'function') { await vfs.mkdir(sourcesDir, { recursive: true }) } const files = await pearCollectProjectFiles(vfs, projectDir) for (const src of files) { const rel = src.slice(projectDir.length).replace(/^\//, '') const dst = pearJoinPath(sourcesDir, 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) } await pearWriteUtf8(vfs, b4a, pearJoinPath(stageDir, 'package.json'), JSON.stringify(pkg, null, 2) + '\n') /** @type {string | null} */ let bundleJs = null let bundleMethod = 'sources-only' /** @type {string | null} */ let bundleError = null const compileFn = pear.bareBundleCompile || bare.bareBundleCompile const packFn = bare.barePack const BundleCtor = bare.bareBundle if (typeof packFn === 'function') { try { const entryUrl = pearPathToFileURL(entryPath) const bundle = await packFn( entryUrl, { preset: 'node' }, pearVfsReadModule(vfs, b4a), pearVfsListPrefix(vfs) ) if (typeof compileFn === 'function') { bundleJs = compileFn(bundle) bundleMethod = 'bare-pack+bare-bundle-compile' } } catch (err) { bundleError = err?.message || String(err) } } if (!bundleJs && typeof compileFn === 'function' && typeof BundleCtor === 'function') { try { const source = await pearReadUtf8(vfs, b4a, entryPath) const entryUrl = pearPathToFileURL(entryPath) const bundle = new BundleCtor() bundle.write(entryUrl.href, source, { main: true }) bundle.main = entryUrl.href bundleJs = compileFn(bundle) bundleMethod = 'single-entry+bare-bundle-compile' } catch (err) { bundleError = err?.message || String(err) } } if (bundleJs) { await pearWriteUtf8(vfs, b4a, pearJoinPath(stageDir, 'app.bundle.js'), bundleJs) await pearWriteUtf8(vfs, b4a, pearJoinPath(stageDir, 'boot.bundle.js'), bundleJs) } const stageMeta = { schema: 1, stagedAtMs: Date.now(), projectDir, stageDir, entry: entryRel, bundleMethod, bundleBytes: bundleJs ? String(bundleJs).length : 0, sourceFileCount: files.length, pear: pkg.pear && typeof pkg.pear === 'object' ? pkg.pear : null, name: pkg.name || null, version: pkg.version || null } if (bundleError && !bundleJs) stageMeta.bundleError = String(bundleError).slice(0, 512) await pearWriteUtf8( vfs, b4a, pearJoinPath(stageDir, 'stage.json'), JSON.stringify(stageMeta, null, 2) + '\n' ) if (typeof ctx.bareOsEmitPearStageHint === 'function') { ctx.bareOsEmitPearStageHint({ stage: stageDir, note: `pear stage ${bundleMethod}` }) } return { ok: true, ...stageMeta } } /** * 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 } } /** * pear — Pear development tools inside Bare OS. * * Provides access to the Pear runtime stack (build, bundle, stage, release, seed) * from within a booted Bare OS guest via the new ctx.pear surface. * * See docs/design/ctx-pear-surface-and-bare-audit-plan.md for the full plan and design. */ async function run(ctx, argv = []) { function printHelp(argv0 = 'pear') { ctx.console.log(`${argv0} — Pear development surface for Bare OS Usage: ${argv0} help ${argv0} info ${argv0} list ${argv0} init [dir] ${argv0} stage [dir] ${argv0} build [dir] (alias for stage) ${argv0} bundle [dir] (alias for stage) ${argv0} release [dir] [--label ] ${argv0} seed [dir] [--wait-ms ] The ctx.pear surface exposes selected Pear and Bare build/bundling packages (pear-build, pear-bundle, bare-bundle-compile, etc.) when BARE_OS_BARE_MODULES is enabled. pear stage writes a deployment tree under /.pear/stage/: package.json, sources/**, stage.json, and app.bundle.js when packing succeeds. pear release publishes the staged tree to a writable HDMS Hyperdrive and prints pear:// links (requires logged-in identity + HDMS). pear seed keeps the release drive replicating on Hyperswarm. Use the pear-dev agent skill together with the appstore skill for autonomous "build Pear app → publish to my store" workflows. `) } async function cmdInfo() { ctx.console.log('pear — Pear development tools (ctx.pear surface)') ctx.console.log('Bare OS version of selected Pear runtime packages.') ctx.console.log('') const bareModulesEnabled = !ctx.env || (ctx.env.BARE_OS_BARE_MODULES !== '0' && ctx.env.BARE_OS_BARE_MODULES !== 'false') if (ctx.pear && typeof ctx.pear === 'object') { const keys = Object.keys(ctx.pear).filter(k => !k.startsWith('_')).sort() if (keys.length > 0) { ctx.console.log(`Exposed on ctx.pear (${keys.length} packages):`) for (const k of keys) { const val = ctx.pear[k] const type = typeof val const fallback = ctx.pear[`_${k}_fromBare`] ? ' (from ctx.bare fallback)' : '' ctx.console.log(` ${k.padEnd(20)} ${type}${fallback}`) } } else { ctx.console.log('ctx.pear exists but is empty.') ctx.console.log('') if (!bareModulesEnabled) { ctx.console.log('Reason: BARE_OS_BARE_MODULES is disabled.') } else { ctx.console.log('Reason: No Pear packages were successfully imported during boot.') ctx.console.log(' This usually means the packages (pear-build, pear-bundle, pear-ref)') ctx.console.log(' are not installed in the booter\'s node_modules.') ctx.console.log('') ctx.console.log('Fix: On the machine running the booter/seeder, run:') ctx.console.log(' npm install') ctx.console.log(' Then rebuild and restage the image.') } } } else { ctx.console.log('ctx.pear property is missing from the guest context.') if (!bareModulesEnabled) { ctx.console.log('BARE_OS_BARE_MODULES is disabled.') } } ctx.console.log('') ctx.console.log('Run "pear list" for the same view.') } async function cmdList() { if (!ctx.pear || typeof ctx.pear !== 'object') { ctx.console.log('ctx.pear is not available in this environment.') ctx.console.log('Run "pear info" for diagnostics.') return } const keys = Object.keys(ctx.pear).filter(k => !k.startsWith('_')).sort() if (keys.length === 0) { ctx.console.log('ctx.pear exists but no packages loaded.') ctx.console.log('Run "pear info" for detailed reasons and fix instructions.') return } ctx.console.log(`ctx.pear — ${keys.length} package(s) available:\n`) for (const k of keys) { ctx.console.log(` ${k}`) } } async function cmdInit(targetDir = '.') { const name = 'my-pear-app' const dir = targetDir === '.' ? `${(ctx.env?.HOME || '/home/guest')}/pear-projects/${name}` : targetDir ctx.console.log(`pear init: creating minimal Pear app skeleton at ${dir}`) try { await ctx.vfs.mkdir(dir, { recursive: true }) const packageJson = { name, version: '0.1.0', main: 'index.js', type: 'module', pear: { name: 'my-pear-app', type: 'desktop' } } await ctx.vfs.writeFile(`${dir}/package.json`, ctx.b4a.from(JSON.stringify(packageJson, null, 2))) await ctx.vfs.writeFile(`${dir}/index.js`, ctx.b4a.from('console.log("Hello from my Pear app!");\n')) ctx.console.log('Created basic package.json + index.js') ctx.console.log(`Next: run "pear stage ${dir}"`) } catch (err) { ctx.console.error('Failed to init:', err?.message || err) ctx.exitCode = 1 } } async function cmdStage(target = '.', opts = {}) { if (!ctx.pear || typeof ctx.pear !== 'object') { ctx.console.error('pear stage: ctx.pear is not available.') ctx.console.error('Run "pear info" for diagnostics.') ctx.exitCode = 1 return } const hasStageTools = ctx.pear.bareBundleCompile || ctx.pear.pearBuild || ctx.pear.pearBundle || (ctx.bare && (ctx.bare.barePack || ctx.bare.bareBundle)) if (!hasStageTools) { ctx.console.error('pear stage: no pack/bundle tools on ctx.pear or ctx.bare.') ctx.console.error('Ensure BARE_OS_BARE_MODULES is enabled and the booter was restaged.') ctx.exitCode = 1 return } try { const result = await pearStageProject(ctx, target, opts) if (opts.json) { ctx.console.log(JSON.stringify(result, null, 2)) return } ctx.console.log(`Staged ${result.name || 'project'} → ${result.stageDir}`) ctx.console.log(` entry: ${result.entry}`) ctx.console.log(` method: ${result.bundleMethod}`) ctx.console.log(` sources: ${result.sourceFileCount} file(s)`) if (result.bundleBytes > 0) { ctx.console.log(` bundle: app.bundle.js (${result.bundleBytes} bytes)`) } else if (result.bundleError) { ctx.console.warn(` bundle: skipped (${result.bundleError})`) ctx.console.warn(' sources mirror is still available under .pear/stage/sources/') } ctx.console.log('') ctx.console.log('Next: pear release (publishes pear:// link via HDMS) or appstore install.') } catch (err) { ctx.console.error('pear stage failed:', err?.message || String(err)) ctx.exitCode = 1 } } async function cmdRelease(target = '.', opts = {}) { try { const result = await pearReleaseProject(ctx, target, opts) if (opts.json) { ctx.console.log(JSON.stringify(result, null, 2)) return } ctx.console.log(`Released ${result.name || 'project'} → ${result.mountRoot}`) ctx.console.log(` HDMS label: ${result.label}`) ctx.console.log(` files: ${result.fileCount}`) ctx.console.log(` length: ${result.length}`) ctx.console.log(` link: ${result.pearLink}`) ctx.console.log(` versioned: ${result.versionedLink}`) ctx.console.log('') ctx.console.log('Share the versioned link for pinned installs; run "pear seed" to replicate.') } catch (err) { ctx.console.error('pear release failed:', err?.message || String(err)) ctx.exitCode = 1 } } async function cmdSeed(target = '.', opts = {}) { try { const result = await pearSeedProject(ctx, target, opts) if (opts.json) { ctx.console.log(JSON.stringify(result, null, 2)) return } ctx.console.log(`Seeding ${result.label} at ${result.mountRoot}`) if (result.versionedLink) ctx.console.log(` versioned: ${result.versionedLink}`) ctx.console.log(` swarm flush: ${result.waitMs}ms (best-effort)`) } catch (err) { ctx.console.error('pear seed failed:', err?.message || String(err)) ctx.exitCode = 1 } } function parseReleaseFlags(args) { const rest = [] const opt = { json: false, label: '', waitMs: undefined } for (let i = 0; i < args.length; i++) { const a = args[i] if (a === '--json') opt.json = true else if (a === '--label' && args[i + 1]) { opt.label = args[++i] } else if (a.startsWith('--label=')) { opt.label = a.slice('--label='.length) } else if (a === '--wait-ms' && args[i + 1]) { opt.waitMs = Number(args[++i]) } else if (a.startsWith('--wait-ms=')) { opt.waitMs = Number(a.slice('--wait-ms='.length)) } else rest.push(a) } return { opt, rest } } function parseFlags(args) { const rest = [] const opt = { json: false } for (const a of args) { if (a === '--json') opt.json = true else rest.push(a) } return { opt, rest } } const sub = (argv[1] || 'help').toLowerCase() const releaseParsed = parseReleaseFlags(argv.slice(2)) const { opt, rest } = sub === 'release' || sub === 'seed' ? releaseParsed : parseFlags(argv.slice(2)) const positional = rest[0] || '.' switch (sub) { case 'help': case '--help': case '-h': printHelp(argv[0] || 'pear') break case 'info': await cmdInfo() break case 'list': case 'ls': await cmdList() break case 'stage': case 'build': case 'bundle': await cmdStage(positional, opt) break case 'init': await cmdInit(positional === '--json' ? '.' : positional) break case 'release': await cmdRelease(positional, opt) break case 'seed': await cmdSeed(positional, opt) break default: ctx.console.error(`Unknown subcommand: ${sub}`) printHelp(argv[0] || 'pear') ctx.exitCode = 1 break } }