@danypops/pi-packed 0.19.7 → 0.19.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/client.d.ts +109 -0
  2. package/dist/client.d.ts.map +1 -0
  3. package/dist/client.js +1 -0
  4. package/dist/protocol.d.ts +221 -0
  5. package/dist/protocol.d.ts.map +1 -0
  6. package/dist/protocol.js +1 -0
  7. package/extension/src/{permission.ts → approval/permission.ts} +1 -1
  8. package/extension/src/index.ts +1 -1
  9. package/extension/src/packed.ts +2 -2
  10. package/extension/src/{discover.ts → tabs/discover.ts} +4 -4
  11. package/extension/src/{resource-config.ts → tabs/resource-config.ts} +4 -4
  12. package/extension/src/{security-tui.ts → tabs/security-tui.ts} +3 -3
  13. package/extension/src/tool-output.ts +1 -1
  14. package/extension/src/tools.ts +2 -2
  15. package/extension/src/tui.ts +4 -4
  16. package/package.json +31 -8
  17. package/service/schema/pi-setup-v1.schema.json +70 -0
  18. package/service/setup/danypops-ecosystem.pi-setup.json +15 -0
  19. package/service/src/adoption/advisories.ts +268 -0
  20. package/service/src/adoption/check.ts +872 -0
  21. package/service/src/adoption/commit-freshness.ts +167 -0
  22. package/service/src/adoption/doctor.ts +135 -0
  23. package/service/src/adoption/install-validation.ts +187 -0
  24. package/service/src/adoption/pack.ts +291 -0
  25. package/service/src/adoption/score.ts +466 -0
  26. package/service/src/adoption/smoke-child.ts +113 -0
  27. package/service/src/adoption/smoke.ts +282 -0
  28. package/service/src/cli/cli.ts +926 -0
  29. package/service/src/daemon/cleanup.ts +76 -0
  30. package/service/src/daemon/client.ts +412 -0
  31. package/service/src/daemon/daemon-service.ts +249 -0
  32. package/service/src/daemon/daemon.ts +110 -0
  33. package/service/src/daemon/service.ts +664 -0
  34. package/service/src/daemon/watcher.ts +92 -0
  35. package/service/src/index/build-index.ts +256 -0
  36. package/service/src/packages/catalog.ts +61 -0
  37. package/service/src/packages/db.ts +224 -0
  38. package/service/src/packages/install.ts +60 -0
  39. package/service/src/packages/installed.ts +123 -0
  40. package/service/src/packages/package.ts +141 -0
  41. package/service/src/packages/resources.ts +203 -0
  42. package/service/src/pi/pi-version.ts +171 -0
  43. package/service/src/public/atomic-json.ts +32 -0
  44. package/service/src/public/client.ts +277 -0
  45. package/service/src/public/protocol.ts +169 -0
  46. package/service/src/publish/publish.ts +855 -0
  47. package/service/src/registry/registry.ts +246 -0
  48. package/service/src/security/security.ts +128 -0
  49. package/service/src/self-update/self-update.ts +148 -0
  50. package/service/src/setup/setup.ts +761 -0
  51. package/service/src/shared/atomic-json.ts +33 -0
  52. package/service/src/shared/cache.ts +21 -0
  53. package/service/src/shared/constants.ts +73 -0
  54. package/service/src/shared/log.ts +21 -0
  55. package/service/src/shared/paths.ts +88 -0
  56. package/service/src/shared/state.ts +15 -0
  57. package/service/src/shared/version.ts +46 -0
  58. package/service/test/advisories.test.ts +287 -0
  59. package/service/test/check.test.ts +368 -0
  60. package/service/test/cleanup.test.ts +220 -0
  61. package/service/test/cli.test.ts +1303 -0
  62. package/service/test/core.test.ts +181 -0
  63. package/service/test/daemon-kit-migration.test.ts +181 -0
  64. package/service/test/daemon-service.test.ts +238 -0
  65. package/service/test/db.test.ts +178 -0
  66. package/service/test/doctor.test.ts +234 -0
  67. package/service/test/domain.test.ts +291 -0
  68. package/service/test/fixtures/install-validation/broken-package/extension/index.ts +3 -0
  69. package/service/test/fixtures/install-validation/broken-package/package.json +8 -0
  70. package/service/test/fixtures/install-validation/healthy-package/extension/index.ts +3 -0
  71. package/service/test/fixtures/install-validation/healthy-package/package.json +8 -0
  72. package/service/test/fixtures/install-validation/no-manifest-package/package.json +5 -0
  73. package/service/test/index.test.ts +353 -0
  74. package/service/test/install-validation.test.ts +114 -0
  75. package/service/test/install.test.ts +113 -0
  76. package/service/test/log.test.ts +42 -0
  77. package/service/test/pack-score.test.ts +513 -0
  78. package/service/test/pi-version.test.ts +318 -0
  79. package/service/test/public-boundary.test.ts +54 -0
  80. package/service/test/public-client.test.ts +127 -0
  81. package/service/test/public-consumer.ts +8 -0
  82. package/service/test/publish.test.ts +333 -0
  83. package/service/test/registry-contract.test.ts +148 -0
  84. package/service/test/resources.test.ts +255 -0
  85. package/service/test/security.test.ts +89 -0
  86. package/service/test/self-update.test.ts +257 -0
  87. package/service/test/service.test.ts +555 -0
  88. package/service/test/setup.test.ts +375 -0
  89. package/service/test/smoke.test.ts +118 -0
  90. package/service/test/version.test.ts +37 -0
  91. package/service/tsconfig.consumer.json +13 -0
  92. package/service/tsconfig.public.json +12 -0
  93. /package/extension/src/{reload.ts → approval/reload.ts} +0 -0
  94. /package/extension/src/{discover-model.ts → tabs/discover-model.ts} +0 -0
