@danypops/pi-packed 0.19.7 → 0.19.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +109 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +1 -0
- package/dist/protocol.d.ts +221 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +1 -0
- package/extension/src/{permission.ts → approval/permission.ts} +1 -1
- package/extension/src/index.ts +1 -1
- package/extension/src/packed.ts +2 -2
- package/extension/src/{discover.ts → tabs/discover.ts} +4 -4
- package/extension/src/{resource-config.ts → tabs/resource-config.ts} +4 -4
- package/extension/src/{security-tui.ts → tabs/security-tui.ts} +3 -3
- package/extension/src/tool-output.ts +1 -1
- package/extension/src/tools.ts +2 -2
- package/extension/src/tui.ts +4 -4
- package/package.json +31 -8
- package/service/schema/pi-setup-v1.schema.json +70 -0
- package/service/setup/danypops-ecosystem.pi-setup.json +15 -0
- package/service/src/adoption/advisories.ts +268 -0
- package/service/src/adoption/check.ts +872 -0
- package/service/src/adoption/commit-freshness.ts +167 -0
- package/service/src/adoption/doctor.ts +135 -0
- package/service/src/adoption/install-validation.ts +187 -0
- package/service/src/adoption/pack.ts +291 -0
- package/service/src/adoption/score.ts +466 -0
- package/service/src/adoption/smoke-child.ts +113 -0
- package/service/src/adoption/smoke.ts +282 -0
- package/service/src/cli/cli.ts +926 -0
- package/service/src/daemon/cleanup.ts +76 -0
- package/service/src/daemon/client.ts +412 -0
- package/service/src/daemon/daemon-service.ts +249 -0
- package/service/src/daemon/daemon.ts +110 -0
- package/service/src/daemon/service.ts +664 -0
- package/service/src/daemon/watcher.ts +92 -0
- package/service/src/index/build-index.ts +256 -0
- package/service/src/packages/catalog.ts +61 -0
- package/service/src/packages/db.ts +224 -0
- package/service/src/packages/install.ts +60 -0
- package/service/src/packages/installed.ts +123 -0
- package/service/src/packages/package.ts +141 -0
- package/service/src/packages/resources.ts +203 -0
- package/service/src/pi/pi-version.ts +171 -0
- package/service/src/public/atomic-json.ts +32 -0
- package/service/src/public/client.ts +277 -0
- package/service/src/public/protocol.ts +169 -0
- package/service/src/publish/publish.ts +855 -0
- package/service/src/registry/registry.ts +246 -0
- package/service/src/security/security.ts +128 -0
- package/service/src/self-update/self-update.ts +148 -0
- package/service/src/setup/setup.ts +761 -0
- package/service/src/shared/atomic-json.ts +33 -0
- package/service/src/shared/cache.ts +21 -0
- package/service/src/shared/constants.ts +73 -0
- package/service/src/shared/log.ts +21 -0
- package/service/src/shared/paths.ts +88 -0
- package/service/src/shared/state.ts +15 -0
- package/service/src/shared/version.ts +46 -0
- package/service/test/advisories.test.ts +287 -0
- package/service/test/check.test.ts +368 -0
- package/service/test/cleanup.test.ts +220 -0
- package/service/test/cli.test.ts +1303 -0
- package/service/test/core.test.ts +181 -0
- package/service/test/daemon-kit-migration.test.ts +181 -0
- package/service/test/daemon-service.test.ts +238 -0
- package/service/test/db.test.ts +178 -0
- package/service/test/doctor.test.ts +234 -0
- package/service/test/domain.test.ts +291 -0
- package/service/test/fixtures/install-validation/broken-package/extension/index.ts +3 -0
- package/service/test/fixtures/install-validation/broken-package/package.json +8 -0
- package/service/test/fixtures/install-validation/healthy-package/extension/index.ts +3 -0
- package/service/test/fixtures/install-validation/healthy-package/package.json +8 -0
- package/service/test/fixtures/install-validation/no-manifest-package/package.json +5 -0
- package/service/test/index.test.ts +353 -0
- package/service/test/install-validation.test.ts +114 -0
- package/service/test/install.test.ts +113 -0
- package/service/test/log.test.ts +42 -0
- package/service/test/pack-score.test.ts +513 -0
- package/service/test/pi-version.test.ts +318 -0
- package/service/test/public-boundary.test.ts +54 -0
- package/service/test/public-client.test.ts +127 -0
- package/service/test/public-consumer.ts +8 -0
- package/service/test/publish.test.ts +333 -0
- package/service/test/registry-contract.test.ts +148 -0
- package/service/test/resources.test.ts +255 -0
- package/service/test/security.test.ts +89 -0
- package/service/test/self-update.test.ts +257 -0
- package/service/test/service.test.ts +555 -0
- package/service/test/setup.test.ts +375 -0
- package/service/test/smoke.test.ts +118 -0
- package/service/test/version.test.ts +37 -0
- package/service/tsconfig.consumer.json +13 -0
- package/service/tsconfig.public.json +12 -0
- /package/extension/src/{reload.ts → approval/reload.ts} +0 -0
- /package/extension/src/{discover-model.ts → tabs/discover-model.ts} +0 -0
|
@@ -0,0 +1,926 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { generateSystemdUnit } from "@danypops/vehicle-server/service";
|
|
5
|
+
import { formatAdvisoryReport, resolveInstalledVersions, scanInstalledPackages } from "../adoption/advisories.ts";
|
|
6
|
+
import { checkPackage, formatCheckReport } from "../adoption/check.ts";
|
|
7
|
+
import { formatDoctorReport, runDoctor } from "../adoption/doctor.ts";
|
|
8
|
+
import { formatPackReport, NpmPackVerifier, type PackReport } from "../adoption/pack.ts";
|
|
9
|
+
import { type AdoptionReport, formatAdoptionReport, scoreTarget } from "../adoption/score.ts";
|
|
10
|
+
import type { PackageDaemonPort } from "../daemon/client.ts";
|
|
11
|
+
import { checkUpdates } from "../daemon/watcher.ts";
|
|
12
|
+
import { generateIndex, indexPath, readIndex } from "../index/build-index.ts";
|
|
13
|
+
import { syncCatalog } from "../packages/catalog.ts";
|
|
14
|
+
import { catalogList, dbPath, getSyncMeta, latestVersion, openDb, searchLocal } from "../packages/db.ts";
|
|
15
|
+
import { defaultPiBin, NAME_RE } from "../packages/install.ts";
|
|
16
|
+
import { npmPackageName, readInstalledPackages, readInstalledPackagesAcrossScopes } from "../packages/installed.ts";
|
|
17
|
+
import type { Installer, Pkg, Registry, UpdateEntry } from "../packages/package.ts";
|
|
18
|
+
/**
|
|
19
|
+
* cli.ts — the CLI entry point; drives the same shared ports (registry, installer, security) the HTTP service also drives.
|
|
20
|
+
* cliRun is pure: ({code, out}) in, no I/O — the entry point prints.
|
|
21
|
+
* Command table follows the go-tool/Cobra convention; flags may appear
|
|
22
|
+
* anywhere (agents put them anywhere).
|
|
23
|
+
*/
|
|
24
|
+
import { buildSearchQuery, clampLimit } from "../packages/package.ts";
|
|
25
|
+
import {
|
|
26
|
+
listPackageResources,
|
|
27
|
+
type PackageResources,
|
|
28
|
+
RESOURCE_FIELDS,
|
|
29
|
+
type ResourceField,
|
|
30
|
+
resolveToggleSettingsPath,
|
|
31
|
+
toggleResource,
|
|
32
|
+
} from "../packages/resources.ts";
|
|
33
|
+
import { checkPiVersion, type PiVersionReport, runPiStatusInteractive, runPiUpdateSelf } from "../pi/pi-version.ts";
|
|
34
|
+
import {
|
|
35
|
+
formatPublishReport,
|
|
36
|
+
npmWebUrl,
|
|
37
|
+
openBrowser,
|
|
38
|
+
PublishManager,
|
|
39
|
+
type PublishSetupReport,
|
|
40
|
+
type PublishStatusReport,
|
|
41
|
+
runInherited,
|
|
42
|
+
runNpmLoginWeb,
|
|
43
|
+
} from "../publish/publish.ts";
|
|
44
|
+
import {
|
|
45
|
+
assertPackagePermission,
|
|
46
|
+
type MutationApproval,
|
|
47
|
+
type PackageOperation,
|
|
48
|
+
packageOperationClassification,
|
|
49
|
+
type SecuritySettingsPort,
|
|
50
|
+
} from "../security/security.ts";
|
|
51
|
+
import { runSelfUpdate, type SelfUpdateReport } from "../self-update/self-update.ts";
|
|
52
|
+
import {
|
|
53
|
+
bundledEcosystemManifestPath,
|
|
54
|
+
formatSetupReport,
|
|
55
|
+
type SetupApplyResult,
|
|
56
|
+
type SetupExportReport,
|
|
57
|
+
SetupManager,
|
|
58
|
+
type SetupPlan,
|
|
59
|
+
type SetupUpdateReport,
|
|
60
|
+
} from "../setup/setup.ts";
|
|
61
|
+
import { resolvePackedPaths } from "../shared/paths.ts";
|
|
62
|
+
|
|
63
|
+
/** A generated unit must never trust a bare "pi" resolving under systemd's
|
|
64
|
+
* own restricted PATH just because it resolves in the shell that generated
|
|
65
|
+
* the unit -- confirmed live: it doesn't. Always pin the real resolved
|
|
66
|
+
* absolute path when one can be found; only omit the pin (bare name) when
|
|
67
|
+
* resolution genuinely fails, which the running daemon will then also
|
|
68
|
+
* fail at identically, not silently. */
|
|
69
|
+
function defaultPiBinForUnit(): string | undefined {
|
|
70
|
+
const b = defaultPiBin();
|
|
71
|
+
if (b !== "pi") return b; // explicit override already given
|
|
72
|
+
return Bun.which("pi") ?? undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
import { MIRROR_PAGE_DELAY_MS, NPM_REGISTRY_BASE, SEARCH_DEFAULT_LIMIT, SEARCH_MAX_LIMIT, SEARCH_PAGE_SIZE } from "../shared/constants.ts";
|
|
76
|
+
import { buildVersionReport, formatVersionReport, VERSION } from "../shared/version.ts";
|
|
77
|
+
|
|
78
|
+
const SOURCE_RE = /^(npm:[A-Za-z0-9@._/-]+|git:[A-Za-z0-9@:._/-]+|https:\/\/[A-Za-z0-9@:._/?=&%~-]+)$/;
|
|
79
|
+
|
|
80
|
+
const USAGE = `packed — package service for the Pi agent
|
|
81
|
+
|
|
82
|
+
usage:
|
|
83
|
+
packed search <query> [--offline] [--json] search npm (or the local mirror with --offline)
|
|
84
|
+
packed info <name> [--json] package details
|
|
85
|
+
packed updates [--project <path>] [--json] updates per the local mirror; --project also checks that project's own .pi/settings.json pins
|
|
86
|
+
packed update <source> [--approve] [--no-service] [--json] update one configured package through Pi; restarts its registered persistent service (if any) to pick up the new code, unless --no-service
|
|
87
|
+
packed update --self [--approve] [--json] update Packed itself (npm-global installs only) and restart its supervised service
|
|
88
|
+
packed mirror [--json] sync upstream into the local SQLite index
|
|
89
|
+
packed installed [--json] installed pi packages
|
|
90
|
+
packed catalog [--json] local package index (apt-cache stats)
|
|
91
|
+
packed index build [--json] regenerate the local static adoption-scoring snapshot (npm-only, no GitHub)
|
|
92
|
+
packed index status [--json] report the local static index's generatedAt and package count
|
|
93
|
+
packed check [path] [--smoke] [--json] diagnose a Pi package; smoke is isolated and opt-in
|
|
94
|
+
packed doctor [--project <path>] [--json] smoke-test every enabled extension (global + project scope) and report tool/command/shortcut/flag name collisions before pi has to
|
|
95
|
+
packed pack [path] [--json] verify exact npm tarball contents without lifecycle scripts
|
|
96
|
+
packed score [path|name] [--json] report adoption-readiness evidence by dimension
|
|
97
|
+
packed publish setup [path] [--force] [--json] generate an OIDC staged-publish workflow
|
|
98
|
+
packed publish status [path] [--json] validate staged-publish prerequisites and handoff
|
|
99
|
+
packed setup export [path] [--force] [--machine-local] [--json] export immutable packages and scoped profiles
|
|
100
|
+
packed setup update [manifest] [--json] deliberately refresh immutable package resolutions
|
|
101
|
+
packed setup plan [manifest] [--prune] [--json] show an additive or exact setup diff without mutation
|
|
102
|
+
packed setup apply [manifest] [--prune] [--approve] [--json] apply an approved setup plan
|
|
103
|
+
packed install <source> [--approve] [--no-service] [--json] pi install npm:|git:|https://… via daemon; auto-registers a detected npm: Vehicle daemon as a service unless --no-service
|
|
104
|
+
packed install-service <source> --approve [--json] register a package's own daemon as a persistent login/boot service (also useful standalone, e.g. re-registering after --no-service)
|
|
105
|
+
packed restart-service <source> --approve [--json] restart a package's already-registered persistent service standalone (packed update already does this automatically)
|
|
106
|
+
packed remove <name> [--approve] [--json] remove by bare npm name via daemon
|
|
107
|
+
packed security [always|never] [--approve] [--json] read or set mutation approval policy
|
|
108
|
+
packed serve run the long-running daemon
|
|
109
|
+
packed service print a systemd user unit
|
|
110
|
+
packed version print version
|
|
111
|
+
`;
|
|
112
|
+
|
|
113
|
+
export interface CliDeps {
|
|
114
|
+
reg: Registry;
|
|
115
|
+
inst: Installer;
|
|
116
|
+
security: SecuritySettingsPort;
|
|
117
|
+
daemon?: PackageDaemonPort;
|
|
118
|
+
stateDir: string;
|
|
119
|
+
dataDir?: string;
|
|
120
|
+
piHome: string;
|
|
121
|
+
execPath?: string; // bun binary (defaults to process.execPath)
|
|
122
|
+
cliPath?: string; // this CLI's entry file (for the systemd unit)
|
|
123
|
+
piBin?: string; // pi binary path to pin into the unit's Environment
|
|
124
|
+
/** Reads the reachable daemon's own live-reported version, or undefined
|
|
125
|
+
* when none is reachable. Defaults to a real probe() over HTTP -- injectable
|
|
126
|
+
* so tests never depend on this machine's actual daemon state. */
|
|
127
|
+
daemonVersionCheck?: () => Promise<string | undefined>;
|
|
128
|
+
packer?: { verify(path: string): Promise<PackReport> };
|
|
129
|
+
scorer?: { score(target: string): Promise<AdoptionReport> };
|
|
130
|
+
publisher?: {
|
|
131
|
+
setup(path: string, options?: { force?: boolean }): Promise<PublishSetupReport>;
|
|
132
|
+
status(path: string): Promise<PublishStatusReport>;
|
|
133
|
+
};
|
|
134
|
+
setup?: {
|
|
135
|
+
export(projectRoot: string, options?: { force?: boolean; machineLocal?: boolean }): Promise<SetupExportReport>;
|
|
136
|
+
update(manifestPath: string): Promise<SetupUpdateReport>;
|
|
137
|
+
plan(manifestPath: string, options?: { prune?: boolean }): Promise<SetupPlan>;
|
|
138
|
+
apply(manifestPath: string, options?: { prune?: boolean }): Promise<SetupApplyResult>;
|
|
139
|
+
};
|
|
140
|
+
daemonService?: {
|
|
141
|
+
install(
|
|
142
|
+
source: string,
|
|
143
|
+
approved?: boolean,
|
|
144
|
+
): Promise<{ output: string; spec?: { name: string; binPath: string; descriptorPath: string } }>;
|
|
145
|
+
restart(
|
|
146
|
+
source: string,
|
|
147
|
+
approved?: boolean,
|
|
148
|
+
): Promise<{ output: string; restarted?: boolean; spec?: { name: string; binPath: string; descriptorPath: string } }>;
|
|
149
|
+
};
|
|
150
|
+
piVersion?: { check(): Promise<PiVersionReport> };
|
|
151
|
+
selfUpdater?: { run(): Promise<SelfUpdateReport> };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface CliResult {
|
|
155
|
+
code: number;
|
|
156
|
+
out: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
interface Flags {
|
|
160
|
+
json: boolean;
|
|
161
|
+
limit: number;
|
|
162
|
+
cached: boolean;
|
|
163
|
+
offline: boolean;
|
|
164
|
+
approved: boolean;
|
|
165
|
+
smoke: boolean;
|
|
166
|
+
force: boolean;
|
|
167
|
+
prune: boolean;
|
|
168
|
+
machineLocal: boolean;
|
|
169
|
+
noService: boolean;
|
|
170
|
+
ecosystem: boolean;
|
|
171
|
+
self: boolean;
|
|
172
|
+
project?: string;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function parseFlags(rest: string[]): { flags: Flags; pos: string[] } {
|
|
176
|
+
const flags: Flags = {
|
|
177
|
+
json: false,
|
|
178
|
+
limit: SEARCH_DEFAULT_LIMIT,
|
|
179
|
+
cached: false,
|
|
180
|
+
offline: false,
|
|
181
|
+
approved: false,
|
|
182
|
+
smoke: false,
|
|
183
|
+
force: false,
|
|
184
|
+
prune: false,
|
|
185
|
+
machineLocal: false,
|
|
186
|
+
noService: false,
|
|
187
|
+
ecosystem: false,
|
|
188
|
+
self: false,
|
|
189
|
+
};
|
|
190
|
+
const pos: string[] = [];
|
|
191
|
+
for (let i = 0; i < rest.length; i++) {
|
|
192
|
+
const a = rest[i]!;
|
|
193
|
+
if (a === "--json") flags.json = true;
|
|
194
|
+
else if (a === "--cached") flags.cached = true;
|
|
195
|
+
else if (a === "--offline") flags.offline = true;
|
|
196
|
+
else if (a === "--approve") flags.approved = true;
|
|
197
|
+
else if (a === "--smoke") flags.smoke = true;
|
|
198
|
+
else if (a === "--force") flags.force = true;
|
|
199
|
+
else if (a === "--prune") flags.prune = true;
|
|
200
|
+
else if (a === "--machine-local") flags.machineLocal = true;
|
|
201
|
+
else if (a === "--no-service") flags.noService = true;
|
|
202
|
+
else if (a === "--ecosystem") flags.ecosystem = true;
|
|
203
|
+
else if (a === "--self") flags.self = true;
|
|
204
|
+
else if (a === "--limit" && i + 1 < rest.length) flags.limit = Number(rest[++i]) || SEARCH_DEFAULT_LIMIT;
|
|
205
|
+
else if (a.startsWith("--limit=")) flags.limit = Number(a.slice(8)) || SEARCH_DEFAULT_LIMIT;
|
|
206
|
+
else if (a === "--project" && i + 1 < rest.length) flags.project = rest[++i];
|
|
207
|
+
else if (a.startsWith("--project=")) flags.project = a.slice(10);
|
|
208
|
+
else pos.push(a);
|
|
209
|
+
}
|
|
210
|
+
return { flags, pos };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
type Command = (rest: string[], d: CliDeps, flags: Flags, pos: string[]) => Promise<CliResult>;
|
|
214
|
+
|
|
215
|
+
const dataDirectory = (deps: CliDeps): string => deps.dataDir ?? deps.stateDir;
|
|
216
|
+
const ok = (out: string): CliResult => ({ code: 0, out });
|
|
217
|
+
const fail = (out: string, code = 1): CliResult => ({ code, out });
|
|
218
|
+
const usageErr = (out: string): CliResult => ({ code: 2, out });
|
|
219
|
+
const evidenceLabel = (pkg: Pick<Pkg, "packageEvidence">): string => {
|
|
220
|
+
const evidence = pkg.packageEvidence;
|
|
221
|
+
if (!evidence?.verified) return "[keyword candidate]";
|
|
222
|
+
return `[verified ${evidence.shape}]`;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const PACKAGE_COMMAND_OPERATIONS: Record<string, PackageOperation | undefined> = {
|
|
226
|
+
search: "search",
|
|
227
|
+
info: "info",
|
|
228
|
+
installed: "installed",
|
|
229
|
+
catalog: "catalog",
|
|
230
|
+
updates: "updates",
|
|
231
|
+
check: "check",
|
|
232
|
+
pack: "check",
|
|
233
|
+
score: "info",
|
|
234
|
+
update: "update",
|
|
235
|
+
mirror: "mirror",
|
|
236
|
+
install: "install",
|
|
237
|
+
"install-service": "install_service",
|
|
238
|
+
"restart-service": "restart_service",
|
|
239
|
+
remove: "remove",
|
|
240
|
+
pi: "pi.status",
|
|
241
|
+
advisories: "advisories.scan",
|
|
242
|
+
doctor: "doctor",
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
function formatResourcesList(result: { global: PackageResources[]; project: PackageResources[] }): string {
|
|
246
|
+
let out = "";
|
|
247
|
+
for (const [scope, groups] of [
|
|
248
|
+
["global", result.global],
|
|
249
|
+
["project", result.project],
|
|
250
|
+
] as const) {
|
|
251
|
+
if (groups.length === 0) continue;
|
|
252
|
+
out += `${scope}:\n`;
|
|
253
|
+
for (const group of groups) {
|
|
254
|
+
out += ` ${group.name} (${group.source}):\n`;
|
|
255
|
+
for (const field of RESOURCE_FIELDS) {
|
|
256
|
+
for (const item of group[field]) out += ` ${item.enabled ? "on " : "off"} ${field}/${item.path}\n`;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return out || "no package resources found\n";
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function formatPiVersionReport(report: PiVersionReport): string {
|
|
264
|
+
if (!report.current && !report.latest)
|
|
265
|
+
return "pi version unknown (pi not found on PATH, and the latest-version check failed or was skipped)\n";
|
|
266
|
+
let out = `pi ${report.current ?? "unknown"}`;
|
|
267
|
+
if (report.latest) out += ` (latest ${report.latest})`;
|
|
268
|
+
out += "\n";
|
|
269
|
+
if (report.upToDate === true) out += "up to date\n";
|
|
270
|
+
else if (report.upToDate === false) out += "update available -- run: pi update --self\n";
|
|
271
|
+
if (report.packageName) out += `note: pi has moved to package "${report.packageName}"\n`;
|
|
272
|
+
if (report.note) out += `${report.note}\n`;
|
|
273
|
+
return out;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const commands: Record<string, { usage: string; run: Command }> = {
|
|
277
|
+
setup: {
|
|
278
|
+
usage:
|
|
279
|
+
"packed setup export [path] [--force] [--machine-local] [--json] | packed setup update [manifest] [--json] | packed setup plan [manifest|--ecosystem] [--prune] [--json] | packed setup apply [manifest|--ecosystem] [--prune] [--approve] [--json]",
|
|
280
|
+
async run(_rest, d, flags, pos) {
|
|
281
|
+
const action = pos[0];
|
|
282
|
+
if (action !== "export" && action !== "update" && action !== "plan" && action !== "apply")
|
|
283
|
+
return usageErr(`usage: ${commands.setup!.usage}\n`);
|
|
284
|
+
// --ecosystem resolves to the curated @danypops starter manifest bundled
|
|
285
|
+
// in this same package -- no separate download, review, or trust surface
|
|
286
|
+
// beyond the npm package itself. Only meaningful for plan/apply.
|
|
287
|
+
const path = flags.ecosystem ? bundledEcosystemManifestPath() : resolve(pos[1] ?? ".");
|
|
288
|
+
let report: SetupExportReport | SetupUpdateReport | SetupPlan | SetupApplyResult;
|
|
289
|
+
if (d.daemon) {
|
|
290
|
+
if (action === "export") report = await d.daemon.setupExport(path, flags.force, flags.machineLocal);
|
|
291
|
+
else if (action === "update") report = await d.daemon.setupUpdate(path);
|
|
292
|
+
else if (action === "plan") report = await d.daemon.setupPlan(path, flags.prune);
|
|
293
|
+
else report = await d.daemon.setupApply(path, flags.approved, flags.prune);
|
|
294
|
+
} else {
|
|
295
|
+
const manager = d.setup ?? new SetupManager(d.reg, d.inst, d.piHome);
|
|
296
|
+
if (action === "export") report = await manager.export(path, { force: flags.force, machineLocal: flags.machineLocal });
|
|
297
|
+
else if (action === "update") report = await manager.update(path);
|
|
298
|
+
else if (action === "plan") report = await manager.plan(path, { prune: flags.prune });
|
|
299
|
+
else {
|
|
300
|
+
assertPackagePermission(await d.security.security(), "setup.apply", flags.approved);
|
|
301
|
+
report = await manager.apply(path, { prune: flags.prune });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const output = formatSetupReport(report, flags.json);
|
|
305
|
+
return report.ok ? ok(output) : fail(output);
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
publish: {
|
|
310
|
+
usage: "packed publish setup [path] [--force] [--json] | packed publish status [path] [--json] [--open-browser]",
|
|
311
|
+
async run(_rest, d, flags, pos) {
|
|
312
|
+
const action = pos[0];
|
|
313
|
+
if (action !== "setup" && action !== "status") return usageErr(`usage: ${commands.publish!.usage}\n`);
|
|
314
|
+
const path = resolve(pos[1] ?? ".");
|
|
315
|
+
const manager = d.publisher ?? new PublishManager(d.reg);
|
|
316
|
+
const report = action === "setup" ? await manager.setup(path, { force: flags.force }) : await manager.status(path);
|
|
317
|
+
const output = formatPublishReport(report, flags.json);
|
|
318
|
+
return ("ready" in report ? report.ready : report.ok) ? ok(output) : fail(output);
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
pack: {
|
|
323
|
+
usage: "packed pack [path] [--json]",
|
|
324
|
+
async run(_rest, d, flags, pos) {
|
|
325
|
+
const path = resolve(pos[0] ?? ".");
|
|
326
|
+
const report = d.daemon ? await d.daemon.pack(path) : await (d.packer ?? new NpmPackVerifier()).verify(path);
|
|
327
|
+
return report.ok ? ok(formatPackReport(report, flags.json)) : fail(formatPackReport(report, flags.json));
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
score: {
|
|
332
|
+
usage: "packed score [path|name] [--json]",
|
|
333
|
+
async run(_rest, d, flags, pos) {
|
|
334
|
+
const target = pos[0] ?? ".";
|
|
335
|
+
const report = d.daemon
|
|
336
|
+
? await d.daemon.score(target)
|
|
337
|
+
: d.scorer
|
|
338
|
+
? await d.scorer.score(target)
|
|
339
|
+
: await scoreTarget(target, d.reg, d.packer);
|
|
340
|
+
return ok(formatAdoptionReport(report, flags.json));
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
check: {
|
|
345
|
+
usage: "packed check [path] [--smoke] [--json]",
|
|
346
|
+
async run(_rest, d, flags, pos) {
|
|
347
|
+
const path = resolve(pos[0] ?? ".");
|
|
348
|
+
const report = d.daemon ? await d.daemon.check(path, flags.smoke) : await checkPackage(path, { smoke: flags.smoke });
|
|
349
|
+
const output = formatCheckReport(report, flags.json);
|
|
350
|
+
return report.ok ? ok(output) : fail(output);
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
doctor: {
|
|
355
|
+
usage: "packed doctor [--project <path>] [--json]",
|
|
356
|
+
async run(_rest, d, flags) {
|
|
357
|
+
const report = d.daemon ? await d.daemon.doctor(flags.project) : await runDoctor(d.piHome, flags.project);
|
|
358
|
+
const output = formatDoctorReport(report, flags.json);
|
|
359
|
+
return report.ok ? ok(output) : fail(output);
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
|
|
363
|
+
search: {
|
|
364
|
+
usage: "packed search <query> [--offline] [--limit N] [--json]",
|
|
365
|
+
async run(_rest, d, flags, pos) {
|
|
366
|
+
const q = pos[0];
|
|
367
|
+
if (!q) return usageErr(`usage: ${commands.search!.usage}\n`);
|
|
368
|
+
const limit = clampLimit(flags.limit, SEARCH_DEFAULT_LIMIT, SEARCH_MAX_LIMIT);
|
|
369
|
+
// --offline: query the SQLite mirror only (apt-cache search analog)
|
|
370
|
+
if (flags.offline) {
|
|
371
|
+
let results: Pkg[];
|
|
372
|
+
if (d.daemon) {
|
|
373
|
+
results = (await d.daemon.search(q, limit, true)).results;
|
|
374
|
+
} else {
|
|
375
|
+
const db = openDb(dbPath(dataDirectory(d)));
|
|
376
|
+
try {
|
|
377
|
+
results = searchLocal(db, q, limit);
|
|
378
|
+
} finally {
|
|
379
|
+
db.close();
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (flags.json) return ok(`${JSON.stringify({ query: q, total: results.length, results, offline: true })}\n`);
|
|
383
|
+
if (results.length === 0) return ok(`no mirrored packages match "${q}" (run: packed mirror)\n`);
|
|
384
|
+
let out = `${results.length} mirrored package(s):\n\n`;
|
|
385
|
+
for (const p of results) out += ` ${p.name}@${p.version} ${evidenceLabel(p)}\n ${p.description ?? ""}\n`;
|
|
386
|
+
return ok(out);
|
|
387
|
+
}
|
|
388
|
+
const { results, total } = await d.reg.search(buildSearchQuery(q), limit);
|
|
389
|
+
if (flags.json) return ok(`${JSON.stringify({ query: q, total, results })}\n`);
|
|
390
|
+
if (results.length === 0) return ok(`no pi packages found for "${q}"\n`);
|
|
391
|
+
let out = `${total} package(s) (showing ${results.length}):\n\n`;
|
|
392
|
+
for (const p of results) out += ` ${p.name}@${p.version} ${evidenceLabel(p)}\n ${p.description ?? ""}\n`;
|
|
393
|
+
return ok(out);
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
mirror: {
|
|
398
|
+
usage: "packed mirror [--json] (sync the upstream registry into the local SQLite index — the apt update analog)",
|
|
399
|
+
async run(_rest, d, flags) {
|
|
400
|
+
const n = d.daemon ? await d.daemon.mirror() : await syncCatalog(d.reg, dataDirectory(d));
|
|
401
|
+
if (flags.json) return ok(`${JSON.stringify({ synced: n })}\n`);
|
|
402
|
+
return ok(`mirrored ${n} packages into the local index\n`);
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
|
|
406
|
+
index: {
|
|
407
|
+
usage:
|
|
408
|
+
"packed index build [--json] | packed index status [--json] (local static adoption-scoring snapshot, npm-only -- createrepo/dpkg-scanpackages analog)",
|
|
409
|
+
async run(_rest, d, flags, pos) {
|
|
410
|
+
const action = pos[0];
|
|
411
|
+
if (action !== "build" && action !== "status") return usageErr(`usage: ${commands.index!.usage}\n`);
|
|
412
|
+
if (action === "build") {
|
|
413
|
+
const index = d.daemon ? await d.daemon.indexBuild() : await generateIndex(d.reg, dataDirectory(d), indexPath(dataDirectory(d)));
|
|
414
|
+
if (flags.json) return ok(`${JSON.stringify(index)}\n`);
|
|
415
|
+
return ok(
|
|
416
|
+
`generated a static index of ${index.packages.length} packages (generatedAt ${index.generatedAt})${index.truncated ? " -- truncated, the local catalog holds more entries than this run's bound" : ""}\n`,
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
const status = d.daemon ? await d.daemon.index() : readIndex(indexPath(dataDirectory(d)));
|
|
420
|
+
if (flags.json) return ok(`${JSON.stringify(status ?? null)}\n`);
|
|
421
|
+
if (!status) return ok("no local index generated yet -- run packed index build\n");
|
|
422
|
+
return ok(
|
|
423
|
+
`local index: ${status.packages.length} packages, generated ${status.generatedAt}${status.truncated ? " (truncated)" : ""}\n`,
|
|
424
|
+
);
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
|
|
428
|
+
info: {
|
|
429
|
+
usage: "packed info <name> [--json]",
|
|
430
|
+
async run(_rest, d, flags, pos) {
|
|
431
|
+
const name = pos[0];
|
|
432
|
+
if (!name) return usageErr(`usage: ${commands.info!.usage}\n`);
|
|
433
|
+
const info = await d.reg.info(name);
|
|
434
|
+
if (flags.json) return ok(`${JSON.stringify(info)}\n`);
|
|
435
|
+
let out = `${info.name}@${info.version}\n${info.description ?? ""}\n`;
|
|
436
|
+
if (info.repository) out += `repo: ${info.repository}\n`;
|
|
437
|
+
if (info.pi) out += `provides: ${Object.keys(info.pi).join(", ")}\n`;
|
|
438
|
+
if (info.packageEvidence)
|
|
439
|
+
out += `shape: ${info.packageEvidence.shape}${info.packageEvidence.verified ? " (tarball verified)" : " (metadata only)"}\n`;
|
|
440
|
+
if (info.publication?.provenanceUrl) out += `provenance: ${info.publication.provenanceUrl}\n`;
|
|
441
|
+
out += `trusted publisher: ${info.publication?.trustedPublisher ?? "unknown"}\n`;
|
|
442
|
+
return ok(out);
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
|
|
446
|
+
updates: {
|
|
447
|
+
usage: "packed updates [--project <path>] [--json] (from the local mirror — run `packed mirror` first)",
|
|
448
|
+
async run(_rest, d, flags) {
|
|
449
|
+
let updates: UpdateEntry[];
|
|
450
|
+
if (flags.project) {
|
|
451
|
+
updates = d.daemon
|
|
452
|
+
? await d.daemon.updatesForProject(flags.project)
|
|
453
|
+
: await (async () => {
|
|
454
|
+
const db = openDb(dbPath(dataDirectory(d)));
|
|
455
|
+
try {
|
|
456
|
+
return checkUpdates((name) => latestVersion(db, name), readInstalledPackagesAcrossScopes(d.piHome, flags.project));
|
|
457
|
+
} finally {
|
|
458
|
+
db.close();
|
|
459
|
+
}
|
|
460
|
+
})();
|
|
461
|
+
} else if (d.daemon) {
|
|
462
|
+
updates = await d.daemon.updates();
|
|
463
|
+
} else {
|
|
464
|
+
const db = openDb(dbPath(dataDirectory(d)));
|
|
465
|
+
try {
|
|
466
|
+
updates = checkUpdates((name) => latestVersion(db, name), readInstalledPackages(d.piHome));
|
|
467
|
+
} finally {
|
|
468
|
+
db.close();
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (flags.json) {
|
|
472
|
+
return ok(`${JSON.stringify({ checkedAt: new Date().toISOString(), updates })}\n`);
|
|
473
|
+
}
|
|
474
|
+
if (updates.length === 0) return ok("all pi packages up to date (per the local mirror)\n");
|
|
475
|
+
let out = `${updates.length} update(s) available:\n\n`;
|
|
476
|
+
for (const u of updates) out += ` ${u.name}${u.scope ? ` [${u.scope}]` : ""} ${u.installed} → ${u.latest}\n`;
|
|
477
|
+
return ok(`${out}\nrun: pi update --extensions\n`);
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
|
|
481
|
+
pi: {
|
|
482
|
+
usage: "packed pi status [--json]",
|
|
483
|
+
async run(_rest, d, flags, pos) {
|
|
484
|
+
if (pos[0] !== "status") return usageErr(`usage: ${commands.pi!.usage}\n`);
|
|
485
|
+
const report = d.daemon ? await d.daemon.piStatus() : await (d.piVersion ?? { check: checkPiVersion }).check();
|
|
486
|
+
if (flags.json) return ok(`${JSON.stringify(report)}\n`);
|
|
487
|
+
return ok(formatPiVersionReport(report));
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
|
|
491
|
+
advisories: {
|
|
492
|
+
usage:
|
|
493
|
+
"packed advisories [name] [--json] (no lockfile required; scans installed npm-sourced Pi packages against npm's bulk advisory endpoint)",
|
|
494
|
+
async run(_rest, d, flags, pos) {
|
|
495
|
+
const name = pos[0];
|
|
496
|
+
const report = d.daemon ? await d.daemon.advisoriesScan(name) : await scanInstalledPackages(resolveInstalledVersions(d.piHome, name));
|
|
497
|
+
return ok(formatAdvisoryReport(report, flags.json));
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
|
|
501
|
+
resources: {
|
|
502
|
+
usage:
|
|
503
|
+
"packed resources list [--project <path>] [--json] | packed resources toggle <source> <field> <path> <on|off> [--project <path>] [--approve] [--json]",
|
|
504
|
+
async run(_rest, d, flags, pos) {
|
|
505
|
+
const action = pos[0];
|
|
506
|
+
if (action === "list") {
|
|
507
|
+
const result = d.daemon ? await d.daemon.resourcesList(flags.project) : listPackageResources(d.piHome, flags.project);
|
|
508
|
+
if (flags.json) return ok(`${JSON.stringify(result)}\n`);
|
|
509
|
+
return ok(formatResourcesList(result));
|
|
510
|
+
}
|
|
511
|
+
if (action === "toggle") {
|
|
512
|
+
const [source, field, path, state] = [pos[1] ?? "", pos[2] ?? "", pos[3] ?? "", pos[4] ?? ""];
|
|
513
|
+
if (!source || !RESOURCE_FIELDS.includes(field as ResourceField) || !path || (state !== "on" && state !== "off")) {
|
|
514
|
+
return usageErr(`usage: ${commands.resources!.usage}\n`);
|
|
515
|
+
}
|
|
516
|
+
const enabled = state === "on";
|
|
517
|
+
try {
|
|
518
|
+
let output: string;
|
|
519
|
+
if (d.daemon) {
|
|
520
|
+
output = await d.daemon.resourcesToggle(source, field as ResourceField, path, enabled, flags.project, flags.approved);
|
|
521
|
+
} else {
|
|
522
|
+
assertPackagePermission(await d.security.security(), "resources.toggle", flags.approved);
|
|
523
|
+
const settingsPath = resolveToggleSettingsPath(d.piHome, flags.project);
|
|
524
|
+
if (flags.project && !existsSync(settingsPath)) throw new Error("no project settings file to toggle");
|
|
525
|
+
const result = await toggleResource({ settingsPath, source, field: field as ResourceField, path, enabled });
|
|
526
|
+
if (!result.ok) throw new Error(result.error ?? "toggle failed");
|
|
527
|
+
output = `${enabled ? "enabled" : "disabled"} ${path}`;
|
|
528
|
+
}
|
|
529
|
+
return flags.json ? ok(`${JSON.stringify({ ok: true, source, field, path, enabled, output })}\n`) : ok(`${output}\n`);
|
|
530
|
+
} catch (e) {
|
|
531
|
+
const error = e instanceof Error ? e.message : String(e);
|
|
532
|
+
return flags.json ? fail(`${JSON.stringify({ ok: false, source, field, path, enabled, error })}\n`) : fail(`${error}\n`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return usageErr(`usage: ${commands.resources!.usage}\n`);
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
|
|
539
|
+
installed: {
|
|
540
|
+
usage: "packed installed [--json]",
|
|
541
|
+
async run(_rest, d, flags) {
|
|
542
|
+
const installed = d.daemon ? await d.daemon.installed() : readInstalledPackages(d.piHome);
|
|
543
|
+
if (flags.json) return ok(`${JSON.stringify(installed)}\n`);
|
|
544
|
+
return ok(installed.map((p) => ` ${p.name}@${p.pinned ?? p.installed ?? "?"}\n`).join(""));
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
|
|
548
|
+
catalog: {
|
|
549
|
+
usage: "packed catalog [--json]",
|
|
550
|
+
async run(_rest, d, flags) {
|
|
551
|
+
let snapshot: { fetchedAt?: string; sha256?: string; packages: Pkg[] };
|
|
552
|
+
if (d.daemon) {
|
|
553
|
+
snapshot = await d.daemon.catalog();
|
|
554
|
+
} else {
|
|
555
|
+
const db = openDb(dbPath(dataDirectory(d)));
|
|
556
|
+
try {
|
|
557
|
+
const meta = getSyncMeta(db);
|
|
558
|
+
snapshot = { fetchedAt: meta?.fetchedAt, sha256: meta?.sha256, packages: catalogList(db) };
|
|
559
|
+
} finally {
|
|
560
|
+
db.close();
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (flags.json) return ok(`${JSON.stringify(snapshot)}\n`);
|
|
564
|
+
let out = `${snapshot.packages.length} packages in the local index`;
|
|
565
|
+
if (snapshot.fetchedAt && snapshot.sha256) out += ` (synced ${snapshot.fetchedAt}, sha256:${snapshot.sha256.slice(0, 12)}…)`;
|
|
566
|
+
out += "\n\n";
|
|
567
|
+
for (const p of snapshot.packages.slice(0, 50)) out += ` ${p.name}@${p.version} ${evidenceLabel(p)}\n`;
|
|
568
|
+
return ok(out);
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
|
|
572
|
+
install: {
|
|
573
|
+
usage: "packed install npm:<pkg>[@ver] | git:<host>/<owner>/<repo>[@ref] | https://… [--no-service] [--json]",
|
|
574
|
+
async run(_rest, d, flags, pos) {
|
|
575
|
+
const source = pos[0] ?? "";
|
|
576
|
+
if (!SOURCE_RE.test(source)) return usageErr(`usage: ${commands.install!.usage}\n`);
|
|
577
|
+
try {
|
|
578
|
+
const output = await d.inst.install(source, { approved: flags.approved });
|
|
579
|
+
// A package's own persistent-service registration piggybacks on the same
|
|
580
|
+
// approval already granted for install -- both are the same "code-execution"
|
|
581
|
+
// mutation tier, so this isn't a new consent surface. Silent for the
|
|
582
|
+
// overwhelmingly common case (most Pi packages aren't daemons at all);
|
|
583
|
+
// --no-service skips the attempt entirely, and a genuine failure (detected
|
|
584
|
+
// a daemon but couldn't register it) is reported without failing the
|
|
585
|
+
// install itself, which already succeeded.
|
|
586
|
+
let serviceInstall: { detected: true; ok: boolean; output: string } | undefined;
|
|
587
|
+
if (!flags.noService && source.startsWith("npm:") && d.daemonService) {
|
|
588
|
+
try {
|
|
589
|
+
const svc = await d.daemonService.install(source, flags.approved);
|
|
590
|
+
serviceInstall = { detected: true, ok: true, output: svc.output };
|
|
591
|
+
} catch (e) {
|
|
592
|
+
if (!(e instanceof Error) || !(e as { notADaemon?: boolean }).notADaemon) {
|
|
593
|
+
serviceInstall = { detected: true, ok: false, output: e instanceof Error ? e.message : String(e) };
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (flags.json) return ok(`${JSON.stringify({ ok: true, source, output, ...(serviceInstall ? { serviceInstall } : {}) })}\n`);
|
|
598
|
+
let human = `${output}\n`;
|
|
599
|
+
if (serviceInstall?.ok) human += `${serviceInstall.output}\n`;
|
|
600
|
+
else if (serviceInstall && !serviceInstall.ok)
|
|
601
|
+
human += `note: detected a persistent-service daemon but could not register it: ${serviceInstall.output}\n`;
|
|
602
|
+
return ok(human);
|
|
603
|
+
} catch (e) {
|
|
604
|
+
const error = e instanceof Error ? e.message : String(e);
|
|
605
|
+
return flags.json ? fail(`${JSON.stringify({ ok: false, source, error })}\n`) : fail(`${error}\n`);
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
|
|
610
|
+
"install-service": {
|
|
611
|
+
usage:
|
|
612
|
+
"packed install-service npm:<pkg>[@ver] --approve [--json] (registers the package's own daemon as a persistent login/boot service; see its packed.daemonService manifest)",
|
|
613
|
+
async run(_rest, d, flags, pos) {
|
|
614
|
+
const source = pos[0] ?? "";
|
|
615
|
+
if (!SOURCE_RE.test(source)) return usageErr(`usage: ${commands["install-service"]!.usage}\n`);
|
|
616
|
+
if (!d.daemonService) return fail("install-service requires a running packed daemon\n");
|
|
617
|
+
try {
|
|
618
|
+
const { output, spec } = await d.daemonService.install(source, flags.approved);
|
|
619
|
+
return flags.json ? ok(`${JSON.stringify({ ok: true, source, output, spec })}\n`) : ok(`${output}\n`);
|
|
620
|
+
} catch (e) {
|
|
621
|
+
const error = e instanceof Error ? e.message : String(e);
|
|
622
|
+
return flags.json ? fail(`${JSON.stringify({ ok: false, source, error })}\n`) : fail(`${error}\n`);
|
|
623
|
+
}
|
|
624
|
+
},
|
|
625
|
+
},
|
|
626
|
+
|
|
627
|
+
"restart-service": {
|
|
628
|
+
usage: "packed restart-service npm:<pkg>[@ver] --approve [--json] (restarts a package's already-registered persistent service)",
|
|
629
|
+
async run(_rest, d, flags, pos) {
|
|
630
|
+
const source = pos[0] ?? "";
|
|
631
|
+
if (!SOURCE_RE.test(source)) return usageErr(`usage: ${commands["restart-service"]!.usage}\n`);
|
|
632
|
+
if (!d.daemonService) return fail("restart-service requires a running packed daemon\n");
|
|
633
|
+
try {
|
|
634
|
+
const { output, restarted, spec } = await d.daemonService.restart(source, flags.approved);
|
|
635
|
+
return flags.json ? ok(`${JSON.stringify({ ok: true, source, output, restarted, spec })}\n`) : ok(`${output}\n`);
|
|
636
|
+
} catch (e) {
|
|
637
|
+
const error = e instanceof Error ? e.message : String(e);
|
|
638
|
+
return flags.json ? fail(`${JSON.stringify({ ok: false, source, error })}\n`) : fail(`${error}\n`);
|
|
639
|
+
}
|
|
640
|
+
},
|
|
641
|
+
},
|
|
642
|
+
|
|
643
|
+
security: {
|
|
644
|
+
usage: "packed security [always|never] [--json]",
|
|
645
|
+
async run(_rest, d, flags, pos) {
|
|
646
|
+
const requested = pos[0];
|
|
647
|
+
if (requested !== undefined && requested !== "always" && requested !== "never") {
|
|
648
|
+
return usageErr(`usage: ${commands.security!.usage}\n`);
|
|
649
|
+
}
|
|
650
|
+
const settings = requested
|
|
651
|
+
? await d.security.setMutationApproval(requested as MutationApproval, { approved: flags.approved })
|
|
652
|
+
: await d.security.security();
|
|
653
|
+
return flags.json ? ok(`${JSON.stringify(settings)}\n`) : ok(`package mutation approval: ${settings.mutationApproval}\n`);
|
|
654
|
+
},
|
|
655
|
+
},
|
|
656
|
+
|
|
657
|
+
update: {
|
|
658
|
+
usage: "packed update <configured-source> [--approve] [--json] | packed update --self [--approve] [--json]",
|
|
659
|
+
async run(_rest, d, flags, pos) {
|
|
660
|
+
if (flags.self) {
|
|
661
|
+
try {
|
|
662
|
+
assertPackagePermission(await d.security.security(), "update.self", flags.approved);
|
|
663
|
+
} catch (e) {
|
|
664
|
+
const error = e instanceof Error ? e.message : String(e);
|
|
665
|
+
return flags.json ? fail(`${JSON.stringify({ ok: false, error })}\n`) : fail(`${error}\n`);
|
|
666
|
+
}
|
|
667
|
+
if (!d.selfUpdater) return fail("update --self requires a running packed daemon\n");
|
|
668
|
+
const report = await d.selfUpdater.run();
|
|
669
|
+
if (flags.json) return report.ok ? ok(`${JSON.stringify(report)}\n`) : fail(`${JSON.stringify(report)}\n`);
|
|
670
|
+
const transition =
|
|
671
|
+
report.latestVersion && report.previousVersion !== report.latestVersion
|
|
672
|
+
? ` (${report.previousVersion} → ${report.latestVersion})`
|
|
673
|
+
: "";
|
|
674
|
+
return report.ok ? ok(`${report.message}${transition}\n`) : fail(`${report.message}\n`);
|
|
675
|
+
}
|
|
676
|
+
const source = pos[0] ?? "";
|
|
677
|
+
if (!SOURCE_RE.test(source)) return usageErr(`usage: ${commands.update!.usage}\n`);
|
|
678
|
+
try {
|
|
679
|
+
const outcome = await d.inst.update(source, { approved: flags.approved });
|
|
680
|
+
if (outcome.alreadyUpToDate) {
|
|
681
|
+
if (flags.json) return ok(`${JSON.stringify({ ok: true, source, ...outcome })}\n`);
|
|
682
|
+
const version = outcome.currentVersion ?? outcome.previousVersion;
|
|
683
|
+
const reason = outcome.pinned
|
|
684
|
+
? `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`
|
|
685
|
+
: `is already up to date${version ? ` at ${version}` : ""}`;
|
|
686
|
+
return ok(`${source} ${reason}\n`);
|
|
687
|
+
}
|
|
688
|
+
// Mirrors install's own auto-registration composition: same approval tier, silent for
|
|
689
|
+
// the common non-daemon case, a failure reported without failing the update itself.
|
|
690
|
+
let serviceRestart: { detected: true; ok: boolean; output: string } | undefined;
|
|
691
|
+
if (!flags.noService && source.startsWith("npm:") && d.daemonService) {
|
|
692
|
+
try {
|
|
693
|
+
const svc = await d.daemonService.restart(source, flags.approved);
|
|
694
|
+
serviceRestart = { detected: true, ok: true, output: svc.output };
|
|
695
|
+
} catch (e) {
|
|
696
|
+
if (!(e instanceof Error) || !(e as { notADaemon?: boolean }).notADaemon) {
|
|
697
|
+
serviceRestart = { detected: true, ok: false, output: e instanceof Error ? e.message : String(e) };
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
if (flags.json) return ok(`${JSON.stringify({ ok: true, source, ...outcome, ...(serviceRestart ? { serviceRestart } : {}) })}\n`);
|
|
702
|
+
const transition =
|
|
703
|
+
outcome.previousVersion && outcome.currentVersion ? ` (${outcome.previousVersion} → ${outcome.currentVersion})` : "";
|
|
704
|
+
let human = `${outcome.output}${transition}\n`;
|
|
705
|
+
if (serviceRestart?.ok) human += `${serviceRestart.output}\n`;
|
|
706
|
+
else if (serviceRestart && !serviceRestart.ok)
|
|
707
|
+
human += `note: could not restart its persistent service: ${serviceRestart.output}\n`;
|
|
708
|
+
return ok(`${human}Reload Pi with /reload to activate the updated package.\n`);
|
|
709
|
+
} catch (error) {
|
|
710
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
711
|
+
return flags.json
|
|
712
|
+
? fail(`${JSON.stringify({ ok: false, source, error: message, reloadRequired: false })}\n`)
|
|
713
|
+
: fail(`${message}\n`);
|
|
714
|
+
}
|
|
715
|
+
},
|
|
716
|
+
},
|
|
717
|
+
|
|
718
|
+
remove: {
|
|
719
|
+
usage: "packed remove <name> [--approve] [--json] (bare npm name, e.g. pi-lsp or @scope/pkg)",
|
|
720
|
+
async run(_rest, d, flags, pos) {
|
|
721
|
+
const name = pos[0] ?? "";
|
|
722
|
+
if (!NAME_RE.test(name)) return usageErr(`usage: ${commands.remove!.usage}\n`);
|
|
723
|
+
try {
|
|
724
|
+
const output = await d.inst.remove(`npm:${name}`, { approved: flags.approved });
|
|
725
|
+
return flags.json ? ok(`${JSON.stringify({ ok: true, name, output })}\n`) : ok(`${output}\n`);
|
|
726
|
+
} catch (e) {
|
|
727
|
+
const error = e instanceof Error ? e.message : String(e);
|
|
728
|
+
return flags.json ? fail(`${JSON.stringify({ ok: false, name, error })}\n`) : fail(`${error}\n`);
|
|
729
|
+
}
|
|
730
|
+
},
|
|
731
|
+
},
|
|
732
|
+
|
|
733
|
+
service: {
|
|
734
|
+
usage: "packed service (print a systemd user unit to stdout)",
|
|
735
|
+
async run(_rest, d) {
|
|
736
|
+
const execPath = d.execPath ?? process.execPath;
|
|
737
|
+
const cliPath = d.cliPath ?? new URL("./cli.ts", import.meta.url).pathname;
|
|
738
|
+
return ok(renderUnit(execPath, cliPath, d.piBin ?? defaultPiBinForUnit()));
|
|
739
|
+
},
|
|
740
|
+
},
|
|
741
|
+
|
|
742
|
+
version: {
|
|
743
|
+
usage: "packed version [--json]",
|
|
744
|
+
async run(rest, d) {
|
|
745
|
+
const json = rest.includes("--json");
|
|
746
|
+
const daemonVersion = d.daemonVersionCheck
|
|
747
|
+
? await d.daemonVersionCheck()
|
|
748
|
+
: await (async () => {
|
|
749
|
+
const { probe } = await import("../daemon/client.ts");
|
|
750
|
+
return (await probe())?.version;
|
|
751
|
+
})();
|
|
752
|
+
const report = buildVersionReport(VERSION, daemonVersion);
|
|
753
|
+
return ok(json ? `${JSON.stringify(report)}\n` : formatVersionReport(report));
|
|
754
|
+
},
|
|
755
|
+
},
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
/** systemd user unit, delegating to vehicle-server's shared generateSystemdUnit -- idle self-exit
|
|
759
|
+
* is disabled (PI_PACKED_IDLE_SECS=0) since systemd's own Restart=always takes over lifecycle. This
|
|
760
|
+
* only ever prints (see the `service` command above); nothing here calls installUserService, so
|
|
761
|
+
* descriptorPath is never actually read/written -- required by ServiceSpec's shape regardless. */
|
|
762
|
+
export function renderUnit(execPath: string, cliPath: string, piBin?: string): string {
|
|
763
|
+
// systemd does not read shell rc files: PI_BIN must be explicit so the
|
|
764
|
+
// daemon's install/remove execs can find the pi binary.
|
|
765
|
+
const env: Record<string, string> = { PI_PACKED_IDLE_SECS: "0" };
|
|
766
|
+
if (piBin) env.PI_BIN = piBin;
|
|
767
|
+
return generateSystemdUnit({
|
|
768
|
+
name: "pi-packed",
|
|
769
|
+
displayName: "pi-packed package service (Pi agent)",
|
|
770
|
+
binPath: execPath,
|
|
771
|
+
args: [cliPath, "serve"],
|
|
772
|
+
env,
|
|
773
|
+
descriptorPath: resolvePackedPaths().serviceDescriptor,
|
|
774
|
+
// Packed's own client (connectPackageDaemon) never auto-spawns -- "start packed.service"
|
|
775
|
+
// or this unit's own systemd supervision is its only recovery path.
|
|
776
|
+
restartOnFailure: true,
|
|
777
|
+
restartSec: 2,
|
|
778
|
+
noNewPrivileges: true,
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
export async function cliRun(args: string[], d: CliDeps): Promise<CliResult> {
|
|
783
|
+
const [name, ...rest] = args;
|
|
784
|
+
if (!name) return usageErr(USAGE);
|
|
785
|
+
if (name === "help" || name === "--help" || name === "-h") return { code: 0, out: USAGE };
|
|
786
|
+
const cmd = commands[name];
|
|
787
|
+
if (!cmd) return usageErr(`unknown command "${name}"\n${USAGE}`);
|
|
788
|
+
const { flags, pos } = parseFlags(rest);
|
|
789
|
+
try {
|
|
790
|
+
const validMutationInput =
|
|
791
|
+
name === "install" || name === "update" || name === "install-service" || name === "restart-service"
|
|
792
|
+
? SOURCE_RE.test(pos[0] ?? "")
|
|
793
|
+
: name === "remove"
|
|
794
|
+
? NAME_RE.test(pos[0] ?? "")
|
|
795
|
+
: name === "security"
|
|
796
|
+
? pos[0] === undefined || pos[0] === "always" || pos[0] === "never"
|
|
797
|
+
: true;
|
|
798
|
+
const operation = name === "security" ? (pos[0] === undefined ? "security.read" : "security.write") : PACKAGE_COMMAND_OPERATIONS[name];
|
|
799
|
+
if (operation && validMutationInput) {
|
|
800
|
+
const classification = packageOperationClassification(operation);
|
|
801
|
+
if (classification === "code-execution" || classification === "settings-mutation" || classification === "security-mutation") {
|
|
802
|
+
assertPackagePermission(await d.security.security(), operation, flags.approved);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return await cmd.run(rest, d, flags, pos);
|
|
806
|
+
} catch (e) {
|
|
807
|
+
return fail(`${name} failed: ${e instanceof Error ? e.message : e}\n`);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// Entry point (bun src/cli/cli.ts …). `serve` is dispatched before any proxying
|
|
812
|
+
// so the daemon always talks directly to npm.
|
|
813
|
+
/** Bounded, TTY-only y/n prompt -- never invoked non-interactively, never
|
|
814
|
+
* assumes an answer. Lives outside cliRun so the pure command table never
|
|
815
|
+
* touches real stdin. */
|
|
816
|
+
function readLine(): Promise<string> {
|
|
817
|
+
return new Promise((resolveLine) => {
|
|
818
|
+
process.stdin.resume();
|
|
819
|
+
process.stdin.setEncoding("utf8");
|
|
820
|
+
process.stdin.once("data", (data: Buffer) => {
|
|
821
|
+
process.stdin.pause();
|
|
822
|
+
resolveLine(data.toString("utf8"));
|
|
823
|
+
});
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
async function confirmInteractive(question: string): Promise<boolean> {
|
|
828
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
|
|
829
|
+
process.stdout.write(`${question} [y/N] `);
|
|
830
|
+
return (await readLine()).trim().toLowerCase().startsWith("y");
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
async function waitForEnter(prompt: string): Promise<void> {
|
|
834
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return;
|
|
835
|
+
process.stdout.write(prompt);
|
|
836
|
+
await readLine();
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/** Smooth, opt-in handoff to npm's own web UI for both login and trust --
|
|
840
|
+
* never publishing, never touching a token, never running an npm CLI
|
|
841
|
+
* subcommand we'd have to keep in sync with npm's own flags, and never run
|
|
842
|
+
* unless a human is actually watching a real terminal and says yes to each
|
|
843
|
+
* step. Headless by default: prints the URL and waits for Enter, exactly
|
|
844
|
+
* like npm's own `--no-browser` config -- nothing here ever launches a
|
|
845
|
+
* real browser unless the human passed --open-browser, since this runs
|
|
846
|
+
* unattended just as often as it runs on someone's own desktop. */
|
|
847
|
+
async function runPublishInteractive(path: string, reg: Registry, openBrowserAuto: boolean): Promise<boolean> {
|
|
848
|
+
const manager = new PublishManager(reg);
|
|
849
|
+
let report = await manager.status(path);
|
|
850
|
+
if (!report.checks.loggedIn) {
|
|
851
|
+
if (await confirmInteractive("Not logged in to npm on this machine. Run npm login --auth-type=web now?")) {
|
|
852
|
+
const result = await runNpmLoginWeb(openBrowserAuto);
|
|
853
|
+
if (result.ok) report = await manager.status(path);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
if (report.checks.trustedPublisher !== "verified" && report.packageName) {
|
|
857
|
+
const accessUrl = npmWebUrl(report.packageName);
|
|
858
|
+
if (await confirmInteractive(`Configure npm Trusted Publisher for ${report.packageName} now?`)) {
|
|
859
|
+
if (openBrowserAuto) await openBrowser(accessUrl);
|
|
860
|
+
await waitForEnter(
|
|
861
|
+
`${openBrowserAuto ? "Opened (or open manually)" : "Open this URL"}: ${accessUrl}\nConfigure "Trusted publisher" for the GitHub Actions workflow there, then press Enter to continue... `,
|
|
862
|
+
);
|
|
863
|
+
report = await manager.status(path);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
process.stdout.write(formatPublishReport(report, false));
|
|
867
|
+
return report.ready;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
if (import.meta.main) {
|
|
871
|
+
const args = process.argv.slice(2);
|
|
872
|
+
if (args[0] === "serve") {
|
|
873
|
+
const { serveMain } = await import("../daemon/daemon.ts");
|
|
874
|
+
serveMain();
|
|
875
|
+
} else {
|
|
876
|
+
const { migrateLegacyPackedState, resolvePackedPaths } = await import("../shared/paths.ts");
|
|
877
|
+
const { dirname } = await import("node:path");
|
|
878
|
+
const { defaultPiHome } = await import("../packages/installed.ts");
|
|
879
|
+
const { connectPackageDaemon, DaemonBackedSecurity, DaemonBackedInstaller, DaemonBackedDaemonServiceInstaller, resolveRegistry } =
|
|
880
|
+
await import("../daemon/client.ts");
|
|
881
|
+
const paths = resolvePackedPaths();
|
|
882
|
+
migrateLegacyPackedState(paths);
|
|
883
|
+
const dir = paths.stateDirectory;
|
|
884
|
+
// mirror talks to UPSTREAM, not the daemon cache — apt update semantics.
|
|
885
|
+
const reg =
|
|
886
|
+
args[0] === "mirror"
|
|
887
|
+
? new (await import("../registry/registry.ts")).HttpRegistry(NPM_REGISTRY_BASE, SEARCH_PAGE_SIZE, MIRROR_PAGE_DELAY_MS)
|
|
888
|
+
: await resolveRegistry(paths, NPM_REGISTRY_BASE);
|
|
889
|
+
const daemon = await connectPackageDaemon(paths).catch(() => undefined);
|
|
890
|
+
const { code, out } = await cliRun(args, {
|
|
891
|
+
reg,
|
|
892
|
+
daemon,
|
|
893
|
+
inst: new DaemonBackedInstaller(paths),
|
|
894
|
+
daemonService: new DaemonBackedDaemonServiceInstaller(paths),
|
|
895
|
+
security: new DaemonBackedSecurity(paths),
|
|
896
|
+
stateDir: dir,
|
|
897
|
+
dataDir: dirname(paths.database),
|
|
898
|
+
piHome: defaultPiHome(),
|
|
899
|
+
selfUpdater: {
|
|
900
|
+
run: () =>
|
|
901
|
+
runSelfUpdate({
|
|
902
|
+
registry: reg,
|
|
903
|
+
isServiceInstalled: () => existsSync(paths.serviceDescriptor),
|
|
904
|
+
restartService:
|
|
905
|
+
process.platform === "linux" ? () => runInherited(["systemctl", "--user", "restart", "pi-packed.service"]) : undefined,
|
|
906
|
+
}),
|
|
907
|
+
},
|
|
908
|
+
});
|
|
909
|
+
const interactive = process.stdin.isTTY && process.stdout.isTTY && !args.includes("--json");
|
|
910
|
+
if (interactive && args[0] === "publish" && args[1] === "status") {
|
|
911
|
+
const ready = await runPublishInteractive(resolve(args[2] ?? "."), reg, args.includes("--open-browser"));
|
|
912
|
+
process.exit(ready ? 0 : 1);
|
|
913
|
+
}
|
|
914
|
+
if (interactive && args[0] === "pi" && args[1] === "status") {
|
|
915
|
+
const report = await runPiStatusInteractive({
|
|
916
|
+
check: () => (daemon ? daemon.piStatus() : checkPiVersion()),
|
|
917
|
+
confirm: confirmInteractive,
|
|
918
|
+
runUpdate: runPiUpdateSelf,
|
|
919
|
+
});
|
|
920
|
+
process.stdout.write(formatPiVersionReport(report));
|
|
921
|
+
process.exit(report.upToDate === false ? 1 : 0);
|
|
922
|
+
}
|
|
923
|
+
process.stdout.write(out);
|
|
924
|
+
process.exit(code);
|
|
925
|
+
}
|
|
926
|
+
}
|