@dzhechkov/harness-cli 0.7.0 → 0.7.2
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/.dz-manifest.json +18 -18
- package/README.md +14 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +75 -17
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/sbom.json +17 -17
- package/src/cli.ts +72 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dzhechkov/harness-cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "The dz CLI — install AI skills for Claude Code, Codex, OpenCode, Hermes, OpenClaude, GitHub Copilot. 67 commands, 14 presets, 10 platform targets.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/sbom.json
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"hashes": [
|
|
26
26
|
{
|
|
27
27
|
"alg": "SHA-256",
|
|
28
|
-
"content": "
|
|
28
|
+
"content": "ca0d42d114330fe7a2d0833abbf2fe90f3a60221e6cffeb3dc3b7edc7f818ec6"
|
|
29
29
|
}
|
|
30
30
|
]
|
|
31
31
|
},
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"hashes": [
|
|
76
76
|
{
|
|
77
77
|
"alg": "SHA-256",
|
|
78
|
-
"content": "
|
|
78
|
+
"content": "5aeeec8ab84e3922ce0c7b7ea711fd1cdff67b68e1ded0b66e920a907e6ac267"
|
|
79
79
|
}
|
|
80
80
|
]
|
|
81
81
|
},
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
"hashes": [
|
|
86
86
|
{
|
|
87
87
|
"alg": "SHA-256",
|
|
88
|
-
"content": "
|
|
88
|
+
"content": "f1cc2dc4a5de433a2d3cbd1e0567bf9c7bb5f3d04e90f72d10c54a9ccc5b00ae"
|
|
89
89
|
}
|
|
90
90
|
]
|
|
91
91
|
},
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
"hashes": [
|
|
96
96
|
{
|
|
97
97
|
"alg": "SHA-256",
|
|
98
|
-
"content": "
|
|
98
|
+
"content": "a1154eea12c861e092af90e33b5074d1219401f2194e2205f29ae9deb742834e"
|
|
99
99
|
}
|
|
100
100
|
]
|
|
101
101
|
},
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
"hashes": [
|
|
106
106
|
{
|
|
107
107
|
"alg": "SHA-256",
|
|
108
|
-
"content": "
|
|
108
|
+
"content": "a1fcb5f0f1524e7564bdec11bcf3b1c8d9d41ddb08ea5502dc72bbb3de439baa"
|
|
109
109
|
}
|
|
110
110
|
]
|
|
111
111
|
},
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
"hashes": [
|
|
116
116
|
{
|
|
117
117
|
"alg": "SHA-256",
|
|
118
|
-
"content": "
|
|
118
|
+
"content": "4ab523bc0a5f777fd66b8ed44c07a8b240a4cf2ce32da779a3916e6ddc53fa33"
|
|
119
119
|
}
|
|
120
120
|
]
|
|
121
121
|
},
|
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
"hashes": [
|
|
126
126
|
{
|
|
127
127
|
"alg": "SHA-256",
|
|
128
|
-
"content": "
|
|
128
|
+
"content": "e378022dd85db141f1d5b4d06d4791072b4caf6900b992dde5781d09b0559c35"
|
|
129
129
|
}
|
|
130
130
|
]
|
|
131
131
|
},
|
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
"hashes": [
|
|
136
136
|
{
|
|
137
137
|
"alg": "SHA-256",
|
|
138
|
-
"content": "
|
|
138
|
+
"content": "0c33ad728e7b3b25211d0b16d2803b9ca35d0aae6d3a570b57aa9b8377fcde1f"
|
|
139
139
|
}
|
|
140
140
|
]
|
|
141
141
|
},
|
|
@@ -145,7 +145,7 @@
|
|
|
145
145
|
"hashes": [
|
|
146
146
|
{
|
|
147
147
|
"alg": "SHA-256",
|
|
148
|
-
"content": "
|
|
148
|
+
"content": "342b0c716f6ac9f853e6fb9e10d2d3ae4c643f047becd200d6f8576a84674fb2"
|
|
149
149
|
}
|
|
150
150
|
]
|
|
151
151
|
},
|
|
@@ -155,7 +155,7 @@
|
|
|
155
155
|
"hashes": [
|
|
156
156
|
{
|
|
157
157
|
"alg": "SHA-256",
|
|
158
|
-
"content": "
|
|
158
|
+
"content": "936596fd99f3767ca0119a02f07dfabf22b5b1c30e7007c63c3ff21cfced3836"
|
|
159
159
|
}
|
|
160
160
|
]
|
|
161
161
|
},
|
|
@@ -165,7 +165,7 @@
|
|
|
165
165
|
"hashes": [
|
|
166
166
|
{
|
|
167
167
|
"alg": "SHA-256",
|
|
168
|
-
"content": "
|
|
168
|
+
"content": "9096b181a0164cd59061bddcf9f4bee59123c1ab776e7a12da8b9808bd5b7936"
|
|
169
169
|
}
|
|
170
170
|
]
|
|
171
171
|
},
|
|
@@ -175,7 +175,7 @@
|
|
|
175
175
|
"hashes": [
|
|
176
176
|
{
|
|
177
177
|
"alg": "SHA-256",
|
|
178
|
-
"content": "
|
|
178
|
+
"content": "8225f1de31f005c0d883fa9cf9b3a106f5e244ebbe5b4b3d2375c8127ba821f8"
|
|
179
179
|
}
|
|
180
180
|
]
|
|
181
181
|
},
|
|
@@ -185,7 +185,7 @@
|
|
|
185
185
|
"hashes": [
|
|
186
186
|
{
|
|
187
187
|
"alg": "SHA-256",
|
|
188
|
-
"content": "
|
|
188
|
+
"content": "02b6b6f1b17a53d652a75555d185a20ea5d3e3110aa915ab228bf5521b0ce5fb"
|
|
189
189
|
}
|
|
190
190
|
]
|
|
191
191
|
},
|
|
@@ -215,7 +215,7 @@
|
|
|
215
215
|
"hashes": [
|
|
216
216
|
{
|
|
217
217
|
"alg": "SHA-256",
|
|
218
|
-
"content": "
|
|
218
|
+
"content": "ec93467aada7c720f48874e6f24a5fb57bb77e8f5e01a450a29bc363b81f7b9a"
|
|
219
219
|
}
|
|
220
220
|
]
|
|
221
221
|
},
|
|
@@ -235,7 +235,7 @@
|
|
|
235
235
|
"hashes": [
|
|
236
236
|
{
|
|
237
237
|
"alg": "SHA-256",
|
|
238
|
-
"content": "
|
|
238
|
+
"content": "a31c2567ee0817e7a01eb034ac4ae8e9c44e18edea85ef722af2d32b4a2dabc4"
|
|
239
239
|
}
|
|
240
240
|
]
|
|
241
241
|
},
|
|
@@ -245,7 +245,7 @@
|
|
|
245
245
|
"hashes": [
|
|
246
246
|
{
|
|
247
247
|
"alg": "SHA-256",
|
|
248
|
-
"content": "
|
|
248
|
+
"content": "b6e2b8d73bb492afec8717f39ecfe3638c62b2f1d2479a9be43c493adebf89d6"
|
|
249
249
|
}
|
|
250
250
|
]
|
|
251
251
|
},
|
|
@@ -255,7 +255,7 @@
|
|
|
255
255
|
"hashes": [
|
|
256
256
|
{
|
|
257
257
|
"alg": "SHA-256",
|
|
258
|
-
"content": "
|
|
258
|
+
"content": "d4258f3b183d03047aa07399ae96bdb2b3af83f0005127a24a39f8932982bf78"
|
|
259
259
|
}
|
|
260
260
|
]
|
|
261
261
|
}
|
package/src/cli.ts
CHANGED
|
@@ -5192,17 +5192,40 @@ function cmdPublish(options: Map<string, string>, flags: Set<string>, cwd: strin
|
|
|
5192
5192
|
const manifestPath = join(pk.dir, MANIFEST_NAME);
|
|
5193
5193
|
const manifestPresent = existsSync(manifestPath);
|
|
5194
5194
|
let verifyOk = false;
|
|
5195
|
+
let artifactUnavailable = false;
|
|
5195
5196
|
if (trustRootPresent && manifestPresent) {
|
|
5197
|
+
// Verify the ARTIFACT, through the SAME extraction `dz sign` uses. This used to walk the
|
|
5198
|
+
// source TREE narrowed to a shipped-path list — a different object by construction, because
|
|
5199
|
+
// npm synthesises a LICENSE into the pack of a package whose tree has none and pnpm rewrites
|
|
5200
|
+
// package.json at pack time. So the gate named two failures that were both true about the
|
|
5201
|
+
// tree and both wrong about what ships, for EVERY package, and a fail-closed gate no input
|
|
5202
|
+
// can satisfy is a gate that gets routed around — it was: the 0.7.0 release went out through
|
|
5203
|
+
// `pnpm publish` and skipped re-signing (ADR-001, features/publish-gate-verifies-the-tarball).
|
|
5204
|
+
let cleanupGate: (() => void) | null = null;
|
|
5196
5205
|
try {
|
|
5197
5206
|
const signed = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
5198
|
-
let
|
|
5199
|
-
try {
|
|
5200
|
-
|
|
5207
|
+
let extracted: { dir: string; cleanup: () => void } | undefined;
|
|
5208
|
+
try {
|
|
5209
|
+
extracted = extractPublishTarball(pk.dir);
|
|
5210
|
+
cleanupGate = extracted.cleanup;
|
|
5211
|
+
} catch (err) {
|
|
5212
|
+
// Cross-family review (codex `gpt-5.6-sol`, 2026-08-22): falling back to the working
|
|
5213
|
+
// TREE here fails the gate OPEN. The gate's whole claim is "what ships matches the
|
|
5214
|
+
// signature"; with no artifact, nothing was compared, and reporting a pass would be a
|
|
5215
|
+
// claim about an object that was never built. Say why, and block.
|
|
5216
|
+
write(`dz publish: could not pack ${pk.name} (${(err as Error).message.split('\n')[0]}) — the artifact was never built, so its signature was not checked`);
|
|
5217
|
+
artifactUnavailable = true;
|
|
5218
|
+
}
|
|
5219
|
+
verifyOk =
|
|
5220
|
+
extracted !== undefined &&
|
|
5221
|
+
verifyManifest(extracted.dir, signed, readFileSync(trustRoot, 'utf8')).ok;
|
|
5201
5222
|
} catch {
|
|
5202
5223
|
verifyOk = false;
|
|
5224
|
+
} finally {
|
|
5225
|
+
cleanupGate?.();
|
|
5203
5226
|
}
|
|
5204
5227
|
}
|
|
5205
|
-
const decision = decidePublishGate({ trustRootPresent, manifestPresent, verifyOk, requireSigning });
|
|
5228
|
+
const decision = decidePublishGate({ trustRootPresent, manifestPresent, verifyOk, requireSigning, artifactUnavailable });
|
|
5206
5229
|
if (decision.action === 'block') {
|
|
5207
5230
|
write(`dz publish: BLOCKED ${pk.name} — ${decision.reason}`);
|
|
5208
5231
|
blocked++;
|
|
@@ -5232,16 +5255,31 @@ function cmdPublish(options: Map<string, string>, flags: Set<string>, cwd: strin
|
|
|
5232
5255
|
// Verify the OUTCOME against the trust root a CONSUMER would use — an existing key may be the
|
|
5233
5256
|
// WRONG key, and enumerating that state is a losing game (round-1 review). The pack NAME travels
|
|
5234
5257
|
// with the verdict so a pass about a different artifact cannot be mistaken for this one.
|
|
5258
|
+
// The identity must be the one the PUBLISHER uses — the npm package name — or the check
|
|
5259
|
+
// compares two vocabularies and can never agree. It did: `basename(packDir)` is `memory`
|
|
5260
|
+
// where the publisher says `@dzhechkov/memory`, so every scoped package (all of them here)
|
|
5261
|
+
// was refused as "a pass about another artifact", and the release routed around the gate
|
|
5262
|
+
// instead. MEASURED 2026-08-22 by running `dz publish --filter memory --yes`.
|
|
5235
5263
|
const trustRoot = resolve(cwd, TRUST_ROOT_REL);
|
|
5236
|
-
|
|
5264
|
+
const identity = packNpmName(packDir);
|
|
5265
|
+
const id = identity === undefined ? {} : { pack: identity };
|
|
5266
|
+
if (!existsSync(trustRoot)) return { ok: false, trustRootPresent: false, ...id };
|
|
5267
|
+
// Verify the SAME OBJECT `reSign` hashed: the extracted tarball. Walking the source tree here
|
|
5268
|
+
// compares a manifest built from the artifact against files npm rewrites at pack time, so it
|
|
5269
|
+
// reported "does not verify — most likely the WRONG signing key" about a correctly signed pack
|
|
5270
|
+
// (MEASURED 2026-08-22, `dz publish --filter memory --yes`). That message sends the operator
|
|
5271
|
+
// hunting for a key problem that does not exist; the object was simply the wrong one.
|
|
5272
|
+
let cleanup: (() => void) | null = null;
|
|
5237
5273
|
try {
|
|
5238
5274
|
const signed = JSON.parse(readFileSync(join(packDir, MANIFEST_NAME), 'utf8')) as never;
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
const res = verifyManifest(
|
|
5242
|
-
return { ok: res.ok, trustRootPresent: true,
|
|
5275
|
+
const extracted = extractPublishTarball(packDir);
|
|
5276
|
+
cleanup = extracted.cleanup;
|
|
5277
|
+
const res = verifyManifest(extracted.dir, signed, readFileSync(trustRoot, 'utf8'));
|
|
5278
|
+
return { ok: res.ok, trustRootPresent: true, ...id };
|
|
5243
5279
|
} catch {
|
|
5244
|
-
return { ok: false, trustRootPresent: true,
|
|
5280
|
+
return { ok: false, trustRootPresent: true, ...id };
|
|
5281
|
+
} finally {
|
|
5282
|
+
cleanup?.();
|
|
5245
5283
|
}
|
|
5246
5284
|
},
|
|
5247
5285
|
reSign: (packDir: string, keyPath: string): void => {
|
|
@@ -9374,6 +9412,19 @@ function scanOneReqeRoot(
|
|
|
9374
9412
|
* authority on what `files[]` ships; reimplementing its globbing would put a second, divergent answer
|
|
9375
9413
|
* next to the real one — which is the class of defect this whole change exists to remove.
|
|
9376
9414
|
*/
|
|
9415
|
+
/**
|
|
9416
|
+
* The npm name of a pack on disk — the identity a publisher, a registry and a consumer all use.
|
|
9417
|
+
* `undefined` when it cannot be read, so an unestablished identity is never asserted as a match.
|
|
9418
|
+
*/
|
|
9419
|
+
function packNpmName(packDir: string): string | undefined {
|
|
9420
|
+
try {
|
|
9421
|
+
const name = (JSON.parse(readFileSync(join(packDir, 'package.json'), 'utf8')) as { name?: unknown }).name;
|
|
9422
|
+
return typeof name === 'string' && name !== '' ? name : undefined;
|
|
9423
|
+
} catch {
|
|
9424
|
+
return undefined;
|
|
9425
|
+
}
|
|
9426
|
+
}
|
|
9427
|
+
|
|
9377
9428
|
function npmPackedPaths(packDir: string): string[] {
|
|
9378
9429
|
// `pnpm`, not `npm`: the PUBLISHER is `pnpm publish` (see `publishArgv`), and the two packers do not
|
|
9379
9430
|
// agree. MEASURED 2026-08-21 on `skills-news`: `npm pack` emits a 1051-byte package.json identical
|
|
@@ -9399,6 +9450,17 @@ function npmPackedPaths(packDir: string): string[] {
|
|
|
9399
9450
|
*/
|
|
9400
9451
|
function extractPublishTarball(packDir: string): { dir: string; cleanup: () => void } {
|
|
9401
9452
|
const tmp = mkdtempSync(join(tmpdir(), 'dz-sign-pack-'));
|
|
9453
|
+
try {
|
|
9454
|
+
return extractIntoTempDir(packDir, tmp);
|
|
9455
|
+
} catch (err) {
|
|
9456
|
+
// The caller never receives a cleanup for a throw, so the directory this function created must
|
|
9457
|
+
// be removed HERE or it leaks once per failed pack (cross-family review, 2026-08-22).
|
|
9458
|
+
try { rmSync(tmp, { recursive: true, force: true }); } catch { /* best-effort */ }
|
|
9459
|
+
throw err;
|
|
9460
|
+
}
|
|
9461
|
+
}
|
|
9462
|
+
|
|
9463
|
+
function extractIntoTempDir(packDir: string, tmp: string): { dir: string; cleanup: () => void } {
|
|
9402
9464
|
const out = execFileSync('pnpm', ['pack', '--pack-destination', tmp, '--json'], {
|
|
9403
9465
|
cwd: packDir,
|
|
9404
9466
|
encoding: 'utf-8',
|