@danypops/pi-packed 0.19.9 → 0.19.11

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 (87) 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/approval/permission.ts +1 -1
  8. package/extension/src/packed.ts +2 -2
  9. package/extension/src/tabs/security-tui.ts +1 -1
  10. package/extension/src/tool-output.ts +1 -1
  11. package/package.json +31 -8
  12. package/service/schema/pi-setup-v1.schema.json +70 -0
  13. package/service/setup/danypops-ecosystem.pi-setup.json +15 -0
  14. package/service/src/adoption/advisories.ts +268 -0
  15. package/service/src/adoption/check.ts +872 -0
  16. package/service/src/adoption/commit-freshness.ts +167 -0
  17. package/service/src/adoption/doctor.ts +135 -0
  18. package/service/src/adoption/install-validation.ts +187 -0
  19. package/service/src/adoption/pack.ts +291 -0
  20. package/service/src/adoption/score.ts +466 -0
  21. package/service/src/adoption/smoke-child.ts +113 -0
  22. package/service/src/adoption/smoke.ts +282 -0
  23. package/service/src/cli/cli.ts +926 -0
  24. package/service/src/daemon/cleanup.ts +76 -0
  25. package/service/src/daemon/client.ts +412 -0
  26. package/service/src/daemon/daemon-service.ts +249 -0
  27. package/service/src/daemon/daemon.ts +110 -0
  28. package/service/src/daemon/service.ts +664 -0
  29. package/service/src/daemon/watcher.ts +92 -0
  30. package/service/src/index/build-index.ts +256 -0
  31. package/service/src/packages/catalog.ts +61 -0
  32. package/service/src/packages/db.ts +224 -0
  33. package/service/src/packages/install.ts +60 -0
  34. package/service/src/packages/installed.ts +123 -0
  35. package/service/src/packages/package.ts +141 -0
  36. package/service/src/packages/resources.ts +203 -0
  37. package/service/src/pi/pi-version.ts +171 -0
  38. package/service/src/public/atomic-json.ts +32 -0
  39. package/service/src/public/client.ts +277 -0
  40. package/service/src/public/protocol.ts +169 -0
  41. package/service/src/publish/publish.ts +855 -0
  42. package/service/src/registry/registry.ts +246 -0
  43. package/service/src/security/security.ts +128 -0
  44. package/service/src/self-update/self-update.ts +148 -0
  45. package/service/src/setup/setup.ts +761 -0
  46. package/service/src/shared/atomic-json.ts +33 -0
  47. package/service/src/shared/cache.ts +21 -0
  48. package/service/src/shared/constants.ts +73 -0
  49. package/service/src/shared/log.ts +21 -0
  50. package/service/src/shared/paths.ts +88 -0
  51. package/service/src/shared/state.ts +15 -0
  52. package/service/src/shared/version.ts +46 -0
  53. package/service/test/advisories.test.ts +287 -0
  54. package/service/test/check.test.ts +368 -0
  55. package/service/test/cleanup.test.ts +220 -0
  56. package/service/test/cli.test.ts +1303 -0
  57. package/service/test/core.test.ts +181 -0
  58. package/service/test/daemon-kit-migration.test.ts +181 -0
  59. package/service/test/daemon-service.test.ts +238 -0
  60. package/service/test/db.test.ts +178 -0
  61. package/service/test/doctor.test.ts +234 -0
  62. package/service/test/domain.test.ts +291 -0
  63. package/service/test/fixtures/install-validation/broken-package/extension/index.ts +3 -0
  64. package/service/test/fixtures/install-validation/broken-package/package.json +8 -0
  65. package/service/test/fixtures/install-validation/healthy-package/extension/index.ts +3 -0
  66. package/service/test/fixtures/install-validation/healthy-package/package.json +8 -0
  67. package/service/test/fixtures/install-validation/no-manifest-package/package.json +5 -0
  68. package/service/test/index.test.ts +353 -0
  69. package/service/test/install-validation.test.ts +114 -0
  70. package/service/test/install.test.ts +113 -0
  71. package/service/test/log.test.ts +42 -0
  72. package/service/test/pack-score.test.ts +513 -0
  73. package/service/test/pi-version.test.ts +318 -0
  74. package/service/test/public-boundary.test.ts +54 -0
  75. package/service/test/public-client.test.ts +127 -0
  76. package/service/test/public-consumer.ts +8 -0
  77. package/service/test/publish.test.ts +333 -0
  78. package/service/test/registry-contract.test.ts +148 -0
  79. package/service/test/resources.test.ts +255 -0
  80. package/service/test/security.test.ts +89 -0
  81. package/service/test/self-update.test.ts +257 -0
  82. package/service/test/service.test.ts +555 -0
  83. package/service/test/setup.test.ts +375 -0
  84. package/service/test/smoke.test.ts +118 -0
  85. package/service/test/version.test.ts +37 -0
  86. package/service/tsconfig.consumer.json +13 -0
  87. package/service/tsconfig.public.json +12 -0
