@danypops/pi-packed 0.19.7 → 0.19.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/client.d.ts +109 -0
  2. package/dist/client.d.ts.map +1 -0
  3. package/dist/client.js +1 -0
  4. package/dist/protocol.d.ts +221 -0
  5. package/dist/protocol.d.ts.map +1 -0
  6. package/dist/protocol.js +1 -0
  7. package/extension/src/{permission.ts → approval/permission.ts} +1 -1
  8. package/extension/src/index.ts +1 -1
  9. package/extension/src/packed.ts +2 -2
  10. package/extension/src/{discover.ts → tabs/discover.ts} +4 -4
  11. package/extension/src/{resource-config.ts → tabs/resource-config.ts} +4 -4
  12. package/extension/src/{security-tui.ts → tabs/security-tui.ts} +3 -3
  13. package/extension/src/tool-output.ts +1 -1
  14. package/extension/src/tools.ts +2 -2
  15. package/extension/src/tui.ts +4 -4
  16. package/package.json +31 -8
  17. package/service/schema/pi-setup-v1.schema.json +70 -0
  18. package/service/setup/danypops-ecosystem.pi-setup.json +15 -0
  19. package/service/src/adoption/advisories.ts +268 -0
  20. package/service/src/adoption/check.ts +872 -0
  21. package/service/src/adoption/commit-freshness.ts +167 -0
  22. package/service/src/adoption/doctor.ts +135 -0
  23. package/service/src/adoption/install-validation.ts +187 -0
  24. package/service/src/adoption/pack.ts +291 -0
  25. package/service/src/adoption/score.ts +466 -0
  26. package/service/src/adoption/smoke-child.ts +113 -0
  27. package/service/src/adoption/smoke.ts +282 -0
  28. package/service/src/cli/cli.ts +926 -0
  29. package/service/src/daemon/cleanup.ts +76 -0
  30. package/service/src/daemon/client.ts +412 -0
  31. package/service/src/daemon/daemon-service.ts +249 -0
  32. package/service/src/daemon/daemon.ts +110 -0
  33. package/service/src/daemon/service.ts +664 -0
  34. package/service/src/daemon/watcher.ts +92 -0
  35. package/service/src/index/build-index.ts +256 -0
  36. package/service/src/packages/catalog.ts +61 -0
  37. package/service/src/packages/db.ts +224 -0
  38. package/service/src/packages/install.ts +60 -0
  39. package/service/src/packages/installed.ts +123 -0
  40. package/service/src/packages/package.ts +141 -0
  41. package/service/src/packages/resources.ts +203 -0
  42. package/service/src/pi/pi-version.ts +171 -0
  43. package/service/src/public/atomic-json.ts +32 -0
  44. package/service/src/public/client.ts +277 -0
  45. package/service/src/public/protocol.ts +169 -0
  46. package/service/src/publish/publish.ts +855 -0
  47. package/service/src/registry/registry.ts +246 -0
  48. package/service/src/security/security.ts +128 -0
  49. package/service/src/self-update/self-update.ts +148 -0
  50. package/service/src/setup/setup.ts +761 -0
  51. package/service/src/shared/atomic-json.ts +33 -0
  52. package/service/src/shared/cache.ts +21 -0
  53. package/service/src/shared/constants.ts +73 -0
  54. package/service/src/shared/log.ts +21 -0
  55. package/service/src/shared/paths.ts +88 -0
  56. package/service/src/shared/state.ts +15 -0
  57. package/service/src/shared/version.ts +46 -0
  58. package/service/test/advisories.test.ts +287 -0
  59. package/service/test/check.test.ts +368 -0
  60. package/service/test/cleanup.test.ts +220 -0
  61. package/service/test/cli.test.ts +1303 -0
  62. package/service/test/core.test.ts +181 -0
  63. package/service/test/daemon-kit-migration.test.ts +181 -0
  64. package/service/test/daemon-service.test.ts +238 -0
  65. package/service/test/db.test.ts +178 -0
  66. package/service/test/doctor.test.ts +234 -0
  67. package/service/test/domain.test.ts +291 -0
  68. package/service/test/fixtures/install-validation/broken-package/extension/index.ts +3 -0
  69. package/service/test/fixtures/install-validation/broken-package/package.json +8 -0
  70. package/service/test/fixtures/install-validation/healthy-package/extension/index.ts +3 -0
  71. package/service/test/fixtures/install-validation/healthy-package/package.json +8 -0
  72. package/service/test/fixtures/install-validation/no-manifest-package/package.json +5 -0
  73. package/service/test/index.test.ts +353 -0
  74. package/service/test/install-validation.test.ts +114 -0
  75. package/service/test/install.test.ts +113 -0
  76. package/service/test/log.test.ts +42 -0
  77. package/service/test/pack-score.test.ts +513 -0
  78. package/service/test/pi-version.test.ts +318 -0
  79. package/service/test/public-boundary.test.ts +54 -0
  80. package/service/test/public-client.test.ts +127 -0
  81. package/service/test/public-consumer.ts +8 -0
  82. package/service/test/publish.test.ts +333 -0
  83. package/service/test/registry-contract.test.ts +148 -0
  84. package/service/test/resources.test.ts +255 -0
  85. package/service/test/security.test.ts +89 -0
  86. package/service/test/self-update.test.ts +257 -0
  87. package/service/test/service.test.ts +555 -0
  88. package/service/test/setup.test.ts +375 -0
  89. package/service/test/smoke.test.ts +118 -0
  90. package/service/test/version.test.ts +37 -0
  91. package/service/tsconfig.consumer.json +13 -0
  92. package/service/tsconfig.public.json +12 -0
  93. /package/extension/src/{reload.ts → approval/reload.ts} +0 -0
  94. /package/extension/src/{discover-model.ts → tabs/discover-model.ts} +0 -0
