| … | … | @@ -79,17 +79,27 @@ function slugify (name) { |
| 79 | 79 | */ |
| 80 | 80 | function vendoredBootstraps (appDir) { |
| 81 | 81 | const out = [] |
| 82 | const seen = new Set() | |
| 83 | const scan = (dist) => { | |
| 84 | let files = [] | |
| 85 | try { files = fs.readdirSync(dist) } catch { return } | |
| 86 | for (const f of files) { | |
| 87 | if (!/-bootstrap-bare-.+\.tar\.gz$/.test(f)) continue | |
| 88 | if (seen.has(f)) continue // own dist/ wins over a vendored copy | |
| 89 | seen.add(f) | |
| 90 | out.push({ name: f, src: path.join(dist, f) }) | |
| 91 | } | |
| 92 | } | |
| 93 | // The app's OWN dist/ first — that's the canonical home for these artifacts | |
| 94 | // (they ship alongside a .sha256), so prefer it over whichever dependency | |
| 95 | // happens to carry a copy. | |
| 96 | scan(path.join(appDir, 'dist')) | |
| 82 | 97 | const nm = path.join(appDir, 'node_modules') |
| 83 | 98 | let pkgs = [] |
| 84 | 99 | try { pkgs = fs.readdirSync(nm) } catch { return out } |
| 85 | 100 | for (const pkg of pkgs) { |
| 86 | 101 | if (pkg.startsWith('.')) continue |
| 87 | const dist = path.join(nm, pkg, 'dist') | |
| 88 | let files = [] | |
| 89 | try { files = fs.readdirSync(dist) } catch { continue } | |
| 90 | for (const f of files) { | |
| 91 | if (/-bootstrap-bare-.+\.tar\.gz$/.test(f)) out.push({ name: f, src: path.join(dist, f) }) | |
| 92 | } | |
| 102 | scan(path.join(nm, pkg, 'dist')) | |
| 93 | 103 | } |
| 94 | 104 | return out |
| 95 | 105 | } |
| … | … | @@ -1,6 +1,6 @@ |
| 1 | 1 | { |
| 2 | 2 | "name": "pgh", |
| 3 | "version": "0.1.128", | |
| 3 | "version": "0.1.129", | |
| 4 | 4 | "description": "pgh — P2P content-addressed package manager and app launcher", |
| 5 | 5 | "type": "module", |
| 6 | 6 | "main": "index.js", |
| … | … | @@ -251,6 +251,36 @@ test('re-promotes when the vendored artifact is replaced underneath us', async ( |
| 251 | 251 | t.is(readFileSync(join(dir, 'pgh-bootstrap-bare-linux-arm64.tar.gz'), 'utf8'), 'V2', 're-promoted, not stale') |
| 252 | 252 | }) |
| 253 | 253 | |
| 254 | test('promotes from the app\'s OWN dist/, which is where these artifacts really live', async (t) => { | |
| 255 | const root = mkdtempSync(join(tmpdir(), 'pgh-ba-owndist-')) | |
| 256 | const store = fakeStore(root); store.init() | |
| 257 | const appDir = mkAppTree(store.appsDir, 'pgh', { label: 'linux-x64', treeHash: 'h1', installSh: true }) | |
| 258 | // pgh/dist/ ships the cross-arch tarball alongside a .sha256 — no node_modules involved | |
| 259 | mkdirSync(join(appDir, 'dist'), { recursive: true }) | |
| 260 | writeFileSync(join(appDir, 'dist', 'pgh-bootstrap-bare-linux-arm64.tar.gz'), 'OWN-DIST') | |
| 261 | writeFileSync(join(appDir, 'dist', 'pgh-bootstrap-bare-linux-arm64.tar.gz.sha256'), 'deadbeef') | |
| 262 | ||
| 263 | await reconcileBootstrapArtifacts(store, { tarTree: fakeTar([]), label: 'linux-x64' }) | |
| 264 | ||
| 265 | const dir = bootstrapDir(store) | |
| 266 | t.is(readFileSync(join(dir, 'pgh-bootstrap-bare-linux-arm64.tar.gz'), 'utf8'), 'OWN-DIST', 'promoted from own dist/') | |
| 267 | t.absent(existsSync(join(dir, 'pgh-bootstrap-bare-linux-arm64.tar.gz.sha256')), 'sidecar files not promoted') | |
| 268 | }) | |
| 269 | ||
| 270 | test('own dist/ wins over a vendored copy of the same tarball', async (t) => { | |
| 271 | const root = mkdtempSync(join(tmpdir(), 'pgh-ba-winner-')) | |
| 272 | const store = fakeStore(root); store.init() | |
| 273 | const appDir = mkAppTree(store.appsDir, 'pgh', { label: 'linux-x64', treeHash: 'h1', installSh: true }) | |
| 274 | mkdirSync(join(appDir, 'dist'), { recursive: true }) | |
| 275 | writeFileSync(join(appDir, 'dist', 'pgh-bootstrap-bare-linux-arm64.tar.gz'), 'CANONICAL') | |
| 276 | mkVendored(appDir, 'somedep', 'pgh-bootstrap-bare-linux-arm64.tar.gz', 'STALE-VENDORED') | |
| 277 | ||
| 278 | await reconcileBootstrapArtifacts(store, { tarTree: fakeTar([]), label: 'linux-x64' }) | |
| 279 | ||
| 280 | const served = readFileSync(join(bootstrapDir(store), 'pgh-bootstrap-bare-linux-arm64.tar.gz'), 'utf8') | |
| 281 | t.is(served, 'CANONICAL', 'own dist/ took precedence, not the vendored copy') | |
| 282 | }) | |
| 283 | ||
| 254 | 284 | test('hostLabel returns os-arch', (t) => { |
| 255 | 285 | t.ok(/^[a-z0-9]+-[a-z0-9]+$/.test(hostLabel()), 'looks like <os>-<arch>') |
| 256 | 286 | }) |
| … | … | @@ -0,0 +1,50 @@ |
| 1 | import test from 'brittle' | |
| 2 | import { run, usage } from '../lib/commands/repo.js' | |
| 3 | ||
| 4 | // `pgh repo create` dispatch. The trap worth pinning: run() ends with | |
| 5 | // return client.call('repo-info', { name: sub }) | |
| 6 | // so ANY unmatched subcommand is silently treated as a repo name to show. A | |
| 7 | // misplaced `create` case wouldn't error — it would quietly become | |
| 8 | // "show me the repo called create". | |
| 9 | ||
| 10 | function fakeClient () { | |
| 11 | const calls = [] | |
| 12 | return { calls, async call (method, params) { calls.push({ method, params }); return { ok: true } } } | |
| 13 | } | |
| 14 | ||
| 15 | test('repo create sends create-repo with the name and default branch main', async (t) => { | |
| 16 | const c = fakeClient() | |
| 17 | await run(c, ['create', 'myapp']) | |
| 18 | t.is(c.calls.length, 1, 'one rpc') | |
| 19 | t.is(c.calls[0].method, 'create-repo', 'called create-repo') | |
| 20 | t.alike(c.calls[0].params, { name: 'myapp', branch: 'main' }, 'name + default branch') | |
| 21 | }) | |
| 22 | ||
| 23 | test('repo create honours --branch', async (t) => { | |
| 24 | const c = fakeClient() | |
| 25 | await run(c, ['create', 'legacy', '--branch', 'master']) | |
| 26 | t.alike(c.calls[0].params, { name: 'legacy', branch: 'master' }, 'branch parsed from flag') | |
| 27 | }) | |
| 28 | ||
| 29 | test('repo create does NOT fall through to repo-info', async (t) => { | |
| 30 | const c = fakeClient() | |
| 31 | await run(c, ['create', 'myapp']) | |
| 32 | t.absent(c.calls.some((x) => x.method === 'repo-info'), 'never hit the bare-name fallthrough') | |
| 33 | }) | |
| 34 | ||
| 35 | test('repo create without a name errors instead of creating something', async (t) => { | |
| 36 | const c = fakeClient() | |
| 37 | await t.exception(() => run(c, ['create']), /usage: pgh repo create/, 'rejected') | |
| 38 | t.is(c.calls.length, 0, 'no rpc issued') | |
| 39 | }) | |
| 40 | ||
| 41 | test('a bare name still shows the repo (fallthrough intact)', async (t) => { | |
| 42 | const c = fakeClient() | |
| 43 | await run(c, ['somerepo']) | |
| 44 | t.is(c.calls[0].method, 'repo-info', 'unmatched subcommand = show that repo') | |
| 45 | t.alike(c.calls[0].params, { name: 'somerepo' }, 'treated as a name') | |
| 46 | }) | |
| 47 | ||
| 48 | test('create is documented in usage', (t) => { | |
| 49 | t.ok(/pgh repo create/.test(usage), 'usage mentions create') | |
| 50 | }) |