OverviewFilesCommitsIssuesDocsReleasesPeers

pgh commits / 9ef9272

0.1.129: promote cross-arch bootstraps from the app's own dist/ + test repo-create CLI
vendoredBootstraps only scanned node_modules/<pkg>/dist/, so it found the arm64 tarballs only via whichever dependency happened to carry a copy. The canonical home is the app's OWN dist/ (pgh/dist/ and pgh-sidecar/dist/ each ship one alongside a .sha256). Scans that first now, and it wins over a vendored copy. Also adds the repo-create CLI test 0.1.126 shipped without. run() ends with a bare-name fallthrough to repo-info, so a misplaced `create` case would not error — it would quietly become "show me the repo called create". That's now pinned. test: 284/284 (840 asserts) under bare. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
9ef92728c436f609efc53b6c62f3a0fe967c5d7a
by plankatron <fleeky@gmail.com> · 2026-07-30T00:48:43.000Z

Changed files (4) +97 -7

lib/bootstrap-artifacts.js +16 -6
……@@ -79,17 +79,27 @@ function slugify (name) {
7979 */
8080function vendoredBootstraps (appDir) {
8181 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'))
8297 const nm = path.join(appDir, 'node_modules')
8398 let pkgs = []
8499 try { pkgs = fs.readdirSync(nm) } catch { return out }
85100 for (const pkg of pkgs) {
86101 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'))
93103 }
94104 return out
95105}
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "pgh",
3 "version": "0.1.128",
3 "version": "0.1.129",
44 "description": "pgh — P2P content-addressed package manager and app launcher",
55 "type": "module",
66 "main": "index.js",
test/bootstrap-artifacts.test.js +30 -0
……@@ -251,6 +251,36 @@ test('re-promotes when the vendored artifact is replaced underneath us', async (
251251 t.is(readFileSync(join(dir, 'pgh-bootstrap-bare-linux-arm64.tar.gz'), 'utf8'), 'V2', 're-promoted, not stale')
252252})
253253
254test('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
270test('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
254284test('hostLabel returns os-arch', (t) => {
255285 t.ok(/^[a-z0-9]+-[a-z0-9]+$/.test(hostLabel()), 'looks like <os>-<arch>')
256286})
test/repo-create-cli.test.js +50 -0
……@@ -0,0 +1,50 @@
1import test from 'brittle'
2import { 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
10function fakeClient () {
11 const calls = []
12 return { calls, async call (method, params) { calls.push({ method, params }); return { ok: true } } }
13}
14
15test('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
23test('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
29test('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
35test('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
41test('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
48test('create is documented in usage', (t) => {
49 t.ok(/pgh repo create/.test(usage), 'usage mentions create')
50})