@@ -0,0 +1,513 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { existsSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { createGithubLastCommitAt, type FetchGithubLastCommitAt } from "../src/adoption/commit-freshness.ts";
6
+ import { formatPackReport, NpmPackVerifier, type PackCommand } from "../src/adoption/pack.ts";
7
+ import { assessLocalAdoption, assessRegistryAdoption, scoreTarget } from "../src/adoption/score.ts";
8
+ import type { PkgInfo, Registry, SearchPage } from "../src/packages/package.ts";
9
+ import { runBounded } from "../src/publish/publish.ts";
10
+
11
+ class FakeRegistry implements Registry {
12
+ constructor(
13
+ private info_: PkgInfo,
14
+ private modifiedByName: Record<string, string | undefined> = {},
15
+ ) {}
16
+ async search(): Promise<SearchPage> {
17
+ return { results: [], total: 0 };
18
+ }
19
+ async searchPage(): Promise<SearchPage> {
20
+ return { results: [], total: 0 };
21
+ }
22
+ async searchAll() {
23
+ return [];
24
+ }
25
+ async info(): Promise<PkgInfo> {
26
+ return this.info_;
27
+ }
28
+ async modifiedAt(name: string): Promise<string | undefined> {
29
+ return this.modifiedByName[name];
30
+ }
31
+ }
32
+
33
+ function fixture(manifest: Record<string, unknown>, readme = ""): string {
34
+ const root = mkdtempSync(join(tmpdir(), "packed-pack-"));
35
+ writeFileSync(join(root, "package.json"), JSON.stringify(manifest));
36
+ if (readme) writeFileSync(join(root, "README.md"), readme);
37
+ return root;
38
+ }
39
+
40
+ const successfulPack: PackCommand = async (_cwd, args) => {
41
+ expect(args).toEqual(["pack", "--dry-run", "--json", "--ignore-scripts"]);
42
+ return {
43
+ code: 0,
44
+ stdout: JSON.stringify([
45
+ {
46
+ id: "pi-demo@1.0.0",
47
+ name: "pi-demo",
48
+ version: "1.0.0",
49
+ size: 500,
50
+ unpackedSize: 1000,
51
+ shasum: "abc",
52
+ integrity: "sha512-test",
53
+ filename: "pi-demo-1.0.0.tgz",
54
+ files: [
55
+ { path: "package.json", size: 200, mode: 420 },
56
+ { path: "extensions/demo.ts", size: 800, mode: 420 },
57
+ ],
58
+ },
59
+ ]),
60
+ stderr: "",
61
+ };
62
+ };
63
+
64
+ describe("npm tarball verification", () => {
65
+ it("uses npm pack dry-run JSON without lifecycle scripts and verifies Pi shape", async () => {
66
+ const root = fixture({ name: "pi-demo", version: "1.0.0", keywords: ["pi-package"], pi: { extensions: ["extensions/demo.ts"] } });
67
+ mkdirSync(join(root, "extensions"));
68
+ writeFileSync(join(root, "extensions/demo.ts"), "export default () => {};");
69
+ const report = await new NpmPackVerifier(successfulPack).verify(root);
70
+ expect(report.ok).toBe(true);
71
+ expect(report.command).toEqual(["npm", "pack", "--dry-run", "--json", "--ignore-scripts"]);
72
+ expect(report.shape).toEqual({ kind: "manifest", verified: true, evidence: ["pi.extensions"] });
73
+ expect(report.integrity).toBe("sha512-test");
74
+ });
75
+
76
+ it("suppresses npm lifecycle scripts in the real dry-run", async () => {
77
+ const root = fixture({
78
+ name: "pi-no-scripts",
79
+ version: "1.0.0",
80
+ scripts: { prepack: "touch lifecycle-ran" },
81
+ pi: { extensions: ["extension.ts"] },
82
+ });
83
+ writeFileSync(join(root, "extension.ts"), "export default () => {};");
84
+ const report = await new NpmPackVerifier().verify(root);
85
+ expect(report.ok).toBe(true);
86
+ expect(existsSync(join(root, "lifecycle-ran"))).toBe(false);
87
+ });
88
+
89
+ it("reports excluded resources and sensitive tarball paths with stable codes", async () => {
90
+ const root = fixture({ name: "pi-demo", version: "1.0.0", pi: { extensions: ["extensions/missing.ts"] } });
91
+ const command: PackCommand = async () => ({
92
+ code: 0,
93
+ stderr: "",
94
+ stdout: JSON.stringify([{ name: "pi-demo", version: "1.0.0", files: [{ path: ".env", size: 20 }], size: 20, unpackedSize: 20 }]),
95
+ });
96
+ const report = await new NpmPackVerifier(command).verify(root);
97
+ expect(report.ok).toBe(false);
98
+ expect(report.diagnostics.map((item) => item.code)).toContain("PACK_DECLARED_RESOURCE_MISSING");
99
+ expect(report.diagnostics.map((item) => item.code)).toContain("PACK_SENSITIVE_FILE");
100
+ });
101
+
102
+ it("keeps bounded JSON output valid", () => {
103
+ const output = formatPackReport(
104
+ {
105
+ root: "/tmp/pkg",
106
+ ok: true,
107
+ command: ["npm", "pack"],
108
+ files: Array.from({ length: 2_000 }, (_, index) => ({ path: `${"x".repeat(500)}/${index}`, size: 1 })),
109
+ shape: { kind: "conventional", verified: true, evidence: ["extensions"] },
110
+ diagnostics: [],
111
+ truncated: false,
112
+ },
113
+ true,
114
+ );
115
+ expect(output.length).toBeLessThanOrEqual(64 * 1024);
116
+ expect(JSON.parse(output).outputTruncated).toBe(true);
117
+ });
118
+
119
+ it("bounds malformed and failed npm output", async () => {
120
+ const root = fixture({ name: "pi-demo", version: "1.0.0" });
121
+ const failed = await new NpmPackVerifier(async () => ({ code: 1, stdout: "x".repeat(100_000), stderr: "npm failed" })).verify(root);
122
+ expect(failed.ok).toBe(false);
123
+ expect(failed.diagnostics[0]?.code).toBe("PACK_COMMAND_FAILED");
124
+ expect(JSON.stringify(failed).length).toBeLessThan(20_000);
125
+ });
126
+ });
127
+
128
+ describe("adoption readiness evidence", () => {
129
+ it("reports transparent local dimensions without turning traction into quality", async () => {
130
+ const root = fixture(
131
+ {
132
+ name: "pi-demo",
133
+ version: "1.0.0",
134
+ description: "Focused Pi demo extension",
135
+ keywords: ["pi-package", "pi-extension"],
136
+ license: "MIT",
137
+ repository: "https://github.com/example/pi-demo",
138
+ bugs: "https://github.com/example/pi-demo/issues",
139
+ peerDependencies: { "@earendil-works/pi-coding-agent": "*" },
140
+ pi: { extensions: ["extensions/demo.ts"] },
141
+ },
142
+ "# pi-demo\n\nInstall: `pi install npm:pi-demo`\n\n## Usage\nRun `/demo`.\n\n![demo](demo.png)\n",
143
+ );
144
+ const pack = await new NpmPackVerifier(successfulPack).verify(root);
145
+ const report = await assessLocalAdoption(root, pack);
146
+ expect(report.dimensions.discoverability.met).toBeGreaterThan(0);
147
+ expect(report.dimensions.firstRun.status).toBe("ready");
148
+ expect(report.dimensions.traction.status).toBe("unknown");
149
+ expect(report.dimensions.traction.evidence.join(" ")).toContain("not a quality signal");
150
+ // this fixture declares the "*" wildcard Pi's own docs recommend -- carries no real signal
151
+ expect(report.dimensions.compatibility.status).toBe("unknown");
152
+ expect(report.dimensions.compatibility.evidence.join(" ")).toContain("Pi's own recommended convention");
153
+ });
154
+
155
+ it("shows provenance and bounded download observations while leaving trusted publisher unknown", () => {
156
+ const report = assessRegistryAdoption({
157
+ name: "pi-demo",
158
+ version: "1.0.0",
159
+ description: "demo",
160
+ keywords: ["pi-package"],
161
+ license: "MIT",
162
+ repository: "https://github.com/example/pi-demo",
163
+ pi: { extensions: ["extension.ts"] },
164
+ publication: { integrity: "sha512-test", provenanceUrl: "https://registry.example/attestation", trustedPublisher: "unknown" },
165
+ downloads: { weekly: 12, monthly: 34, observedAt: "2026-07-27T00:00:00.000Z" },
166
+ });
167
+ expect(report.dimensions.trust.evidence.join(" ")).toContain("provenance");
168
+ expect(report.dimensions.trust.actions.join(" ")).toContain("trusted publisher");
169
+ expect(report.dimensions.traction.evidence.join(" ")).toContain("12 weekly");
170
+ });
171
+ });
172
+
173
+ describe("trust: lifecycle-install-script detection", () => {
174
+ it("flags declared lifecycle scripts as evidence, without failing the dimension", () => {
175
+ const report = assessRegistryAdoption({
176
+ name: "pi-demo",
177
+ version: "1.0.0",
178
+ license: "MIT",
179
+ repository: "https://github.com/example/pi-demo",
180
+ scripts: { postinstall: "node ./setup.js", test: "bun test" },
181
+ });
182
+ expect(report.dimensions.trust.evidence.join(" ")).toContain("postinstall");
183
+ expect(report.dimensions.trust.evidence.join(" ")).not.toContain("test:");
184
+ });
185
+
186
+ it("reports no lifecycle-script finding when none are declared", () => {
187
+ const report = assessRegistryAdoption({
188
+ name: "pi-demo",
189
+ version: "1.0.0",
190
+ license: "MIT",
191
+ repository: "https://github.com/example/pi-demo",
192
+ scripts: { test: "bun test", build: "bun build" },
193
+ });
194
+ expect(report.dimensions.trust.evidence.join(" ")).not.toContain("lifecycle script");
195
+ });
196
+
197
+ it("assessLocalAdoption reads scripts from the checkout's own package.json", async () => {
198
+ const root = fixture({
199
+ name: "pi-demo",
200
+ version: "1.0.0",
201
+ scripts: { preinstall: "echo hi" },
202
+ });
203
+ const pack = await new NpmPackVerifier(successfulPack).verify(root);
204
+ const report = await assessLocalAdoption(root, pack);
205
+ expect(report.dimensions.trust.evidence.join(" ")).toContain("preinstall");
206
+ });
207
+ });
208
+
209
+ describe("freshness (npm publish-date proxy for commit recency)", () => {
210
+ it("is unknown when neither a real commit date nor a publish date is available for the candidate", () => {
211
+ const report = assessRegistryAdoption({ name: "pi-demo", version: "1.0.0" }, undefined, "2026-07-25T12:47:12.883Z");
212
+ expect(report.dimensions.freshness).toEqual({
213
+ status: "unknown",
214
+ met: 0,
215
+ total: 0,
216
+ evidence: ["candidate's commit history and npm publish date are both unavailable"],
217
+ actions: [],
218
+ });
219
+ });
220
+
221
+ it("is unknown when pi-coding-agent's own publish date is unavailable", () => {
222
+ const report = assessRegistryAdoption(
223
+ { name: "pi-demo", version: "1.0.0", modified: "2026-06-01T00:00:00.000Z" },
224
+ undefined,
225
+ undefined,
226
+ );
227
+ expect(report.dimensions.freshness).toEqual({
228
+ status: "unknown",
229
+ met: 0,
230
+ total: 0,
231
+ evidence: ["pi-coding-agent's own latest npm publish date is unavailable"],
232
+ actions: [],
233
+ });
234
+ });
235
+
236
+ it("reports a signed day-delta as observational evidence, never a pass/fail gate", () => {
237
+ const report = assessRegistryAdoption(
238
+ { name: "pi-demo", version: "1.0.0", modified: "2026-06-01T00:00:00.000Z" },
239
+ undefined,
240
+ "2026-07-25T00:00:00.000Z",
241
+ );
242
+ expect(report.dimensions.freshness.status).toBe("observed");
243
+ expect(report.dimensions.freshness.met).toBe(0);
244
+ expect(report.dimensions.freshness.total).toBe(0);
245
+ expect(report.dimensions.freshness.evidence.join(" ")).toContain("54d before pi-coding-agent's latest publish");
246
+ });
247
+
248
+ it("reports the candidate publishing after pi-coding-agent's latest, not just before", () => {
249
+ const report = assessRegistryAdoption(
250
+ { name: "pi-demo", version: "1.0.0", modified: "2026-08-01T00:00:00.000Z" },
251
+ undefined,
252
+ "2026-07-25T00:00:00.000Z",
253
+ );
254
+ expect(report.dimensions.freshness.evidence.join(" ")).toContain("7d after pi-coding-agent's latest publish");
255
+ });
256
+
257
+ it("is unknown for local-checkout scoring when the checkout is not a git repository and pi's publish date is unavailable", async () => {
258
+ const root = fixture({ name: "pi-demo", version: "1.0.0" });
259
+ const pack = await new NpmPackVerifier(successfulPack).verify(root);
260
+ const report = await assessLocalAdoption(root, pack);
261
+ expect(report.dimensions.freshness.status).toBe("unknown");
262
+ });
263
+
264
+ it("scoreTarget resolves both the candidate's and pi-coding-agent's publish dates through the optional modifiedAt port method", async () => {
265
+ const registry = new FakeRegistry(
266
+ { name: "pi-demo", version: "1.0.0" },
267
+ { "pi-demo": "2026-06-01T00:00:00.000Z", "@earendil-works/pi-coding-agent": "2026-07-25T00:00:00.000Z" },
268
+ );
269
+ const report = await scoreTarget(
270
+ "pi-demo",
271
+ registry,
272
+ new NpmPackVerifier(successfulPack),
273
+ async () => undefined,
274
+ async () => undefined,
275
+ );
276
+ expect(report.dimensions.freshness.status).toBe("observed");
277
+ expect(report.dimensions.freshness.evidence.join(" ")).toContain("54d before");
278
+ expect(report.dimensions.freshness.evidence.join(" ")).toContain("npm publish date (commit history unavailable");
279
+ });
280
+
281
+ it("scoreTarget leaves freshness unknown when the registry has no modifiedAt method at all", async () => {
282
+ class NoModifiedAtRegistry implements Registry {
283
+ async search(): Promise<SearchPage> {
284
+ return { results: [], total: 0 };
285
+ }
286
+ async searchPage(): Promise<SearchPage> {
287
+ return { results: [], total: 0 };
288
+ }
289
+ async searchAll() {
290
+ return [];
291
+ }
292
+ async info(): Promise<PkgInfo> {
293
+ return { name: "pi-demo", version: "1.0.0" };
294
+ }
295
+ }
296
+ const report = await scoreTarget(
297
+ "pi-demo",
298
+ new NoModifiedAtRegistry(),
299
+ new NpmPackVerifier(successfulPack),
300
+ async () => undefined,
301
+ async () => undefined,
302
+ );
303
+ expect(report.dimensions.freshness.status).toBe("unknown");
304
+ });
305
+ });
306
+
307
+ describe("freshness: real commit-date source (preferred over the npm publish-date proxy)", () => {
308
+ it("prefers a real commit date over the publish-date proxy when both are available", () => {
309
+ const report = assessRegistryAdoption(
310
+ { name: "pi-demo", version: "1.0.0", modified: "2026-05-01T00:00:00.000Z" },
311
+ undefined,
312
+ "2026-07-25T00:00:00.000Z",
313
+ "2026-07-20T00:00:00.000Z",
314
+ );
315
+ expect(report.dimensions.freshness.evidence.join(" ")).toContain("last real commit: 2026-07-20");
316
+ expect(report.dimensions.freshness.evidence.join(" ")).not.toContain("2026-05-01");
317
+ });
318
+
319
+ it("assessLocalAdoption reads the checkout's real last-commit date via git log, scoped to no network at all", async () => {
320
+ const root = fixture({ name: "pi-demo", version: "1.0.0" });
321
+ await runBounded(["git", "-C", root, "init", "--quiet"]);
322
+ await runBounded(["git", "-C", root, "config", "user.email", "test@example.com"]);
323
+ await runBounded(["git", "-C", root, "config", "user.name", "Test"]);
324
+ await runBounded(["git", "-C", root, "add", "."]);
325
+ await runBounded(["git", "-C", root, "commit", "--quiet", "-m", "init", "--date", "2026-07-20T00:00:00+00:00"]);
326
+ const pack = await new NpmPackVerifier(successfulPack).verify(root);
327
+ const report = await assessLocalAdoption(root, pack, undefined, "2026-07-25T00:00:00.000Z");
328
+ expect(report.dimensions.freshness.status).toBe("observed");
329
+ expect(report.dimensions.freshness.evidence.join(" ")).toContain("last real commit");
330
+ });
331
+
332
+ it("assessLocalAdoption falls back to unknown, never a guess, for a checkout that is not a git repository", async () => {
333
+ const root = fixture({ name: "pi-demo", version: "1.0.0" });
334
+ const pack = await new NpmPackVerifier(successfulPack).verify(root);
335
+ const report = await assessLocalAdoption(root, pack, undefined, "2026-07-25T00:00:00.000Z");
336
+ expect(report.dimensions.freshness.status).toBe("unknown");
337
+ expect(report.dimensions.freshness.evidence.join(" ")).toContain("both unavailable");
338
+ });
339
+
340
+ it("scoreTarget resolves the candidate's real commit date from GitHub's Commits API, preferring it over the publish-date proxy", async () => {
341
+ await using server = Bun.serve({
342
+ port: 0,
343
+ fetch: (req) => {
344
+ expect(new URL(req.url).pathname).toBe("/repos/example/pi-demo/commits");
345
+ return Response.json([{ commit: { committer: { date: "2026-07-20T00:00:00.000Z" } } }]);
346
+ },
347
+ });
348
+ const fetchGithubCommit = createGithubLastCommitAt(`http://127.0.0.1:${server.port}`);
349
+ const registry = new FakeRegistry(
350
+ { name: "pi-demo", version: "1.0.0", repository: "https://github.com/example/pi-demo", modified: "2026-05-01T00:00:00.000Z" },
351
+ { "@earendil-works/pi-coding-agent": "2026-07-25T00:00:00.000Z" },
352
+ );
353
+ const report = await scoreTarget("pi-demo", registry, new NpmPackVerifier(successfulPack), async () => undefined, fetchGithubCommit);
354
+ expect(report.dimensions.freshness.evidence.join(" ")).toContain("last real commit: 2026-07-20");
355
+ });
356
+
357
+ it("scopes the GitHub Commits API lookup to the package's monorepo subdirectory", async () => {
358
+ await using server = Bun.serve({
359
+ port: 0,
360
+ fetch: (req) => {
361
+ expect(new URL(req.url).searchParams.get("path")).toBe("adapters/pi");
362
+ return Response.json([{ commit: { committer: { date: "2026-07-20T00:00:00.000Z" } } }]);
363
+ },
364
+ });
365
+ const fetchGithubCommit = createGithubLastCommitAt(`http://127.0.0.1:${server.port}`);
366
+ const date = await fetchGithubCommit("https://github.com/example/mono", "adapters/pi");
367
+ expect(date).toBe("2026-07-20T00:00:00.000Z");
368
+ });
369
+
370
+ it("never makes a network call for a non-GitHub repository host (GitLab, Bitbucket, sourcehut)", async () => {
371
+ const neverCalled: FetchGithubLastCommitAt = createGithubLastCommitAt("http://127.0.0.1:1");
372
+ expect(await neverCalled("https://gitlab.com/example/pi-demo")).toBeUndefined();
373
+ expect(await neverCalled(undefined)).toBeUndefined();
374
+ });
375
+
376
+ it("falls back to unknown, never throws, when GitHub responds with a rate-limit-style failure", async () => {
377
+ await using server = Bun.serve({ port: 0, fetch: () => new Response("rate limited", { status: 403 }) });
378
+ const fetchGithubCommit = createGithubLastCommitAt(`http://127.0.0.1:${server.port}`);
379
+ expect(await fetchGithubCommit("https://github.com/example/pi-demo")).toBeUndefined();
380
+ });
381
+
382
+ it("self-throttles: retries a secondary rate limit's short Retry-After and succeeds", async () => {
383
+ let calls = 0;
384
+ await using server = Bun.serve({
385
+ port: 0,
386
+ fetch: () => {
387
+ calls++;
388
+ if (calls === 1) return new Response("secondary rate limited", { status: 403, headers: { "retry-after": "0.05" } });
389
+ return Response.json([{ commit: { committer: { date: "2026-07-20T00:00:00.000Z" } } }]);
390
+ },
391
+ });
392
+ const fetchGithubCommit = createGithubLastCommitAt(`http://127.0.0.1:${server.port}`);
393
+ expect(await fetchGithubCommit("https://github.com/example/pi-demo")).toBe("2026-07-20T00:00:00.000Z");
394
+ expect(calls).toBe(2);
395
+ });
396
+
397
+ it("self-throttles: retries a transient 5xx and succeeds, same shape as a rate limit", async () => {
398
+ let calls = 0;
399
+ await using server = Bun.serve({
400
+ port: 0,
401
+ fetch: () => {
402
+ calls++;
403
+ if (calls === 1) return new Response("server error", { status: 500 });
404
+ return Response.json([{ commit: { committer: { date: "2026-07-20T00:00:00.000Z" } } }]);
405
+ },
406
+ });
407
+ const fetchGithubCommit = createGithubLastCommitAt(`http://127.0.0.1:${server.port}`);
408
+ expect(await fetchGithubCommit("https://github.com/example/pi-demo")).toBe("2026-07-20T00:00:00.000Z");
409
+ expect(calls).toBe(2);
410
+ });
411
+
412
+ it("self-throttles: a primary-limit reset far in the future is treated as immediately exhausted, never blocking anywhere near that long", async () => {
413
+ let calls = 0;
414
+ const farFutureReset = Math.floor(Date.now() / 1000) + 3_600; // an hour away
415
+ await using server = Bun.serve({
416
+ port: 0,
417
+ fetch: () => {
418
+ calls++;
419
+ return new Response("primary rate limit exhausted", {
420
+ status: 403,
421
+ headers: { "x-ratelimit-remaining": "0", "x-ratelimit-reset": String(farFutureReset) },
422
+ });
423
+ },
424
+ });
425
+ const fetchGithubCommit = createGithubLastCommitAt(`http://127.0.0.1:${server.port}`);
426
+ const start = Date.now();
427
+ expect(await fetchGithubCommit("https://github.com/example/pi-demo")).toBeUndefined();
428
+ expect(Date.now() - start).toBeLessThan(2_000); // real proof it never waited toward the hour-away reset
429
+ expect(calls).toBe(1); // gave up on the first attempt, never retried into the budget
430
+ });
431
+
432
+ it("self-throttles: gives up after the bounded attempt count, never retries forever", async () => {
433
+ let calls = 0;
434
+ await using server = Bun.serve({
435
+ port: 0,
436
+ fetch: () => {
437
+ calls++;
438
+ return new Response("server error", { status: 500 });
439
+ },
440
+ });
441
+ const fetchGithubCommit = createGithubLastCommitAt(`http://127.0.0.1:${server.port}`);
442
+ expect(await fetchGithubCommit("https://github.com/example/pi-demo")).toBeUndefined();
443
+ expect(calls).toBe(4); // GITHUB_RETRY_MAX_ATTEMPTS
444
+ }, 15_000); // real exponential backoff across 4 attempts (1+2+4s) exceeds bun's default 5s test timeout
445
+ });
446
+
447
+ describe("compatibility (informal Pi peer-range signal)", () => {
448
+ function withPeerRange(range: string | undefined) {
449
+ return { name: "pi-demo", version: "1.0.0", ...(range ? { peerDependencies: { "@earendil-works/pi-coding-agent": range } } : {}) };
450
+ }
451
+
452
+ it('is unknown (not missing) when undeclared -- matches Pi\'s own recommended "*" convention', () => {
453
+ const report = assessRegistryAdoption(withPeerRange(undefined), "0.82.1");
454
+ expect(report.dimensions.compatibility).toEqual({
455
+ status: "unknown",
456
+ met: 0,
457
+ total: 0,
458
+ evidence: ['no declared Pi peer range (matches Pi\'s own recommended "*" convention)'],
459
+ actions: [],
460
+ });
461
+ });
462
+
463
+ it('treats an explicit "*" identically to undeclared -- a wildcard carries no real signal', () => {
464
+ const report = assessRegistryAdoption(withPeerRange("*"), "0.82.1");
465
+ expect(report.dimensions.compatibility.status).toBe("unknown");
466
+ expect(report.dimensions.compatibility.evidence.join(" ")).toContain("carries no real signal");
467
+ });
468
+
469
+ it("is unknown when a real range is declared but the running Pi version could not be determined", () => {
470
+ const report = assessRegistryAdoption(withPeerRange("^0.75.0"), undefined);
471
+ expect(report.dimensions.compatibility.status).toBe("unknown");
472
+ expect(report.dimensions.compatibility.evidence.join(" ")).toContain("could not be determined");
473
+ expect(report.dimensions.compatibility.actions.join(" ")).toContain("packed pi status");
474
+ });
475
+
476
+ it("is unknown, never a guess, for a range shape satisfiesRange cannot evaluate", () => {
477
+ const report = assessRegistryAdoption(withPeerRange(">=0.75.0"), "0.82.1");
478
+ expect(report.dimensions.compatibility.status).toBe("unknown");
479
+ expect(report.dimensions.compatibility.evidence.join(" ")).toContain("could not be evaluated");
480
+ });
481
+
482
+ it("is ready when the declared range is satisfied by the running Pi version", () => {
483
+ // 0.x caret is patch-only per real npm semver (^0.82.0 excludes 0.83.x) --
484
+ // this fixture stays within that same 0.82.x window on purpose.
485
+ const report = assessRegistryAdoption(withPeerRange("^0.82.0"), "0.82.1");
486
+ expect(report.dimensions.compatibility).toEqual({
487
+ status: "ready",
488
+ met: 1,
489
+ total: 1,
490
+ evidence: ["declared Pi peer range ^0.82.0 is satisfied by the running pi 0.82.1"],
491
+ actions: [],
492
+ });
493
+ });
494
+
495
+ it("is missing (unsatisfied), with a disclaimer that this never blocks install, when the declared range excludes the running Pi version", () => {
496
+ // the real earendil-works/pi#4907 incident: pi-tool-display@0.4.0 declared ^0.75.4
497
+ const report = assessRegistryAdoption(withPeerRange("^0.75.4"), "0.74.2");
498
+ expect(report.dimensions.compatibility.status).toBe("missing");
499
+ expect(report.dimensions.compatibility.met).toBe(0);
500
+ expect(report.dimensions.compatibility.evidence.join(" ")).toContain("NOT satisfied");
501
+ expect(report.dimensions.compatibility.actions.join(" ")).toContain("never blocks install");
502
+ });
503
+
504
+ it("scoreTarget threads an injected current-Pi-version resolver through to the compatibility dimension, never invoking a real subprocess in tests", async () => {
505
+ const registry = new FakeRegistry({
506
+ name: "pi-demo",
507
+ version: "1.0.0",
508
+ peerDependencies: { "@earendil-works/pi-coding-agent": "^0.82.0" },
509
+ });
510
+ const report = await scoreTarget("pi-demo", registry, new NpmPackVerifier(successfulPack), async () => "0.82.1");
511
+ expect(report.dimensions.compatibility.status).toBe("ready");
512
+ });
513
+ });