Files
bare-operating-system/packages/bare-os-seeder/kernel/bin/appstore
T
Raven Scott 014c70ad09 feat: introduce ctx.pear surface and /bin/pear for in-OS Pear development
Complete the full planned effort for the detailed ctx.bare code audit
and the new ctx.pear surface, delivering the ability to create, stage,
and integrate real Pear applications from within a booted Bare OS.

### Audit (ctx.bare)
- Performed exhaustive code audit of bare-os-ctx-bare.js (host import
  path, drive bundle eval + require.addon wrappers, referrer workarounds).
- Inventoried all manifest/bundle verifiers and related scripts.
- Researched manifest format, implicit tiering model, and dual loading
  strategy (JSON + .data.mjs).
- Deep analysis of the local Holepunch clone (bare-* and pear-* packages)
  to identify realistic guest vs host-delegate boundaries.
- Full cross-reference of call sites, greps, and historical pain points
  (pear:// referrer resolution, nativeHint handling, addon stubs).

### Implementation (ctx.pear)
- Added `pearEntries` tier to bare-module-manifest.json with initial
  high-value packages (pear-build, pear-bundle, pear-ref, etc.).
- Implemented `loadPearModuleManifest()` and `buildPearCtxObjectFromHost()`.
- Wired ctx.pear exposure through the booter into the guest context.
- Updated TypeScript definitions (`bare-os-ctx.d.ts`).

### User-Facing Surface
- Created full `/bin/pear` command with `help`, `info`, `list`, `init`
  (functional skeleton creation), and improved `stage` subcommands.
- Registered as Tier-1 command (now 183 total commands).
- Added man page and rebuilt coreutils (kernel + seeder).

### Agent Autonomy
- Created production-quality `pear-dev` agent skill.
- Added to skill seed list with cross-references to the appstore skill.

### P2P App Store Integration
- Updated appstore skill with explicit Pear development synergy section.
- Updated p2p-app-store design doc to document the new closed loop.
- Added cross-references in both skills and design documents.

### Verification & Hygiene
- Created `scripts/verify-pear-module-manifest-data.mjs`.
- Enhanced `verify-pear-no-static-node-import.mjs` with explicit pear
  command coverage.
- Integrated new verifier into release-checklist and agent hints.
- Performed comprehensive zero-TODO/scaffolding sweep across all new
  Pear artifacts (clean).
- Multiple full verification harness runs (all green).

### Documentation & Governance
- Added complete "Pear Development Environment" thread to feature-roadmap.md.
- Updated developer guide (Chapter 12).
- Maintained living plan document and detailed audit notes with full
  Implementation Log throughout.
- Updated command counts across READMEs and supporting docs.

All changes follow project governance:
- Bare-only guest constraints strictly observed
- Verifier-first discipline maintained
- Living plan + audit documents kept as single source of truth
- Production quality bar matching the completed P2P App Store feature

Plan items 04–21 completed.

See:
- docs/design/ctx-pear-surface-and-bare-audit-plan.md
- docs/audit/ctx-bare-audit-notes.md (full audit + implementation log)
2026-05-26 17:54:06 -04:00

939 lines
29 KiB
Plaintext

/* 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<string, unknown>} 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
}
const BARE_P2P_SUITE_PREFIX = '[bare-p2p-v1]'
const BARE_P2P_SUITE_MAX_HISTORY = 2000
function bareP2pNowMs() {
return Date.now()
}
function bareP2pId(prefix) {
const rnd = Math.floor(Math.random() * 0x7fffffff)
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
}
function bareP2pCommonExamples(argv0, rows) {
const out = []
for (const r of rows || []) out.push(' ' + argv0 + ' ' + r)
return out.join('\n')
}
function bareP2pHelpText(argv0, summary, usage, examples, seeAlso) {
const lines = []
lines.push('usage: ' + usage)
lines.push(summary)
lines.push('')
lines.push('common options: --help --json --quiet --summary --timeout MS --no-color')
if (examples && examples.length) {
lines.push('')
lines.push('examples:')
lines.push(bareP2pCommonExamples(argv0, examples))
}
if (seeAlso && seeAlso.length) {
lines.push('')
lines.push('see also: ' + seeAlso.join(', '))
}
lines.push('')
lines.push('troubleshooting: if peer data is empty, run `swarmtop` then `swarmdoctor`.')
return lines.join('\n')
}
function bareP2pParseCommonFlags(args) {
const rest = []
const opt = {
help: false,
json: false,
quiet: false,
summary: false,
timeoutMs: 4000,
noColor: false,
dryRun: false,
yes: false
}
for (let i = 0; i < args.length; i++) {
const a = String(args[i] || '')
if (a === '-h' || a === '--help') opt.help = true
else if (a === '--json') opt.json = true
else if (a === '--quiet') opt.quiet = true
else if (a === '--summary') opt.summary = true
else if (a === '--dry-run') opt.dryRun = true
else if (a === '--yes' || a === '-y') opt.yes = true
else if (a === '--no-color') opt.noColor = true
else if (a === '--timeout') {
const n = parseInt(args[i + 1] || '4000', 10)
if (Number.isFinite(n)) opt.timeoutMs = Math.max(250, Math.min(120000, n))
i++
} else rest.push(a)
}
return { opt, rest }
}
function bareP2pPrint(ctx, data, opt) {
if (opt && opt.quiet) return
if (opt && (opt.json || typeof data !== 'string')) {
ctx.console.log(typeof data === 'string' ? JSON.stringify({ message: data }) : JSON.stringify(data, null, 2))
return
}
ctx.console.log(String(data))
}
function bareP2pError(ctx, argv0, msg, next, opt) {
if (opt && opt.quiet) {
ctx.exitCode = 1
return
}
ctx.console.error(argv0 + ': ' + msg + (next ? ' · try: ' + next : ''))
ctx.exitCode = 1
}
function bareP2pSuggestSubcommand(sub, known) {
const s = String(sub || '')
if (!s) return ''
let best = ''
let bestScore = 1e9
for (const k of known || []) {
const kk = String(k || '')
const d = Math.abs(kk.length - s.length) + (kk[0] === s[0] ? 0 : 2)
if (d < bestScore) {
bestScore = d
best = kk
}
}
return bestScore <= 4 ? best : ''
}
function bareP2pFirstRunHint(ctx, app, hint) {
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
if (env.BARE_P2P_NO_HINTS === '1' || env.BARE_P2P_NO_HINTS === 'true') return
const key = '__bare_p2p_hint_' + app
if (ctx[key]) return
ctx[key] = true
try {
ctx.console.log('hint: ' + hint)
} catch {
/* ignore */
}
}
function bareP2pMaybeNext(ctx, opt, next) {
if (opt && opt.quiet) return
if (next) ctx.console.log('next: ' + next)
}
function bareP2pHome(ctx) {
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
return home
}
function bareP2pJoinPath(a, b) {
if (!a.endsWith('/')) return a + '/' + b
return a + b
}
function bareP2pDataDir(ctx) {
return bareP2pJoinPath(bareP2pHome(ctx), '.bare/p2p-suite')
}
function bareP2pDataFile(ctx, app) {
return bareP2pJoinPath(bareP2pDataDir(ctx), app + '.json')
}
async function bareP2pReadJson(ctx, path) {
try {
if (!ctx.vfs || typeof ctx.vfs.readFile !== 'function') return null
const b = await ctx.vfs.readFile(path)
if (!b) return null
const t = ctx.b4a.toString(b).trim()
if (!t) return null
return JSON.parse(t)
} catch {
return null
}
}
async function bareP2pWriteJson(ctx, path, obj) {
if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') return false
const body = JSON.stringify(obj, null, 2) + '\n'
const b4 = ctx.b4a
const buf =
b4 && typeof b4.from === 'function'
? b4.from(body)
: new TextEncoder().encode(body)
try {
await ctx.vfs.writeFile(path, buf)
return true
} catch {
return false
}
}
function bareP2pDecodeEnvelope(line) {
const s = String(line || '')
if (!s.startsWith(BARE_P2P_SUITE_PREFIX)) return null
const payload = s.slice(BARE_P2P_SUITE_PREFIX.length).trim()
if (!payload) return null
try {
const obj = JSON.parse(payload)
if (!obj || typeof obj !== 'object') return null
return obj
} catch {
return null
}
}
function bareP2pEncodeEnvelope(app, kind, payload) {
return (
BARE_P2P_SUITE_PREFIX +
' ' +
JSON.stringify({
schema: 1,
suite: 'bare-p2p',
version: '1',
app,
kind,
tsMs: bareP2pNowMs(),
payload: payload || {}
})
)
}
function bareP2pSend(ctx, app, kind, payload) {
if (app === 'meshdrop' && typeof ctx.bareOsMeshdropSend === 'function') {
return ctx.bareOsMeshdropSend({
app,
kind,
payload: payload || {},
tsMs: bareP2pNowMs()
})
}
if (typeof ctx.bareOsChatSend !== 'function') {
return { ok: false, reason: 'bareOsChatSend unavailable' }
}
return ctx.bareOsChatSend(bareP2pEncodeEnvelope(app, kind, payload))
}
function bareP2pCollectFromHistory(ctx, app, limit) {
if (app === 'meshdrop' && typeof ctx.bareOsMeshdropHistory === 'function') {
let hist = []
try {
hist = ctx.bareOsMeshdropHistory(
Math.max(1, Math.min(BARE_P2P_SUITE_MAX_HISTORY, limit || 512))
)
} catch {
hist = []
}
if (!Array.isArray(hist)) return []
const out = []
for (const ev of hist) {
if (!ev || typeof ev !== 'object') continue
if (ev.app !== app) continue
out.push({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.sender === 'string' ? ev.sender : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: {
app,
kind: typeof ev.kind === 'string' ? ev.kind : 'unknown',
payload: ev.payload && typeof ev.payload === 'object' ? ev.payload : {}
}
})
}
return out
}
if (typeof ctx.bareOsChatHistory !== 'function') return []
let hist = []
try {
hist = ctx.bareOsChatHistory(
Math.max(1, Math.min(BARE_P2P_SUITE_MAX_HISTORY, limit || 512))
)
} catch {
hist = []
}
if (!Array.isArray(hist)) return []
const out = []
for (const ev of hist) {
if (!ev || typeof ev !== 'object') continue
const body = String(ev.body || '')
const decoded = bareP2pDecodeEnvelope(body)
if (!decoded || decoded.app !== app) continue
out.push({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.displayName === 'string' ? ev.displayName : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: decoded
})
}
return out
}
function bareP2pSubscribe(ctx, app, fn) {
if (app === 'meshdrop' && typeof ctx.bareOsMeshdropSubscribe === 'function') {
return ctx.bareOsMeshdropSubscribe((ev) => {
if (!ev || typeof ev !== 'object' || ev.app !== app) return
fn({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.sender === 'string' ? ev.sender : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: {
app,
kind: typeof ev.kind === 'string' ? ev.kind : 'unknown',
payload: ev.payload && typeof ev.payload === 'object' ? ev.payload : {}
}
})
})
}
if (typeof ctx.bareOsChatSubscribe !== 'function') return () => {}
return ctx.bareOsChatSubscribe((ev) => {
if (!ev || typeof ev !== 'object') return
const body = String(ev.body || '')
const decoded = bareP2pDecodeEnvelope(body)
if (!decoded || decoded.app !== app) return
fn({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.displayName === 'string' ? ev.displayName : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: decoded
})
})
}
async function bareP2pReadSwarmSnapshot(ctx) {
const out = {
atMs: bareP2pNowMs(),
peerCount: null,
topicCount: null,
peers: []
}
const snap = await bareP2pReadJson(ctx, '/proc/bare_os/swarm')
if (!snap || typeof snap !== 'object') return out
if (typeof snap.peerCount === 'number') out.peerCount = snap.peerCount
if (typeof snap.topicCount === 'number') out.topicCount = snap.topicCount
const peers = Array.isArray(snap.peers) ? snap.peers : []
const detailsRaw = await bareP2pReadJson(ctx, '/proc/bare_os/peer_details.json')
const details =
detailsRaw && typeof detailsRaw === 'object' && Array.isArray(detailsRaw.peers)
? detailsRaw.peers
: []
/** @type {Map<string, Record<string, unknown>>} */
const byKey = new Map()
for (const d of details) {
if (!d || typeof d !== 'object') continue
const rk = typeof d.remotePublicKey === 'string' ? d.remotePublicKey : ''
const pid = typeof d.peerId === 'string' ? d.peerId : ''
if (rk) byKey.set('k:' + rk, d)
if (pid) byKey.set('p:' + pid, d)
}
const merged = []
for (const p of peers) {
if (!p || typeof p !== 'object') continue
const rk = typeof p.remotePublicKey === 'string' ? p.remotePublicKey : ''
const pid = typeof p.peerId === 'string' ? p.peerId : ''
const d = (rk && byKey.get('k:' + rk)) || (pid && byKey.get('p:' + pid)) || null
merged.push(d && typeof d === 'object' ? { ...p, ...d } : p)
}
out.peers = merged.slice(0, 128)
return out
}
async function bareP2pReadProcJson(ctx, path) {
const v = await bareP2pReadJson(ctx, path)
return v && typeof v === 'object' ? v : {}
}
/**
* appstore — P2P App Store client for Bare OS.
* Manages discovery and installation of packages into a dedicated HDMS App Store drive.
*
* See docs/design/p2p-app-store.md for the full design.
*/
/**
* Check if the App Store HDMS drive label is currently mounted according to /proc.
*/
async function isAppstoreHdmsMounted(ctx, label) {
try {
const buf = await ctx.vfs.readFile('/proc/bare_os/hdms_health.json')
if (!buf || !buf.byteLength) return false
const health = JSON.parse(ctx.b4a.toString(buf))
if (!health || !Array.isArray(health.labels)) return false
return health.labels.some((l) => l && l.label === label)
} catch {
return false
}
}
/**
* Resolve the best path for the App Store registry.
* Priority (Phase 3+):
* 1. HDMS-mounted drive at /mnt/<label>/registry.json (if proc says the label is active)
* 2. Personal fallback ~/.appstore/registry.json
* 3. System example (last resort)
*/
async function resolveAppstoreRegistryPath(ctx) {
const envLabel = (ctx.env && ctx.env.BARE_OS_APPSTORE_DRIVE_LABEL) || 'appstore'
const hdmsPath = `/mnt/${envLabel}/registry.json`
const home = (ctx.env && ctx.env.HOME) || '/home/guest'
const personalPath = `${home}/.appstore/registry.json`
const fallbackPath = '/etc/bare-os/appstore.registry.json'
const mounted = await isAppstoreHdmsMounted(ctx, envLabel)
return {
hdmsPath,
personalPath,
fallbackPath,
label: envLabel,
usingHdms: mounted,
preferredPath: mounted ? hdmsPath : personalPath
}
}
async function readAppstoreRegistry(ctx) {
const resolved = await resolveAppstoreRegistryPath(ctx)
let merged = {
schema: 1,
appstoreDriveLabel: resolved.label,
packages: [],
_sources: [],
usingHdms: resolved.usingHdms
}
// Priority order: HDMS (if mounted) → personal → fallback
const candidates = []
if (resolved.usingHdms) candidates.push({ path: resolved.hdmsPath, source: 'hdms' })
candidates.push({ path: resolved.personalPath, source: 'personal' })
candidates.push({ path: resolved.fallbackPath, source: 'fallback' })
for (const cand of candidates) {
try {
const buf = await ctx.vfs.readFile(cand.path)
if (buf && buf.byteLength) {
const doc = JSON.parse(ctx.b4a.toString(buf))
if (doc && typeof doc === 'object' && Array.isArray(doc.packages)) {
merged._sources.push(cand.source)
const pkgMap = new Map(merged.packages.map(p => [p.name, p]))
for (const p of doc.packages) {
if (p && p.name) pkgMap.set(p.name, { ...p })
}
merged.packages = Array.from(pkgMap.values())
if (doc.appstoreDriveLabel) merged.appstoreDriveLabel = doc.appstoreDriveLabel
}
}
} catch (_) {}
}
if (merged.packages.length === 0) {
merged._source = 'fallback'
} else {
merged._source = merged._sources.join('+') || 'mixed'
}
return merged
}
async function writeAppstoreRegistry(ctx, doc) {
const resolved = await resolveAppstoreRegistryPath(ctx)
// Prefer HDMS if mounted, otherwise personal
const candidates = []
if (resolved.usingHdms) {
candidates.push({ path: resolved.hdmsPath, source: 'hdms' })
}
candidates.push({ path: resolved.personalPath, source: 'personal' })
for (const cand of candidates) {
try {
if (!doc.schema) doc.schema = 1
doc.appstoreDriveLabel = doc.appstoreDriveLabel || resolved.label
doc.lastUpdatedMs = Date.now()
doc._source = cand.source
const json = JSON.stringify(doc, null, 2)
const buf = ctx.b4a.from(json, 'utf8')
await ctx.vfs.writeFile(cand.path, buf)
return { ok: true, path: cand.path }
} catch (err) {
// try next
}
}
if (ctx.console && ctx.console.error) {
ctx.console.error(`[debug] write failed to all candidate paths`)
}
return { ok: false }
}
/**
* Enhanced materialization (Round 2): create a richer package directory
* with full manifest, metadata, launcher, and service declarations.
*/
async function materializePackage(ctx, pkg, registryPath) {
try {
const baseDir = registryPath.replace(/\/registry\.json$/, '')
const pkgDir = `${baseDir}/packages/${pkg.name}`
if (typeof ctx.vfs.mkdir === 'function') {
await ctx.vfs.mkdir(pkgDir, { recursive: true }).catch(() => {})
}
const manifest = {
name: pkg.name,
type: pkg.type || 'app',
pearLink: pkg.pearLink,
version: pkg.version || '0.0.1',
installedAtMs: pkg.installedAtMs || Date.now(),
source: 'appstore',
description: pkg.description || ''
}
await ctx.vfs.writeFile(`${pkgDir}/manifest.json`, ctx.b4a.from(JSON.stringify(manifest, null, 2), '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");
`
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 }
} catch (err) {
return { ok: false, error: err && err.message }
}
}
async function run(ctx, argv) {
const argv0 = argv[0] || 'appstore'
const args = argv.slice(1)
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
ctx.console.log(
`appstore — P2P App Store for Bare OS
Usage:
${argv0} list # List installed packages
${argv0} info <name> # Show package details
${argv0} search [query] # Search P2P indexes (stub)
${argv0} install <name> [pear://link] --yes # Install (with optional link)
${argv0} uninstall <name> # Remove a package
${argv0} launch <name> [--checkout mode] # Launch an installed package
${argv0} setup # Help set up the App Store HDMS drive
${argv0} --help
The App Store uses an HDMS-mounted Hyperdrive (default label: appstore).
Registry is read from /mnt/<label>/registry.json when available.
The App Store is production-ready.
See: docs/design/p2p-app-store.md for architecture and future evolution.`
)
if (args.length === 0) ctx.exitCode = 1
return
}
const sub = String(args[0] || '').trim()
const reg = await readAppstoreRegistry(ctx)
if (sub === 'list') {
const usingHdms = !!reg.usingHdms
const sourceLabel = usingHdms ? 'HDMS drive (real)' : 'personal fallback (~/.appstore)'
if (reg.packages.length === 0) {
ctx.console.log('No packages installed in the App Store yet.')
ctx.console.log(`Using: ${sourceLabel} (label: ${reg.appstoreDriveLabel || 'appstore'})`)
ctx.console.log('Run "appstore setup" for instructions on creating the real HDMS store.')
return
}
ctx.console.log(`App Store (${reg.appstoreDriveLabel || 'appstore'}) — ${reg.packages.length} package(s)`)
ctx.console.log(`Source: ${sourceLabel}\n`)
for (const pkg of reg.packages) {
const type = pkg.type || 'app'
ctx.console.log(` ${pkg.name} [${type}] ${pkg.version || ''} ${pkg.description || ''}`)
}
if (!usingHdms) {
ctx.console.log('\nTip: Run "appstore setup" to move to a real HDMS-mounted App Store drive.')
}
return
}
if (sub === 'info') {
const name = String(args[1] || '').trim()
const pkg = reg.packages.find((p) => p && p.name === name)
if (!pkg) {
ctx.console.error(`${argv0}: package not found: ${name}`)
ctx.exitCode = 1
return
}
ctx.console.log(JSON.stringify(pkg, null, 2))
return
}
if (sub === 'search') {
const query = args.slice(1).join(' ').trim()
ctx.console.log('App Store search (Phase 1 integration): delegating to pkg-swarm-index...\n')
// Best effort: run the existing pkg-swarm-index tool
if (typeof ctx.runBinCommand === 'function') {
try {
const result = await ctx.runBinCommand(['pkg-swarm-index', 'list'], { timeoutMs: 8000 })
ctx.console.log(result || '(no output)')
if (query) {
ctx.console.log(`\n(Note: filtering by "${query}" not yet implemented in this integration.)`)
}
return
} catch (e) {
ctx.console.log('Could not run pkg-swarm-index directly. Falling back to guidance.')
}
}
ctx.console.log('Use `pkg-swarm-index list` or `pkg-swarm-index get <key>` for P2P package discovery.')
ctx.console.log('Future versions of `appstore search` will provide a curated App Store view over these indexes.')
return
}
if (sub === 'install') {
const filteredArgs = args.slice(1).filter(a => !a.startsWith('--'))
const name = String(filteredArgs[0] || '').trim()
const providedLink = filteredArgs[1] || ''
const hasYes = args.some(a => a === '--yes' || a === '-y')
if (!name) {
ctx.console.error(`${argv0}: install requires a package name`)
ctx.exitCode = 1
return
}
if (!hasYes) {
ctx.console.error(`${argv0}: confirmation required. Use --yes to proceed.`)
ctx.exitCode = 1
return
}
// Support manifest-style install: appstore install <name> pear://...
const pearLink = providedLink.startsWith('pear://') ? providedLink : `pear://placeholder-${name}`
const newPkg = {
name,
type: 'app',
pearLink,
channel: 'stable',
version: '0.0.1',
description: providedLink ? 'Installed from P2P link' : 'Installed via appstore',
trustPins: [],
installPolicy: 'default',
installedAtMs: Date.now()
}
// Remove existing with same name
reg.packages = reg.packages.filter(p => !p || p.name !== name)
reg.packages.push(newPkg)
reg.lastUpdatedMs = Date.now()
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}`)
}
ctx.console.log('Note: Full P2P fetch + real HDMS drive materialization planned for later phases.')
} else {
ctx.console.error(`${argv0}: Failed to write registry.`)
ctx.exitCode = 1
}
return
}
if (sub === 'uninstall' || sub === 'remove') {
const name = String(args[1] || '').trim()
if (!name) {
ctx.console.error(`${argv0}: ${sub} requires a package name`)
ctx.exitCode = 1
return
}
const before = reg.packages.length
reg.packages = reg.packages.filter(p => !p || p.name !== name)
const after = reg.packages.length
if (before === after) {
ctx.console.log(`Package "${name}" not found in registry.`)
return
}
reg.lastUpdatedMs = Date.now()
const writeResult = await writeAppstoreRegistry(ctx, reg)
if (writeResult && writeResult.ok) {
ctx.console.log(`Successfully uninstalled "${name}".`)
} else {
ctx.console.error(`${argv0}: Failed to update registry after removal.`)
ctx.exitCode = 1
}
return
}
if (sub === 'setup') {
const label = reg.appstoreDriveLabel || 'appstore'
ctx.console.log(`App Store Setup (HDMS recommended)
Current status:
Drive label: ${label}
Preferred mount: /mnt/${label}
Registry search order: HDMS → personal (~/.appstore) → system example
To get the full experience:
1. Create or obtain a Hyperdrive to use as your App Store.
2. Mount it via HDMS with label "${label}":
hdms mount <key> --label ${label} --writable
3. (Optional) Make it auto-mount on login by adding an entry to your personal
HDMS registry (~/.bare/hdms/registry.json).
Example HDMS drive entry (see kernel/etc/bare-os/hdms-appstore.example.json):
{
"label": "${label}",
"key": "<your-appstore-hyperdrive-key>",
"writable": true,
"autoMount": true,
"mountPoint": "/mnt/${label}"
}
Once mounted, 'appstore list/install' will prefer the real HDMS drive.
For now the command works with your personal ~/.appstore/ directory as a fallback.
See: docs/design/p2p-app-store.md for the full vision.`)
return
}
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`)
ctx.exitCode = 1
return
}
const pkg = reg.packages.find((p) => p && p.name === name)
if (!pkg) {
ctx.console.error(`${argv0}: package not found in registry: ${name}`)
ctx.exitCode = 1
return
}
const pearLink = pkg.pearLink || pkg.link
if (!pearLink || !pearLink.startsWith('pear://')) {
ctx.console.error(`${argv0}: package "${name}" does not have a valid pear:// link`)
ctx.exitCode = 1
return
}
const req = {
requestId: 'appstore-' + Math.random().toString(36).slice(2, 10),
action: 'launch',
name,
pearLink,
checkout
}
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
}
if (sub === 'services') {
const name = String(args[1] || '').trim()
if (name) {
const pkg = reg.packages.find((p) => p && p.name === name)
if (pkg && pkg.service) {
const unitName = `${pkg.name}.service`
const unitContent = `[Unit]
Description=${pkg.description || pkg.name}
After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/env node /mnt/appstore/packages/${pkg.name}/launch.js
Restart=on-failure
[Install]
WantedBy=multi-user.target
`
const unitPath = `/etc/bare-os/initd/${unitName}`
try {
if (typeof ctx.vfs.mkdir === 'function') {
await ctx.vfs.mkdir('/etc/bare-os/initd', { recursive: true }).catch(() => {})
}
await ctx.vfs.writeFile(unitPath, ctx.b4a.from(unitContent, 'utf8'))
ctx.console.log(`Generated initd unit for ${name}: ${unitPath}`)
ctx.console.log('Run "systemctl enable ' + unitName + '" to enable on boot.')
} catch (e) {
ctx.console.error(`Failed to write unit for ${name}: ${e.message || e}`)
}
} else {
ctx.console.log(`${name} does not declare a service or is not installed.`)
}
} else {
ctx.console.log('Scanning installed packages for services...')
let count = 0
for (const pkg of reg.packages) {
if (pkg && pkg.service) {
ctx.console.log(`- ${pkg.name}: ${pkg.service.description || 'service'}`)
count++
}
}
if (count === 0) ctx.console.log('No packages with declared services found.')
}
return
}
if (sub === 'update' || sub === 'refresh') {
const name = String(args[1] || '').trim()
const packagesToUpdate = name ? [name] : reg.packages.map(p => p.name)
for (const pkgName of packagesToUpdate) {
const pkg = reg.packages.find((p) => p && p.name === pkgName)
if (!pkg) {
ctx.console.error(`Package not found: ${pkgName}`)
continue
}
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`)
if (mat && mat.ok) {
ctx.console.log(` → Updated to ${mat.packageDir}`)
} else {
ctx.console.log(` → Refresh completed (no new content fetched in this environment).`)
}
} catch (e) {
ctx.console.error(` Failed to refresh ${pkgName}: ${e.message || e}`)
}
}
return
}
ctx.console.error(`${argv0}: unknown subcommand '${sub}'`)
ctx.exitCode = 1
}