Files
bare-operating-system/packages/bare-os-coreutils/src/pear.js
T
2026-05-26 23:45:54 +00:00

281 lines
9.7 KiB
JavaScript

/**
* 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 <hdms-label>]
${argv0} seed [dir] [--wait-ms <n>]
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 <project>/.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
}
}