import { readFile } from 'node:fs/promises' import path from 'node:path' import { fileURLToPath } from 'node:url' import test from 'brittle' import b4a from 'b4a' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor async function loadPearBin() { const runtime = await readFile(path.join(__dirname, '../lib/runtime.js'), 'utf8') const stage = await readFile(path.join(__dirname, '../lib/pear-stage.js'), 'utf8') const release = await readFile(path.join(__dirname, '../lib/pear-release.js'), 'utf8') const body = await readFile(path.join(__dirname, '../src/pear.js'), 'utf8') return new AsyncFunction( 'ctx', 'argv', `${runtime}\n${stage}\n${release}\n${body}\nif (typeof run === 'function') return await run(ctx, argv)\n` ) } function mkMemVfs(initial = {}) { /** @type {Map} */ const files = new Map(Object.entries(initial)) const vfs = { env: {}, files, getcwd() { return '/home/guest/pear-projects/my-pear-app' }, async mkdir(p) { void p }, async readdir(p) { const prefix = p.endsWith('/') ? p : p + '/' const names = new Set() for (const key of files.keys()) { if (!key.startsWith(prefix)) continue const rest = key.slice(prefix.length) const seg = rest.split('/')[0] if (seg) names.add(seg) } return [...names] }, async stat(p) { if (!files.has(p)) { for (const key of files.keys()) { if (key.startsWith(p + '/')) return { isDirectory: true, type: 'directory' } } throw new Error('ENOENT') } return { isDirectory: false, type: 'file' } }, async readFile(p) { if (!files.has(p)) throw new Error('ENOENT ' + p) return files.get(p) }, async writeFile(p, buf) { files.set(p, buf instanceof Uint8Array ? buf : new Uint8Array(buf)) } } return vfs } function mkFakeHdms() { const key = b4a.from('abc123') const drive = { key, version: 7, discoveryKey: b4a.from('disc'), async flush() {} } /** @type {Map} */ const byLabel = new Map() const hdms = { active: true, byLabel, registry: { version: 1, drives: [] }, swarm: { join() {}, flush: async () => {} }, async create(ctx, label) { byLabel.set(label, { drive, writable: true, entry: { label, mode: 'writable', key: 'mockkey6' } }) this.registry.drives.push({ label, mode: 'writable', key: 'mockkey6' }) ctx.console.log('Created ' + label) }, async _openEntry(entry) { byLabel.set(entry.label, { drive, writable: entry.mode === 'writable', entry }) } } return hdms } test('pear release mirrors stage to HDMS mount and writes release.json', async (t) => { const project = '/home/guest/pear-projects/my-pear-app' const stageDir = `${project}/.pear/stage` const vfs = mkMemVfs({ [`${project}/.pear/stage/stage.json`]: b4a.from( JSON.stringify({ name: 'my-pear-app', stageDir, entry: 'index.js', bundleMethod: 'test' }), 'utf8' ), [`${project}/.pear/stage/package.json`]: b4a.from('{"name":"my-pear-app"}\n', 'utf8'), [`${project}/.pear/stage/app.bundle.js`]: b4a.from('console.log(1)\n', 'utf8') }) const hdms = mkFakeHdms() const logs = [] const ctx = { vfs, b4a, env: { HOME: '/home/guest', PWD: project }, identity: { state: 'unlocked' }, disk: { hdmsController: hdms }, bare: { hypercoreIdEncoding: { encode(buf) { return 'z32_' + b4a.toString(buf, 'utf8') }, decode() { return b4a.alloc(0) } } }, console: { log(...a) { logs.push(a.join(' ')) }, error() {}, warn() {} } } const run = await loadPearBin() await run(ctx, ['pear', 'release', '.']) t.ok(vfs.files.has('/mnt/pear-my-pear-app/app.bundle.js')) t.ok(vfs.files.has(`${project}/.pear/release.json`)) const rel = JSON.parse(b4a.toString(vfs.files.get(`${project}/.pear/release.json`), 'utf8')) t.is(rel.label, 'pear-my-pear-app') t.is(rel.latest.length, 7) t.ok(rel.latest.pearLink.startsWith('pear://')) t.ok(logs.some((l) => l.includes('versioned:'))) }) test('pear release requires staged tree', async (t) => { const project = '/home/guest/pear-projects/empty' const vfs = mkMemVfs({}) const ctx = { vfs, b4a, env: { HOME: '/home/guest', PWD: project }, identity: { state: 'unlocked' }, disk: { hdmsController: mkFakeHdms() }, bare: { hypercoreIdEncoding: { encode: () => 'k', decode: () => b4a.alloc(0) } }, console: { log() {}, error() {}, warn() {} }, exitCode: 0 } const run = await loadPearBin() await run(ctx, ['pear', 'release', '.']) t.is(ctx.exitCode, 1) }) test('pear release works without ctx.bare (HDMS registry key)', async (t) => { const project = '/home/guest/pear-projects/my-pear-app' const stageDir = `${project}/.pear/stage` const vfs = mkMemVfs({ [`${project}/.pear/stage/stage.json`]: b4a.from( JSON.stringify({ name: 'my-pear-app', stageDir, entry: 'index.js' }), 'utf8' ), [`${project}/.pear/stage/index.js`]: b4a.from('console.log(1)\n', 'utf8') }) const hdms = mkFakeHdms() const ctx = { vfs, b4a, env: { HOME: '/home/guest', PWD: project }, identity: { state: 'unlocked' }, disk: { hdmsController: hdms }, console: { log() {}, error() {}, warn() {} } } const run = await loadPearBin() await run(ctx, ['pear', 'release', '.', '--json']) const rel = JSON.parse(b4a.toString(vfs.files.get(`${project}/.pear/release.json`), 'utf8')) t.is(rel.latest.keyZ32, 'mockkey6') }) test('pear seed opens existing release mount', async (t) => { const project = '/home/guest/pear-projects/my-pear-app' const vfs = mkMemVfs({ [`${project}/.pear/release.json`]: b4a.from( JSON.stringify({ schema: 1, label: 'pear-my-pear-app', latest: { label: 'pear-my-pear-app', pearLink: 'pear://z32_abc123', versionedLink: 'pear://0.7.z32_abc123', length: 7 } }), 'utf8' ) }) const hdms = mkFakeHdms() hdms.registry.drives.push({ label: 'pear-my-pear-app', mode: 'writable', key: 'mockkey6' }) const ctx = { vfs, b4a, env: { HOME: '/home/guest', PWD: project }, identity: { state: 'unlocked' }, disk: { hdmsController: hdms }, bare: { hypercoreIdEncoding: { encode(buf) { return 'z32_' + b4a.toString(buf, 'utf8') }, decode() { return b4a.alloc(0) } } }, console: { log() {}, error() {}, warn() {} } } const run = await loadPearBin() await run(ctx, ['pear', 'seed', '.', '--wait-ms', '0']) t.ok(hdms.byLabel.has('pear-my-pear-app')) })