/** * 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 } }