Pear Apps work

This commit is contained in:
2026-05-26 23:45:54 +00:00
parent ab3f091671
commit 21580f21bc
28 changed files with 4740 additions and 764 deletions
+420 -49
View File
@@ -436,6 +436,350 @@ async function bareP2pReadProcJson(ctx, path) {
return v && typeof v === 'object' ? v : {}
}
/**
* App Store ↔ Pear link resolution, VFS materialization, and in-guest launch.
*/
function appstorePearJoinPath(...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 appstorePearDirname(p) {
const s = String(p || '').replace(/\\/g, '/')
const i = s.lastIndexOf('/')
if (i <= 0) return s.startsWith('/') ? '/' : '.'
return s.slice(0, i) || '/'
}
function appstoreParsePearLink(link) {
const raw = String(link || '').trim()
if (!raw.startsWith('pear://')) {
throw new Error('invalid pear:// link')
}
let rest = raw.slice('pear://'.length).split('/')[0]
let length = null
if (rest.startsWith('0.')) {
const dot = rest.indexOf('.', 2)
if (dot > 2) {
const n = Number(rest.slice(2, dot))
if (Number.isFinite(n) && n >= 0) length = Math.floor(n)
rest = rest.slice(dot + 1)
}
}
const keyZ32 = rest.trim()
if (!keyZ32) throw new Error('invalid pear:// link (missing key)')
return { keyZ32, length, pearLink: raw }
}
function appstoreFindHdmsMountByKey(ctx, keyZ32) {
const hdms = ctx.disk && ctx.disk.hdmsController
if (!hdms || !hdms.active || !keyZ32) return null
for (const [label, slot] of hdms.byLabel) {
const k = slot?.entry?.key
if (k && String(k) === String(keyZ32)) {
return { label, mountRoot: appstorePearJoinPath('/mnt', label), drive: slot.drive, local: true }
}
}
for (const entry of hdms.registry?.drives || []) {
if (!entry || String(entry.key || '') !== String(keyZ32)) continue
const slot = hdms.byLabel.get(entry.label)
if (slot?.drive) {
return {
label: entry.label,
mountRoot: appstorePearJoinPath('/mnt', entry.label),
drive: slot.drive,
local: true
}
}
}
return null
}
function appstoreEphemeralFetchLabel(keyZ32) {
const slug = String(keyZ32 || 'fetch')
.replace(/[^a-zA-Z0-9]/g, '')
.slice(0, 48)
let label = 'as-ro-' + (slug || 'fetch')
if (label.length > 63) label = label.slice(0, 63)
if (!/^[a-zA-Z0-9]/.test(label)) label = 'as-ro-' + label.replace(/^[^a-zA-Z0-9]+/, '')
if (label.length > 63) label = label.slice(0, 63)
return label
}
async function appstoreSleep(ms) {
await new Promise((resolve) => setTimeout(resolve, ms))
}
async function appstoreVfsExists(vfs, path) {
try {
if (typeof vfs.stat === 'function') {
await vfs.stat(path)
return true
}
await vfs.readFile(path)
return true
} catch {
return false
}
}
async function appstoreCollectFiles(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 (name === '.bareos_empty') continue
const abs = appstorePearJoinPath(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 appstoreMirrorVfsDir(vfs, srcDir, dstDir) {
const files = await appstoreCollectFiles(vfs, srcDir)
let copied = 0
for (const src of files) {
const rel = src.slice(srcDir.length).replace(/^\//, '')
const dst = appstorePearJoinPath(dstDir, rel)
if (typeof vfs.mkdir === 'function') {
await vfs.mkdir(appstorePearDirname(dst), { recursive: true }).catch(() => {})
}
const buf = await vfs.readFile(src)
await vfs.writeFile(dst, buf)
copied++
}
return copied
}
async function appstoreWaitForTree(vfs, root, timeoutMs = 12000) {
const deadline = Date.now() + timeoutMs
while (Date.now() < deadline) {
if (await appstoreVfsExists(vfs, appstorePearJoinPath(root, 'package.json'))) return true
if (await appstoreVfsExists(vfs, appstorePearJoinPath(root, 'sources/index.js'))) return true
if (await appstoreVfsExists(vfs, appstorePearJoinPath(root, 'app.bundle.js'))) return true
await appstoreSleep(300)
}
return false
}
/**
* Open a readonly HDMS mount for a pear key (ephemeral) when not already local.
* @param {Record<string, unknown>} ctx
* @param {string} keyZ32
*/
async function appstoreOpenPearMount(ctx, keyZ32) {
const local = appstoreFindHdmsMountByKey(ctx, keyZ32)
if (local) return local
const hdms = ctx.disk && ctx.disk.hdmsController
if (!hdms || !hdms.active) {
throw new Error(
'appstore fetch: log in (unlock identity) so HDMS can mount the pear:// drive'
)
}
if (ctx.identity?.state !== 'unlocked') {
throw new Error('appstore fetch: identity must be unlocked (use login)')
}
let label = appstoreEphemeralFetchLabel(keyZ32)
if (hdms.byLabel.has(label)) {
for (let n = 2; n <= 99; n++) {
const candidate = (label.slice(0, 58) + '-' + n).slice(0, 63)
if (!hdms.byLabel.has(candidate)) {
label = candidate
break
}
}
}
await hdms.addReadonly(ctx, label, keyZ32, { persist: false })
const mountRoot = appstorePearJoinPath('/mnt', label)
const swarm = hdms.swarm
if (swarm && typeof swarm.flush === 'function') {
await Promise.race([swarm.flush().catch(() => {}), appstoreSleep(8000)])
}
const ready = await appstoreWaitForTree(ctx.vfs, mountRoot)
if (!ready) {
throw new Error(
`appstore fetch: timed out waiting for content on ${mountRoot} (is pear seed replicating?)`
)
}
return { label, mountRoot, ephemeral: true, local: false }
}
/**
* Copy pear release tree into an appstore package directory.
* @param {Record<string, unknown>} ctx
* @param {string} pearLink
* @param {string} pkgDir
*/
async function appstoreFetchPearTree(ctx, pearLink, pkgDir) {
const vfs = ctx.vfs
if (!vfs || typeof vfs.readFile !== 'function' || typeof vfs.writeFile !== 'function') {
throw new Error('appstore fetch: ctx.vfs unavailable')
}
const parsed = appstoreParsePearLink(pearLink)
const mount = await appstoreOpenPearMount(ctx, parsed.keyZ32)
if (typeof vfs.mkdir === 'function') {
await vfs.mkdir(pkgDir, { recursive: true }).catch(() => {})
}
const fileCount = await appstoreMirrorVfsDir(vfs, mount.mountRoot, pkgDir)
if (fileCount <= 0) {
throw new Error(`appstore fetch: no files copied from ${mount.mountRoot}`)
}
/** @type {Record<string, unknown>} */
let pkgJson = null
try {
const buf = await vfs.readFile(appstorePearJoinPath(pkgDir, 'package.json'))
if (buf && buf.byteLength) {
pkgJson = JSON.parse(ctx.b4a.toString(buf, 'utf8'))
}
} catch {
/* ignore */
}
return {
ok: true,
fileCount,
method: mount.local ? 'hdms-local-mount' : 'hdms-readonly-fetch',
mountLabel: mount.label,
mountRoot: mount.mountRoot,
keyZ32: parsed.keyZ32,
length: parsed.length,
packageJson: pkgJson
}
}
async function appstoreReadUtf8(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))
}
/**
* Resolve runnable entry script under a materialized package dir.
* @param {Record<string, unknown>} ctx
* @param {string} pkgDir
*/
async function appstoreResolvePackageEntry(ctx, pkgDir) {
const vfs = ctx.vfs
const b4a = ctx.b4a
let main = 'index.js'
try {
const raw = await appstoreReadUtf8(vfs, b4a, appstorePearJoinPath(pkgDir, 'package.json'))
if (raw) {
const pkg = JSON.parse(raw)
if (pkg && pkg.main) main = String(pkg.main).replace(/^\.\//, '')
}
} catch {
/* ignore */
}
const candidates = [
appstorePearJoinPath(pkgDir, 'sources', main),
appstorePearJoinPath(pkgDir, main),
appstorePearJoinPath(pkgDir, 'sources/index.js'),
appstorePearJoinPath(pkgDir, 'index.js')
]
for (const p of candidates) {
if (await appstoreVfsExists(vfs, p)) return p
}
return null
}
/**
* Execute a plain JS entry (console.log apps) inside the guest shell.
* @param {Record<string, unknown>} ctx
* @param {string} scriptPath
*/
async function appstoreExecGuestScript(ctx, scriptPath) {
const vfs = ctx.vfs
const b4a = ctx.b4a
const consoleRef = ctx.console
if (!consoleRef || typeof consoleRef.log !== 'function') {
throw new Error('appstore launch: ctx.console unavailable')
}
let src = await appstoreReadUtf8(vfs, b4a, scriptPath)
if (!src) throw new Error('appstore launch: empty script: ' + scriptPath)
src = src.replace(/^\uFEFF/, '').replace(/^#![^\n]*\n/, '')
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor
const fn = new AsyncFunction('console', src)
await fn(consoleRef)
}
/**
* Launch a materialized Pear app package in-guest.
* @param {Record<string, unknown>} ctx
* @param {string} pkgDir
* @param {{ refetch?: boolean, pearLink?: string }} [opts]
*/
async function appstoreLaunchPackage(ctx, pkgDir, opts = {}) {
const vfs = ctx.vfs
if (!vfs) throw new Error('appstore launch: ctx.vfs unavailable')
let entry = await appstoreResolvePackageEntry(ctx, pkgDir)
if ((!entry || opts.refetch) && opts.pearLink) {
await appstoreFetchPearTree(ctx, opts.pearLink, pkgDir)
entry = await appstoreResolvePackageEntry(ctx, pkgDir)
}
if (!entry) {
throw new Error(
`appstore launch: no entry script under ${pkgDir} (expected sources/index.js or package.json main)`
)
}
await appstoreExecGuestScript(ctx, entry)
return { ok: true, entry, pkgDir }
}
/**
* appstore — P2P App Store client for Bare OS.
* Manages discovery and installation of packages into a dedicated HDMS App Store drive.
@@ -561,8 +905,7 @@ async function writeAppstoreRegistry(ctx, doc) {
}
/**
* Enhanced materialization (Round 2): create a richer package directory
* with full manifest, metadata, launcher, and service declarations.
* Materialize package tree by fetching from pear:// into the store packages dir.
*/
async function materializePackage(ctx, pkg, registryPath) {
try {
@@ -573,36 +916,68 @@ async function materializePackage(ctx, pkg, registryPath) {
await ctx.vfs.mkdir(pkgDir, { recursive: true }).catch(() => {})
}
const pearLink = String(pkg.pearLink || pkg.link || '').trim()
if (!pearLink.startsWith('pear://')) {
return { ok: false, error: 'package has no pear:// link' }
}
const fetchResult = await appstoreFetchPearTree(ctx, pearLink, pkgDir)
const manifest = {
name: pkg.name,
type: pkg.type || 'app',
pearLink: pkg.pearLink,
version: pkg.version || '0.0.1',
pearLink,
version:
(fetchResult.packageJson && fetchResult.packageJson.version) ||
pkg.version ||
'0.0.1',
installedAtMs: pkg.installedAtMs || Date.now(),
source: 'appstore',
description: pkg.description || ''
description:
(fetchResult.packageJson && fetchResult.packageJson.description) ||
pkg.description ||
'',
materialization: {
schema: 1,
method: fetchResult.method,
fileCount: fetchResult.fileCount,
mountLabel: fetchResult.mountLabel,
keyZ32: fetchResult.keyZ32,
fetchedAtMs: Date.now()
}
}
await ctx.vfs.writeFile(`${pkgDir}/manifest.json`, ctx.b4a.from(JSON.stringify(manifest, null, 2), 'utf8'))
await ctx.vfs.writeFile(
`${pkgDir}/manifest.json`,
ctx.b4a.from(JSON.stringify(manifest, null, 2) + '\n', 'utf8')
)
// Basic launcher stub (user can replace)
const launcher = `#!/usr/bin/env node
// Launcher stub for ${pkg.name}
// Replace this with real entrypoint or use "appstore launch ${pkg.name}"
console.log("Launched ${pkg.name} (stub) from App Store");
const meta = {
installedFrom: pearLink,
materializationVersion: 3,
fetch: fetchResult
}
await ctx.vfs.writeFile(
`${pkgDir}/appstore-meta.json`,
ctx.b4a.from(JSON.stringify(meta, null, 2) + '\n', 'utf8')
)
const launcher = `// ${pkg.name} — run with: appstore launch ${pkg.name}
// Materialized Pear tree; entry is resolved from package.json main under sources/.
`
await ctx.vfs.writeFile(`${pkgDir}/launch.js`, ctx.b4a.from(launcher, 'utf8'))
// Metadata file for future tools
const meta = { installedFrom: pkg.pearLink, materializationVersion: 2 }
await ctx.vfs.writeFile(`${pkgDir}/appstore-meta.json`, ctx.b4a.from(JSON.stringify(meta, null, 2), 'utf8'))
return { ok: true, packageDir: pkgDir }
return { ok: true, packageDir: pkgDir, fetch: fetchResult }
} catch (err) {
return { ok: false, error: err && err.message }
return { ok: false, error: err?.message || String(err) }
}
}
function appstorePackageDir(registryPath, name) {
const baseDir = registryPath.replace(/\/registry\.json$/, '')
return `${baseDir}/packages/${name}`
}
async function run(ctx, argv) {
const argv0 = argv[0] || 'appstore'
const args = argv.slice(1)
@@ -731,15 +1106,21 @@ See: docs/design/p2p-app-store.md for architecture and future evolution.`
const writeResult = await writeAppstoreRegistry(ctx, reg)
if (writeResult && writeResult.ok) {
// Phase 2: materialize package directory + manifest
const mat = await materializePackage(ctx, newPkg, writeResult.path)
ctx.console.log(`Successfully installed "${name}" (Phase 2).`)
ctx.console.log(`Registry: ${writeResult.path}`)
if (mat && mat.ok) {
ctx.console.log(`Materialized to: ${mat.packageDir}`)
if (!mat || !mat.ok) {
ctx.console.error(`${argv0}: install registry updated but fetch failed: ${mat?.error || 'unknown'}`)
ctx.exitCode = 1
return
}
ctx.console.log('Note: Full P2P fetch + real HDMS drive materialization planned for later phases.')
ctx.console.log(`Successfully installed "${name}".`)
ctx.console.log(`Registry: ${writeResult.path}`)
ctx.console.log(`Materialized to: ${mat.packageDir}`)
ctx.console.log(
` fetched: ${mat.fetch.fileCount} file(s) via ${mat.fetch.method} (${mat.fetch.mountRoot})`
)
ctx.console.log(`Run: appstore launch ${name}`)
} else {
ctx.console.error(`${argv0}: Failed to write registry.`)
ctx.exitCode = 1
@@ -814,8 +1195,6 @@ See: docs/design/p2p-app-store.md for the full vision.`)
if (sub === 'launch') {
const name = String(args[1] || '').trim()
const checkoutIdx = args.indexOf('--checkout')
const checkout = (checkoutIdx !== -1 && args[checkoutIdx + 1]) ? args[checkoutIdx + 1] : 'released'
if (!name) {
ctx.console.error(`${argv0}: launch requires a package name`)
@@ -837,28 +1216,16 @@ See: docs/design/p2p-app-store.md for the full vision.`)
return
}
const req = {
requestId: 'appstore-' + Math.random().toString(36).slice(2, 10),
action: 'launch',
name,
pearLink,
checkout
const resolved = await resolveAppstoreRegistryPath(ctx)
const pkgDir = appstorePackageDir(resolved.preferredPath, name)
try {
const result = await appstoreLaunchPackage(ctx, pkgDir, { pearLink })
ctx.console.log(`Launched "${name}" (${result.entry})`)
} catch (err) {
ctx.console.error(`${argv0}: launch failed: ${err?.message || String(err)}`)
ctx.exitCode = 1
}
ctx.console.log(`Requesting launch of "${name}" (${pearLink}) via peerctl...`)
if (typeof bareP2pSend === 'function') {
const r = bareP2pSend(ctx, 'peerctl', 'appstore.launch', req)
if (r && r.ok === false) {
ctx.console.error(`${argv0}: launch request failed: ${r.reason || 'unknown'}`)
ctx.exitCode = 1
return
}
} else {
ctx.console.log('(peerctl delegation not available in this environment)')
}
ctx.console.log(`Launch requested for "${name}".`)
return
}
@@ -920,11 +1287,15 @@ WantedBy=multi-user.target
}
ctx.console.log(`Refreshing ${pkgName}...`)
try {
const mat = await materializePackage(ctx, pkg, reg._source === 'hdms' ? '/mnt/appstore/registry.json' : `${(ctx.env.HOME || '/home/guest')}/.appstore/registry.json`)
const regPath =
reg.usingHdms || reg._source === 'hdms'
? `/mnt/${reg.appstoreDriveLabel || 'appstore'}/registry.json`
: `${(ctx.env.HOME || '/home/guest')}/.appstore/registry.json`
const mat = await materializePackage(ctx, pkg, regPath)
if (mat && mat.ok) {
ctx.console.log(` → Updated to ${mat.packageDir}`)
ctx.console.log(` → Updated ${mat.packageDir} (${mat.fetch.fileCount} files)`)
} else {
ctx.console.log(` → Refresh completed (no new content fetched in this environment).`)
ctx.console.error(` → Refresh failed: ${mat?.error || 'unknown'}`)
}
} catch (e) {
ctx.console.error(` Failed to refresh ${pkgName}: ${e.message || e}`)
+802 -26
View File
@@ -87,6 +87,670 @@ function bareOsEmitRaw(ctx, chunk) {
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<string, unknown>} 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<string, unknown>} 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<string, unknown>} 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.
*
@@ -104,22 +768,23 @@ Usage:
${argv0} help
${argv0} info
${argv0} list
${argv0} stage [dir]
${argv0} build [dir]
${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.
Many operations (full release, seeding, live sidecar IPC) currently delegate to
a host Pear sidecar (when available) following the same pattern as peerctl and
the P2P App Store.
pear stage writes a deployment tree under <project>/.pear/stage/:
package.json, sources/**, stage.json, and app.bundle.js when packing succeeds.
Pear apps you create here are natural citizens of the P2P App Store:
- Stage your app with Pear tooling
- Publish it via appstore (or directly as a pear:// package)
- Other users can discover and run it
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.
@@ -156,9 +821,6 @@ autonomous "build Pear app → publish to my store" workflows.
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.')
ctx.console.log('')
ctx.console.log('Note: On developer machines the local mirror may provide them.')
ctx.console.log(' On production servers they must come from npm (see optionalDependencies).')
}
}
} else {
@@ -178,7 +840,7 @@ autonomous "build Pear app → publish to my store" workflows.
ctx.console.log('Run "pear info" for diagnostics.')
return
}
const keys = Object.keys(ctx.pear).sort()
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.')
@@ -211,13 +873,129 @@ autonomous "build Pear app → publish to my store" workflows.
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}" (uses ctx.pear.pearBuild when available)`)
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':
@@ -234,24 +1012,22 @@ autonomous "build Pear app → publish to my store" workflows.
break
case 'stage':
case 'build':
const target = argv[2] || '.'
if (ctx.pear && ctx.pear.pearBuild) {
ctx.console.log(`Attempting stage of ${target} using ctx.pear.pearBuild...`)
ctx.console.log('ctx.pear.pearBuild is available. Full pipeline implementation in progress.')
ctx.console.log('Exposed build-related keys:', Object.keys(ctx.pear).filter(k =>
k.toLowerCase().includes('build') || k.toLowerCase().includes('bundle')
))
} else {
ctx.console.log('pear stage / build: ctx.pear.pearBuild not available.')
ctx.console.log('Run "pear info" for status and instructions on making Pear build tooling available.')
}
case 'bundle':
await cmdStage(positional, opt)
break
case 'init':
await cmdInit(argv[2])
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
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"schema": 2,
"profileId": "bare-os-posix-like",
"generatedAt": "2026-05-26T23:16:52.392Z",
"generatedAt": "2026-05-26T23:42:58.195Z",
"note": "Sparse POSIX Issue 7 coverage hints; commandIndex is generated each coreutils build.",
"commandIndex": [
{
+174 -174
View File
@@ -37,18 +37,18 @@
"bareEncoding"
]
},
{
"path": "/lib/bare/bundles/bareEvents.js",
"keys": [
"bareEvents"
]
},
{
"path": "/lib/bare/bundles/barePath.js",
"keys": [
"barePath"
]
},
{
"path": "/lib/bare/bundles/bareEvents.js",
"keys": [
"bareEvents"
]
},
{
"path": "/lib/bare/bundles/protomux.js",
"keys": [
@@ -79,24 +79,30 @@
"bareAddonResolve"
]
},
{
"path": "/lib/bare/bundles/bareReadline.js",
"keys": [
"bareReadline"
]
},
{
"path": "/lib/bare/bundles/bareAppKit.js",
"keys": [
"bareAppKit"
]
},
{
"path": "/lib/bare/bundles/bareReadline.js",
"keys": [
"bareReadline"
]
},
{
"path": "/lib/bare/bundles/bareCrypto.js",
"keys": [
"bareCrypto"
]
},
{
"path": "/lib/bare/bundles/bareAsyncHooks.js",
"keys": [
"bareAsyncHooks"
]
},
{
"path": "/lib/bare/bundles/fetch.js",
"keys": [
@@ -104,9 +110,9 @@
]
},
{
"path": "/lib/bare/bundles/bareAsyncHooks.js",
"path": "/lib/bare/bundles/bareBmp.js",
"keys": [
"bareAsyncHooks"
"bareBmp"
]
},
{
@@ -122,9 +128,9 @@
]
},
{
"path": "/lib/bare/bundles/bareBmp.js",
"path": "/lib/bare/bundles/bareBundleCompile.js",
"keys": [
"bareBmp"
"bareBundleCompile"
]
},
{
@@ -133,48 +139,42 @@
"bareBundle"
]
},
{
"path": "/lib/bare/bundles/bareBundleCompile.js",
"keys": [
"bareBundleCompile"
]
},
{
"path": "/lib/bare/bundles/bareBuffer.js",
"keys": [
"bareBuffer"
]
},
{
"path": "/lib/bare/bundles/bareApk.js",
"keys": [
"bareApk"
]
},
{
"path": "/lib/bare/bundles/bareBluetoothApple.js",
"keys": [
"bareBluetoothApple"
]
},
{
"path": "/lib/bare/bundles/bareApk.js",
"keys": [
"bareApk"
]
},
{
"path": "/lib/bare/bundles/bareConsole.js",
"keys": [
"bareConsole"
]
},
{
"path": "/lib/bare/bundles/bareBoot.js",
"keys": [
"bareBoot"
]
},
{
"path": "/lib/bare/bundles/bareChannel.js",
"keys": [
"bareChannel"
]
},
{
"path": "/lib/bare/bundles/bareBoot.js",
"keys": [
"bareBoot"
]
},
{
"path": "/lib/bare/bundles/bareBundleEvaluate.js",
"keys": [
@@ -205,30 +205,30 @@
"bareCov"
]
},
{
"path": "/lib/bare/bundles/bareBundleId.js",
"keys": [
"bareBundleId"
]
},
{
"path": "/lib/bare/bundles/bareDns.js",
"keys": [
"bareDns"
]
},
{
"path": "/lib/bare/bundles/bareExif.js",
"keys": [
"bareExif"
]
},
{
"path": "/lib/bare/bundles/bareEnv.js",
"keys": [
"bareEnv"
]
},
{
"path": "/lib/bare/bundles/bareBundleId.js",
"keys": [
"bareBundleId"
]
},
{
"path": "/lib/bare/bundles/bareExif.js",
"keys": [
"bareExif"
]
},
{
"path": "/lib/bare/bundles/bareDaemon.js",
"keys": [
@@ -241,12 +241,6 @@
"bareFfmpeg"
]
},
{
"path": "/lib/bare/bundles/bareDgram.js",
"keys": [
"bareDgram"
]
},
{
"path": "/lib/bare/bundles/bareFfmpegEncodings.js",
"keys": [
@@ -254,9 +248,9 @@
]
},
{
"path": "/lib/bare/bundles/bareFormat.js",
"path": "/lib/bare/bundles/bareDgram.js",
"keys": [
"bareFormat"
"bareDgram"
]
},
{
@@ -265,6 +259,12 @@
"bareGif"
]
},
{
"path": "/lib/bare/bundles/bareFormat.js",
"keys": [
"bareFormat"
]
},
{
"path": "/lib/bare/bundles/bareFormData.js",
"keys": [
@@ -277,18 +277,18 @@
"bareHeif"
]
},
{
"path": "/lib/bare/bundles/bareGtk.js",
"keys": [
"bareGtk"
]
},
{
"path": "/lib/bare/bundles/bareHrtime.js",
"keys": [
"bareHrtime"
]
},
{
"path": "/lib/bare/bundles/bareGtk.js",
"keys": [
"bareGtk"
]
},
{
"path": "/lib/bare/bundles/bareFileLogger.js",
"keys": [
@@ -307,54 +307,54 @@
"bareIco"
]
},
{
"path": "/lib/bare/bundles/bareImageResample.js",
"keys": [
"bareImageResample"
]
},
{
"path": "/lib/bare/bundles/bareFs.js",
"keys": [
"bareFs"
]
},
{
"path": "/lib/bare/bundles/bareImageResample.js",
"keys": [
"bareImageResample"
]
},
{
"path": "/lib/bare/bundles/bareHttp1.js",
"keys": [
"bareHttp1"
]
},
{
"path": "/lib/bare/bundles/bareHttps.js",
"keys": [
"bareHttps"
]
},
{
"path": "/lib/bare/bundles/bareInspect.js",
"keys": [
"bareInspect"
]
},
{
"path": "/lib/bare/bundles/bareHttps.js",
"keys": [
"bareHttps"
]
},
{
"path": "/lib/bare/bundles/bareJpeg.js",
"keys": [
"bareJpeg"
]
},
{
"path": "/lib/bare/bundles/bareIpc.js",
"keys": [
"bareIpc"
]
},
{
"path": "/lib/bare/bundles/bareIntl.js",
"keys": [
"bareIntl"
]
},
{
"path": "/lib/bare/bundles/bareIpc.js",
"keys": [
"bareIpc"
]
},
{
"path": "/lib/bare/bundles/bareInspector.js",
"keys": [
@@ -367,12 +367,6 @@
"bareLogger"
]
},
{
"path": "/lib/bare/bundles/bareLink.js",
"keys": [
"bareLink"
]
},
{
"path": "/lib/bare/bundles/bareDev.js",
"keys": [
@@ -386,9 +380,9 @@
]
},
{
"path": "/lib/bare/bundles/bareMake.js",
"path": "/lib/bare/bundles/bareLink.js",
"keys": [
"bareMake"
"bareLink"
]
},
{
@@ -397,6 +391,12 @@
"bareModuleResolve"
]
},
{
"path": "/lib/bare/bundles/bareMake.js",
"keys": [
"bareMake"
]
},
{
"path": "/lib/bare/bundles/bareNative.js",
"keys": [
@@ -409,12 +409,6 @@
"bareMedia"
]
},
{
"path": "/lib/bare/bundles/bareModule.js",
"keys": [
"bareModule"
]
},
{
"path": "/lib/bare/bundles/bareNdk.js",
"keys": [
@@ -427,6 +421,12 @@
"bareNodeFetch"
]
},
{
"path": "/lib/bare/bundles/bareModuleLexer.js",
"keys": [
"bareModuleLexer"
]
},
{
"path": "/lib/bare/bundles/bareOpen.js",
"keys": [
@@ -434,9 +434,9 @@
]
},
{
"path": "/lib/bare/bundles/bareModuleLexer.js",
"path": "/lib/bare/bundles/bareModule.js",
"keys": [
"bareModuleLexer"
"bareModule"
]
},
{
@@ -469,12 +469,6 @@
"barePng"
]
},
{
"path": "/lib/bare/bundles/bareNodeRuntime.js",
"keys": [
"bareNodeRuntime"
]
},
{
"path": "/lib/bare/bundles/barePipe.js",
"keys": [
@@ -482,15 +476,9 @@
]
},
{
"path": "/lib/bare/bundles/barePackDrive.js",
"path": "/lib/bare/bundles/bareNodeRuntime.js",
"keys": [
"barePackDrive"
]
},
{
"path": "/lib/bare/bundles/barePunycode.js",
"keys": [
"barePunycode"
"bareNodeRuntime"
]
},
{
@@ -500,9 +488,9 @@
]
},
{
"path": "/lib/bare/bundles/bareQuerystring.js",
"path": "/lib/bare/bundles/barePunycode.js",
"keys": [
"bareQuerystring"
"barePunycode"
]
},
{
@@ -511,6 +499,24 @@
"bareProcess"
]
},
{
"path": "/lib/bare/bundles/barePackDrive.js",
"keys": [
"barePackDrive"
]
},
{
"path": "/lib/bare/bundles/bareQuerystring.js",
"keys": [
"bareQuerystring"
]
},
{
"path": "/lib/bare/bundles/barePrebuild.js",
"keys": [
"barePrebuild"
]
},
{
"path": "/lib/bare/bundles/bareQueueMicrotask.js",
"keys": [
@@ -523,12 +529,6 @@
"bareRealm"
]
},
{
"path": "/lib/bare/bundles/barePrebuild.js",
"keys": [
"barePrebuild"
]
},
{
"path": "/lib/bare/bundles/bareRuntime.js",
"keys": [
@@ -553,18 +553,18 @@
"bareRpc"
]
},
{
"path": "/lib/bare/bundles/barePromClient.js",
"keys": [
"barePromClient"
]
},
{
"path": "/lib/bare/bundles/bareSignals.js",
"keys": [
"bareSignals"
]
},
{
"path": "/lib/bare/bundles/barePromClient.js",
"keys": [
"barePromClient"
]
},
{
"path": "/lib/bare/bundles/bareRepl.js",
"keys": [
@@ -608,9 +608,9 @@
]
},
{
"path": "/lib/bare/bundles/bareStructuredClone.js",
"path": "/lib/bare/bundles/bareSystemLogger.js",
"keys": [
"bareStructuredClone"
"bareSystemLogger"
]
},
{
@@ -619,6 +619,18 @@
"bareStorage"
]
},
{
"path": "/lib/bare/bundles/bareTap.js",
"keys": [
"bareTap"
]
},
{
"path": "/lib/bare/bundles/bareStructuredClone.js",
"keys": [
"bareStructuredClone"
]
},
{
"path": "/lib/bare/bundles/bareTiff.js",
"keys": [
@@ -631,18 +643,6 @@
"bareSubprocess"
]
},
{
"path": "/lib/bare/bundles/bareSystemLogger.js",
"keys": [
"bareSystemLogger"
]
},
{
"path": "/lib/bare/bundles/bareTap.js",
"keys": [
"bareTap"
]
},
{
"path": "/lib/bare/bundles/bareTimers.js",
"keys": [
@@ -655,12 +655,6 @@
"bareTpl"
]
},
{
"path": "/lib/bare/bundles/bareTcp.js",
"keys": [
"bareTcp"
]
},
{
"path": "/lib/bare/bundles/bareType.js",
"keys": [
@@ -668,15 +662,15 @@
]
},
{
"path": "/lib/bare/bundles/bareUiKit.js",
"path": "/lib/bare/bundles/bareTcp.js",
"keys": [
"bareUiKit"
"bareTcp"
]
},
{
"path": "/lib/bare/bundles/bareUnpack.js",
"path": "/lib/bare/bundles/bareUiKit.js",
"keys": [
"bareUnpack"
"bareUiKit"
]
},
{
@@ -692,15 +686,15 @@
]
},
{
"path": "/lib/bare/bundles/bareV8.js",
"path": "/lib/bare/bundles/bareUnpack.js",
"keys": [
"bareV8"
"bareUnpack"
]
},
{
"path": "/lib/bare/bundles/bareThread.js",
"path": "/lib/bare/bundles/bareV8.js",
"keys": [
"bareThread"
"bareV8"
]
},
{
@@ -716,9 +710,9 @@
]
},
{
"path": "/lib/bare/bundles/bareWebp.js",
"path": "/lib/bare/bundles/bareThread.js",
"keys": [
"bareWebp"
"bareThread"
]
},
{
@@ -727,36 +721,42 @@
"bareWebKit"
]
},
{
"path": "/lib/bare/bundles/bareWebKitGtk.js",
"keys": [
"bareWebKitGtk"
]
},
{
"path": "/lib/bare/bundles/bareUnionBundle.js",
"keys": [
"bareUnionBundle"
]
},
{
"path": "/lib/bare/bundles/bareWebp.js",
"keys": [
"bareWebp"
]
},
{
"path": "/lib/bare/bundles/bareWebKitGtk.js",
"keys": [
"bareWebKitGtk"
]
},
{
"path": "/lib/bare/bundles/bareUtils.js",
"keys": [
"bareUtils"
]
},
{
"path": "/lib/bare/bundles/bareWinUi.js",
"keys": [
"bareWinUi"
]
},
{
"path": "/lib/bare/bundles/bareV8ToIstanbul.js",
"keys": [
"bareV8ToIstanbul"
]
},
{
"path": "/lib/bare/bundles/bareWinUi.js",
"keys": [
"bareWinUi"
]
},
{
"path": "/lib/bare/bundles/bareXdiff.js",
"keys": [
@@ -769,12 +769,6 @@
"bareZlib"
]
},
{
"path": "/lib/bare/bundles/bareZmq.js",
"keys": [
"bareZmq"
]
},
{
"path": "/lib/bare/bundles/bareWs.js",
"keys": [
@@ -782,9 +776,9 @@
]
},
{
"path": "/lib/bare/bundles/bareWorker.js",
"path": "/lib/bare/bundles/bareZmq.js",
"keys": [
"bareWorker"
"bareZmq"
]
},
{
@@ -793,6 +787,12 @@
"bareWhich"
]
},
{
"path": "/lib/bare/bundles/bareWorker.js",
"keys": [
"bareWorker"
]
},
{
"path": "/lib/bare/bundles/holesail.js",
"keys": [
@@ -1740,8 +1740,8 @@
],
"bundleProvenance": {
"schemaVersion": 1,
"generatedAt": "2026-05-26T23:16:54.405Z",
"gitCommit": "d7b019d3c31ffb9a915e33bb517c8c69f4162258",
"generatedAt": "2026-05-26T23:43:00.338Z",
"gitCommit": "ab3f0916711cbabb5268e8dd8be1c58041138886",
"nodeVersion": "v20.19.4",
"bundleTier": "all",
"normativeManifest": "packages/bare-os-booter/lib/bare-module-manifest.json",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"schema": 1,
"atMs": 1779837412390,
"atMs": 1779838978194,
"commands": [
"agent",
"appctl",
@@ -60,20 +60,19 @@ run_command "appstore info simple-file-share"
run_command "appstore install my-p2p-app --yes"
```
**Install from explicit P2P link:**
**Install from explicit P2P link (copies release tree from HDMS / swarm):**
```
run_command "appstore install keet pear://<key>/stable --yes"
run_command "appstore install my-p2p-app pear://0.<length>.<key> --yes"
```
**Uninstall:**
```
run_command "appstore uninstall my-p2p-app"
```
**Launch an installed package:**
**Launch (runs sources/index.js in the guest shell — prints app output):**
```
run_command "appstore launch my-p2p-app"
run_command "appstore launch my-p2p-app --checkout staged"
```
**Refresh after a new pear release:**
```
run_command "appstore update my-p2p-app"
```
**When user asks to install something:**
@@ -1,72 +1,74 @@
# pear-dev Skill
---
name: pear-dev
version: 0.2.0
description: Create, stage, build, release, and seed Pear applications entirely from inside a booted Bare OS using ctx.pear and /bin/pear.
tags: [pear, p2p, build, bundle, stage, release, seed, ctx.pear, development]
---
**name:** pear-dev
**description:** Create, stage, build, and release real Pear applications entirely from inside a booted Bare OS using the new `ctx.pear` surface and `/bin/pear` command.
**tags:** [pear, p2p, build, bundle, stage, release, ctx.pear, development]
# pear-dev Skill
## When to Use This Skill
Use this skill whenever the user (or another agent) wants to do Pear development work inside Bare OS:
- Scaffold a new Pear app (`pear init` equivalent)
- Stage / bundle an app for deployment
- Inspect what Pear tooling is currently available via `ctx.pear`
- Integrate Pear app creation with the P2P App Store (materialize + launch workflows)
- Autonomous "create → stage → test → release to my HDMS store" loops
Use this skill whenever the user (or another agent) wants Pear development work inside Bare OS:
- Scaffold a new Pear app (`pear init`)
- Stage / bundle an app for deployment (`pear stage`)
- Publish to a real `pear://` link from the guest shell (`pear release`)
- Keep the release replicating (`pear seed`)
- Inspect what Pear tooling is available via `ctx.pear`
- Integrate Pear app creation with the P2P App Store
## Core Capabilities Available Right Now
## Core Capabilities
- `ctx.pear` the new Pear development surface (exposed when `BARE_OS_BARE_MODULES` is enabled).
- `ctx.pear.pearBuild` → from `pear-build` package
- `ctx.pear.pearBundle` → from `pear-bundle` package
- `ctx.pear.pearRef`, `ctx.pear.bareBundleCompile`, `ctx.pear.bareBundleEvaluate`
- `/bin/pear` command (real binary after coreutils build):
- `pear help`
- `pear info`
- `pear list` ← shows exactly what is live on `ctx.pear`
- `pear stage` / `pear build` (currently informative stub pointing at ctx.pear)
- `ctx.pear` — Pear development surface (when `BARE_OS_BARE_MODULES` is enabled):
- `ctx.pear.pearBuild`, `ctx.pear.pearBundle`, `ctx.pear.pearRef`
- `ctx.pear.bareBundleCompile`, `ctx.pear.bareBundleEvaluate`
- `/bin/pear` command:
- `pear help` | `pear info` | `pear list`
- `pear init [dir]` — scaffold project skeleton
- `pear stage [dir]` — write `<project>/.pear/stage/` (sources + bundle)
- `pear release [dir]` — publish stage to HDMS Hyperdrive, print `pear://` links
- `pear seed [dir]` — swarm flush / replication for the release drive
- `pear build` / `pear bundle` — aliases for `pear stage`
Full `init` / `stage` / `release` / `seed` end-to-end will be completed across the remaining plan items.
## End-to-End Workflow (Guest Shell Only)
## Recommended Workflows (Current State)
The full pipeline runs inside the Bare OS shell. No host-side `pear stage` / `pear release` is required.
### 1. Discover what Pear tooling is available
```bash
pear list
pear info
pear init
cd ~/pear-projects/my-pear-app
pear stage
login # unlock identity vault if not already logged in
pear release
pear seed
```
### 2. Basic autonomous development loop (v1)
The agent should:
1. Use `run_command "pear list"` to see current surface.
2. Explain to the user what is and isn't fully wired yet.
3. For real work, guide the user to use the exposed `ctx.pear.pearBuild` / `ctx.pear.pearBundle` directly from JS (via `run_js_script`) until the higher-level `pear stage` command is complete.
4. Once staging is real, the loop becomes: create project → `pear stage` → verify output → optionally push to HDMS App Store.
**Requirements for release/seed:**
- Identity unlocked (`login`) so HDMS is active
- Prior successful `pear stage` (`.pear/stage/stage.json` must exist)
### 3. Integration with P2P App Store
After a Pear app is staged/bundled, the natural next step is usually:
- `appstore install ./my-staged-pear-app` (or via pear:// link once materialization is wired)
- Then launch via the App Store or directly with Pear primitives.
Release creates (or reuses) a writable HDMS mount `pear-<app-name>` at `/mnt/pear-<app-name>/`, mirrors the staged tree there, and writes `.pear/release.json` with `pearLink` and `versionedLink`.
## Safety & Limitations (Be Honest)
## Recommended Agent Loop
- This is early in the implementation of the full ctx.pear plan.
- Many high-value operations (full release to a swarm, seeding, talking to a live Pear sidecar for `pear release`) still require a host Pear sidecar (delegation pattern, same as peerctl).
- Do **not** claim that `pear stage` or `pear init` are fully functional until the corresponding plan items are marked complete and verifiers pass.
- Never suggest running untrusted Pear apps outside the existing sandbox/worker model.
1. `run_command "pear list"` — confirm ctx.pear tools loaded
2. `run_command "pear init"` or scaffold in project dir
3. `run_command "pear stage ."`
4. Ensure user is logged in (HDMS active); if guest, prompt `login`
5. `run_command "pear release ."`
6. `run_command "pear seed ."` for replication
7. Optional: `appstore install <name> <pear://link> --yes`
## Future Vision (What We Are Building Toward)
## Safety & Limits
- Complete `pear init`, `pear stage`, `pear bundle`, `pear release`, `pear seed` subcommands.
- Dedicated `pear-dev` HDMS drive convention (similar to `appstore` label).
- Rich autonomous agent workflows that can create production-grade Pear apps, stage them, test them, and publish them to the user's personal P2P App Store without the user typing a single command.
- Deep integration so "I want a new P2P notes app" → agent uses this skill + appstore skill end-to-end.
- Release drives live on the user's personal HDMS namespace (same trust model as `hdms create`).
- Re-release overwrites files on the release mount by path; stale paths are not purged automatically.
- `pear seed` is best-effort swarm flush — not a guarantee of wide replication.
- Do not claim host Pear sidecar is needed for release; the guest `/bin/pear` path is authoritative.
## Files & References
- Implementation plan: `docs/design/ctx-pear-surface-and-bare-audit-plan.md`
- Audit notes: `docs/audit/ctx-bare-audit-notes.md`
- Command source: `packages/bare-os-coreutils/src/pear.js`
- Manifest tier: `packages/bare-os-booter/lib/bare-module-manifest.json` (pearEntries section)
- ctx surface: `packages/bare-os-booter/lib/bare-os-ctx-bare.js` (buildPearCtxObjectFromHost)
Keep this skill updated as the Pear surface matures. The goal is production-grade autonomous Pear development inside Bare OS (matching the standard set by the appstore skill after its 50-round polish).
- Plan: `docs/design/ctx-pear-surface-and-bare-audit-plan.md`
- Staging: `packages/bare-os-coreutils/lib/pear-stage.js`
- Release: `packages/bare-os-coreutils/lib/pear-release.js`
- Command: `packages/bare-os-coreutils/src/pear.js`
+1 -1
View File
@@ -1,6 +1,6 @@
{
"schemaVersion": 1,
"generatedAt": "2026-05-26T23:16:52.758Z",
"generatedAt": "2026-05-26T23:42:58.565Z",
"pages": [
{
"name": "agent",