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

337 lines
10 KiB
JavaScript

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