@danypops/pi-packed 0.19.9 → 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 (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,76 @@
1
+ /**
2
+ * cleanup.ts — the optional pi.cleanup convention: a package may declare a
3
+ * bounded array of its own relative paths that packed remove reports and
4
+ * removes explicitly, before delegating the actual uninstall to pi remove.
5
+ * Addresses a real, documented gap: pi remove fires no uninstall hook
6
+ * (pi-crew's own README documents this directly, as a known limitation
7
+ * with a manual cleanup workaround). A package that declares nothing here
8
+ * changes packed remove's behavior not at all -- readCleanupManifest
9
+ * returns [] for every package that never opts in, and runCleanup is a
10
+ * no-op on an empty declaration.
11
+ */
12
+ import { existsSync, readFileSync, rmSync } from "node:fs";
13
+ import { isAbsolute, join } from "node:path";
14
+ import { isContainedFile } from "../adoption/check.ts";
15
+
16
+ export const MAX_CLEANUP_ENTRIES = 50;
17
+ export const MAX_CLEANUP_PATH_BYTES = 512;
18
+
19
+ export type CleanupStatus = "removed" | "missing" | "escaped" | "failed";
20
+
21
+ export interface CleanupTarget {
22
+ /** exactly as declared in the package's own pi.cleanup array */
23
+ declared: string;
24
+ status: CleanupStatus;
25
+ }
26
+
27
+ /** Reads and bounds a package's own optional pi.cleanup declaration.
28
+ * Missing, malformed, non-array, or absent entirely all resolve to [] --
29
+ * the caller never needs its own separate "did this package opt in?"
30
+ * check. */
31
+ export function readCleanupManifest(installedDir: string): string[] {
32
+ try {
33
+ const pkg = JSON.parse(readFileSync(join(installedDir, "package.json"), "utf8")) as { pi?: { cleanup?: unknown } };
34
+ const declared = pkg.pi?.cleanup;
35
+ if (!Array.isArray(declared)) return [];
36
+ return declared
37
+ .filter((item): item is string => typeof item === "string" && item.trim().length > 0 && item.length <= MAX_CLEANUP_PATH_BYTES)
38
+ .slice(0, MAX_CLEANUP_ENTRIES);
39
+ } catch {
40
+ return [];
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Removes every path a package declares via pi.cleanup, reporting every
46
+ * declared entry's outcome -- never silently, and never partially: a
47
+ * caller always gets one CleanupTarget back per declared string. Fails
48
+ * closed (status "escaped", never deleted) on any path escaping the
49
+ * package's own installed directory, matching check.ts's own
50
+ * symlink/resource-escape discipline exactly: a static isAbsolute/".."
51
+ * check first, then a realpath containment check that also catches a
52
+ * symlink inside the package pointing outside it.
53
+ */
54
+ export function runCleanup(installedDir: string): CleanupTarget[] {
55
+ return readCleanupManifest(installedDir).map((declared): CleanupTarget => {
56
+ const trimmed = declared.trim();
57
+ if (isAbsolute(trimmed) || trimmed.split("/").includes("..")) return { declared, status: "escaped" };
58
+ if (!existsSync(join(installedDir, trimmed))) return { declared, status: "missing" };
59
+ if (!isContainedFile(installedDir, trimmed)) return { declared, status: "escaped" };
60
+ try {
61
+ rmSync(join(installedDir, trimmed), { recursive: true, force: true });
62
+ return { declared, status: "removed" };
63
+ } catch {
64
+ return { declared, status: "failed" };
65
+ }
66
+ });
67
+ }
68
+
69
+ /** Human-readable summary appended to packed remove's own output -- "" when
70
+ * there was nothing declared, so a package that never opted in produces
71
+ * zero visible difference in packed remove's output either. */
72
+ export function formatCleanupSummary(targets: CleanupTarget[]): string {
73
+ if (targets.length === 0) return "";
74
+ const lines = targets.map((t) => ` ${t.status}: ${t.declared}`);
75
+ return `\ncleanup (pi.cleanup):\n${lines.join("\n")}\n`;
76
+ }
@@ -0,0 +1,412 @@
1
+ /** Authenticated clients for Packed's vehicle-server operation registry. */
2
+ import { readFileSync } from "node:fs";
3
+ import { AuthenticatedRpcClient } from "@danypops/vehicle-client/rpc-client";
4
+ import { readDaemonHandle } from "@danypops/vehicle-server/paths";
5
+ import type { AdvisoryReport } from "../adoption/advisories.ts";
6
+ import type { CheckReport } from "../adoption/check.ts";
7
+ import type { DoctorReport } from "../adoption/doctor.ts";
8
+ import type { PackReport } from "../adoption/pack.ts";
9
+ import type { AdoptionReport } from "../adoption/score.ts";
10
+ import type { PackageIndex } from "../index/build-index.ts";
11
+ import type { InstalledPkg, Installer, Pkg, PkgInfo, Registry, SearchPage, UpdateEntry, UpdateOutcome } from "../packages/package.ts";
12
+ import type { PackageResources, ResourceField } from "../packages/resources.ts";
13
+ import type { PiVersionReport } from "../pi/pi-version.ts";
14
+ import { HttpRegistry } from "../registry/registry.ts";
15
+ import type { MutationApproval, SecuritySettings } from "../security/security.ts";
16
+ import type { SetupApplyResult, SetupExportReport, SetupPlan, SetupUpdateReport } from "../setup/setup.ts";
17
+ import {
18
+ INDEX_OPERATION_TIMEOUT_MS,
19
+ MIRROR_OPERATION_TIMEOUT_MS,
20
+ PROBE_TIMEOUT_MS,
21
+ REGISTRY_FETCH_TIMEOUT_MS,
22
+ } from "../shared/constants.ts";
23
+ import { type PackedPaths, resolvePackedPaths } from "../shared/paths.ts";
24
+ import type { OperationInputs, OperationName, OperationOutputs } from "./service.ts";
25
+
26
+ export type FetchTransport = (request: Request) => Promise<Response>;
27
+ type RpcClient = AuthenticatedRpcClient<OperationName, OperationInputs, OperationOutputs>;
28
+
29
+ export interface PackageDaemonPort {
30
+ search(query: string, limit: number, offline?: boolean): Promise<{ query: string; total: number; results: SearchPage["results"] }>;
31
+ info(name: string): Promise<PkgInfo>;
32
+ installed(): Promise<InstalledPkg[]>;
33
+ catalog(): Promise<{ fetchedAt?: string; sha256?: string; packages: Pkg[] }>;
34
+ mirror(): Promise<number>;
35
+ index(): Promise<PackageIndex | undefined>;
36
+ indexBuild(): Promise<PackageIndex>;
37
+ updates(): Promise<UpdateEntry[]>;
38
+ updatesForProject(projectRoot: string): Promise<UpdateEntry[]>;
39
+ check(path: string, smoke?: boolean): Promise<CheckReport>;
40
+ pack(path: string): Promise<PackReport>;
41
+ score(target: string): Promise<AdoptionReport>;
42
+ setupExport(projectRoot: string, force?: boolean, machineLocal?: boolean): Promise<SetupExportReport>;
43
+ setupUpdate(manifestPath: string): Promise<SetupUpdateReport>;
44
+ setupPlan(manifestPath: string, prune?: boolean): Promise<SetupPlan>;
45
+ setupApply(manifestPath: string, approved?: boolean, prune?: boolean): Promise<SetupApplyResult>;
46
+ security(): Promise<SecuritySettings>;
47
+ setMutationApproval(value: MutationApproval, approved?: boolean): Promise<SecuritySettings>;
48
+ install(source: string, approved?: boolean): Promise<string>;
49
+ installService(
50
+ source: string,
51
+ approved?: boolean,
52
+ ): Promise<{ output: string; spec?: { name: string; binPath: string; descriptorPath: string } }>;
53
+ restartService(
54
+ source: string,
55
+ approved?: boolean,
56
+ ): Promise<{ output: string; restarted?: boolean; spec?: { name: string; binPath: string; descriptorPath: string } }>;
57
+ remove(name: string, approved?: boolean): Promise<string>;
58
+ update(source: string, approved?: boolean): Promise<UpdateOutcome>;
59
+ piStatus(): Promise<PiVersionReport>;
60
+ resourcesList(projectRoot?: string): Promise<{ global: PackageResources[]; project: PackageResources[] }>;
61
+ resourcesToggle(
62
+ source: string,
63
+ field: ResourceField,
64
+ path: string,
65
+ enabled: boolean,
66
+ projectRoot?: string,
67
+ approved?: boolean,
68
+ ): Promise<string>;
69
+ advisoriesScan(name?: string): Promise<AdvisoryReport>;
70
+ doctor(projectRoot?: string): Promise<DoctorReport>;
71
+ }
72
+
73
+ export class PackageDaemonError extends Error {
74
+ /** True only for install_service's "this package isn't Vehicle-shaped at all" outcome -- distinct from a real failure (systemctl unavailable, spec resolved but the install itself failed). Lets a caller composing install + install-service (see cli.ts's `install` command) stay silent instead of surfacing a false alarm on every ordinary, non-daemon package install. */
75
+ constructor(
76
+ message: string,
77
+ readonly operation: string,
78
+ readonly status?: number,
79
+ readonly notADaemon?: boolean,
80
+ ) {
81
+ super(message);
82
+ this.name = "PackageDaemonError";
83
+ }
84
+ }
85
+
86
+ function timeoutTransport(transport: FetchTransport, timeoutMs = REGISTRY_FETCH_TIMEOUT_MS): FetchTransport {
87
+ return (request) =>
88
+ transport(
89
+ new Request(request, {
90
+ signal: AbortSignal.any([request.signal, AbortSignal.timeout(timeoutMs)]),
91
+ }),
92
+ );
93
+ }
94
+
95
+ export class PackageDaemonClient implements PackageDaemonPort {
96
+ private readonly rpc: RpcClient;
97
+ private readonly maintenanceRpc: RpcClient;
98
+ private readonly indexRpc: RpcClient;
99
+
100
+ constructor(base: string, token: string, transport: FetchTransport = fetch) {
101
+ this.rpc = new AuthenticatedRpcClient(base, token, { label: "Packed", transport: timeoutTransport(transport) });
102
+ this.maintenanceRpc = new AuthenticatedRpcClient(base, token, {
103
+ label: "Packed",
104
+ transport: timeoutTransport(transport, MIRROR_OPERATION_TIMEOUT_MS),
105
+ });
106
+ this.indexRpc = new AuthenticatedRpcClient(base, token, {
107
+ label: "Packed",
108
+ transport: timeoutTransport(transport, INDEX_OPERATION_TIMEOUT_MS),
109
+ });
110
+ }
111
+
112
+ private async call<Name extends OperationName>(operation: Name, input: OperationInputs[Name]): Promise<OperationOutputs[Name]> {
113
+ try {
114
+ return await this.rpc.call(operation, input);
115
+ } catch (error) {
116
+ throw new PackageDaemonError(error instanceof Error ? error.message : String(error), operation);
117
+ }
118
+ }
119
+
120
+ async search(query: string, limit: number, offline = false): Promise<{ query: string; total: number; results: SearchPage["results"] }> {
121
+ return this.call("package.search", { query, limit, offline });
122
+ }
123
+
124
+ info(name: string): Promise<PkgInfo> {
125
+ return this.call("package.info", { name });
126
+ }
127
+
128
+ installed(): Promise<InstalledPkg[]> {
129
+ return this.call("package.installed", {});
130
+ }
131
+
132
+ catalog(): Promise<{ fetchedAt?: string; sha256?: string; packages: Pkg[] }> {
133
+ return this.call("package.catalog", {});
134
+ }
135
+
136
+ async mirror(): Promise<number> {
137
+ try {
138
+ return (await this.maintenanceRpc.call("package.catalog.sync", {})).synced;
139
+ } catch (error) {
140
+ throw new PackageDaemonError(error instanceof Error ? error.message : String(error), "package.catalog.sync");
141
+ }
142
+ }
143
+
144
+ index(): Promise<PackageIndex | undefined> {
145
+ return this.call("package.index", {});
146
+ }
147
+
148
+ async indexBuild(): Promise<PackageIndex> {
149
+ try {
150
+ return await this.indexRpc.call("package.index.build", {});
151
+ } catch (error) {
152
+ throw new PackageDaemonError(error instanceof Error ? error.message : String(error), "package.index.build");
153
+ }
154
+ }
155
+
156
+ async updates(): Promise<UpdateEntry[]> {
157
+ return (await this.call("package.updates", {})).updates;
158
+ }
159
+
160
+ async updatesForProject(projectRoot: string): Promise<UpdateEntry[]> {
161
+ return (await this.call("package.updates.project", { projectRoot })).updates;
162
+ }
163
+
164
+ piStatus(): Promise<PiVersionReport> {
165
+ return this.call("pi.status", {});
166
+ }
167
+
168
+ resourcesList(projectRoot?: string): Promise<{ global: PackageResources[]; project: PackageResources[] }> {
169
+ return this.call("resources.list", { projectRoot });
170
+ }
171
+
172
+ async resourcesToggle(
173
+ source: string,
174
+ field: ResourceField,
175
+ path: string,
176
+ enabled: boolean,
177
+ projectRoot?: string,
178
+ approved?: boolean,
179
+ ): Promise<string> {
180
+ return (await this.call("resources.toggle", { source, field, path, enabled, projectRoot, approved })).output;
181
+ }
182
+
183
+ advisoriesScan(name?: string): Promise<AdvisoryReport> {
184
+ return this.call("advisories.scan", { name });
185
+ }
186
+
187
+ doctor(projectRoot?: string): Promise<DoctorReport> {
188
+ return this.call("doctor.run", { projectRoot });
189
+ }
190
+
191
+ check(path: string, smoke = false): Promise<CheckReport> {
192
+ return this.call("package.check", { path, smoke });
193
+ }
194
+
195
+ pack(path: string): Promise<PackReport> {
196
+ return this.maintenanceRpc.call("package.pack", { path });
197
+ }
198
+
199
+ score(target: string): Promise<AdoptionReport> {
200
+ return this.maintenanceRpc.call("package.score", { target });
201
+ }
202
+
203
+ setupExport(projectRoot: string, force = false, machineLocal = false): Promise<SetupExportReport> {
204
+ return this.maintenanceRpc.call("setup.export", { projectRoot, force, machineLocal });
205
+ }
206
+
207
+ setupUpdate(manifestPath: string): Promise<SetupUpdateReport> {
208
+ return this.maintenanceRpc.call("setup.update", { manifestPath });
209
+ }
210
+
211
+ setupPlan(manifestPath: string, prune = false): Promise<SetupPlan> {
212
+ return this.maintenanceRpc.call("setup.plan", { manifestPath, prune });
213
+ }
214
+
215
+ setupApply(manifestPath: string, approved = false, prune = false): Promise<SetupApplyResult> {
216
+ return this.maintenanceRpc.call("setup.apply", { manifestPath, approved, prune });
217
+ }
218
+
219
+ security(): Promise<SecuritySettings> {
220
+ return this.call("package.security.get", {});
221
+ }
222
+
223
+ setMutationApproval(mutationApproval: MutationApproval, approved = false): Promise<SecuritySettings> {
224
+ return this.call("package.security.set", { mutationApproval, approved });
225
+ }
226
+
227
+ async install(source: string, approved = false): Promise<string> {
228
+ const result = await this.call("package.install", { source, approved });
229
+ if (!result.ok) throw new PackageDaemonError(result.output || `failed to install ${source}`, "package.install");
230
+ return result.output;
231
+ }
232
+
233
+ async installService(
234
+ source: string,
235
+ approved = false,
236
+ ): Promise<{ output: string; spec?: { name: string; binPath: string; descriptorPath: string } }> {
237
+ const result = await this.call("package.install_service", { source, approved });
238
+ if (!result.ok)
239
+ throw new PackageDaemonError(
240
+ result.output || `failed to install a persistent service for ${source}`,
241
+ "package.install_service",
242
+ undefined,
243
+ result.notADaemon === true,
244
+ );
245
+ return { output: result.output, spec: result.spec };
246
+ }
247
+
248
+ async restartService(
249
+ source: string,
250
+ approved = false,
251
+ ): Promise<{ output: string; restarted?: boolean; spec?: { name: string; binPath: string; descriptorPath: string } }> {
252
+ const result = await this.call("package.restart_service", { source, approved });
253
+ if (!result.ok)
254
+ throw new PackageDaemonError(
255
+ result.output || `failed to restart the persistent service for ${source}`,
256
+ "package.restart_service",
257
+ undefined,
258
+ result.notADaemon === true,
259
+ );
260
+ return { output: result.output, restarted: result.restarted, spec: result.spec };
261
+ }
262
+
263
+ async remove(name: string, approved = false): Promise<string> {
264
+ const result = await this.call("package.remove", { name, approved });
265
+ if (!result.ok) throw new PackageDaemonError(result.output || `failed to remove ${name}`, "package.remove");
266
+ return result.output;
267
+ }
268
+
269
+ async update(source: string, approved = false): Promise<UpdateOutcome> {
270
+ const result = await this.call("package.update", { source, approved });
271
+ if (!result.ok) throw new PackageDaemonError(result.output || `failed to update ${source}`, "package.update");
272
+ return {
273
+ output: result.output,
274
+ reloadRequired: result.reloadRequired ?? true,
275
+ alreadyUpToDate: result.alreadyUpToDate ?? false,
276
+ pinned: result.pinned ?? false,
277
+ previousVersion: result.previousVersion,
278
+ currentVersion: result.currentVersion,
279
+ };
280
+ }
281
+ }
282
+
283
+ export class PackageDaemonInstaller implements Installer {
284
+ constructor(private readonly client: PackageDaemonClient) {}
285
+ install(source: string, options?: { approved?: boolean; local?: boolean }): Promise<string> {
286
+ return this.client.install(source, options?.approved);
287
+ }
288
+ remove(source: string, options?: { approved?: boolean; local?: boolean }): Promise<string> {
289
+ if (!source.startsWith("npm:") || source.length <= 4)
290
+ throw new PackageDaemonError("daemon package removal requires an npm: source", "package.remove");
291
+ return this.client.remove(source.slice(4), options?.approved);
292
+ }
293
+ update(source: string, options?: { approved?: boolean; local?: boolean }): Promise<UpdateOutcome> {
294
+ return this.client.update(source, options?.approved);
295
+ }
296
+ }
297
+
298
+ export class DaemonBackedSecurity {
299
+ constructor(private readonly paths: PackedPaths = resolvePackedPaths()) {}
300
+ async security(): Promise<SecuritySettings> {
301
+ return (await connectPackageDaemon(this.paths)).security();
302
+ }
303
+ async setMutationApproval(value: MutationApproval, options?: { approved?: boolean }): Promise<SecuritySettings> {
304
+ return (await connectPackageDaemon(this.paths)).setMutationApproval(value, options?.approved);
305
+ }
306
+ }
307
+
308
+ export class DaemonBackedInstaller implements Installer {
309
+ constructor(private readonly paths: PackedPaths = resolvePackedPaths()) {}
310
+ async install(source: string, options?: { approved?: boolean }): Promise<string> {
311
+ return (await connectPackageDaemon(this.paths)).install(source, options?.approved);
312
+ }
313
+ async remove(source: string, options?: { approved?: boolean }): Promise<string> {
314
+ return new PackageDaemonInstaller(await connectPackageDaemon(this.paths)).remove(source, options);
315
+ }
316
+ async update(source: string, options?: { approved?: boolean }): Promise<UpdateOutcome> {
317
+ return (await connectPackageDaemon(this.paths)).update(source, options?.approved);
318
+ }
319
+ }
320
+
321
+ export interface DaemonServiceInstallerPort {
322
+ install(
323
+ source: string,
324
+ approved?: boolean,
325
+ ): Promise<{ output: string; spec?: { name: string; binPath: string; descriptorPath: string } }>;
326
+ restart(
327
+ source: string,
328
+ approved?: boolean,
329
+ ): Promise<{ output: string; restarted?: boolean; spec?: { name: string; binPath: string; descriptorPath: string } }>;
330
+ }
331
+
332
+ export class DaemonBackedDaemonServiceInstaller implements DaemonServiceInstallerPort {
333
+ constructor(private readonly paths: PackedPaths = resolvePackedPaths()) {}
334
+ async restart(
335
+ source: string,
336
+ approved?: boolean,
337
+ ): Promise<{ output: string; restarted?: boolean; spec?: { name: string; binPath: string; descriptorPath: string } }> {
338
+ return (await connectPackageDaemon(this.paths)).restartService(source, approved);
339
+ }
340
+ async install(
341
+ source: string,
342
+ approved?: boolean,
343
+ ): Promise<{ output: string; spec?: { name: string; binPath: string; descriptorPath: string } }> {
344
+ return (await connectPackageDaemon(this.paths)).installService(source, approved);
345
+ }
346
+ }
347
+
348
+ export class DaemonRegistry implements Registry {
349
+ private readonly client: PackageDaemonClient;
350
+ constructor(base: string, token: string, transport: FetchTransport = fetch) {
351
+ this.client = new PackageDaemonClient(base, token, transport);
352
+ }
353
+ async search(query: string, limit: number): Promise<SearchPage> {
354
+ const body = await this.client.search(query, limit);
355
+ return { results: body.results, total: body.total };
356
+ }
357
+ async searchPage(query: string, _from: number, size: number): Promise<SearchPage> {
358
+ return this.search(query, size).catch(() => ({ results: [], total: 0 }));
359
+ }
360
+ async searchAll(): Promise<never> {
361
+ throw new Error("searchAll is not supported via the daemon proxy");
362
+ }
363
+ info(name: string): Promise<PkgInfo> {
364
+ return this.client.info(name);
365
+ }
366
+ }
367
+
368
+ /** version is the daemon's own live-reported version (its real /health
369
+ * response, not a guess) -- an auto-spawned or supervised daemon process
370
+ * can outlive the source/package it was started from, silently serving
371
+ * stale code for as long as it stays reachable. Confirmed live: an 11-
372
+ * hour-old daemon kept flagging false "update available" rows because it
373
+ * predated a same-day fix to the exact comparison logic deciding that. */
374
+ export interface DaemonHandle {
375
+ base: string;
376
+ token: string;
377
+ version: string;
378
+ }
379
+
380
+ export async function probe(paths: PackedPaths = resolvePackedPaths()): Promise<DaemonHandle | undefined> {
381
+ const handle = readDaemonHandle(paths.handle);
382
+ if (!handle) return undefined;
383
+ let token: string;
384
+ try {
385
+ token = readFileSync(paths.token, "utf8").trim();
386
+ if (!/^[a-f0-9]{64}$/.test(token)) return undefined;
387
+ } catch {
388
+ return undefined;
389
+ }
390
+ const base = `http://${handle.host}:${handle.port}`;
391
+ try {
392
+ const rpc = new AuthenticatedRpcClient<OperationName, OperationInputs, OperationOutputs>(base, token, {
393
+ label: "Packed",
394
+ transport: timeoutTransport(fetch, PROBE_TIMEOUT_MS),
395
+ });
396
+ const health = await rpc.health();
397
+ return { base, token, version: health.version };
398
+ } catch {
399
+ return undefined;
400
+ }
401
+ }
402
+
403
+ export async function connectPackageDaemon(paths: PackedPaths = resolvePackedPaths()): Promise<PackageDaemonClient> {
404
+ const handle = await probe(paths);
405
+ if (!handle) throw new Error("pi-packed daemon is unavailable; start packed.service");
406
+ return new PackageDaemonClient(handle.base, handle.token);
407
+ }
408
+
409
+ export async function resolveRegistry(paths: PackedPaths, npmBase: string): Promise<Registry> {
410
+ const handle = await probe(paths);
411
+ return handle ? new DaemonRegistry(handle.base, handle.token) : new HttpRegistry(npmBase);
412
+ }