cueline 0.2.1 → 0.3.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.
Files changed (67) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +58 -0
  4. package/README.ja.md +12 -12
  5. package/README.ko.md +12 -12
  6. package/README.md +12 -12
  7. package/README.zh-CN.md +12 -12
  8. package/README.zh-TW.md +12 -12
  9. package/dist/src/api-controller-handoff.js +1 -1
  10. package/dist/src/api-controller-handoff.js.map +1 -1
  11. package/dist/src/api-run-prune.d.ts +41 -0
  12. package/dist/src/api-run-prune.js +218 -0
  13. package/dist/src/api-run-prune.js.map +1 -0
  14. package/dist/src/api-runtime-lifecycle.js +4 -4
  15. package/dist/src/api-runtime-lifecycle.js.map +1 -1
  16. package/dist/src/api.d.ts +3 -0
  17. package/dist/src/api.js +3 -0
  18. package/dist/src/api.js.map +1 -1
  19. package/dist/src/cli/health-commands.js +35 -0
  20. package/dist/src/cli/health-commands.js.map +1 -1
  21. package/dist/src/cli/main.js +95 -3
  22. package/dist/src/cli/main.js.map +1 -1
  23. package/dist/src/cli/observation-commands.js +94 -1
  24. package/dist/src/cli/observation-commands.js.map +1 -1
  25. package/dist/src/core/controller-command-execution.d.ts +1 -1
  26. package/dist/src/core/controller-command-execution.js +7 -7
  27. package/dist/src/core/controller-command-execution.js.map +1 -1
  28. package/dist/src/core/controller-evidence.d.ts +1 -0
  29. package/dist/src/core/controller-evidence.js +20 -0
  30. package/dist/src/core/controller-evidence.js.map +1 -1
  31. package/dist/src/core/controller-loop.js +11 -7
  32. package/dist/src/core/controller-loop.js.map +1 -1
  33. package/dist/src/core/controller-turn.d.ts +1 -1
  34. package/dist/src/core/controller-turn.js +16 -8
  35. package/dist/src/core/controller-turn.js.map +1 -1
  36. package/dist/src/diagnostics/offline-self-test.d.ts +23 -0
  37. package/dist/src/diagnostics/offline-self-test.js +163 -0
  38. package/dist/src/diagnostics/offline-self-test.js.map +1 -0
  39. package/dist/src/diagnostics/secret-audit.d.ts +25 -0
  40. package/dist/src/diagnostics/secret-audit.js +114 -0
  41. package/dist/src/diagnostics/secret-audit.js.map +1 -0
  42. package/dist/src/diagnostics/upgrade-preflight.d.ts +41 -0
  43. package/dist/src/diagnostics/upgrade-preflight.js +182 -0
  44. package/dist/src/diagnostics/upgrade-preflight.js.map +1 -0
  45. package/dist/src/observation/run-bundle.d.ts +27 -0
  46. package/dist/src/observation/run-bundle.js +55 -0
  47. package/dist/src/observation/run-bundle.js.map +1 -0
  48. package/dist/src/version.d.ts +1 -1
  49. package/dist/src/version.js +1 -1
  50. package/docs/1.0/ideas/machine-output-contracts.md +47 -0
  51. package/docs/1.0/ideas/node26-contract.md +50 -0
  52. package/docs/1.0/ideas/offline-self-test.md +52 -0
  53. package/docs/1.0/ideas/upgrade-preflight.md +46 -0
  54. package/docs/compatibility.md +5 -5
  55. package/docs/runner-contract.md +1 -1
  56. package/package.json +12 -2
  57. package/schemas/cli-doctor.schema.json +77 -0
  58. package/schemas/cli-routing-explain.schema.json +88 -0
  59. package/schemas/cli-routing.schema.json +65 -0
  60. package/schemas/cli-run-audit-secrets.schema.json +72 -0
  61. package/schemas/cli-run-export.schema.json +410 -0
  62. package/schemas/cli-runs-prune.schema.json +93 -0
  63. package/scripts/artifact-integrity.mjs +114 -0
  64. package/scripts/release-check.mjs +118 -0
  65. package/scripts/validate-cli-contracts.mjs +59 -0
  66. package/scripts/validate-doc-versions.mjs +79 -0
  67. package/scripts/validate-node-support.mjs +118 -0
