@danypops/pi-packed 0.5.2 → 0.6.0

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.
package/src/catalog.ts DELETED
@@ -1,64 +0,0 @@
1
- /**
2
- * catalog.ts — the sync pipeline (apt update / pkg update analog).
3
- * Paginates the upstream registry into the local SQLite mirror and records
4
- * sync metadata (the Release/repomd checksum role) in sync_meta.
5
- */
6
- import type { Pkg, Registry } from "./ports.ts";
7
- import { PI_PACKAGE_KEYWORD } from "./constants.ts";
8
- import { openDb, replaceAll, getSyncMeta, dbPath } from "./db.ts";
9
- import { createLogger } from "./log.ts";
10
-
11
- const log = createLogger("catalog");
12
- import type { SyncMeta } from "./db.ts";
13
-
14
- export interface CatalogStatus {
15
- stale: boolean;
16
- meta?: SyncMeta;
17
- }
18
-
19
- export function catalogStatus(dir: string, ttlMs: number): CatalogStatus {
20
- const db = openDb(dbPath(dir));
21
- try {
22
- const meta = getSyncMeta(db);
23
- if (!meta) return { stale: true };
24
- return { stale: Date.now() - Date.parse(meta.fetchedAt) > ttlMs, meta };
25
- } finally {
26
- db.close();
27
- }
28
- }
29
-
30
- /** Full mirror sync: upstream pages → atomic SQLite replace. */
31
- export async function syncCatalog(reg: Registry, dir: string, query: string = PI_PACKAGE_KEYWORD): Promise<number> {
32
- const t0 = Date.now();
33
- log.info("mirror sync started", { query });
34
- const packages = await reg.searchAll(query);
35
- const db = openDb(dbPath(dir));
36
- try {
37
- replaceAll(db, packages, "npm:" + query);
38
- log.info("mirror sync complete", { packages: packages.length, ms: Date.now() - t0 });
39
- return packages.length;
40
- } finally {
41
- db.close();
42
- }
43
- }
44
-
45
- export function startCatalogSync(
46
- reg: Registry,
47
- dir: string,
48
- ttlMs: number,
49
- onError?: (e: unknown) => void,
50
- ): () => void {
51
- async function sync(): Promise<void> {
52
- try {
53
- if (catalogStatus(dir, ttlMs).stale) {
54
- const n = await syncCatalog(reg, dir);
55
- log.info("scheduled sync complete", { packages: n });
56
- }
57
- } catch (e) {
58
- onError?.(e);
59
- }
60
- }
61
- void sync();
62
- const timer = setInterval(() => void sync(), ttlMs);
63
- return () => clearInterval(timer);
64
- }
package/src/cli.ts DELETED
@@ -1,374 +0,0 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * cli.ts — second driving adapter (humans drive the same hexagon ports).
4
- * cliRun is pure: ({code, out}) in, no I/O — the entry point prints.
5
- * Command table follows the go-tool/Cobra convention; flags may appear
6
- * anywhere (agents put them anywhere).
7
- */
8
- import { buildSearchQuery, clampLimit } from "./ports.ts";
9
- import type { Installer, Registry } from "./ports.ts";
10
- import { npmPackageName, readInstalledPackages } from "./installed.ts";
11
- import { checkUpdates } from "./watcher.ts";
12
- import { syncCatalog } from "./catalog.ts";
13
- import { openDb, searchLocal, catalogList, getSyncMeta, latestVersion, dbPath } from "./db.ts";
14
- import { NAME_RE, defaultPiBin } from "./install.ts";
15
- import {
16
- assertPackagePermission,
17
- type MutationApproval,
18
- type PackageOperation,
19
- type SecuritySettingsPort,
20
- } from "./security.ts";
21
-
22
- function defaultPiBinForUnit(): string | undefined {
23
- const b = defaultPiBin();
24
- return b === "pi" ? undefined : b; // bare name needs no pin
25
- }
26
- import {
27
- SEARCH_DEFAULT_LIMIT, SEARCH_MAX_LIMIT, NPM_REGISTRY_BASE, SEARCH_PAGE_SIZE, MIRROR_PAGE_DELAY_MS,
28
- } from "./constants.ts";
29
- import { VERSION } from "./version.ts";
30
-
31
- const SOURCE_RE = /^(npm:[A-Za-z0-9@._/-]+|git:[A-Za-z0-9@:._/-]+|https:\/\/[A-Za-z0-9@:._/?=&%~-]+)$/;
32
-
33
- const USAGE = `packed — package service for the Pi agent
34
-
35
- usage:
36
- packed search <query> [--offline] [--json] search npm (or the local mirror with --offline)
37
- packed info <name> [--json] package details
38
- packed updates [--json] updates per the local mirror
39
- packed update <source> [--approve] [--json] update one configured package through Pi
40
- packed mirror [--json] sync upstream into the local SQLite index
41
- packed installed [--json] installed pi packages
42
- packed catalog [--json] local package index (apt-cache stats)
43
- packed install <source> [--approve] [--json] pi install npm:|git:|https://… via daemon
44
- packed remove <name> [--approve] [--json] remove by bare npm name via daemon
45
- packed security [always|never] [--approve] [--json] read or set mutation approval policy
46
- packed serve run the long-running daemon
47
- packed service print a systemd user unit
48
- packed version print version
49
- `;
50
-
51
- export interface CliDeps {
52
- reg: Registry;
53
- inst: Installer;
54
- security: SecuritySettingsPort;
55
- stateDir: string;
56
- piHome: string;
57
- execPath?: string; // bun binary (defaults to process.execPath)
58
- cliPath?: string; // this CLI's entry file (for the systemd unit)
59
- piBin?: string; // pi binary path to pin into the unit's Environment
60
- }
61
-
62
- export interface CliResult {
63
- code: number;
64
- out: string;
65
- }
66
-
67
- interface Flags {
68
- json: boolean;
69
- limit: number;
70
- cached: boolean;
71
- offline: boolean;
72
- approved: boolean;
73
- }
74
-
75
- function parseFlags(rest: string[]): { flags: Flags; pos: string[] } {
76
- const flags: Flags = { json: false, limit: SEARCH_DEFAULT_LIMIT, cached: false, offline: false, approved: false };
77
- const pos: string[] = [];
78
- for (let i = 0; i < rest.length; i++) {
79
- const a = rest[i]!;
80
- if (a === "--json") flags.json = true;
81
- else if (a === "--cached") flags.cached = true;
82
- else if (a === "--offline") flags.offline = true;
83
- else if (a === "--approve") flags.approved = true;
84
- else if (a === "--limit" && i + 1 < rest.length) flags.limit = Number(rest[++i]) || SEARCH_DEFAULT_LIMIT;
85
- else if (a.startsWith("--limit=")) flags.limit = Number(a.slice(8)) || SEARCH_DEFAULT_LIMIT;
86
- else pos.push(a);
87
- }
88
- return { flags, pos };
89
- }
90
-
91
- type Command = (rest: string[], d: CliDeps, flags: Flags, pos: string[]) => Promise<CliResult>;
92
-
93
- const ok = (out: string): CliResult => ({ code: 0, out });
94
- const fail = (out: string, code = 1): CliResult => ({ code, out });
95
- const usageErr = (out: string): CliResult => ({ code: 2, out });
96
-
97
- const PACKAGE_COMMAND_OPERATIONS: Record<string, PackageOperation | undefined> = {
98
- search: "search",
99
- info: "info",
100
- installed: "installed",
101
- catalog: "catalog",
102
- updates: "updates",
103
- update: "update",
104
- mirror: "mirror",
105
- install: "install",
106
- remove: "remove",
107
- };
108
-
109
- const commands: Record<string, { usage: string; run: Command }> = {
110
- search: {
111
- usage: "packed search <query> [--offline] [--limit N] [--json]",
112
- async run(_rest, d, flags, pos) {
113
- const q = pos[0];
114
- if (!q) return usageErr(`usage: ${commands["search"]!.usage}\n`);
115
- const limit = clampLimit(flags.limit, SEARCH_DEFAULT_LIMIT, SEARCH_MAX_LIMIT);
116
- // --offline: query the SQLite mirror only (apt-cache search analog)
117
- if (flags.offline) {
118
- const db = openDb(dbPath(d.stateDir));
119
- try {
120
- const results = searchLocal(db, q, limit);
121
- if (flags.json) return ok(JSON.stringify({ query: q, total: results.length, results, offline: true }) + "\n");
122
- if (results.length === 0) return ok(`no mirrored packages match "${q}" (run: packed mirror)\n`);
123
- let out = `${results.length} mirrored package(s):\n\n`;
124
- for (const p of results) out += ` ${p.name}@${p.version}\n ${p.description ?? ""}\n`;
125
- return ok(out);
126
- } finally {
127
- db.close();
128
- }
129
- }
130
- const { results, total } = await d.reg.search(buildSearchQuery(q), limit);
131
- if (flags.json) return ok(JSON.stringify({ query: q, total, results }) + "\n");
132
- if (results.length === 0) return ok(`no pi packages found for "${q}"\n`);
133
- let out = `${total} package(s) (showing ${results.length}):\n\n`;
134
- for (const p of results) out += ` ${p.name}@${p.version}\n ${p.description ?? ""}\n`;
135
- return ok(out);
136
- },
137
- },
138
-
139
- mirror: {
140
- usage: "packed mirror [--json] (sync the upstream registry into the local SQLite index — the apt update analog)",
141
- async run(_rest, d, flags) {
142
- const n = await syncCatalog(d.reg, d.stateDir);
143
- if (flags.json) return ok(JSON.stringify({ synced: n }) + "\n");
144
- return ok(`mirrored ${n} packages into the local index\n`);
145
- },
146
- },
147
-
148
- info: {
149
- usage: "packed info <name> [--json]",
150
- async run(_rest, d, flags, pos) {
151
- const name = pos[0];
152
- if (!name) return usageErr(`usage: ${commands["info"]!.usage}\n`);
153
- const info = await d.reg.info(name);
154
- if (flags.json) return ok(JSON.stringify(info) + "\n");
155
- let out = `${info.name}@${info.version}\n${info.description ?? ""}\n`;
156
- if (info.repository) out += `repo: ${info.repository}\n`;
157
- if (info.pi) out += `provides: ${Object.keys(info.pi).join(", ")}\n`;
158
- return ok(out);
159
- },
160
- },
161
-
162
- updates: {
163
- usage: "packed updates [--json] (from the local mirror — run `packed mirror` first)",
164
- async run(_rest, d, flags) {
165
- const db = openDb(dbPath(d.stateDir));
166
- let updates;
167
- try {
168
- updates = checkUpdates((name) => latestVersion(db, name), readInstalledPackages(d.piHome));
169
- } finally {
170
- db.close();
171
- }
172
- if (flags.json) {
173
- return ok(JSON.stringify({ checkedAt: new Date().toISOString(), updates }) + "\n");
174
- }
175
- if (updates.length === 0) return ok("all pi packages up to date (per the local mirror)\n");
176
- let out = `${updates.length} update(s) available:\n\n`;
177
- for (const u of updates) out += ` ${u.name} ${u.installed} → ${u.latest}\n`;
178
- return ok(out + "\nrun: pi update --extensions\n");
179
- },
180
- },
181
-
182
- installed: {
183
- usage: "packed installed [--json]",
184
- async run(_rest, d, flags) {
185
- const installed = readInstalledPackages(d.piHome);
186
- if (flags.json) return ok(JSON.stringify(installed) + "\n");
187
- return ok(installed.map((p) => ` ${p.name}@${p.pinned ?? p.installed ?? "?"}\n`).join(""));
188
- },
189
- },
190
-
191
- catalog: {
192
- usage: "packed catalog [--json]",
193
- async run(_rest, d, flags) {
194
- const db = openDb(dbPath(d.stateDir));
195
- try {
196
- const meta = getSyncMeta(db);
197
- const packages = catalogList(db);
198
- if (flags.json) {
199
- return ok(JSON.stringify({ fetchedAt: meta?.fetchedAt, sha256: meta?.sha256, packages }) + "\n");
200
- }
201
- let out = `${packages.length} packages in the local index`;
202
- if (meta) out += ` (synced ${meta.fetchedAt}, sha256:${meta.sha256.slice(0, 12)}…)`;
203
- out += "\n\n";
204
- for (const p of packages.slice(0, 50)) out += ` ${p.name}@${p.version}\n`;
205
- return ok(out);
206
- } finally {
207
- db.close();
208
- }
209
- },
210
- },
211
-
212
- install: {
213
- usage: "packed install npm:<pkg>[@ver] | git:<host>/<owner>/<repo>[@ref] | https://… [--json]",
214
- async run(_rest, d, flags, pos) {
215
- const source = pos[0] ?? "";
216
- if (!SOURCE_RE.test(source)) return usageErr(`usage: ${commands["install"]!.usage}\n`);
217
- try {
218
- const output = await d.inst.install(source, { approved: flags.approved });
219
- return flags.json ? ok(`${JSON.stringify({ ok: true, source, output })}\n`) : ok(`${output}\n`);
220
- } catch (e) {
221
- const error = e instanceof Error ? e.message : String(e);
222
- return flags.json ? fail(`${JSON.stringify({ ok: false, source, error })}\n`) : fail(`${error}\n`);
223
- }
224
- },
225
- },
226
-
227
- security: {
228
- usage: "packed security [always|never] [--json]",
229
- async run(_rest, d, flags, pos) {
230
- const requested = pos[0];
231
- if (requested !== undefined && requested !== "always" && requested !== "never") {
232
- return usageErr(`usage: ${commands["security"]!.usage}\n`);
233
- }
234
- const settings = requested
235
- ? await d.security.setMutationApproval(requested as MutationApproval, { approved: flags.approved })
236
- : await d.security.security();
237
- return flags.json
238
- ? ok(`${JSON.stringify(settings)}\n`)
239
- : ok(`package mutation approval: ${settings.mutationApproval}\n`);
240
- },
241
- },
242
-
243
- update: {
244
- usage: "packed update <configured-source> [--approve] [--json]",
245
- async run(_rest, d, flags, pos) {
246
- const source = pos[0] ?? "";
247
- if (!SOURCE_RE.test(source)) return usageErr(`usage: ${commands["update"]!.usage}\n`);
248
- try {
249
- const outcome = await d.inst.update(source, { approved: flags.approved });
250
- if (flags.json) return ok(`${JSON.stringify({ ok: true, source, ...outcome })}\n`);
251
- if (outcome.alreadyUpToDate) {
252
- const version = outcome.currentVersion ?? outcome.previousVersion;
253
- const reason = outcome.pinned
254
- ? `is pinned to ${version ?? "an exact version"} — pi update intentionally leaves pinned packages unchanged; run \`packed install npm:${npmPackageName(source) ?? source}\` to move off the pin`
255
- : `is already up to date${version ? ` at ${version}` : ""}`;
256
- return ok(`${source} ${reason}\n`);
257
- }
258
- const transition =
259
- outcome.previousVersion && outcome.currentVersion ? ` (${outcome.previousVersion} → ${outcome.currentVersion})` : "";
260
- return ok(`${outcome.output}${transition}\nReload Pi with /reload to activate the updated package.\n`);
261
- } catch (error) {
262
- const message = error instanceof Error ? error.message : String(error);
263
- return flags.json
264
- ? fail(`${JSON.stringify({ ok: false, source, error: message, reloadRequired: false })}\n`)
265
- : fail(`${message}\n`);
266
- }
267
- },
268
- },
269
-
270
- remove: {
271
- usage: "packed remove <name> [--approve] [--json] (bare npm name, e.g. pi-lsp or @scope/pkg)",
272
- async run(_rest, d, flags, pos) {
273
- const name = pos[0] ?? "";
274
- if (!NAME_RE.test(name)) return usageErr(`usage: ${commands["remove"]!.usage}\n`);
275
- try {
276
- const output = await d.inst.remove(`npm:${name}`, { approved: flags.approved });
277
- return flags.json ? ok(`${JSON.stringify({ ok: true, name, output })}\n`) : ok(`${output}\n`);
278
- } catch (e) {
279
- const error = e instanceof Error ? e.message : String(e);
280
- return flags.json ? fail(`${JSON.stringify({ ok: false, name, error })}\n`) : fail(`${error}\n`);
281
- }
282
- },
283
- },
284
-
285
- service: {
286
- usage: "packed service (print a systemd user unit to stdout)",
287
- async run(_rest, d) {
288
- const execPath = d.execPath ?? process.execPath;
289
- const cliPath = d.cliPath ?? new URL("./cli.ts", import.meta.url).pathname;
290
- return ok(renderUnit(execPath, cliPath, d.piBin ?? defaultPiBinForUnit()));
291
- },
292
- },
293
-
294
- version: {
295
- usage: "packed version",
296
- async run() {
297
- return ok(VERSION + "\n");
298
- },
299
- },
300
- };
301
-
302
- /** systemd user unit. Idle self-exit is disabled — systemd owns the
303
- * lifecycle (Restart=on-failure takes over). */
304
- export function renderUnit(execPath: string, cliPath: string, piBin?: string): string {
305
- // systemd does not read shell rc files: PI_BIN must be explicit so the
306
- // daemon's install/remove execs can find the pi binary.
307
- const piEnv = piBin ? `Environment=PI_BIN=${piBin}\n` : "";
308
- return `[Unit]
309
- Description=pi-packed package service (Pi agent)
310
-
311
- [Service]
312
- Type=simple
313
- ExecStart=${execPath} ${cliPath} serve
314
- Restart=on-failure
315
- RestartSec=2
316
- Environment=PI_PACKED_IDLE_SECS=0
317
- ${piEnv}NoNewPrivileges=true
318
-
319
- [Install]
320
- WantedBy=default.target
321
- `;
322
- }
323
-
324
- export async function cliRun(args: string[], d: CliDeps): Promise<CliResult> {
325
- const [name, ...rest] = args;
326
- if (!name) return usageErr(USAGE);
327
- if (name === "help" || name === "--help" || name === "-h") return { code: 0, out: USAGE };
328
- const cmd = commands[name];
329
- if (!cmd) return usageErr(`unknown command "${name}"\n${USAGE}`);
330
- const { flags, pos } = parseFlags(rest);
331
- try {
332
- const validMutationInput = name === "install" || name === "update"
333
- ? SOURCE_RE.test(pos[0] ?? "")
334
- : name === "remove" ? NAME_RE.test(pos[0] ?? "")
335
- : name === "security" ? (pos[0] === undefined || pos[0] === "always" || pos[0] === "never")
336
- : true;
337
- const operation = name === "security"
338
- ? (pos[0] === undefined ? "security.read" : "security.write")
339
- : PACKAGE_COMMAND_OPERATIONS[name];
340
- if (operation && validMutationInput) assertPackagePermission(await d.security.security(), operation, flags.approved);
341
- return await cmd.run(rest, d, flags, pos);
342
- } catch (e) {
343
- return fail(`${name} failed: ${e instanceof Error ? e.message : e}\n`);
344
- }
345
- }
346
-
347
- // Entry point (bun src/cli.ts …). `serve` is dispatched before any proxying
348
- // so the daemon always talks directly to npm.
349
- if (import.meta.main) {
350
- const args = process.argv.slice(2);
351
- if (args[0] === "serve") {
352
- const { serveMain } = await import("./daemon.ts");
353
- serveMain();
354
- } else {
355
- const { stateDir } = await import("./state.ts");
356
- const { defaultPiHome } = await import("./installed.ts");
357
- const { DaemonBackedSecurity, DaemonBackedInstaller, resolveRegistry } = await import("./client.ts");
358
- const dir = stateDir();
359
- // mirror talks to UPSTREAM, not the daemon cache — apt update semantics.
360
- const reg =
361
- args[0] === "mirror"
362
- ? new (await import("./registry.ts")).HttpRegistry(NPM_REGISTRY_BASE, SEARCH_PAGE_SIZE, MIRROR_PAGE_DELAY_MS)
363
- : await resolveRegistry(dir, NPM_REGISTRY_BASE);
364
- const { code, out } = await cliRun(args, {
365
- reg,
366
- inst: new DaemonBackedInstaller(dir),
367
- security: new DaemonBackedSecurity(dir),
368
- stateDir: dir,
369
- piHome: defaultPiHome(),
370
- });
371
- process.stdout.write(out);
372
- process.exit(code);
373
- }
374
- }
package/src/client.ts DELETED
@@ -1,247 +0,0 @@
1
- /**
2
- * client.ts — authenticated loopback clients for the supervised package daemon.
3
- * The CLI may fall back to npm for read-only registry queries; Pi extensions use
4
- * PackageDaemonClient exclusively so Bun execution and SQLite remain daemon-owned.
5
- */
6
- import { readFileSync } from "node:fs";
7
- import { join } from "node:path";
8
- import type { InstalledPkg, Installer, PkgInfo, Registry, SearchPage, UpdateEntry, UpdateOutcome, UpdatesSnapshot } from "./ports.ts";
9
- import { HttpRegistry } from "./registry.ts";
10
- import { DAEMON_HOST, PROBE_TIMEOUT_MS, REGISTRY_FETCH_TIMEOUT_MS, PORT_FILE, TOKEN_FILE } from "./constants.ts";
11
- import type { MutationApproval, SecuritySettings } from "./security.ts";
12
-
13
- export type FetchTransport = (request: Request) => Promise<Response>;
14
-
15
- export interface PackageDaemonPort {
16
- search(query: string, limit: number, offline?: boolean): Promise<{ query: string; total: number; results: SearchPage["results"] }>;
17
- info(name: string): Promise<PkgInfo>;
18
- installed(): Promise<InstalledPkg[]>;
19
- updates(): Promise<UpdateEntry[]>;
20
- security(): Promise<SecuritySettings>;
21
- setMutationApproval(value: MutationApproval, approved?: boolean): Promise<SecuritySettings>;
22
- install(source: string, approved?: boolean): Promise<string>;
23
- remove(name: string, approved?: boolean): Promise<string>;
24
- update(source: string, approved?: boolean): Promise<UpdateOutcome>;
25
- }
26
-
27
- interface MutationResponse {
28
- ok: boolean;
29
- output: string;
30
- }
31
-
32
- interface UpdateMutationResponse extends MutationResponse, Partial<Omit<UpdateOutcome, "output">> {}
33
-
34
- export class PackageDaemonError extends Error {
35
- constructor(
36
- message: string,
37
- readonly operation: string,
38
- readonly status?: number,
39
- ) {
40
- super(message);
41
- this.name = "PackageDaemonError";
42
- }
43
- }
44
-
45
- export class PackageDaemonClient implements PackageDaemonPort {
46
- constructor(
47
- private readonly base: string,
48
- private readonly token: string,
49
- private readonly transport: FetchTransport = fetch,
50
- ) {}
51
-
52
- private async request<T>(path: string, init: RequestInit = {}): Promise<T> {
53
- const headers = new Headers(init.headers);
54
- headers.set("authorization", `Bearer ${this.token}`);
55
- if (init.body !== undefined) headers.set("content-type", "application/json");
56
- const response = await this.transport(new Request(`${this.base}${path}`, {
57
- ...init,
58
- headers,
59
- signal: init.signal ?? AbortSignal.timeout(REGISTRY_FETCH_TIMEOUT_MS),
60
- }));
61
- let body: unknown;
62
- try {
63
- body = await response.json();
64
- } catch {
65
- throw new PackageDaemonError(`package daemon returned invalid JSON (HTTP ${response.status})`, path, response.status);
66
- }
67
- if (!response.ok) {
68
- const message = typeof body === "object" && body !== null && typeof (body as { error?: unknown }).error === "string"
69
- ? (body as { error: string }).error
70
- : `package daemon HTTP ${response.status}`;
71
- throw new PackageDaemonError(message, path, response.status);
72
- }
73
- return body as T;
74
- }
75
-
76
- async search(query: string, limit: number, offline = false): Promise<{ query: string; total: number; results: SearchPage["results"] }> {
77
- const params = new URLSearchParams({ q: query, limit: String(limit) });
78
- if (offline) params.set("offline", "1");
79
- return this.request(`/search?${params}`);
80
- }
81
-
82
- info(name: string): Promise<PkgInfo> {
83
- return this.request(`/info?name=${encodeURIComponent(name)}`);
84
- }
85
-
86
- installed(): Promise<InstalledPkg[]> {
87
- return this.request("/installed");
88
- }
89
-
90
- async updates(): Promise<UpdateEntry[]> {
91
- return (await this.request<UpdatesSnapshot>("/updates")).updates;
92
- }
93
-
94
- security(): Promise<SecuritySettings> {
95
- return this.request("/security");
96
- }
97
-
98
- setMutationApproval(mutationApproval: MutationApproval, approved = false): Promise<SecuritySettings> {
99
- return this.request("/security", { method: "POST", body: JSON.stringify({ mutationApproval, approved }) });
100
- }
101
-
102
- async install(source: string, approved = false): Promise<string> {
103
- const result = await this.request<MutationResponse>("/install", {
104
- method: "POST",
105
- body: JSON.stringify({ source, approved }),
106
- });
107
- if (!result.ok) throw new PackageDaemonError(result.output || `failed to install ${source}`, "install");
108
- return result.output;
109
- }
110
-
111
- async remove(name: string, approved = false): Promise<string> {
112
- const result = await this.request<MutationResponse>("/remove", {
113
- method: "POST",
114
- body: JSON.stringify({ name, approved }),
115
- });
116
- if (!result.ok) throw new PackageDaemonError(result.output || `failed to remove ${name}`, "remove");
117
- return result.output;
118
- }
119
-
120
- async update(source: string, approved = false): Promise<UpdateOutcome> {
121
- const result = await this.request<UpdateMutationResponse>("/update", {
122
- method: "POST",
123
- body: JSON.stringify({ source, approved }),
124
- });
125
- if (!result.ok) throw new PackageDaemonError(result.output || `failed to update ${source}`, "update");
126
- // Older daemons (pre-honest-update) only ever sent {ok, output}; default
127
- // to the historical "assume it changed" signal rather than claiming
128
- // certainty the response doesn't actually contain.
129
- return {
130
- output: result.output,
131
- reloadRequired: result.reloadRequired ?? true,
132
- alreadyUpToDate: result.alreadyUpToDate ?? false,
133
- pinned: result.pinned ?? false,
134
- previousVersion: result.previousVersion,
135
- currentVersion: result.currentVersion,
136
- };
137
- }
138
- }
139
-
140
- export class PackageDaemonInstaller implements Installer {
141
- constructor(private readonly client: PackageDaemonClient) {}
142
-
143
- install(source: string, options?: { approved?: boolean }): Promise<string> {
144
- return this.client.install(source, options?.approved);
145
- }
146
-
147
- remove(source: string, options?: { approved?: boolean }): Promise<string> {
148
- if (!source.startsWith("npm:") || source.length <= 4) {
149
- throw new PackageDaemonError("daemon package removal requires an npm: source", "remove");
150
- }
151
- return this.client.remove(source.slice(4), options?.approved);
152
- }
153
-
154
- update(source: string, options?: { approved?: boolean }): Promise<UpdateOutcome> {
155
- return this.client.update(source, options?.approved);
156
- }
157
- }
158
-
159
- export class DaemonBackedSecurity {
160
- constructor(private readonly stateDirectory: string) {}
161
- async security(): Promise<SecuritySettings> {
162
- return (await connectPackageDaemon(this.stateDirectory)).security();
163
- }
164
- async setMutationApproval(value: MutationApproval, options?: { approved?: boolean }): Promise<SecuritySettings> {
165
- return (await connectPackageDaemon(this.stateDirectory)).setMutationApproval(value, options?.approved);
166
- }
167
- }
168
-
169
- export class DaemonBackedInstaller implements Installer {
170
- constructor(private readonly stateDirectory: string) {}
171
-
172
- async install(source: string, options?: { approved?: boolean }): Promise<string> {
173
- return (await connectPackageDaemon(this.stateDirectory)).install(source, options?.approved);
174
- }
175
-
176
- async remove(source: string, options?: { approved?: boolean }): Promise<string> {
177
- return new PackageDaemonInstaller(await connectPackageDaemon(this.stateDirectory)).remove(source, options);
178
- }
179
-
180
- async update(source: string, options?: { approved?: boolean }): Promise<UpdateOutcome> {
181
- return (await connectPackageDaemon(this.stateDirectory)).update(source, options?.approved);
182
- }
183
- }
184
-
185
- export class DaemonRegistry implements Registry {
186
- private readonly client: PackageDaemonClient;
187
-
188
- constructor(base: string, token: string, transport: FetchTransport = fetch) {
189
- this.client = new PackageDaemonClient(base, token, transport);
190
- }
191
-
192
- async search(query: string, limit: number): Promise<SearchPage> {
193
- const body = await this.client.search(query, limit);
194
- return { results: body.results, total: body.total };
195
- }
196
-
197
- async searchPage(query: string, _from: number, size: number): Promise<SearchPage> {
198
- return this.search(query, size).catch(() => ({ results: [], total: 0 }));
199
- }
200
-
201
- async searchAll(): Promise<never> {
202
- throw new Error("searchAll is not supported via the daemon proxy");
203
- }
204
-
205
- info(name: string): Promise<PkgInfo> {
206
- return this.client.info(name);
207
- }
208
- }
209
-
210
- export interface DaemonHandle {
211
- base: string;
212
- token: string;
213
- }
214
-
215
- export async function probe(dir: string): Promise<DaemonHandle | undefined> {
216
- let port: string;
217
- let token: string;
218
- try {
219
- port = readFileSync(join(dir, PORT_FILE), "utf8").trim();
220
- token = readFileSync(join(dir, TOKEN_FILE), "utf8").trim();
221
- } catch {
222
- return undefined;
223
- }
224
- const base = `http://${DAEMON_HOST}:${port}`;
225
- try {
226
- const res = await fetch(`${base}/health`, {
227
- headers: { authorization: `Bearer ${token}` },
228
- signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
229
- });
230
- if (res.ok) return { base, token };
231
- } catch {
232
- /* dead daemon or stale files */
233
- }
234
- return undefined;
235
- }
236
-
237
- export async function connectPackageDaemon(dir: string): Promise<PackageDaemonClient> {
238
- const handle = await probe(dir);
239
- if (!handle) throw new Error("pi-packed daemon is unavailable; start packed.service");
240
- return new PackageDaemonClient(handle.base, handle.token);
241
- }
242
-
243
- export async function resolveRegistry(dir: string, npmBase: string): Promise<Registry> {
244
- const handle = await probe(dir);
245
- if (handle) return new DaemonRegistry(handle.base, handle.token);
246
- return new HttpRegistry(npmBase);
247
- }