@@ -0,0 +1,1303 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from "bun:test";
2
+ import { spawnSync } from "node:child_process";
3
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join, resolve } from "node:path";
6
+ import { writeDaemonHandle } from "@danypops/vehicle-server/paths";
7
+ import type { Server } from "bun";
8
+ import { type CliDeps, cliRun } from "../src/cli/cli.ts";
9
+ import {
10
+ DaemonRegistry,
11
+ PackageDaemonClient,
12
+ PackageDaemonInstaller,
13
+ type PackageDaemonPort,
14
+ probe,
15
+ resolveRegistry,
16
+ } from "../src/daemon/client.ts";
17
+ import { createApp } from "../src/daemon/service.ts";
18
+ import { catalogList, dbPath, openDb, replaceAll } from "../src/packages/db.ts";
19
+ import type { Installer, Pkg, PkgInfo, Registry, SearchPage, UpdateOutcome } from "../src/packages/package.ts";
20
+ import { HttpRegistry } from "../src/registry/registry.ts";
21
+ import { type PackedPaths, resolvePackedPaths } from "../src/shared/paths.ts";
22
+ import { VERSION } from "../src/shared/version.ts";
23
+
24
+ class FakeRegistry implements Registry {
25
+ constructor(
26
+ private results: Pkg[] = [],
27
+ private versions: Record<string, string> = {},
28
+ ) {}
29
+ async search(_q: string, _limit: number): Promise<SearchPage> {
30
+ return { results: this.results, total: this.results.length };
31
+ }
32
+ async searchPage(): Promise<SearchPage> {
33
+ return { results: this.results, total: this.results.length };
34
+ }
35
+ async searchAll(): Promise<import("../src/packages/package.ts").Pkg[]> {
36
+ return this.results;
37
+ }
38
+ async info(name: string): Promise<PkgInfo> {
39
+ return { name, version: this.versions[name] ?? "1.0.0", description: "desc" };
40
+ }
41
+ }
42
+
43
+ class FakeInstaller implements Installer {
44
+ gotSource = "";
45
+ removed = "";
46
+ updated = "";
47
+ fail = false;
48
+ approved = false;
49
+ /** Override per-test to exercise the alreadyUpToDate/pinned reporting paths. */
50
+ updateOutcome: Partial<UpdateOutcome> = {};
51
+ async install(source: string, options?: { approved?: boolean }): Promise<string> {
52
+ this.gotSource = source;
53
+ this.approved = options?.approved === true;
54
+ if (this.fail) throw new Error("installer failed");
55
+ return `Installed ${source}`;
56
+ }
57
+ async remove(source: string, options?: { approved?: boolean }): Promise<string> {
58
+ this.removed = source;
59
+ this.approved = options?.approved === true;
60
+ return `Removed ${source}`;
61
+ }
62
+ async update(source: string, options?: { approved?: boolean }): Promise<UpdateOutcome> {
63
+ this.updated = source;
64
+ this.approved = options?.approved === true;
65
+ return {
66
+ output: `Updated ${source}`,
67
+ reloadRequired: true,
68
+ alreadyUpToDate: false,
69
+ pinned: false,
70
+ ...this.updateOutcome,
71
+ };
72
+ }
73
+ }
74
+
75
+ class FakeDaemonServiceInstaller {
76
+ gotSource = "";
77
+ approved = false;
78
+ fail = false;
79
+ restartGotSource = "";
80
+ restartApproved = false;
81
+ restartFail = false;
82
+ async install(
83
+ source: string,
84
+ approved?: boolean,
85
+ ): Promise<{ output: string; spec?: { name: string; binPath: string; descriptorPath: string } }> {
86
+ this.gotSource = source;
87
+ this.approved = approved === true;
88
+ if (this.fail) throw new Error("install-service failed");
89
+ return {
90
+ output: `installed a persistent service for ${source}`,
91
+ spec: { name: "probe", binPath: "/opt/probe/cli.js", descriptorPath: "/tmp/probe.service" },
92
+ };
93
+ }
94
+ async restart(
95
+ source: string,
96
+ approved?: boolean,
97
+ ): Promise<{ output: string; restarted?: boolean; spec?: { name: string; binPath: string; descriptorPath: string } }> {
98
+ this.restartGotSource = source;
99
+ this.restartApproved = approved === true;
100
+ if (this.restartFail) throw new Error("restart-service failed");
101
+ return {
102
+ output: `restarted the persistent service for ${source}`,
103
+ restarted: true,
104
+ spec: { name: "probe", binPath: "/opt/probe/cli.js", descriptorPath: "/tmp/probe.service" },
105
+ };
106
+ }
107
+ }
108
+
109
+ function deps(over: Partial<CliDeps> = {}): CliDeps {
110
+ return {
111
+ reg: new FakeRegistry(),
112
+ inst: new FakeInstaller(),
113
+ daemonService: new FakeDaemonServiceInstaller(),
114
+ security: {
115
+ async security() {
116
+ return { mutationApproval: "always" as const };
117
+ },
118
+ async setMutationApproval(mutationApproval) {
119
+ return { mutationApproval };
120
+ },
121
+ },
122
+ stateDir: mkdtempSync(join(tmpdir(), "packed-")),
123
+ piHome: mkdtempSync(join(tmpdir(), "packed-pihome-")),
124
+ ...over,
125
+ };
126
+ }
127
+
128
+ describe("CLI", () => {
129
+ it("publishes an executable packed binary", () => {
130
+ const manifest = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")) as { bin?: Record<string, string> };
131
+ expect(manifest.bin).toEqual({ packed: "service/src/cli/cli.ts" });
132
+ expect(readFileSync(new URL("../src/cli/cli.ts", import.meta.url), "utf8").startsWith("#!/usr/bin/env bun\n")).toBe(true);
133
+ });
134
+
135
+ it("security reads and writes stable JSON through the daemon port", async () => {
136
+ let mutationApproval: "always" | "never" = "always";
137
+ const d = deps({
138
+ security: {
139
+ async security() {
140
+ return { mutationApproval };
141
+ },
142
+ async setMutationApproval(value, options) {
143
+ expect(options?.approved).toBe(true);
144
+ mutationApproval = value;
145
+ return { mutationApproval };
146
+ },
147
+ },
148
+ });
149
+ expect((await cliRun(["security", "--json"], d)).out).toBe('{"mutationApproval":"always"}\n');
150
+ expect((await cliRun(["security", "never", "--approve", "--json"], d)).out).toBe('{"mutationApproval":"never"}\n');
151
+ });
152
+
153
+ it("runs static check standalone without consulting daemon security state", async () => {
154
+ const d = deps({
155
+ security: {
156
+ async security(): Promise<never> {
157
+ throw new Error("daemon unavailable");
158
+ },
159
+ async setMutationApproval(): Promise<never> {
160
+ throw new Error("daemon unavailable");
161
+ },
162
+ },
163
+ });
164
+ const root = new URL("fixtures/install-validation/no-manifest-package", import.meta.url).pathname;
165
+ const result = await cliRun(["check", root, "--json"], d);
166
+ expect(result.code).toBe(1);
167
+ expect(JSON.parse(result.out).diagnostics.some((item: { code: string }) => item.code === "PI_NO_RESOURCES")).toBe(true);
168
+ expect(JSON.parse(result.out).root).toBe(root.replace(/\/$/, ""));
169
+ });
170
+
171
+ it("packs and scores standalone through bounded application ports", async () => {
172
+ const empty = { status: "unknown" as const, met: 0, total: 0, evidence: [], actions: [] };
173
+ const d = deps({
174
+ packer: {
175
+ async verify(path) {
176
+ return {
177
+ root: path,
178
+ ok: true,
179
+ command: ["npm", "pack", "--dry-run", "--json", "--ignore-scripts"],
180
+ files: [],
181
+ shape: { kind: "manifest", verified: true, evidence: ["pi.extensions"] },
182
+ diagnostics: [],
183
+ truncated: false,
184
+ };
185
+ },
186
+ },
187
+ scorer: {
188
+ async score(target) {
189
+ return {
190
+ target,
191
+ source: "registry" as const,
192
+ package: { name: target, version: "1" },
193
+ dimensions: {
194
+ discoverability: empty,
195
+ firstRun: empty,
196
+ trust: empty,
197
+ maintenance: empty,
198
+ traction: empty,
199
+ compatibility: empty,
200
+ freshness: empty,
201
+ },
202
+ };
203
+ },
204
+ },
205
+ });
206
+ expect(JSON.parse((await cliRun(["pack", ".", "--json"], d)).out).shape.verified).toBe(true);
207
+ expect(JSON.parse((await cliRun(["score", "pi-demo", "--json"], d)).out).target).toBe("pi-demo");
208
+ });
209
+
210
+ it("generates and checks staged-publish workflows without agent-callable publishing", async () => {
211
+ const calls: string[] = [];
212
+ const d = deps({
213
+ publisher: {
214
+ async setup(path, options) {
215
+ calls.push(`setup:${path}:${options?.force}`);
216
+ return {
217
+ root: path,
218
+ ok: true,
219
+ wrote: true,
220
+ workflowPath: `${path}/.github/workflows/packed-stage-publish.yml`,
221
+ packageName: "pi-demo",
222
+ repository: "example/pi-demo",
223
+ diagnostics: [],
224
+ };
225
+ },
226
+ async status(path) {
227
+ calls.push(`status:${path}`);
228
+ return {
229
+ root: path,
230
+ ready: true,
231
+ workflowPath: `${path}/.github/workflows/packed-stage-publish.yml`,
232
+ checks: {
233
+ packageExists: true,
234
+ repository: true,
235
+ workflow: true,
236
+ lockfile: true,
237
+ node: true,
238
+ npm: true,
239
+ trustedPublisher: "unknown",
240
+ coreFirst: true,
241
+ loggedIn: true,
242
+ },
243
+ diagnostics: [],
244
+ nextSteps: [],
245
+ };
246
+ },
247
+ },
248
+ });
249
+ const root = resolve(".");
250
+ expect(JSON.parse((await cliRun(["publish", "setup", ".", "--force", "--json"], d)).out).wrote).toBe(true);
251
+ expect(JSON.parse((await cliRun(["publish", "status", ".", "--json"], d)).out).ready).toBe(true);
252
+ expect(calls).toEqual([`setup:${root}:true`, `status:${root}`]);
253
+ expect((await cliRun(["publish", "approve"], d)).code).toBe(2);
254
+ });
255
+
256
+ it("exports, plans, and approval-gates setup apply standalone", async () => {
257
+ const calls: string[] = [];
258
+ const d = deps({
259
+ setup: {
260
+ async export(projectRoot, options) {
261
+ calls.push(`export:${projectRoot}:${options?.force}`);
262
+ return {
263
+ ok: true,
264
+ path: `${projectRoot}/pi-setup.json`,
265
+ manifest: { $schema: "./schema/pi-setup-v1.schema.json", schemaVersion: 1, packages: [], profiles: {} },
266
+ diagnostics: [],
267
+ wrote: true,
268
+ };
269
+ },
270
+ async update(manifestPath) {
271
+ calls.push(`update:${manifestPath}`);
272
+ return {
273
+ ok: true,
274
+ path: manifestPath,
275
+ manifest: { $schema: "./schema/pi-setup-v1.schema.json", schemaVersion: 1, packages: [], profiles: {} },
276
+ diagnostics: [],
277
+ wrote: true,
278
+ updated: 0,
279
+ };
280
+ },
281
+ async plan(manifestPath) {
282
+ calls.push(`plan:${manifestPath}`);
283
+ return { ok: true, manifestPath, operations: [], diagnostics: [] };
284
+ },
285
+ async apply(manifestPath) {
286
+ calls.push(`apply:${manifestPath}`);
287
+ return { ok: true, manifestPath, operations: [], reloadRequired: false, diagnostics: [] };
288
+ },
289
+ },
290
+ });
291
+ const root = resolve(".");
292
+ expect((await cliRun(["setup", "export", ".", "--force", "--json"], d)).code).toBe(0);
293
+ expect((await cliRun(["setup", "update", ".", "--json"], d)).code).toBe(0);
294
+ expect((await cliRun(["setup", "plan", ".", "--json"], d)).code).toBe(0);
295
+ expect((await cliRun(["setup", "apply", ".", "--json"], d)).code).toBe(1);
296
+ expect((await cliRun(["setup", "apply", ".", "--approve", "--json"], d)).code).toBe(0);
297
+ expect(calls).toEqual([`export:${root}:true`, `update:${root}`, `plan:${root}`, `apply:${root}`]);
298
+ });
299
+
300
+ it("setup plan/apply --ecosystem resolves the bundled @danypops starter manifest, not the cwd", async () => {
301
+ const calls: string[] = [];
302
+ const d = deps({
303
+ setup: {
304
+ async export() {
305
+ throw new Error("not exercised");
306
+ },
307
+ async update() {
308
+ throw new Error("not exercised");
309
+ },
310
+ async plan(manifestPath) {
311
+ calls.push(`plan:${manifestPath}`);
312
+ return { ok: true, manifestPath, operations: [], diagnostics: [] };
313
+ },
314
+ async apply(manifestPath) {
315
+ calls.push(`apply:${manifestPath}`);
316
+ return { ok: true, manifestPath, operations: [], reloadRequired: false, diagnostics: [] };
317
+ },
318
+ },
319
+ });
320
+ expect((await cliRun(["setup", "plan", "--ecosystem", "--json"], d)).code).toBe(0);
321
+ expect((await cliRun(["setup", "apply", "--ecosystem", "--approve", "--json"], d)).code).toBe(0);
322
+ expect(calls[0]).toContain("setup/danypops-ecosystem.pi-setup.json");
323
+ expect(calls[1]).toContain("setup/danypops-ecosystem.pi-setup.json");
324
+ });
325
+
326
+ it("search --json", async () => {
327
+ const d = deps({ reg: new FakeRegistry([{ name: "pi-lsp", version: "0.3.0" }]) });
328
+ const { code, out } = await cliRun(["search", "lsp", "--json"], d);
329
+ expect(code).toBe(0);
330
+ expect(out).toContain('"name":"pi-lsp"');
331
+ });
332
+
333
+ it("search human", async () => {
334
+ const d = deps({ reg: new FakeRegistry([{ name: "pi-lsp", version: "0.3.0", description: "LSP" }]) });
335
+ const { code, out } = await cliRun(["search", "lsp"], d);
336
+ expect(code).toBe(0);
337
+ expect(out).toContain("pi-lsp@0.3.0");
338
+ expect(out).toContain("LSP");
339
+ });
340
+
341
+ it("info --json", async () => {
342
+ const { code, out } = await cliRun(["info", "pi-lsp", "--json"], deps());
343
+ expect(code).toBe(0);
344
+ expect(out).toContain('"name":"pi-lsp"');
345
+ });
346
+
347
+ it("installed --json (string and object settings forms)", async () => {
348
+ const d = deps();
349
+ writeFileSync(
350
+ join(d.piHome, "settings.json"),
351
+ JSON.stringify({ packages: ["npm:pi-extension-manager@0.8.2", { source: "npm:obj@2.0.0" }] }),
352
+ );
353
+ const { code, out } = await cliRun(["installed", "--json"], d);
354
+ expect(code).toBe(0);
355
+ expect(out).toContain('"name":"pi-extension-manager"');
356
+ expect(out).toContain('"pinned":"0.8.2"');
357
+ });
358
+
359
+ it("updates computes drift from the local mirror", async () => {
360
+ const d = deps();
361
+ writeFileSync(join(d.piHome, "settings.json"), JSON.stringify({ packages: ["npm:pi-extension-manager@0.8.2"] }));
362
+ const db = openDb(dbPath(d.stateDir));
363
+ replaceAll(db, [{ name: "pi-extension-manager", version: "0.9.0" }], "test");
364
+ db.close();
365
+ const { code, out } = await cliRun(["updates", "--json"], d);
366
+ expect(code).toBe(0);
367
+ expect(out).toContain('"latest":"0.9.0"');
368
+ });
369
+
370
+ it("updates --project also checks a project's own .pi/settings.json pins -- global-only misses them entirely", async () => {
371
+ const d = deps();
372
+ writeFileSync(join(d.piHome, "settings.json"), JSON.stringify({ packages: ["npm:pi-global@1.0.0"] }));
373
+ const projectRoot = mkdtempSync(join(tmpdir(), "packed-project-"));
374
+ const projectHome = join(projectRoot, ".pi");
375
+ mkdirSync(projectHome, { recursive: true });
376
+ writeFileSync(join(projectHome, "settings.json"), JSON.stringify({ packages: ["npm:papyrus@0.21.2"] }));
377
+ const db = openDb(dbPath(d.stateDir));
378
+ replaceAll(
379
+ db,
380
+ [
381
+ { name: "pi-global", version: "1.0.0" },
382
+ { name: "papyrus", version: "0.38.1" },
383
+ ],
384
+ "test",
385
+ );
386
+ db.close();
387
+
388
+ const globalOnly = await cliRun(["updates", "--json"], d);
389
+ expect(JSON.parse(globalOnly.out).updates).toEqual([]);
390
+
391
+ const withProject = await cliRun(["updates", "--project", projectRoot, "--json"], d);
392
+ const updates = JSON.parse(withProject.out).updates;
393
+ expect(updates).toEqual([
394
+ { name: "papyrus", installed: "0.21.2", latest: "0.38.1", detectedAt: updates[0].detectedAt, scope: "project" },
395
+ ]);
396
+ const human = await cliRun(["updates", "--project", projectRoot], d);
397
+ expect(human.out).toContain("papyrus [project]");
398
+ });
399
+
400
+ it("catalog reads the SQLite mirror", async () => {
401
+ const d = deps();
402
+ const db = openDb(dbPath(d.stateDir));
403
+ replaceAll(db, [{ name: "a", version: "1" }], "test");
404
+ db.close();
405
+ const { code, out } = await cliRun(["catalog", "--json"], d);
406
+ expect(code).toBe(0);
407
+ expect(out).toContain('"name":"a"');
408
+ expect(out).toContain('"sha256"');
409
+ });
410
+
411
+ it("search --offline queries the mirror only", async () => {
412
+ const d = deps();
413
+ const db = openDb(dbPath(d.stateDir));
414
+ replaceAll(db, [{ name: "pi-lsp", version: "1", description: "LSP tools" }], "test");
415
+ db.close();
416
+ const { code, out } = await cliRun(["search", "lsp", "--offline", "--json"], d);
417
+ expect(code).toBe(0);
418
+ expect(out).toContain('"name":"pi-lsp"');
419
+ expect(out).toContain('"offline":true');
420
+ });
421
+
422
+ it("mirror syncs upstream into the local index", async () => {
423
+ const d = deps({ reg: new FakeRegistry([{ name: "pi-lsp", version: "1" }]) });
424
+ const { code, out } = await cliRun(["mirror", "--json"], d);
425
+ expect(code).toBe(0);
426
+ expect(out).toContain('"synced":1');
427
+ const db = openDb(dbPath(d.stateDir));
428
+ expect(catalogList(db)).toHaveLength(1);
429
+ db.close();
430
+ });
431
+
432
+ it("index build generates a local snapshot from the SQLite mirror standalone, and index status reads it back", async () => {
433
+ const d = deps({ reg: new FakeRegistry([], { "pi-lsp": "1.0.0" }) });
434
+ const db = openDb(dbPath(d.stateDir));
435
+ replaceAll(db, [{ name: "pi-lsp", version: "1.0.0" }], "test");
436
+ db.close();
437
+ expect((await cliRun(["index", "status", "--json"], d)).out).toContain("null"); // nothing built yet
438
+ const built = await cliRun(["index", "build", "--json"], d);
439
+ expect(built.code).toBe(0);
440
+ expect(JSON.parse(built.out).packages[0]).toMatchObject({ name: "pi-lsp", version: "1.0.0" });
441
+ const status = await cliRun(["index", "status", "--json"], d);
442
+ expect(JSON.parse(status.out).packages).toHaveLength(1);
443
+ expect((await cliRun(["index", "status"], d)).out).toContain("1 packages");
444
+ expect((await cliRun(["index", "bogus"], d)).code).toBe(2);
445
+ });
446
+
447
+ it("install validates source", async () => {
448
+ const d = deps();
449
+ const { code } = await cliRun(["install", "foo; rm -rf ~"], d);
450
+ expect(code).toBe(2);
451
+ expect((d.inst as FakeInstaller).gotSource).toBe("");
452
+ });
453
+
454
+ it("install runs with stable human and JSON output", async () => {
455
+ const d = deps();
456
+ expect((await cliRun(["install", "npm:foo"], d)).code).toBe(1);
457
+ // --no-service scopes this test to the base install output; service
458
+ // auto-registration composition has its own dedicated tests below.
459
+ const human = await cliRun(["install", "npm:foo", "--approve", "--no-service"], d);
460
+ expect(human.code).toBe(0);
461
+ expect(human.out).toContain("Installed npm:foo");
462
+ expect((d.inst as FakeInstaller).approved).toBe(true);
463
+ const json = await cliRun(["install", "npm:foo", "--approve", "--no-service", "--json"], d);
464
+ expect(json.code).toBe(0);
465
+ expect(JSON.parse(json.out)).toEqual({ ok: true, source: "npm:foo", output: "Installed npm:foo" });
466
+ });
467
+
468
+ it("install auto-registers a detected daemon service under the same approval, silently for a non-daemon package", async () => {
469
+ const d = deps();
470
+ const human = await cliRun(["install", "npm:foo", "--approve"], d);
471
+ expect(human.code).toBe(0);
472
+ expect(human.out).toContain("Installed npm:foo");
473
+ expect(human.out).toContain("installed a persistent service for npm:foo");
474
+ expect((d.daemonService as FakeDaemonServiceInstaller).approved).toBe(true);
475
+ const json = await cliRun(["install", "npm:foo", "--approve", "--json"], d);
476
+ expect(json.code).toBe(0);
477
+ expect(JSON.parse(json.out)).toEqual({
478
+ ok: true,
479
+ source: "npm:foo",
480
+ output: "Installed npm:foo",
481
+ serviceInstall: { detected: true, ok: true, output: "installed a persistent service for npm:foo" },
482
+ });
483
+
484
+ // notADaemon: the overwhelmingly common case (an ordinary, non-daemon package) stays silent.
485
+ const notADaemon = deps();
486
+ (notADaemon.daemonService as FakeDaemonServiceInstaller).install = async () => {
487
+ throw Object.assign(
488
+ new Error("foo does not declare a packed.daemonService manifest and no Vehicle-shaped daemon dependency was detected"),
489
+ { notADaemon: true },
490
+ );
491
+ };
492
+ const silent = await cliRun(["install", "npm:foo", "--approve", "--json"], notADaemon);
493
+ expect(JSON.parse(silent.out)).toEqual({ ok: true, source: "npm:foo", output: "Installed npm:foo" });
494
+
495
+ // A genuine failure (a daemon was detected but registration itself failed) is reported
496
+ // without failing the install, which already succeeded.
497
+ const realFailure = deps();
498
+ (realFailure.daemonService as FakeDaemonServiceInstaller).fail = true;
499
+ const failed = await cliRun(["install", "npm:foo", "--approve", "--json"], realFailure);
500
+ expect(failed.code).toBe(0);
501
+ expect(JSON.parse(failed.out)).toEqual({
502
+ ok: true,
503
+ source: "npm:foo",
504
+ output: "Installed npm:foo",
505
+ serviceInstall: { detected: true, ok: false, output: "install-service failed" },
506
+ });
507
+
508
+ // --no-service skips the attempt entirely -- the daemonService fake is never called.
509
+ const skipped = deps();
510
+ await cliRun(["install", "npm:foo", "--approve", "--no-service"], skipped);
511
+ expect((skipped.daemonService as FakeDaemonServiceInstaller).gotSource).toBe("");
512
+
513
+ // A non-npm source never attempts service detection -- daemon-service resolution only supports npm: today.
514
+ const gitSource = deps();
515
+ await cliRun(["install", "git:github.com/u/r@v1", "--approve"], gitSource);
516
+ expect((gitSource.daemonService as FakeDaemonServiceInstaller).gotSource).toBe("");
517
+ });
518
+
519
+ it("install-service validates source", async () => {
520
+ const d = deps();
521
+ const { code } = await cliRun(["install-service", "foo; rm -rf ~"], d);
522
+ expect(code).toBe(2);
523
+ expect((d.daemonService as FakeDaemonServiceInstaller).gotSource).toBe("");
524
+ });
525
+
526
+ it("install-service requires approval, then runs with stable human and JSON output", async () => {
527
+ const d = deps();
528
+ expect((await cliRun(["install-service", "npm:foo"], d)).code).toBe(1);
529
+ const human = await cliRun(["install-service", "npm:foo", "--approve"], d);
530
+ expect(human.code).toBe(0);
531
+ expect(human.out).toContain("installed a persistent service for npm:foo");
532
+ expect((d.daemonService as FakeDaemonServiceInstaller).approved).toBe(true);
533
+ const json = await cliRun(["install-service", "npm:foo", "--approve", "--json"], d);
534
+ expect(json.code).toBe(0);
535
+ expect(JSON.parse(json.out)).toEqual({
536
+ ok: true,
537
+ source: "npm:foo",
538
+ output: "installed a persistent service for npm:foo",
539
+ spec: { name: "probe", binPath: "/opt/probe/cli.js", descriptorPath: "/tmp/probe.service" },
540
+ });
541
+ });
542
+
543
+ it("install-service reports a resolver/installer failure in-band with exit code 1", async () => {
544
+ const d = deps();
545
+ (d.daemonService as FakeDaemonServiceInstaller).fail = true;
546
+ const { code, out } = await cliRun(["install-service", "npm:foo", "--approve"], d);
547
+ expect(code).toBe(1);
548
+ expect(out).toContain("install-service failed");
549
+ });
550
+
551
+ it("install-service fails closed without a running daemon", async () => {
552
+ const d = deps({ daemonService: undefined });
553
+ const { code, out } = await cliRun(["install-service", "npm:foo", "--approve"], d);
554
+ expect(code).toBe(1);
555
+ expect(out).toContain("requires a running packed daemon");
556
+ });
557
+
558
+ it("update delegates one configured source with stable output and approval", async () => {
559
+ const d = deps();
560
+ expect((await cliRun(["update", "npm:foo"], d)).code).toBe(1);
561
+ // --no-service scopes this test to the base update output; service restart
562
+ // composition has its own dedicated test below, mirroring install's own split.
563
+ const human = await cliRun(["update", "npm:foo", "--approve", "--no-service"], d);
564
+ expect(human.out).toContain("Updated npm:foo");
565
+ expect((d.inst as FakeInstaller).updated).toBe("npm:foo");
566
+ expect((d.inst as FakeInstaller).approved).toBe(true);
567
+ const json = await cliRun(["update", "npm:foo", "--approve", "--no-service", "--json"], d);
568
+ expect(JSON.parse(json.out)).toEqual({
569
+ ok: true,
570
+ source: "npm:foo",
571
+ output: "Updated npm:foo",
572
+ reloadRequired: true,
573
+ alreadyUpToDate: false,
574
+ pinned: false,
575
+ });
576
+ });
577
+
578
+ it("update restarts a registered daemon service after a real change, silently for a non-daemon package", async () => {
579
+ const d = deps();
580
+ const human = await cliRun(["update", "npm:foo", "--approve"], d);
581
+ expect(human.code).toBe(0);
582
+ expect(human.out).toContain("Updated npm:foo");
583
+ expect(human.out).toContain("restarted the persistent service for npm:foo");
584
+ expect((d.daemonService as FakeDaemonServiceInstaller).restartApproved).toBe(true);
585
+ const json = await cliRun(["update", "npm:foo", "--approve", "--json"], d);
586
+ expect(JSON.parse(json.out)).toEqual({
587
+ ok: true,
588
+ source: "npm:foo",
589
+ output: "Updated npm:foo",
590
+ reloadRequired: true,
591
+ alreadyUpToDate: false,
592
+ pinned: false,
593
+ serviceRestart: { detected: true, ok: true, output: "restarted the persistent service for npm:foo" },
594
+ });
595
+
596
+ // notADaemon: the overwhelmingly common case (an ordinary, non-daemon package) stays silent.
597
+ const notADaemon = deps();
598
+ (notADaemon.daemonService as FakeDaemonServiceInstaller).restart = async () => {
599
+ throw Object.assign(
600
+ new Error("foo does not declare a packed.daemonService manifest and no Vehicle-shaped daemon dependency was detected"),
601
+ { notADaemon: true },
602
+ );
603
+ };
604
+ const silent = await cliRun(["update", "npm:foo", "--approve", "--json"], notADaemon);
605
+ expect(JSON.parse(silent.out)).toEqual({
606
+ ok: true,
607
+ source: "npm:foo",
608
+ output: "Updated npm:foo",
609
+ reloadRequired: true,
610
+ alreadyUpToDate: false,
611
+ pinned: false,
612
+ });
613
+
614
+ // A genuine failure (a daemon was detected but restarting it failed) is reported
615
+ // without failing the update, which already succeeded.
616
+ const realFailure = deps();
617
+ (realFailure.daemonService as FakeDaemonServiceInstaller).restartFail = true;
618
+ const failed = await cliRun(["update", "npm:foo", "--approve", "--json"], realFailure);
619
+ expect(failed.code).toBe(0);
620
+ const failedBody = JSON.parse(failed.out);
621
+ expect(failedBody.serviceRestart).toEqual({ detected: true, ok: false, output: "restart-service failed" });
622
+
623
+ // --no-service skips the attempt entirely -- the daemonService fake is never called.
624
+ const skipped = deps();
625
+ await cliRun(["update", "npm:foo", "--approve", "--no-service"], skipped);
626
+ expect((skipped.daemonService as FakeDaemonServiceInstaller).restartGotSource).toBe("");
627
+
628
+ // A non-npm source never attempts service detection -- daemon-service resolution only supports npm: today.
629
+ const gitSource = deps({ inst: new FakeInstaller() });
630
+ await cliRun(["update", "git:github.com/u/r@v1", "--approve"], gitSource);
631
+ expect((gitSource.daemonService as FakeDaemonServiceInstaller).restartGotSource).toBe("");
632
+ });
633
+
634
+ it("update --self requires approval under the guarded default, same as every other mutation", async () => {
635
+ const d = deps({
636
+ selfUpdater: {
637
+ async run() {
638
+ throw new Error("must never run without approval");
639
+ },
640
+ },
641
+ });
642
+ const { code, out } = await cliRun(["update", "--self"], d);
643
+ expect(code).toBe(1);
644
+ expect(out).toContain("approval required");
645
+ });
646
+
647
+ it("update --self delegates to the injected selfUpdater once approved, reporting the version transition", async () => {
648
+ const d = deps({
649
+ selfUpdater: {
650
+ async run() {
651
+ return {
652
+ ok: true,
653
+ previousVersion: "0.1.1",
654
+ latestVersion: "0.2.0",
655
+ updated: true,
656
+ restarted: true,
657
+ message: "updated via npm and restarted the pi-packed service",
658
+ };
659
+ },
660
+ },
661
+ });
662
+ const human = await cliRun(["update", "--self", "--approve"], d);
663
+ expect(human.code).toBe(0);
664
+ expect(human.out).toContain("updated via npm and restarted the pi-packed service");
665
+ expect(human.out).toContain("(0.1.1 \u2192 0.2.0)");
666
+ const json = await cliRun(["update", "--self", "--approve", "--json"], d);
667
+ expect(JSON.parse(json.out)).toEqual({
668
+ ok: true,
669
+ previousVersion: "0.1.1",
670
+ latestVersion: "0.2.0",
671
+ updated: true,
672
+ restarted: true,
673
+ message: "updated via npm and restarted the pi-packed service",
674
+ });
675
+ });
676
+
677
+ it("update --self reports failure with exit code 1 when the selfUpdater itself reports not ok", async () => {
678
+ const d = deps({
679
+ selfUpdater: {
680
+ async run() {
681
+ return {
682
+ ok: false,
683
+ previousVersion: "0.1.1",
684
+ updated: false,
685
+ restarted: false,
686
+ message: "npm install --global @danypops/pi-packed@latest failed (exit 1)",
687
+ };
688
+ },
689
+ },
690
+ });
691
+ const { code, out } = await cliRun(["update", "--self", "--approve"], d);
692
+ expect(code).toBe(1);
693
+ expect(out).toContain("npm install --global");
694
+ });
695
+
696
+ it("update --self fails closed without a running daemon, matching install-service", async () => {
697
+ const d = deps({ selfUpdater: undefined });
698
+ const { code, out } = await cliRun(["update", "--self", "--approve"], d);
699
+ expect(code).toBe(1);
700
+ expect(out).toContain("requires a running packed daemon");
701
+ });
702
+
703
+ it("update reports honestly when pi update is a no-op (pinned or already latest)", async () => {
704
+ const d = deps();
705
+ (d.inst as FakeInstaller).updateOutcome = {
706
+ alreadyUpToDate: true,
707
+ reloadRequired: false,
708
+ pinned: true,
709
+ previousVersion: "1.2.3",
710
+ currentVersion: "1.2.3",
711
+ };
712
+ const human = await cliRun(["update", "npm:foo@1.2.3", "--approve"], d);
713
+ expect(human.out).toContain("pinned to 1.2.3");
714
+ expect(human.out).not.toContain("Reload Pi");
715
+ const json = await cliRun(["update", "npm:foo@1.2.3", "--approve", "--json"], d);
716
+ expect(JSON.parse(json.out)).toEqual({
717
+ ok: true,
718
+ source: "npm:foo@1.2.3",
719
+ output: "Updated npm:foo@1.2.3",
720
+ reloadRequired: false,
721
+ alreadyUpToDate: true,
722
+ pinned: true,
723
+ previousVersion: "1.2.3",
724
+ currentVersion: "1.2.3",
725
+ });
726
+ });
727
+
728
+ it("remove wants a bare name and has stable JSON output", async () => {
729
+ const d = deps();
730
+ expect((await cliRun(["remove", "npm:foo"], d)).code).toBe(2);
731
+ const { code } = await cliRun(["remove", "pi-lsp", "--approve"], d);
732
+ expect(code).toBe(0);
733
+ expect((d.inst as FakeInstaller).removed).toBe("npm:pi-lsp");
734
+ expect((d.inst as FakeInstaller).approved).toBe(true);
735
+ const json = await cliRun(["remove", "pi-lsp", "--approve", "--json"], d);
736
+ expect(JSON.parse(json.out)).toEqual({ ok: true, name: "pi-lsp", output: "Removed npm:pi-lsp" });
737
+ });
738
+
739
+ it("routes daemon-owned catalog reads and refresh through the authenticated client", async () => {
740
+ const calls: string[] = [];
741
+ const daemon: PackageDaemonPort = {
742
+ async search(query, _limit, offline) {
743
+ calls.push(`search:${offline}`);
744
+ return { query, total: 1, results: [{ name: "pi-daemon", version: "1.0.0" }] };
745
+ },
746
+ async info(name) {
747
+ return { name, version: "1.0.0" };
748
+ },
749
+ async installed() {
750
+ calls.push("installed");
751
+ return [{ name: "pi-daemon", pinned: "1.0.0" }];
752
+ },
753
+ async catalog() {
754
+ calls.push("catalog");
755
+ return { fetchedAt: "2026-01-01T00:00:00.000Z", sha256: "a".repeat(64), packages: [{ name: "pi-daemon", version: "1.0.0" }] };
756
+ },
757
+ async mirror() {
758
+ calls.push("mirror");
759
+ return 1;
760
+ },
761
+ async index() {
762
+ calls.push("index");
763
+ return {
764
+ generatedAt: "2026-01-01T00:00:00.000Z",
765
+ packages: [{ name: "pi-daemon", version: "1.0.0", dimensions: {} as never }],
766
+ truncated: false,
767
+ };
768
+ },
769
+ async indexBuild() {
770
+ calls.push("indexBuild");
771
+ return {
772
+ generatedAt: "2026-01-01T00:00:00.000Z",
773
+ packages: [{ name: "pi-daemon", version: "1.0.0", dimensions: {} as never }],
774
+ truncated: false,
775
+ };
776
+ },
777
+ async updates() {
778
+ calls.push("updates");
779
+ return [{ name: "pi-daemon", installed: "1.0.0", latest: "1.1.0", detectedAt: "2026-01-01T00:00:00.000Z" }];
780
+ },
781
+ async check(path, smoke) {
782
+ calls.push(`check:${path}:${smoke}`);
783
+ return { root: path, ok: true, diagnostics: [], summary: { errors: 0, warnings: 0, info: 0 }, checkedFiles: 1, truncated: false };
784
+ },
785
+ async pack(path) {
786
+ calls.push(`pack:${path}`);
787
+ return {
788
+ root: path,
789
+ ok: true,
790
+ command: ["npm", "pack"],
791
+ files: [],
792
+ shape: { kind: "manifest", verified: true, evidence: [] },
793
+ diagnostics: [],
794
+ truncated: false,
795
+ };
796
+ },
797
+ async score(target) {
798
+ calls.push(`score:${target}`);
799
+ return {
800
+ target,
801
+ source: "registry",
802
+ package: { name: target, version: "1" },
803
+ dimensions: {
804
+ discoverability: { status: "ready", met: 1, total: 1, evidence: [], actions: [] },
805
+ firstRun: { status: "unknown", met: 0, total: 0, evidence: [], actions: [] },
806
+ trust: { status: "unknown", met: 0, total: 0, evidence: [], actions: [] },
807
+ maintenance: { status: "unknown", met: 0, total: 0, evidence: [], actions: [] },
808
+ traction: { status: "unknown", met: 0, total: 0, evidence: [], actions: [] },
809
+ compatibility: { status: "unknown", met: 0, total: 0, evidence: [], actions: [] },
810
+ freshness: { status: "unknown", met: 0, total: 0, evidence: [], actions: [] },
811
+ },
812
+ };
813
+ },
814
+ async setupExport(projectRoot, force) {
815
+ calls.push(`setupExport:${projectRoot}:${force}`);
816
+ return {
817
+ ok: true,
818
+ path: `${projectRoot}/pi-setup.json`,
819
+ manifest: { $schema: "./schema/pi-setup-v1.schema.json", schemaVersion: 1, packages: [], profiles: {} },
820
+ diagnostics: [],
821
+ wrote: true,
822
+ };
823
+ },
824
+ async setupUpdate(manifestPath) {
825
+ calls.push(`setupUpdate:${manifestPath}`);
826
+ return {
827
+ ok: true,
828
+ path: manifestPath,
829
+ manifest: { $schema: "./schema/pi-setup-v1.schema.json", schemaVersion: 1, packages: [], profiles: {} },
830
+ diagnostics: [],
831
+ wrote: true,
832
+ updated: 0,
833
+ };
834
+ },
835
+ async setupPlan(manifestPath) {
836
+ calls.push(`setupPlan:${manifestPath}`);
837
+ return { ok: true, manifestPath, operations: [], diagnostics: [] };
838
+ },
839
+ async setupApply(manifestPath, approved) {
840
+ calls.push(`setupApply:${manifestPath}:${approved}`);
841
+ return { ok: true, manifestPath, operations: [], reloadRequired: false, diagnostics: [] };
842
+ },
843
+ async security() {
844
+ return { mutationApproval: "always" };
845
+ },
846
+ async setMutationApproval(mutationApproval) {
847
+ return { mutationApproval };
848
+ },
849
+ async install(source) {
850
+ return source;
851
+ },
852
+ async installService(source) {
853
+ calls.push(`installService:${source}`);
854
+ return { output: source, spec: { name: "probe", binPath: "/opt/probe/cli.js", descriptorPath: "/tmp/probe.service" } };
855
+ },
856
+ async restartService(source) {
857
+ calls.push(`restartService:${source}`);
858
+ return {
859
+ output: source,
860
+ restarted: true,
861
+ spec: { name: "probe", binPath: "/opt/probe/cli.js", descriptorPath: "/tmp/probe.service" },
862
+ };
863
+ },
864
+ async remove(name) {
865
+ return name;
866
+ },
867
+ async update(source) {
868
+ return { output: source, reloadRequired: false, alreadyUpToDate: true, pinned: false };
869
+ },
870
+ async piStatus() {
871
+ calls.push("piStatus");
872
+ return { current: "0.82.1", latest: "0.83.0", upToDate: false };
873
+ },
874
+ async resourcesList(projectRoot) {
875
+ calls.push(`resourcesList:${projectRoot}`);
876
+ return {
877
+ global: [
878
+ {
879
+ source: "npm:pi-daemon",
880
+ name: "pi-daemon",
881
+ scope: "global" as const,
882
+ extensions: [{ path: "extensions/index.ts", enabled: true }],
883
+ skills: [],
884
+ prompts: [],
885
+ themes: [],
886
+ },
887
+ ],
888
+ project: [],
889
+ };
890
+ },
891
+ async resourcesToggle(source, field, path, enabled) {
892
+ calls.push(`resourcesToggle:${source}:${field}:${path}:${enabled}`);
893
+ return `${enabled ? "enabled" : "disabled"} ${path}`;
894
+ },
895
+ async advisoriesScan(name) {
896
+ calls.push(`advisoriesScan:${name}`);
897
+ return { scanned: 1, findings: [], diagnostics: [], truncated: false };
898
+ },
899
+ async doctor(projectRoot) {
900
+ calls.push(`doctor:${projectRoot}`);
901
+ return { ok: true, conflicts: [], extensions: [], scanned: 0, truncated: false };
902
+ },
903
+ async updatesForProject(projectRoot) {
904
+ calls.push(`updatesForProject:${projectRoot}`);
905
+ return [];
906
+ },
907
+ };
908
+ const d = deps({ daemon });
909
+ expect(JSON.parse((await cliRun(["search", "daemon", "--offline", "--json"], d)).out).results[0].name).toBe("pi-daemon");
910
+ expect(JSON.parse((await cliRun(["installed", "--json"], d)).out)[0].name).toBe("pi-daemon");
911
+ expect(JSON.parse((await cliRun(["updates", "--json"], d)).out).updates[0].name).toBe("pi-daemon");
912
+ expect(JSON.parse((await cliRun(["catalog", "--json"], d)).out).packages[0].name).toBe("pi-daemon");
913
+ expect(JSON.parse((await cliRun(["mirror", "--json"], d)).out).synced).toBe(1);
914
+ expect(JSON.parse((await cliRun(["index", "status", "--json"], d)).out).packages[0].name).toBe("pi-daemon");
915
+ expect(JSON.parse((await cliRun(["index", "build", "--json"], d)).out).packages[0].name).toBe("pi-daemon");
916
+ expect(JSON.parse((await cliRun(["check", "/tmp/package", "--smoke", "--json"], d)).out).root).toBe("/tmp/package");
917
+ expect(JSON.parse((await cliRun(["pack", "/tmp/package", "--json"], d)).out).shape.verified).toBe(true);
918
+ expect(JSON.parse((await cliRun(["score", "pi-daemon", "--json"], d)).out).target).toBe("pi-daemon");
919
+ expect((await cliRun(["setup", "export", "/tmp/project", "--force", "--json"], d)).code).toBe(0);
920
+ expect((await cliRun(["setup", "update", "/tmp/project/pi-setup.json", "--json"], d)).code).toBe(0);
921
+ expect((await cliRun(["setup", "plan", "/tmp/project/pi-setup.json", "--json"], d)).code).toBe(0);
922
+ expect((await cliRun(["setup", "apply", "/tmp/project/pi-setup.json", "--approve", "--json"], d)).code).toBe(0);
923
+ expect(JSON.parse((await cliRun(["pi", "status", "--json"], d)).out)).toEqual({ current: "0.82.1", latest: "0.83.0", upToDate: false });
924
+ expect(JSON.parse((await cliRun(["resources", "list", "--json"], d)).out).global[0].name).toBe("pi-daemon");
925
+ expect(
926
+ JSON.parse(
927
+ (await cliRun(["resources", "toggle", "npm:pi-daemon", "extensions", "extensions/index.ts", "off", "--approve", "--json"], d)).out,
928
+ ),
929
+ ).toMatchObject({ ok: true, enabled: false });
930
+ expect(JSON.parse((await cliRun(["advisories", "--json"], d)).out)).toEqual({
931
+ scanned: 1,
932
+ findings: [],
933
+ diagnostics: [],
934
+ truncated: false,
935
+ });
936
+ expect(JSON.parse((await cliRun(["doctor", "--json"], d)).out)).toEqual({
937
+ ok: true,
938
+ conflicts: [],
939
+ extensions: [],
940
+ scanned: 0,
941
+ truncated: false,
942
+ });
943
+ expect(JSON.parse((await cliRun(["updates", "--project", "/tmp/project", "--json"], d)).out).updates).toEqual([]);
944
+ expect(calls).toEqual([
945
+ "search:true",
946
+ "installed",
947
+ "updates",
948
+ "catalog",
949
+ "mirror",
950
+ "index",
951
+ "indexBuild",
952
+ "check:/tmp/package:true",
953
+ "pack:/tmp/package",
954
+ "score:pi-daemon",
955
+ "setupExport:/tmp/project:true",
956
+ "setupUpdate:/tmp/project/pi-setup.json",
957
+ "setupPlan:/tmp/project/pi-setup.json",
958
+ "setupApply:/tmp/project/pi-setup.json:true",
959
+ "piStatus",
960
+ "resourcesList:undefined",
961
+ "resourcesToggle:npm:pi-daemon:extensions:extensions/index.ts:false",
962
+ "advisoriesScan:undefined",
963
+ "doctor:undefined",
964
+ "updatesForProject:/tmp/project",
965
+ ]);
966
+ });
967
+
968
+ it("advisories runs standalone without a daemon and degrades to zero findings, never a real network call, when nothing is installed", async () => {
969
+ const d = deps({ piHome: mkdtempSync(join(tmpdir(), "packed-advisories-cli-")) });
970
+ const result = await cliRun(["advisories", "--json"], d);
971
+ expect(result.code).toBe(0);
972
+ expect(JSON.parse(result.out)).toEqual({ scanned: 0, findings: [], diagnostics: [], truncated: false });
973
+ });
974
+
975
+ it("pi status runs standalone without a daemon, through an injectable check -- never a real subprocess/network call in tests", async () => {
976
+ const d = deps({ piVersion: { check: async () => ({ current: "0.82.1", latest: "0.83.0", upToDate: false }) } });
977
+ const { code, out } = await cliRun(["pi", "status", "--json"], d);
978
+ expect(code).toBe(0);
979
+ expect(JSON.parse(out)).toEqual({ current: "0.82.1", latest: "0.83.0", upToDate: false });
980
+ const human = await cliRun(["pi", "status"], d);
981
+ expect(human.out).toContain("pi 0.82.1 (latest 0.83.0)");
982
+ expect(human.out).toContain("pi update --self");
983
+ expect((await cliRun(["pi", "bogus"], d)).code).toBe(2);
984
+ });
985
+
986
+ it("resources list and toggle run standalone without a daemon (CLI parity for the daemon-only resources.list/toggle operations)", async () => {
987
+ const piHome = mkdtempSync(join(tmpdir(), "packed-resources-cli-"));
988
+ writeFileSync(join(piHome, "settings.json"), JSON.stringify({ packages: ["npm:pi-demo"] }));
989
+ const pkgDir = join(piHome, "npm", "node_modules", "pi-demo");
990
+ mkdirSync(pkgDir, { recursive: true });
991
+ writeFileSync(
992
+ join(pkgDir, "package.json"),
993
+ JSON.stringify({ name: "pi-demo", version: "1.0.0", pi: { extensions: ["extensions/index.ts"] } }),
994
+ );
995
+ mkdirSync(join(pkgDir, "extensions"), { recursive: true });
996
+ writeFileSync(join(pkgDir, "extensions", "index.ts"), "export default function () {}");
997
+ const d = deps({ piHome });
998
+
999
+ const list = await cliRun(["resources", "list", "--json"], d);
1000
+ expect(list.code).toBe(0);
1001
+ const parsed = JSON.parse(list.out);
1002
+ expect(parsed.global[0].extensions).toEqual([{ path: "extensions/index.ts", enabled: true }]);
1003
+ const human = await cliRun(["resources", "list"], d);
1004
+ expect(human.out).toContain("pi-demo");
1005
+ expect(human.out).toContain("extensions/index.ts");
1006
+
1007
+ expect((await cliRun(["resources", "toggle", "npm:pi-demo", "extensions", "extensions/index.ts", "off"], d)).code).toBe(1); // approval required
1008
+ const toggled = await cliRun(
1009
+ ["resources", "toggle", "npm:pi-demo", "extensions", "extensions/index.ts", "off", "--approve", "--json"],
1010
+ d,
1011
+ );
1012
+ expect(JSON.parse(toggled.out)).toMatchObject({ ok: true, enabled: false });
1013
+ const after = JSON.parse((await cliRun(["resources", "list", "--json"], d)).out);
1014
+ expect(after.global[0].extensions).toEqual([{ path: "extensions/index.ts", enabled: false }]);
1015
+
1016
+ expect((await cliRun(["resources", "toggle", "npm:pi-demo", "bogus-field", "x", "on"], d)).code).toBe(2);
1017
+ expect((await cliRun(["resources", "bogus"], d)).code).toBe(2);
1018
+ });
1019
+
1020
+ // Same sandbox-availability probe as smoke.test.ts/doctor.test.ts: binary
1021
+ // presence alone doesn't prove bwrap actually works under this host's
1022
+ // user namespaces.
1023
+ const bwrapUsable =
1024
+ existsSync("/usr/bin/bwrap") &&
1025
+ spawnSync("/usr/bin/bwrap", ["--ro-bind", "/", "/", "--unshare-all", "--", "/bin/true"], { timeout: 5_000 }).status === 0;
1026
+ (bwrapUsable ? it : it.skip)(
1027
+ "doctor runs standalone without a daemon and reproduces the jittor incident through the real CLI (CLI parity for the daemon-only doctor.run operation)",
1028
+ async () => {
1029
+ const piHome = mkdtempSync(join(tmpdir(), "packed-doctor-cli-"));
1030
+ writeFileSync(join(piHome, "settings.json"), JSON.stringify({ packages: ["npm:pi-papyrus"] }));
1031
+ const globalPkg = join(piHome, "npm", "node_modules", "pi-papyrus");
1032
+ mkdirSync(join(globalPkg, "extension"), { recursive: true });
1033
+ writeFileSync(
1034
+ join(globalPkg, "package.json"),
1035
+ JSON.stringify({ name: "pi-papyrus", version: "1.0.0", pi: { extensions: ["extension/index.ts"] } }),
1036
+ );
1037
+ writeFileSync(join(globalPkg, "extension", "index.ts"), 'export default function (pi: any) { pi.registerTool({ name: "tasks" }); }');
1038
+ const projectRoot = mkdtempSync(join(tmpdir(), "packed-doctor-cli-project-"));
1039
+ const projectHome = join(projectRoot, ".pi");
1040
+ mkdirSync(projectHome, { recursive: true });
1041
+ writeFileSync(join(projectHome, "settings.json"), JSON.stringify({ packages: ["npm:papyrus"] }));
1042
+ const projectPkg = join(projectHome, "npm", "node_modules", "papyrus");
1043
+ mkdirSync(join(projectPkg, "extension"), { recursive: true });
1044
+ writeFileSync(
1045
+ join(projectPkg, "package.json"),
1046
+ JSON.stringify({ name: "papyrus", version: "1.0.0", pi: { extensions: ["extension/index.ts"] } }),
1047
+ );
1048
+ writeFileSync(join(projectPkg, "extension", "index.ts"), 'export default function (pi: any) { pi.registerTool({ name: "tasks" }); }');
1049
+ const d = deps({ piHome });
1050
+
1051
+ const clean = await cliRun(["doctor", "--json"], d);
1052
+ expect(clean.code).toBe(0);
1053
+ expect(JSON.parse(clean.out)).toMatchObject({ ok: true, conflicts: [] });
1054
+
1055
+ const withProject = await cliRun(["doctor", "--project", projectRoot, "--json"], d);
1056
+ expect(withProject.code).toBe(1);
1057
+ const report = JSON.parse(withProject.out);
1058
+ expect(report.ok).toBe(false);
1059
+ expect(report.conflicts).toHaveLength(1);
1060
+ expect(report.conflicts[0]).toMatchObject({ kind: "tool", name: "tasks" });
1061
+ const human = await cliRun(["doctor", "--project", projectRoot], d);
1062
+ expect(human.out).toContain('CONFLICT tool "tasks"');
1063
+ },
1064
+ );
1065
+
1066
+ it("version reports just the installed version when no daemon is reachable", async () => {
1067
+ const { code, out } = await cliRun(["version", "--json"], deps({ daemonVersionCheck: async () => undefined }));
1068
+ expect(code).toBe(0);
1069
+ expect(JSON.parse(out)).toEqual({ installed: VERSION });
1070
+ });
1071
+
1072
+ it("version reports a matching daemon as up to date", async () => {
1073
+ const { out } = await cliRun(["version", "--json"], deps({ daemonVersionCheck: async () => VERSION }));
1074
+ expect(JSON.parse(out)).toEqual({ installed: VERSION, daemon: { version: VERSION, stale: false } });
1075
+ });
1076
+
1077
+ it("version flags a stale daemon clearly, with a concrete restart suggestion, human-readable", async () => {
1078
+ const { out } = await cliRun(["version"], deps({ daemonVersionCheck: async () => "0.0.1" }));
1079
+ expect(out).toContain("STALE");
1080
+ expect(out).toContain("0.0.1");
1081
+ expect(out).toContain("systemctl --user restart pi-packed.service");
1082
+ });
1083
+
1084
+ it("unknown command → usage, code 2", async () => {
1085
+ const { code, out } = await cliRun(["frobnicate"], deps());
1086
+ expect(code).toBe(2);
1087
+ expect(out).toContain("usage");
1088
+ });
1089
+ });
1090
+
1091
+ describe("daemon client", () => {
1092
+ let server: Server<undefined>;
1093
+ let daemonDir: string;
1094
+ let daemonPaths: PackedPaths;
1095
+ let daemonInstaller: FakeInstaller;
1096
+ const daemonToken = "d".repeat(64);
1097
+
1098
+ beforeAll(async () => {
1099
+ daemonDir = mkdtempSync(join(tmpdir(), "packed-daemon-"));
1100
+ daemonPaths = resolvePackedPaths({ env: { PI_PACKED_HOME: daemonDir } });
1101
+ writeFileSync(daemonPaths.token, `${daemonToken}\n`);
1102
+ daemonInstaller = new FakeInstaller();
1103
+ const app = createApp({
1104
+ reg: new FakeRegistry([{ name: "pi-lsp", version: "0.3.0" }]),
1105
+ inst: daemonInstaller,
1106
+ daemonServiceInstaller: {
1107
+ install: () => ({
1108
+ ok: true,
1109
+ result: { installed: true },
1110
+ spec: { name: "pi-lsp", binPath: "/opt/pi-lsp/cli.js", descriptorPath: "/tmp/pi-lsp.service" },
1111
+ }),
1112
+ restart: () => ({
1113
+ ok: true,
1114
+ restarted: true,
1115
+ spec: { name: "pi-lsp", binPath: "/opt/pi-lsp/cli.js", descriptorPath: "/tmp/pi-lsp.service" },
1116
+ }),
1117
+ },
1118
+ token: daemonToken,
1119
+ stateDir: daemonDir,
1120
+ piHome: mkdtempSync(join(tmpdir(), "packed-daemon-pi-")),
1121
+ packer: {
1122
+ async verify(path) {
1123
+ return {
1124
+ root: path,
1125
+ ok: true,
1126
+ command: ["npm", "pack"],
1127
+ files: [],
1128
+ shape: { kind: "manifest", verified: true, evidence: ["pi.extensions"] },
1129
+ diagnostics: [],
1130
+ truncated: false,
1131
+ };
1132
+ },
1133
+ },
1134
+ scorer: {
1135
+ async score(target) {
1136
+ const empty = { status: "unknown" as const, met: 0, total: 0, evidence: [], actions: [] };
1137
+ return {
1138
+ target,
1139
+ source: "registry" as const,
1140
+ package: { name: target, version: "1" },
1141
+ dimensions: {
1142
+ discoverability: empty,
1143
+ firstRun: empty,
1144
+ trust: empty,
1145
+ maintenance: empty,
1146
+ traction: empty,
1147
+ compatibility: empty,
1148
+ freshness: empty,
1149
+ },
1150
+ };
1151
+ },
1152
+ },
1153
+ setup: {
1154
+ async export(projectRoot) {
1155
+ return {
1156
+ ok: true,
1157
+ path: `${projectRoot}/pi-setup.json`,
1158
+ manifest: { $schema: "./schema/pi-setup-v1.schema.json", schemaVersion: 1, packages: [], profiles: {} },
1159
+ diagnostics: [],
1160
+ wrote: true,
1161
+ };
1162
+ },
1163
+ async update(manifestPath) {
1164
+ return {
1165
+ ok: true,
1166
+ path: manifestPath,
1167
+ manifest: { $schema: "./schema/pi-setup-v1.schema.json", schemaVersion: 1, packages: [], profiles: {} },
1168
+ diagnostics: [],
1169
+ wrote: true,
1170
+ updated: 0,
1171
+ };
1172
+ },
1173
+ async plan(manifestPath) {
1174
+ return { ok: true, manifestPath, operations: [], diagnostics: [] };
1175
+ },
1176
+ async apply(manifestPath) {
1177
+ return { ok: true, manifestPath, operations: [], reloadRequired: false, diagnostics: [] };
1178
+ },
1179
+ },
1180
+ piVersion: {
1181
+ async check() {
1182
+ return { current: "0.82.1", latest: "0.83.0", upToDate: false };
1183
+ },
1184
+ },
1185
+ });
1186
+ server = Bun.serve({ port: 0, hostname: "127.0.0.1", fetch: (req) => app.fetch(req) });
1187
+ writeDaemonHandle(daemonPaths.handle, { host: "127.0.0.1", port: server.port!, pid: process.pid });
1188
+ });
1189
+ afterAll(() => server.stop(true));
1190
+
1191
+ it("probe finds a live daemon", async () => {
1192
+ const found = await probe(daemonPaths);
1193
+ expect(found).toBeDefined();
1194
+ expect(found?.token).toBe(daemonToken);
1195
+ });
1196
+
1197
+ // Real bug, diagnosed live: a daemon process can run for hours holding
1198
+ // stale in-memory code while its own source/package.json moves on --
1199
+ // ps+git-log correlation was the only way to notice. probe() surfacing
1200
+ // the daemon's own live-reported version (from its real /health
1201
+ // endpoint, not a guess) is the durable, scriptable replacement for that.
1202
+ it("probe reports the live daemon's own real version from its /health endpoint", async () => {
1203
+ const found = await probe(daemonPaths);
1204
+ expect(found?.version).toBe(VERSION); // this test daemon runs the current on-disk code
1205
+ });
1206
+
1207
+ it("probe rejects dead state", async () => {
1208
+ const directory = mkdtempSync(join(tmpdir(), "packed-"));
1209
+ expect(await probe(resolvePackedPaths({ env: { PI_PACKED_HOME: directory } }))).toBeUndefined();
1210
+ });
1211
+
1212
+ it("DaemonRegistry proxies search/info over HTTP with auth", async () => {
1213
+ const found = (await probe(daemonPaths))!;
1214
+ const reg = new DaemonRegistry(found.base, found.token);
1215
+ const { results } = await reg.search("keywords:pi-package lsp", 10);
1216
+ expect(results[0]?.name).toBe("pi-lsp");
1217
+ const info = await reg.info("pi-lsp");
1218
+ expect(info.name).toBe("pi-lsp");
1219
+ });
1220
+
1221
+ it("PackageDaemonClient exposes authenticated install, remove, and package reads", async () => {
1222
+ const found = (await probe(daemonPaths))!;
1223
+ const client = new PackageDaemonClient(found.base, found.token);
1224
+ expect((await client.search("lsp", 10)).results[0]?.name).toBe("pi-lsp");
1225
+ expect((await client.info("pi-lsp")).version).toBe("1.0.0");
1226
+ expect(await client.installed()).toEqual([]);
1227
+ expect((await client.catalog()).packages).toEqual([]);
1228
+ expect(await client.index()).toBeUndefined(); // nothing generated yet
1229
+ const built = await client.indexBuild();
1230
+ expect(built.packages).toEqual([]); // empty catalog in this fixture daemon -- proves the RPC round-trips a real PackageIndex shape
1231
+ expect(typeof built.generatedAt).toBe("string");
1232
+ expect(await client.index()).toEqual(built); // now persisted, readable back through the same RPC
1233
+ expect(await client.updates()).toEqual([]);
1234
+ const checkRoot = new URL("../..", import.meta.url).pathname;
1235
+ expect((await client.check(checkRoot)).root).toBe(checkRoot.replace(/\/$/, ""));
1236
+ expect((await client.check(checkRoot, true)).smoke?.extensions.length).toBeGreaterThan(0);
1237
+ expect((await client.pack(checkRoot)).shape.verified).toBe(true);
1238
+ expect((await client.score("pi-lsp")).target).toBe("pi-lsp");
1239
+ expect((await client.setupExport(checkRoot)).wrote).toBe(true);
1240
+ expect((await client.setupUpdate(`${checkRoot}/pi-setup.json`)).updated).toBe(0);
1241
+ expect((await client.setupPlan(`${checkRoot}/pi-setup.json`)).ok).toBe(true);
1242
+ await expect(client.setupApply(`${checkRoot}/pi-setup.json`)).rejects.toThrow("approval required");
1243
+ expect((await client.setupApply(`${checkRoot}/pi-setup.json`, true)).ok).toBe(true);
1244
+ expect(await client.security()).toEqual({ mutationApproval: "always" });
1245
+ await expect(client.install("npm:pi-lsp")).rejects.toThrow("approval required");
1246
+ expect(await client.install("npm:pi-lsp", true)).toBe("Installed npm:pi-lsp");
1247
+ await expect(client.installService("npm:pi-lsp")).rejects.toThrow("approval required");
1248
+ expect(await client.installService("npm:pi-lsp", true)).toEqual({
1249
+ output: "installed a persistent service for pi-lsp",
1250
+ spec: { name: "pi-lsp", binPath: "/opt/pi-lsp/cli.js", descriptorPath: "/tmp/pi-lsp.service" },
1251
+ });
1252
+ expect(await client.remove("pi-lsp", true)).toBe("Removed npm:pi-lsp");
1253
+ expect(await client.piStatus()).toEqual({ current: "0.82.1", latest: "0.83.0", upToDate: false });
1254
+ expect(await client.update("npm:pi-lsp", true)).toEqual({
1255
+ output: "Updated npm:pi-lsp",
1256
+ reloadRequired: true,
1257
+ alreadyUpToDate: false,
1258
+ pinned: false,
1259
+ previousVersion: undefined,
1260
+ currentVersion: undefined,
1261
+ });
1262
+ const installer = new PackageDaemonInstaller(client);
1263
+ expect(await installer.install("npm:pi-lsp@1.0.0", { approved: true })).toBe("Installed npm:pi-lsp@1.0.0");
1264
+ expect(await installer.remove("npm:pi-lsp", { approved: true })).toBe("Removed npm:pi-lsp");
1265
+ expect((await installer.update("npm:pi-lsp", { approved: true })).output).toBe("Updated npm:pi-lsp");
1266
+ expect(await client.setMutationApproval("never", true)).toEqual({ mutationApproval: "never" });
1267
+ expect(daemonInstaller.gotSource).toBe("npm:pi-lsp@1.0.0");
1268
+ expect(daemonInstaller.removed).toBe("npm:pi-lsp");
1269
+
1270
+ daemonInstaller.fail = true;
1271
+ await expect(client.install("npm:missing")).rejects.toThrow("installer failed");
1272
+ daemonInstaller.fail = false;
1273
+ });
1274
+
1275
+ it("resolveRegistry prefers daemon, falls back direct", async () => {
1276
+ const viaDaemon = await resolveRegistry(daemonPaths, "https://registry.npmjs.org");
1277
+ expect(viaDaemon).toBeInstanceOf(DaemonRegistry);
1278
+ const directory = mkdtempSync(join(tmpdir(), "packed-"));
1279
+ const direct = await resolveRegistry(resolvePackedPaths({ env: { PI_PACKED_HOME: directory } }), "https://registry.npmjs.org");
1280
+ expect(direct).toBeInstanceOf(HttpRegistry);
1281
+ });
1282
+ });
1283
+
1284
+ describe("packed service (systemd unit)", () => {
1285
+ // Delegates to vehicle-server's shared generateSystemdUnit (see cli.ts's renderUnit) --
1286
+ // ExecStart is now shell-quoted per argument, and Restart=on-failure became Restart=always
1287
+ // (vehicle-server's restartOnFailure only supports "always" -- an intentional, documented
1288
+ // tightening: Packed's own client never auto-spawns, so systemd is its only recovery path
1289
+ // regardless of whether the prior exit was clean or a crash).
1290
+ it("renders a user unit with runtime paths and idle disabled", async () => {
1291
+ const d = deps({ execPath: "/usr/bin/bun", cliPath: "/opt/pi-packed/src/cli.ts", piBin: "/home/x/.cache/.bun/bin/pi" });
1292
+ const { code, out } = await cliRun(["service"], d);
1293
+ expect(code).toBe(0);
1294
+ expect(out).toContain("[Service]");
1295
+ expect(out).toContain('ExecStart="/usr/bin/bun" "/opt/pi-packed/src/cli.ts" "serve"');
1296
+ expect(out).toContain("Restart=always");
1297
+ expect(out).toContain("RestartSec=2");
1298
+ expect(out).toContain("NoNewPrivileges=true");
1299
+ expect(out).toContain('Environment="PI_PACKED_IDLE_SECS=0"');
1300
+ expect(out).toContain('Environment="PI_BIN=/home/x/.cache/.bun/bin/pi"');
1301
+ expect(out).toContain("WantedBy=default.target");
1302
+ });
1303
+ });