@@ -0,0 +1,410 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://cueline.dev/schemas/cli-run-export.schema.json",
4
+ "title": "cueline run export --json output (support bundle)",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "schema",
9
+ "protocol",
10
+ "version",
11
+ "generatedAt",
12
+ "runId",
13
+ "status",
14
+ "verification",
15
+ "diagnosis",
16
+ "timeline"
17
+ ],
18
+ "properties": {
19
+ "schema": { "const": "cueline-run-export/1" },
20
+ "protocol": { "const": "cueline-run-bundle/0.1" },
21
+ "version": { "type": "string", "minLength": 1 },
22
+ "generatedAt": { "$ref": "#/$defs/isoTimestamp" },
23
+ "runId": { "type": "string", "minLength": 1 },
24
+ "status": { "$ref": "#/$defs/status" },
25
+ "verification": { "$ref": "#/$defs/verification" },
26
+ "diagnosis": { "$ref": "#/$defs/diagnosis" },
27
+ "timeline": { "$ref": "#/$defs/timeline" }
28
+ },
29
+ "$defs": {
30
+ "isoTimestamp": {
31
+ "type": "string",
32
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?Z$"
33
+ },
34
+ "status": {
35
+ "type": "object",
36
+ "additionalProperties": false,
37
+ "required": [
38
+ "runId",
39
+ "status",
40
+ "executor",
41
+ "allowProcessExecution",
42
+ "phase",
43
+ "round",
44
+ "maxRounds",
45
+ "lastEventSequence",
46
+ "runtime",
47
+ "cancellation",
48
+ "controller",
49
+ "jobs",
50
+ "continueAllowed",
51
+ "safeNextAction"
52
+ ],
53
+ "properties": {
54
+ "runId": { "type": "string", "minLength": 1 },
55
+ "status": {
56
+ "enum": ["running", "complete", "blocked", "cancelled", "failed"]
57
+ },
58
+ "executor": { "enum": ["caller", "process"] },
59
+ "allowProcessExecution": { "type": "boolean" },
60
+ "phase": { "type": "string", "minLength": 1 },
61
+ "round": { "type": "integer", "minimum": 0 },
62
+ "maxRounds": { "type": "integer", "minimum": 0 },
63
+ "lastEventSequence": { "type": "integer", "minimum": 0 },
64
+ "runtime": {
65
+ "type": "object",
66
+ "additionalProperties": false,
67
+ "required": ["ownership"],
68
+ "properties": {
69
+ "ownership": {
70
+ "enum": ["missing", "active", "stale", "released", "invalid"]
71
+ },
72
+ "heartbeatAt": { "type": "string", "minLength": 1 },
73
+ "ageMs": { "type": "number", "minimum": 0 },
74
+ "pid": { "anyOf": [{ "type": "string" }, { "type": "number" }] }
75
+ }
76
+ },
77
+ "cancellation": {
78
+ "type": "object",
79
+ "additionalProperties": false,
80
+ "required": ["runRequested", "jobRequests"],
81
+ "properties": {
82
+ "runRequested": { "type": "boolean" },
83
+ "jobRequests": {
84
+ "type": "array",
85
+ "items": { "type": "string", "minLength": 1 }
86
+ }
87
+ }
88
+ },
89
+ "controller": {
90
+ "type": "object",
91
+ "additionalProperties": false,
92
+ "required": [
93
+ "pendingTurns",
94
+ "acceptedCommands",
95
+ "responseAccepted",
96
+ "lastAcceptedAction",
97
+ "lastAcceptedRequestId",
98
+ "lastAcceptedJobKeys"
99
+ ],
100
+ "properties": {
101
+ "pendingTurns": { "type": "integer", "minimum": 0 },
102
+ "acceptedCommands": { "type": "integer", "minimum": 0 },
103
+ "responseAccepted": { "type": "boolean" },
104
+ "lastAcceptedAction": {
105
+ "enum": ["dispatch", "wait", "inspect", "complete", "blocked", null]
106
+ },
107
+ "lastAcceptedRequestId": { "anyOf": [{ "type": "string" }, { "type": "null" }] },
108
+ "lastAcceptedJobKeys": {
109
+ "type": "array",
110
+ "items": { "type": "string", "minLength": 1 }
111
+ },
112
+ "reconciliation": {
113
+ "type": "object",
114
+ "additionalProperties": false,
115
+ "required": [
116
+ "requiredReason",
117
+ "operatorConfirmation",
118
+ "abandonedRequestId",
119
+ "retryRequestId",
120
+ "promptHash",
121
+ "resendBlockedReason"
122
+ ],
123
+ "properties": {
124
+ "requiredReason": { "anyOf": [{ "type": "string" }, { "type": "null" }] },
125
+ "operatorConfirmation": {
126
+ "enum": ["not_sent_confirmed", "manual_send_confirmed", null]
127
+ },
128
+ "abandonedRequestId": { "anyOf": [{ "type": "string" }, { "type": "null" }] },
129
+ "retryRequestId": { "anyOf": [{ "type": "string" }, { "type": "null" }] },
130
+ "promptHash": { "anyOf": [{ "type": "string" }, { "type": "null" }] },
131
+ "resendBlockedReason": { "anyOf": [{ "type": "string" }, { "type": "null" }] }
132
+ }
133
+ },
134
+ "archive": {
135
+ "type": "object",
136
+ "additionalProperties": false,
137
+ "required": ["enabled", "status", "code", "proof"],
138
+ "properties": {
139
+ "enabled": { "const": true },
140
+ "status": { "type": "string", "minLength": 1 },
141
+ "code": { "anyOf": [{ "type": "string" }, { "type": "null" }] },
142
+ "proof": {}
143
+ }
144
+ }
145
+ }
146
+ },
147
+ "jobs": {
148
+ "type": "object",
149
+ "additionalProperties": false,
150
+ "required": ["total", "counts", "items"],
151
+ "properties": {
152
+ "total": { "type": "integer", "minimum": 0 },
153
+ "counts": {
154
+ "type": "object",
155
+ "additionalProperties": false,
156
+ "required": [
157
+ "pending",
158
+ "running",
159
+ "succeeded",
160
+ "failed",
161
+ "timed_out",
162
+ "cancelled",
163
+ "ambiguous",
164
+ "orphaned"
165
+ ],
166
+ "properties": {
167
+ "pending": { "type": "integer", "minimum": 0 },
168
+ "running": { "type": "integer", "minimum": 0 },
169
+ "succeeded": { "type": "integer", "minimum": 0 },
170
+ "failed": { "type": "integer", "minimum": 0 },
171
+ "timed_out": { "type": "integer", "minimum": 0 },
172
+ "cancelled": { "type": "integer", "minimum": 0 },
173
+ "ambiguous": { "type": "integer", "minimum": 0 },
174
+ "orphaned": { "type": "integer", "minimum": 0 }
175
+ }
176
+ },
177
+ "items": {
178
+ "type": "array",
179
+ "items": {
180
+ "type": "object",
181
+ "additionalProperties": false,
182
+ "required": [
183
+ "jobId",
184
+ "jobKey",
185
+ "required",
186
+ "lane",
187
+ "mode",
188
+ "status",
189
+ "persistedStatus"
190
+ ],
191
+ "properties": {
192
+ "jobId": { "type": "string", "minLength": 1 },
193
+ "jobKey": { "type": "string", "minLength": 1 },
194
+ "required": { "type": "boolean" },
195
+ "lane": { "type": "string", "minLength": 1 },
196
+ "mode": { "enum": ["advise", "work"] },
197
+ "status": {
198
+ "enum": [
199
+ "pending",
200
+ "running",
201
+ "succeeded",
202
+ "failed",
203
+ "timed_out",
204
+ "cancelled",
205
+ "ambiguous",
206
+ "orphaned"
207
+ ]
208
+ },
209
+ "persistedStatus": { "type": "string", "minLength": 1 },
210
+ "workClaim": {
211
+ "type": "object",
212
+ "additionalProperties": false,
213
+ "required": ["claimed"],
214
+ "properties": {
215
+ "claimed": { "type": "boolean" },
216
+ "claimedAt": { "type": "string", "minLength": 1 },
217
+ "heartbeatAt": { "type": "string", "minLength": 1 },
218
+ "expiresAt": { "type": "string", "minLength": 1 },
219
+ "startedAt": { "type": "string", "minLength": 1 }
220
+ }
221
+ },
222
+ "execution": {
223
+ "type": "object",
224
+ "additionalProperties": false,
225
+ "properties": {
226
+ "runnerId": { "type": "string", "minLength": 1 },
227
+ "pid": { "type": "number" },
228
+ "model": { "type": "string", "minLength": 1 },
229
+ "provider": { "type": "string", "minLength": 1 },
230
+ "phase": { "type": "string", "minLength": 1 },
231
+ "lastProgressAt": { "type": "string", "minLength": 1 }
232
+ }
233
+ }
234
+ }
235
+ }
236
+ }
237
+ }
238
+ },
239
+ "continueAllowed": { "type": "boolean" },
240
+ "safeNextAction": { "type": "string", "minLength": 1 }
241
+ }
242
+ },
243
+ "verification": {
244
+ "type": "object",
245
+ "additionalProperties": false,
246
+ "required": [
247
+ "runId",
248
+ "outcome",
249
+ "marker",
250
+ "eventLog",
251
+ "snapshot",
252
+ "runtimeOwnership",
253
+ "findings"
254
+ ],
255
+ "properties": {
256
+ "runId": { "type": "string", "minLength": 1 },
257
+ "outcome": { "enum": ["verified", "degraded", "unreadable"] },
258
+ "marker": { "enum": ["valid", "missing", "invalid"] },
259
+ "eventLog": {
260
+ "type": "object",
261
+ "additionalProperties": false,
262
+ "required": [
263
+ "readable",
264
+ "totalEvents",
265
+ "authoritativeEvents",
266
+ "lastSequence"
267
+ ],
268
+ "properties": {
269
+ "readable": { "type": "boolean" },
270
+ "totalEvents": { "type": "integer", "minimum": 0 },
271
+ "authoritativeEvents": { "type": "integer", "minimum": 0 },
272
+ "lastSequence": { "anyOf": [{ "type": "integer" }, { "type": "null" }] }
273
+ }
274
+ },
275
+ "snapshot": { "enum": ["missing", "valid", "stale", "invalid"] },
276
+ "runtimeOwnership": {
277
+ "enum": ["missing", "active", "stale", "released", "invalid"]
278
+ },
279
+ "findings": {
280
+ "type": "array",
281
+ "items": {
282
+ "type": "object",
283
+ "additionalProperties": false,
284
+ "required": ["code", "severity", "surface", "message"],
285
+ "properties": {
286
+ "code": { "type": "string", "minLength": 1 },
287
+ "severity": { "enum": ["warning", "error"] },
288
+ "surface": {
289
+ "enum": ["marker", "events", "snapshot", "runtime", "jobs"]
290
+ },
291
+ "message": { "type": "string", "minLength": 1 }
292
+ }
293
+ }
294
+ }
295
+ }
296
+ },
297
+ "diagnosis": {
298
+ "type": "object",
299
+ "additionalProperties": false,
300
+ "required": [
301
+ "runId",
302
+ "outcome",
303
+ "phase",
304
+ "summary",
305
+ "nextAction",
306
+ "eventSequence",
307
+ "findings"
308
+ ],
309
+ "properties": {
310
+ "runId": { "type": "string", "minLength": 1 },
311
+ "outcome": { "enum": ["healthy", "action_required", "blocked"] },
312
+ "phase": { "type": "string", "minLength": 1 },
313
+ "summary": { "type": "string", "minLength": 1 },
314
+ "nextAction": { "type": "string", "minLength": 1 },
315
+ "eventSequence": { "type": "integer", "minimum": 0 },
316
+ "findings": {
317
+ "type": "array",
318
+ "items": {
319
+ "type": "object",
320
+ "additionalProperties": false,
321
+ "required": ["code", "severity", "message", "action", "evidence"],
322
+ "properties": {
323
+ "code": { "type": "string", "minLength": 1 },
324
+ "severity": { "enum": ["info", "warning", "error"] },
325
+ "message": { "type": "string", "minLength": 1 },
326
+ "action": { "type": "string", "minLength": 1 },
327
+ "evidence": {
328
+ "type": "object",
329
+ "additionalProperties": {
330
+ "anyOf": [
331
+ { "type": "string" },
332
+ { "type": "number" },
333
+ { "type": "boolean" },
334
+ { "type": "null" }
335
+ ]
336
+ }
337
+ }
338
+ }
339
+ }
340
+ }
341
+ }
342
+ },
343
+ "timeline": {
344
+ "type": "object",
345
+ "additionalProperties": false,
346
+ "required": [
347
+ "schema",
348
+ "runId",
349
+ "sourceEventsPath",
350
+ "afterSequence",
351
+ "limit",
352
+ "totalEvents",
353
+ "latestSequence",
354
+ "returnedEvents",
355
+ "hasMore",
356
+ "nextAfterSequence",
357
+ "entries"
358
+ ],
359
+ "properties": {
360
+ "schema": { "const": "cueline-timeline/0.1" },
361
+ "runId": { "type": "string", "minLength": 1 },
362
+ "sourceEventsPath": { "type": "string", "minLength": 1 },
363
+ "afterSequence": { "type": "integer", "minimum": 0 },
364
+ "limit": { "type": "integer", "minimum": 1 },
365
+ "totalEvents": { "type": "integer", "minimum": 0 },
366
+ "latestSequence": { "type": "integer", "minimum": 0 },
367
+ "returnedEvents": { "type": "integer", "minimum": 0 },
368
+ "hasMore": { "type": "boolean" },
369
+ "nextAfterSequence": { "type": "integer", "minimum": 0 },
370
+ "entries": {
371
+ "type": "array",
372
+ "items": {
373
+ "type": "object",
374
+ "additionalProperties": false,
375
+ "required": [
376
+ "sequence",
377
+ "timestamp",
378
+ "type",
379
+ "category",
380
+ "summary",
381
+ "attributes",
382
+ "payloadHash"
383
+ ],
384
+ "properties": {
385
+ "sequence": { "type": "integer", "minimum": 1 },
386
+ "timestamp": {
387
+ "anyOf": [{ "$ref": "#/$defs/isoTimestamp" }, { "type": "null" }]
388
+ },
389
+ "type": { "type": "string", "minLength": 1 },
390
+ "category": { "type": "string", "minLength": 1 },
391
+ "summary": { "type": "string" },
392
+ "attributes": {
393
+ "type": "object",
394
+ "additionalProperties": {
395
+ "anyOf": [
396
+ { "type": "string" },
397
+ { "type": "number" },
398
+ { "type": "boolean" }
399
+ ]
400
+ }
401
+ },
402
+ "payloadHash": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
403
+ "ownerFingerprint": { "type": "string", "minLength": 1 }
404
+ }
405
+ }
406
+ }
407
+ }
408
+ }
409
+ }
410
+ }
@@ -0,0 +1,93 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://cueline.dev/schemas/cli-runs-prune.schema.json",
4
+ "title": "cueline runs prune --json output",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "schema",
9
+ "version",
10
+ "home",
11
+ "apply",
12
+ "olderThanMs",
13
+ "cutoff",
14
+ "states",
15
+ "decisions",
16
+ "prunedRuns",
17
+ "eligibleRuns",
18
+ "keptRuns",
19
+ "removedJobRecords",
20
+ "errors"
21
+ ],
22
+ "properties": {
23
+ "schema": { "const": "cueline-runs-prune/1" },
24
+ "version": { "type": "string", "minLength": 1 },
25
+ "home": { "type": "string", "minLength": 1 },
26
+ "apply": { "type": "boolean" },
27
+ "olderThanMs": { "type": "integer", "minimum": 0 },
28
+ "cutoff": {
29
+ "type": "string",
30
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?Z$"
31
+ },
32
+ "states": {
33
+ "type": "array",
34
+ "minItems": 1,
35
+ "uniqueItems": true,
36
+ "items": { "enum": ["complete", "blocked", "cancelled"] }
37
+ },
38
+ "decisions": {
39
+ "type": "array",
40
+ "items": {
41
+ "type": "object",
42
+ "additionalProperties": false,
43
+ "required": ["runId", "decision"],
44
+ "allOf": [
45
+ {
46
+ "if": {
47
+ "properties": { "decision": { "const": "kept" } },
48
+ "required": ["decision"]
49
+ },
50
+ "then": { "required": ["reason"], "properties": { "reason": {} } },
51
+ "else": { "not": { "required": ["reason"], "properties": { "reason": {} } } }
52
+ }
53
+ ],
54
+ "properties": {
55
+ "runId": { "type": "string", "minLength": 1 },
56
+ "decision": { "enum": ["pruned", "eligible", "kept"] },
57
+ "reason": {
58
+ "enum": [
59
+ "unreadable",
60
+ "non_terminal",
61
+ "state_excluded",
62
+ "runtime_active",
63
+ "active_jobs",
64
+ "too_recent",
65
+ "delete_failed"
66
+ ]
67
+ },
68
+ "status": { "type": "string", "minLength": 1 },
69
+ "lastEventAt": {
70
+ "type": "string",
71
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?Z$"
72
+ }
73
+ }
74
+ }
75
+ },
76
+ "prunedRuns": { "type": "integer", "minimum": 0 },
77
+ "eligibleRuns": { "type": "integer", "minimum": 0 },
78
+ "keptRuns": { "type": "integer", "minimum": 0 },
79
+ "removedJobRecords": { "type": "integer", "minimum": 0 },
80
+ "errors": {
81
+ "type": "array",
82
+ "items": {
83
+ "type": "object",
84
+ "additionalProperties": false,
85
+ "required": ["runId", "message"],
86
+ "properties": {
87
+ "runId": { "type": "string", "minLength": 1 },
88
+ "message": { "type": "string", "minLength": 1 }
89
+ }
90
+ }
91
+ }
92
+ }
93
+ }
@@ -0,0 +1,114 @@
1
+ import { createHash } from "node:crypto";
2
+ import { execFile as execFileCallback } from "node:child_process";
3
+ import { createReadStream } from "node:fs";
4
+ import { lstat, mkdir, readFile, realpath, writeFile } from "node:fs/promises";
5
+ import path from "node:path";
6
+ import { promisify } from "node:util";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ const execFile = promisify(execFileCallback);
10
+ const SCHEMA = "cueline-artifact-manifest/1";
11
+
12
+ export async function sha256File(file) {
13
+ const hash = createHash("sha256");
14
+ for await (const chunk of createReadStream(file)) hash.update(chunk);
15
+ return hash.digest("hex");
16
+ }
17
+
18
+ function isInside(parent, candidate) {
19
+ return candidate.startsWith(`${parent}${path.sep}`);
20
+ }
21
+
22
+ export async function prepareOutputDirectory(root, requested = "release-artifacts") {
23
+ const rootReal = await realpath(root);
24
+ const output = path.resolve(rootReal, requested);
25
+ if (!isInside(rootReal, output)) {
26
+ throw new Error("artifact output must be a child directory of the repository");
27
+ }
28
+ await mkdir(output, { recursive: true });
29
+ if ((await lstat(output)).isSymbolicLink()) {
30
+ throw new Error("artifact output directory must not be a symbolic link");
31
+ }
32
+ const outputReal = await realpath(output);
33
+ if (!isInside(rootReal, outputReal)) {
34
+ throw new Error("artifact output resolved outside the repository");
35
+ }
36
+ return outputReal;
37
+ }
38
+
39
+ function safeArtifactName(value) {
40
+ return typeof value === "string" && value !== "" && value === path.basename(value) && !value.includes("..") && value.endsWith(".tgz");
41
+ }
42
+
43
+ export async function verifyArtifactManifest(manifestPath) {
44
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
45
+ if (manifest.schema !== SCHEMA) throw new Error("unsupported artifact manifest schema");
46
+ if (!safeArtifactName(manifest.artifact?.filename)) throw new Error("unsafe artifact filename");
47
+ if (!/^[0-9a-f]{64}$/.test(manifest.artifact.sha256)) throw new Error("invalid artifact sha256");
48
+ const directory = path.dirname(manifestPath);
49
+ const artifactPath = path.join(directory, manifest.artifact.filename);
50
+ const checksumPath = `${artifactPath}.sha256`;
51
+ const actual = await sha256File(artifactPath);
52
+ if (actual !== manifest.artifact.sha256) throw new Error("artifact sha256 mismatch");
53
+ const checksum = await readFile(checksumPath, "utf8");
54
+ if (checksum !== `${actual} ${manifest.artifact.filename}\n`) throw new Error("checksum file mismatch");
55
+ const size = (await lstat(artifactPath)).size;
56
+ if (size !== manifest.artifact.size) throw new Error("artifact size mismatch");
57
+ return { schema: SCHEMA, status: "ok", filename: manifest.artifact.filename, sha256: actual, size };
58
+ }
59
+
60
+ export async function buildArtifact(root, requestedOutput = "release-artifacts") {
61
+ const output = await prepareOutputDirectory(root, requestedOutput);
62
+ const { stdout } = await execFile("npm", ["pack", "--json", "--pack-destination", output], {
63
+ cwd: root,
64
+ maxBuffer: 10 * 1024 * 1024,
65
+ });
66
+ const report = JSON.parse(stdout)[0];
67
+ if (!safeArtifactName(report?.filename)) throw new Error("npm pack returned an unsafe filename");
68
+ const artifactPath = path.join(output, report.filename);
69
+ const sha256 = await sha256File(artifactPath);
70
+ const files = [...(report.files ?? [])]
71
+ .map(({ path: file, size, mode }) => ({ path: file, size, mode }))
72
+ .sort((left, right) => left.path.localeCompare(right.path));
73
+ const manifest = {
74
+ schema: SCHEMA,
75
+ package: { name: report.name, version: report.version },
76
+ artifact: {
77
+ filename: report.filename,
78
+ sha256,
79
+ npmShasum: report.shasum,
80
+ npmIntegrity: report.integrity,
81
+ size: report.size,
82
+ unpackedSize: report.unpackedSize,
83
+ fileCount: files.length,
84
+ },
85
+ files,
86
+ };
87
+ const manifestPath = `${artifactPath}.manifest.json`;
88
+ await writeFile(`${artifactPath}.sha256`, `${sha256} ${report.filename}\n`, { mode: 0o600 });
89
+ await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
90
+ const verification = await verifyArtifactManifest(manifestPath);
91
+ return { ...verification, manifestPath, checksumPath: `${artifactPath}.sha256`, fileCount: files.length };
92
+ }
93
+
94
+ function parseArguments(args) {
95
+ const [command, value] = args;
96
+ if (command === "build" && args.length <= 2) return { command, output: value ?? "release-artifacts" };
97
+ if (command === "verify" && args.length === 2) return { command, manifest: value };
98
+ throw new Error("usage: artifact-integrity.mjs build [output-directory] | verify <manifest-path>");
99
+ }
100
+
101
+ const invokedPath = process.argv[1] === undefined ? "" : path.resolve(process.argv[1]);
102
+ if (invokedPath === fileURLToPath(import.meta.url)) {
103
+ try {
104
+ const root = path.resolve(fileURLToPath(new URL("..", import.meta.url)));
105
+ const args = parseArguments(process.argv.slice(2));
106
+ const report = args.command === "build"
107
+ ? await buildArtifact(root, args.output)
108
+ : await verifyArtifactManifest(path.resolve(root, args.manifest));
109
+ console.log(JSON.stringify(report, null, 2));
110
+ } catch (error) {
111
+ console.error(JSON.stringify({ schema: SCHEMA, status: "error", message: error instanceof Error ? error.message : "artifact operation failed" }));
112
+ process.exitCode = 1;
113
+ }
114
+ }