281 lines
9.7 KiB
JavaScript
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
|
|
}
|
|
}
|