@@ -0,0 +1,466 @@
1
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
2
+ import { basename, join, resolve } from "node:path";
3
+ import type { PkgInfo, Registry } from "../packages/package.ts";
4
+ import { resolveCurrentPiVersion } from "../pi/pi-version.ts";
5
+ import { satisfiesRange } from "../publish/publish.ts";
6
+ import { type FetchGithubLastCommitAt, githubLastCommitAt, lastLocalCommitAt } from "./commit-freshness.ts";
7
+ import { NpmPackVerifier, type PackReport } from "./pack.ts";
8
+
9
+ /** Exported so bulk index generation can resolve pi's own last-publish
10
+ * date once per run without duplicating the literal. */
11
+ export const PI_COMMAND_NAME = "@earendil-works/pi-coding-agent";
12
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
13
+
14
+ /** The peer range Pi's own docs actually recommend -- carries no real
15
+ * compatibility signal, so it's treated identically to an absent range. */
16
+ const WILDCARD_PI_RANGE = "*";
17
+ const PI_PEER_DEPENDENCY = "@earendil-works/pi-coding-agent";
18
+
19
+ export type ReadinessStatus = "ready" | "partial" | "missing" | "unknown" | "observed";
20
+
21
+ export interface AdoptionDimension {
22
+ status: ReadinessStatus;
23
+ met: number;
24
+ total: number;
25
+ evidence: string[];
26
+ actions: string[];
27
+ }
28
+
29
+ export interface AdoptionReport {
30
+ target: string;
31
+ source: "local" | "registry";
32
+ package: { name: string; version: string };
33
+ dimensions: {
34
+ discoverability: AdoptionDimension;
35
+ firstRun: AdoptionDimension;
36
+ trust: AdoptionDimension;
37
+ maintenance: AdoptionDimension;
38
+ traction: AdoptionDimension;
39
+ compatibility: AdoptionDimension;
40
+ freshness: AdoptionDimension;
41
+ };
42
+ }
43
+
44
+ const MAX_TEXT_BYTES = 128 * 1024;
45
+
46
+ function dimension(met: number, total: number, evidence: string[], actions: string[], unknown = false): AdoptionDimension {
47
+ return { status: unknown ? "unknown" : met === total ? "ready" : met === 0 ? "missing" : "partial", met, total, evidence, actions };
48
+ }
49
+
50
+ function readText(path: string): string {
51
+ try {
52
+ return readFileSync(path).subarray(0, MAX_TEXT_BYTES).toString();
53
+ } catch {
54
+ return "";
55
+ }
56
+ }
57
+
58
+ function localManifest(root: string): Record<string, unknown> {
59
+ try {
60
+ return JSON.parse(
61
+ readFileSync(join(root, "package.json"))
62
+ .subarray(0, 1024 * 1024)
63
+ .toString(),
64
+ ) as Record<string, unknown>;
65
+ } catch {
66
+ return {};
67
+ }
68
+ }
69
+
70
+ function stringField(value: unknown): string | undefined {
71
+ if (typeof value === "string") return value.slice(0, 4_096);
72
+ if (value && typeof value === "object") {
73
+ const record = value as Record<string, unknown>;
74
+ return typeof record.url === "string" ? record.url.slice(0, 4_096) : undefined;
75
+ }
76
+ return undefined;
77
+ }
78
+
79
+ function strings(value: unknown): string[] {
80
+ return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string").slice(0, 50) : [];
81
+ }
82
+
83
+ function discoverability(info: PkgInfo): AdoptionDimension {
84
+ const evidence: string[] = [];
85
+ const actions: string[] = [];
86
+ let met = 0;
87
+ if (info.name && info.name.length <= 64) {
88
+ met++;
89
+ evidence.push(`focused package name: ${info.name}`);
90
+ } else actions.push("use a focused package name");
91
+ if (info.description && info.description.length >= 10 && info.description.length <= 160) {
92
+ met++;
93
+ evidence.push("bounded package description");
94
+ } else actions.push("add a 10-160 character description");
95
+ if (info.keywords?.includes("pi-package")) {
96
+ met++;
97
+ evidence.push("pi-package keyword");
98
+ } else actions.push("add the pi-package keyword");
99
+ let total = 4;
100
+ if (info.readmeAvailable === false) {
101
+ total = 3;
102
+ evidence.push("README content is unavailable from the bounded registry metadata endpoint");
103
+ } else if ((info.readme ?? "").trim().length > 0) {
104
+ met++;
105
+ evidence.push("README published");
106
+ } else actions.push("publish a README");
107
+ return dimension(met, total, evidence, actions);
108
+ }
109
+
110
+ function firstRun(info: PkgInfo): AdoptionDimension {
111
+ if (info.readmeAvailable === false)
112
+ return {
113
+ status: "unknown",
114
+ met: 0,
115
+ total: 0,
116
+ evidence: ["README content is unavailable from the bounded registry metadata endpoint"],
117
+ actions: ["run packed score against a checkout for first-run evidence"],
118
+ };
119
+ const readme = info.readme ?? "";
120
+ const evidence: string[] = [];
121
+ const actions: string[] = [];
122
+ let met = 0;
123
+ if (/pi\s+install\s+(?:npm:|git:|https:)/i.test(readme)) {
124
+ met++;
125
+ evidence.push("Pi install command in README");
126
+ } else actions.push("add one copyable pi install command");
127
+ if (/(^|\n)#{1,3}\s+(usage|example)|```[\s\S]{0,2000}```/i.test(readme)) {
128
+ met++;
129
+ evidence.push("usage example in README");
130
+ } else actions.push("add one concrete usage example");
131
+ if (/!\[[^\]]*\]\([^)]*\)|pi\.(?:gallery|image|video)/i.test(readme)) {
132
+ met++;
133
+ evidence.push("visual/gallery evidence");
134
+ } else actions.push("add a screenshot or Pi gallery image/video when UI behavior benefits from it");
135
+ return dimension(met, 3, evidence, actions);
136
+ }
137
+
138
+ const LIFECYCLE_SCRIPT_NAMES = new Set(["preinstall", "install", "postinstall", "prepublish", "preuninstall", "postuninstall", "prepare"]);
139
+
140
+ function trust(info: PkgInfo): AdoptionDimension {
141
+ const evidence: string[] = [];
142
+ const actions: string[] = [];
143
+ let met = 0;
144
+ if (info.repository && /^(?:git\+)?https?:\/\//.test(info.repository)) {
145
+ met++;
146
+ evidence.push("public repository URL declared");
147
+ } else actions.push("declare a public repository URL");
148
+ if (info.license) {
149
+ met++;
150
+ evidence.push(`license: ${info.license}`);
151
+ } else actions.push("declare a license");
152
+ if (info.bugs?.startsWith("http")) {
153
+ met++;
154
+ evidence.push("issue tracker declared");
155
+ } else actions.push("declare an issue tracker");
156
+ if (info.publication?.integrity) {
157
+ met++;
158
+ evidence.push("npm tarball integrity published");
159
+ } else actions.push("publish an integrity-bound npm tarball");
160
+ if (info.publication?.provenanceUrl) {
161
+ met++;
162
+ evidence.push("npm provenance attestation published");
163
+ } else actions.push("publish with provenance");
164
+ if (info.publication?.trustedPublisher === "verified") {
165
+ met++;
166
+ evidence.push("trusted publisher verified");
167
+ } else actions.push("verify trusted publisher configuration separately; provenance alone does not prove it");
168
+ if (info.packageEvidence?.verified) {
169
+ met++;
170
+ evidence.push(`${info.packageEvidence.shape} Pi shape verified from tarball contents`);
171
+ } else actions.push("verify the exact tarball's Pi shape with packed pack");
172
+ const declaredLifecycleScripts = Object.keys(info.scripts ?? {}).filter((name) => LIFECYCLE_SCRIPT_NAMES.has(name));
173
+ if (declaredLifecycleScripts.length > 0) {
174
+ // Presence alone is evidence, not a trust failure -- lifecycle scripts
175
+ // execute on install (a top supply-chain attack vector), so this is
176
+ // surfaced for a human to judge rather than counted against met/total.
177
+ evidence.push(`declares lifecycle script(s) that execute on install: ${declaredLifecycleScripts.join(", ")}`);
178
+ }
179
+ actions.push("run packed check --smoke when bounded extension startup evidence is needed");
180
+ return dimension(met, 7, evidence, actions);
181
+ }
182
+
183
+ function maintenance(info: PkgInfo): AdoptionDimension {
184
+ const evidence: string[] = [];
185
+ const actions: string[] = [];
186
+ let met = 0;
187
+ if (info.modified) {
188
+ const age = Date.now() - Date.parse(info.modified);
189
+ if (Number.isFinite(age) && age <= 366 * 24 * 60 * 60 * 1000) {
190
+ met++;
191
+ evidence.push(`release metadata updated ${info.modified}`);
192
+ } else actions.push("publish a compatible maintenance release or explain maintenance status");
193
+ } else actions.push("registry release recency is unavailable");
194
+ if (info.peerDependencies?.["@earendil-works/pi-coding-agent"]) {
195
+ met++;
196
+ evidence.push(`Pi compatibility: ${info.peerDependencies["@earendil-works/pi-coding-agent"]}`);
197
+ } else actions.push("declare the supported Pi peer range");
198
+ const readme = info.readme ?? "";
199
+ if (/changelog|release notes/i.test(readme)) {
200
+ met++;
201
+ evidence.push("changelog or release notes linked");
202
+ } else actions.push("link release notes or a changelog");
203
+ return dimension(met, 3, evidence, actions);
204
+ }
205
+
206
+ interface FreshnessCandidate {
207
+ date: string;
208
+ /** "commit" is a real git-history date (local checkout, or GitHub's
209
+ * Commits API); "publish" is the npm publish-date proxy -- used only
210
+ * when a real commit date could not be resolved. */
211
+ source: "commit" | "publish";
212
+ }
213
+
214
+ /**
215
+ * Compares the candidate's most accurate available date -- a real commit
216
+ * (local `git log`, or GitHub's Commits API for a pre-install registry
217
+ * candidate) when resolvable, falling back to the npm publish-date proxy
218
+ * otherwise -- against @earendil-works/pi-coding-agent's own last publish
219
+ * date. The publish-date proxy is reasonable but still a proxy: real
220
+ * published packages in this ecosystem publish via CI immediately on
221
+ * tag/commit (confirmed live across dozens of real packages: OIDC
222
+ * trusted-publisher via GitHub Actions is the dominant pattern), which is
223
+ * exactly why a real commit date, when available, is preferred instead.
224
+ * Observational only, like traction() -- no invented threshold for "how
225
+ * stale is bad"; Pi enforces nothing here. Unknown (never a guess)
226
+ * whenever neither source resolved, or pi's own publish date is
227
+ * unavailable.
228
+ */
229
+ function freshness(candidate: FreshnessCandidate | undefined, piModified: string | undefined): AdoptionDimension {
230
+ if (!candidate) {
231
+ return {
232
+ status: "unknown",
233
+ met: 0,
234
+ total: 0,
235
+ evidence: ["candidate's commit history and npm publish date are both unavailable"],
236
+ actions: [],
237
+ };
238
+ }
239
+ if (!piModified) {
240
+ return { status: "unknown", met: 0, total: 0, evidence: ["pi-coding-agent's own latest npm publish date is unavailable"], actions: [] };
241
+ }
242
+ const candidateMs = Date.parse(candidate.date);
243
+ const piMs = Date.parse(piModified);
244
+ if (!Number.isFinite(candidateMs) || !Number.isFinite(piMs)) {
245
+ return { status: "unknown", met: 0, total: 0, evidence: ["dates could not be parsed"], actions: [] };
246
+ }
247
+ const deltaDays = Math.round((piMs - candidateMs) / MS_PER_DAY);
248
+ const relation =
249
+ deltaDays > 0
250
+ ? `${deltaDays}d before pi-coding-agent's latest publish`
251
+ : deltaDays < 0
252
+ ? `${-deltaDays}d after pi-coding-agent's latest publish`
253
+ : "the same day as pi-coding-agent's latest publish";
254
+ const sourceLabel =
255
+ candidate.source === "commit" ? "last real commit" : "npm publish date (commit history unavailable; proxy for commit recency)";
256
+ return {
257
+ status: "observed",
258
+ met: 0,
259
+ total: 0,
260
+ evidence: [`candidate's ${sourceLabel}: ${candidate.date}`, `pi-coding-agent last published ${piModified} -- candidate is ${relation}`],
261
+ actions: [],
262
+ };
263
+ }
264
+
265
+ function traction(info: PkgInfo): AdoptionDimension {
266
+ const observations = info.downloads;
267
+ if (!observations || (observations.weekly === undefined && observations.monthly === undefined)) {
268
+ return {
269
+ status: "unknown",
270
+ met: 0,
271
+ total: 0,
272
+ evidence: ["traction is observational, not a quality signal; npm download data unavailable"],
273
+ actions: [],
274
+ };
275
+ }
276
+ const evidence = [
277
+ `${observations.weekly ?? "unknown"} weekly npm downloads`,
278
+ `${observations.monthly ?? "unknown"} monthly npm downloads`,
279
+ `observed ${observations.observedAt}; downloads are not a quality signal`,
280
+ ];
281
+ return { status: "observed", met: 0, total: 0, evidence, actions: [] };
282
+ }
283
+
284
+ /**
285
+ * Compares a candidate's declared @earendil-works/pi-coding-agent peer
286
+ * range against the running Pi version. This is an informal signal, not an
287
+ * official Pi mechanism: Pi's own docs tell authors to declare "*" (no
288
+ * real constraint), real published packages ignore that guidance anyway
289
+ * (earendil-works/pi#4907), and Pi's own installer performs zero
290
+ * validation of this range at install or load time. Never blocks install
291
+ * by itself -- Packed does not gate on this, and neither does Pi.
292
+ */
293
+ function compatibility(info: PkgInfo, currentPiVersion: string | undefined): AdoptionDimension {
294
+ const declared = info.peerDependencies?.[PI_PEER_DEPENDENCY]?.trim();
295
+ if (!declared || declared === WILDCARD_PI_RANGE) {
296
+ return {
297
+ status: "unknown",
298
+ met: 0,
299
+ total: 0,
300
+ evidence: [
301
+ declared
302
+ ? `declared Pi peer range is "*" (Pi's own recommended convention; carries no real signal)`
303
+ : 'no declared Pi peer range (matches Pi\'s own recommended "*" convention)',
304
+ ],
305
+ actions: [],
306
+ };
307
+ }
308
+ if (!currentPiVersion) {
309
+ return {
310
+ status: "unknown",
311
+ met: 0,
312
+ total: 0,
313
+ evidence: [`declared Pi peer range ${declared}, but the running Pi version could not be determined`],
314
+ actions: ["run packed pi status to check pi's own version detection"],
315
+ };
316
+ }
317
+ const satisfied = satisfiesRange(currentPiVersion, declared);
318
+ if (satisfied === undefined) {
319
+ return {
320
+ status: "unknown",
321
+ met: 0,
322
+ total: 0,
323
+ evidence: [`declared Pi peer range ${declared} could not be evaluated (only exact, ^, and ~ ranges are supported)`],
324
+ actions: [],
325
+ };
326
+ }
327
+ if (satisfied) {
328
+ return {
329
+ status: "ready",
330
+ met: 1,
331
+ total: 1,
332
+ evidence: [`declared Pi peer range ${declared} is satisfied by the running pi ${currentPiVersion}`],
333
+ actions: [],
334
+ };
335
+ }
336
+ return {
337
+ status: "missing",
338
+ met: 0,
339
+ total: 1,
340
+ evidence: [`declared Pi peer range ${declared} is NOT satisfied by the running pi ${currentPiVersion}`],
341
+ actions: [
342
+ "this is an informal, Pi-unenforced signal (see earendil-works/pi#4907) -- packed never blocks install on it, but this package may not work correctly on the currently running Pi version",
343
+ ],
344
+ };
345
+ }
346
+
347
+ export function assessRegistryAdoption(
348
+ info: PkgInfo,
349
+ currentPiVersion?: string,
350
+ piModified?: string,
351
+ candidateCommitAt?: string,
352
+ ): AdoptionReport {
353
+ const candidate: FreshnessCandidate | undefined = candidateCommitAt
354
+ ? { date: candidateCommitAt, source: "commit" }
355
+ : info.modified
356
+ ? { date: info.modified, source: "publish" }
357
+ : undefined;
358
+ return {
359
+ target: info.name,
360
+ source: "registry",
361
+ package: { name: info.name, version: info.version },
362
+ dimensions: {
363
+ discoverability: discoverability(info),
364
+ firstRun: firstRun(info),
365
+ trust: trust(info),
366
+ maintenance: maintenance(info),
367
+ traction: traction(info),
368
+ compatibility: compatibility(info, currentPiVersion),
369
+ freshness: freshness(candidate, piModified),
370
+ },
371
+ };
372
+ }
373
+
374
+ export async function assessLocalAdoption(
375
+ packagePath: string,
376
+ pack: PackReport,
377
+ currentPiVersion?: string,
378
+ piModified?: string,
379
+ ): Promise<AdoptionReport> {
380
+ const root = realpathSync(resolve(packagePath));
381
+ const pkg = localManifest(root);
382
+ const readmeName = ["README.md", "README", "readme.md"].find((name) => existsSync(join(root, name)));
383
+ const info: PkgInfo = {
384
+ name: typeof pkg.name === "string" ? pkg.name.slice(0, 214) : basename(root),
385
+ version: typeof pkg.version === "string" ? pkg.version.slice(0, 128) : "",
386
+ description: typeof pkg.description === "string" ? pkg.description.slice(0, 512) : undefined,
387
+ keywords: strings(pkg.keywords),
388
+ license: stringField(pkg.license),
389
+ repository: stringField(pkg.repository),
390
+ bugs: stringField(pkg.bugs),
391
+ peerDependencies:
392
+ pkg.peerDependencies && typeof pkg.peerDependencies === "object" ? (pkg.peerDependencies as Record<string, string>) : undefined,
393
+ scripts: pkg.scripts && typeof pkg.scripts === "object" ? (pkg.scripts as Record<string, string>) : undefined,
394
+ pi: pkg.pi && typeof pkg.pi === "object" ? (pkg.pi as Record<string, unknown>) : undefined,
395
+ readme: readmeName ? readText(join(root, readmeName)) : undefined,
396
+ readmeAvailable: true,
397
+ publication: { integrity: pack.integrity, trustedPublisher: "unknown" },
398
+ packageEvidence: { shape: pack.shape.kind, verified: pack.shape.verified, evidence: pack.shape.evidence },
399
+ };
400
+ // A local checkout has no npm publish date, but does have real git
401
+ // history -- prefer that over the (unavailable) publish-date proxy.
402
+ const commitAt = await lastLocalCommitAt(root);
403
+ const report = assessRegistryAdoption(info, currentPiVersion, piModified, commitAt);
404
+ report.target = root;
405
+ report.source = "local";
406
+ if (!pack.ok) report.dimensions.trust.actions.push("resolve npm tarball verification errors");
407
+ if (existsSync(join(root, "CHANGELOG.md"))) {
408
+ report.dimensions.maintenance.met = Math.min(report.dimensions.maintenance.total, report.dimensions.maintenance.met + 1);
409
+ report.dimensions.maintenance.evidence.push("CHANGELOG.md present");
410
+ report.dimensions.maintenance.actions = report.dimensions.maintenance.actions.filter((action) => !action.includes("release notes"));
411
+ report.dimensions.maintenance.status = report.dimensions.maintenance.met === report.dimensions.maintenance.total ? "ready" : "partial";
412
+ }
413
+ return report;
414
+ }
415
+
416
+ export async function scoreTarget(
417
+ target: string,
418
+ registry: Registry,
419
+ verifier: Pick<NpmPackVerifier, "verify"> = new NpmPackVerifier(),
420
+ currentPiVersion: () => Promise<string | undefined> = resolveCurrentPiVersion,
421
+ fetchGithubCommit: FetchGithubLastCommitAt = githubLastCommitAt,
422
+ ): Promise<AdoptionReport> {
423
+ const piVersion = await currentPiVersion();
424
+ let piModified: string | undefined;
425
+ if (registry.modifiedAt) {
426
+ try {
427
+ piModified = await registry.modifiedAt(PI_COMMAND_NAME);
428
+ } catch {
429
+ /* freshness remains explicitly unknown */
430
+ }
431
+ }
432
+ if (existsSync(resolve(target))) {
433
+ const pack = await verifier.verify(target);
434
+ return await assessLocalAdoption(target, pack, piVersion, piModified);
435
+ }
436
+ const info = await registry.info(target);
437
+ if (registry.downloads) {
438
+ try {
439
+ info.downloads = await registry.downloads(target);
440
+ } catch {
441
+ /* traction remains explicitly unknown */
442
+ }
443
+ }
444
+ if (registry.modifiedAt) {
445
+ try {
446
+ info.modified = await registry.modifiedAt(target);
447
+ } catch {
448
+ /* freshness proxy remains explicitly unknown */
449
+ }
450
+ }
451
+ // githubLastCommitAt never throws -- undefined for a non-GitHub host, a
452
+ // missing repository field, a rate limit, or any other failure.
453
+ const candidateCommitAt = await fetchGithubCommit(info.repository, info.repositoryDirectory);
454
+ return assessRegistryAdoption(info, piVersion, piModified, candidateCommitAt);
455
+ }
456
+
457
+ export function formatAdoptionReport(report: AdoptionReport, json = false): string {
458
+ if (json) return `${JSON.stringify(report)}\n`;
459
+ let out = `${report.package.name}@${report.package.version} adoption readiness (${report.source})\n`;
460
+ for (const [name, value] of Object.entries(report.dimensions)) {
461
+ out += `\n${name}: ${value.status}${value.total ? ` (${value.met}/${value.total})` : ""}\n`;
462
+ for (const evidence of value.evidence) out += ` evidence: ${evidence}\n`;
463
+ for (const action of value.actions) out += ` action: ${action}\n`;
464
+ }
465
+ return out.slice(0, 16 * 1024);
466
+ }
@@ -0,0 +1,113 @@
1
+ import { createJiti } from "jiti";
2
+
3
+ interface Registrations {
4
+ tools: string[];
5
+ commands: string[];
6
+ shortcuts: string[];
7
+ flags: string[];
8
+ providers: string[];
9
+ events: string[];
10
+ renderers: string[];
11
+ }
12
+
13
+ const MAX_REGISTRATIONS_TOTAL = 100;
14
+ const MAX_NAME_LENGTH = 128;
15
+ const originalWrite = process.stdout.write.bind(process.stdout);
16
+ console.log = () => {};
17
+ console.info = () => {};
18
+ console.warn = () => {};
19
+ console.error = () => {};
20
+ globalThis.fetch = Object.assign(
21
+ async () => {
22
+ const error = new Error("network disabled by Packed smoke sandbox") as Error & { code: string };
23
+ error.code = "ENETUNREACH";
24
+ throw error;
25
+ },
26
+ { preconnect() {} },
27
+ ) as typeof fetch;
28
+ const denyProcess = () => {
29
+ const error = new Error("subprocesses disabled by Packed smoke sandbox") as Error & { code: string };
30
+ error.code = "EPERM";
31
+ throw error;
32
+ };
33
+ const bunRuntime = Bun as unknown as Record<string, unknown>;
34
+ bunRuntime.spawn = denyProcess;
35
+ bunRuntime.spawnSync = denyProcess;
36
+ bunRuntime.$ = denyProcess;
37
+
38
+ const registrations: Registrations = {
39
+ tools: [],
40
+ commands: [],
41
+ shortcuts: [],
42
+ flags: [],
43
+ providers: [],
44
+ events: [],
45
+ renderers: [],
46
+ };
47
+
48
+ function capture(kind: keyof Registrations, value: unknown): void {
49
+ if (Object.values(registrations).reduce((total, values) => total + values.length, 0) >= MAX_REGISTRATIONS_TOTAL) return;
50
+ const name = typeof value === "string" ? value : "";
51
+ registrations[kind].push(name.slice(0, MAX_NAME_LENGTH));
52
+ }
53
+
54
+ const api = new Proxy<Record<string, unknown>>(
55
+ {},
56
+ {
57
+ get(_target, property) {
58
+ switch (property) {
59
+ case "registerTool":
60
+ return (definition: { name?: unknown }) => capture("tools", definition?.name);
61
+ case "registerCommand":
62
+ return (name: unknown) => capture("commands", name);
63
+ case "registerShortcut":
64
+ return (name: unknown) => capture("shortcuts", name);
65
+ case "registerFlag":
66
+ return (name: unknown) => capture("flags", name);
67
+ case "registerProvider":
68
+ return (name: unknown) => capture("providers", name);
69
+ case "registerMessageRenderer":
70
+ return (name: unknown) => capture("renderers", name);
71
+ case "on":
72
+ return (name: unknown) => capture("events", name);
73
+ case "getAllTools":
74
+ return () => [];
75
+ case "getActiveTools":
76
+ return () => [];
77
+ case "getCommands":
78
+ return () => [];
79
+ default:
80
+ return () => undefined;
81
+ }
82
+ },
83
+ },
84
+ );
85
+
86
+ function classify(error: unknown): "capability-denied" | "crash" {
87
+ const value = error as { code?: unknown; message?: unknown; cause?: { code?: unknown } };
88
+ const code = String(value?.code ?? value?.cause?.code ?? "");
89
+ const message = String(value?.message ?? error ?? "");
90
+ return /^(?:EACCES|EPERM|EROFS|ENETUNREACH|EAI_AGAIN|ECONNREFUSED|EAGAIN)$/.test(code) ||
91
+ /permission denied|operation not permitted|read-only file system|network is unreachable|failed to connect|unable to connect|resource temporarily unavailable/i.test(
92
+ message,
93
+ )
94
+ ? "capability-denied"
95
+ : "crash";
96
+ }
97
+
98
+ async function main(): Promise<void> {
99
+ const extensionPath = process.argv[2];
100
+ if (!extensionPath) throw new Error("extension path is required");
101
+ try {
102
+ const jiti = createJiti(import.meta.url, { interopDefault: true, tryNative: false });
103
+ const factory = (await jiti.import(extensionPath, { default: true })) as unknown;
104
+ if (typeof factory !== "function") throw new Error("extension has no default factory export");
105
+ await factory(api);
106
+ originalWrite(`${JSON.stringify({ status: "ok", registrations })}\n`);
107
+ } catch (error) {
108
+ const message = (error instanceof Error ? error.message : String(error)).slice(0, 1_000);
109
+ originalWrite(`${JSON.stringify({ status: classify(error), message, registrations })}\n`);
110
+ }
111
+ }
112
+
113
+ await main();