@dzhechkov/harness-cli 0.3.233 → 0.3.235
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 +16 -16
- package/README.md +43 -3
- package/dist/cli.d.ts +17 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +319 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/sbom.json +15 -15
- package/src/cli.ts +334 -1
- package/src/index.ts +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -6,5 +6,5 @@
|
|
|
6
6
|
/** Package version. Kept in sync with `package.json`. */
|
|
7
7
|
export declare const HARNESS_CLI_VERSION: string;
|
|
8
8
|
export { runCli } from './cli.js';
|
|
9
|
-
export type { CliIo } from './cli.js';
|
|
9
|
+
export type { CliIo, ReleaseExecRunner } from './cli.js';
|
|
10
10
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,EAAE,MACkD,CAAC;AAErF,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,YAAY,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,EAAE,MACkD,CAAC;AAErF,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,YAAY,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dzhechkov/harness-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.235",
|
|
4
4
|
"description": "The dz CLI — install AI skills for Claude Code, Codex, OpenCode, Hermes, OpenClaude, GitHub Copilot. 35 commands, 13 presets, 6 platform targets.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"@dzhechkov/skills-reverse-engineering": "^0.1.0",
|
|
58
58
|
"@dzhechkov/skills-presentation-storyteller": "^0.1.0",
|
|
59
59
|
"@dzhechkov/skills-website-cloner": "^0.1.0",
|
|
60
|
-
"@dzhechkov/harness-core": "0.3.
|
|
60
|
+
"@dzhechkov/harness-core": "0.3.126"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/node": "^25.6.0",
|
package/sbom.json
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"hashes": [
|
|
26
26
|
{
|
|
27
27
|
"alg": "SHA-256",
|
|
28
|
-
"content": "
|
|
28
|
+
"content": "99fa5178a48b6953b8f3a6f1690204a3b5449004b7dd105e7a3155ad3658266c"
|
|
29
29
|
}
|
|
30
30
|
]
|
|
31
31
|
},
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"hashes": [
|
|
46
46
|
{
|
|
47
47
|
"alg": "SHA-256",
|
|
48
|
-
"content": "
|
|
48
|
+
"content": "cab99dd5d4068013a0c5f4f1c2d29dbec07729dc47c1d77adba6a53b773d0026"
|
|
49
49
|
}
|
|
50
50
|
]
|
|
51
51
|
},
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"hashes": [
|
|
56
56
|
{
|
|
57
57
|
"alg": "SHA-256",
|
|
58
|
-
"content": "
|
|
58
|
+
"content": "2cc80ec378c1f9cd2588676588ee1b73bfe4f2f7ef3b96879c295eb86dccd2c2"
|
|
59
59
|
}
|
|
60
60
|
]
|
|
61
61
|
},
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
"hashes": [
|
|
86
86
|
{
|
|
87
87
|
"alg": "SHA-256",
|
|
88
|
-
"content": "
|
|
88
|
+
"content": "84842f3916c558b1f4f2da2a5b0236587f146dda1f6ab4ba4c31ed7b8b17f943"
|
|
89
89
|
}
|
|
90
90
|
]
|
|
91
91
|
},
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
"hashes": [
|
|
106
106
|
{
|
|
107
107
|
"alg": "SHA-256",
|
|
108
|
-
"content": "
|
|
108
|
+
"content": "378ef9c8fcde4e77594b29a5f043b57f880f5df2b40fce679addfae756670154"
|
|
109
109
|
}
|
|
110
110
|
]
|
|
111
111
|
},
|
|
@@ -175,7 +175,7 @@
|
|
|
175
175
|
"hashes": [
|
|
176
176
|
{
|
|
177
177
|
"alg": "SHA-256",
|
|
178
|
-
"content": "
|
|
178
|
+
"content": "628c55bb864dd8a8f337a9cb3fe4271f0df7c0bf9237b1150427171f0577fd57"
|
|
179
179
|
}
|
|
180
180
|
]
|
|
181
181
|
},
|
|
@@ -185,7 +185,7 @@
|
|
|
185
185
|
"hashes": [
|
|
186
186
|
{
|
|
187
187
|
"alg": "SHA-256",
|
|
188
|
-
"content": "
|
|
188
|
+
"content": "45c0c1328829a56cfe0ea0ebc74c15477a00bb683b496404688ab4b7b7a88ef8"
|
|
189
189
|
}
|
|
190
190
|
]
|
|
191
191
|
},
|
|
@@ -195,7 +195,7 @@
|
|
|
195
195
|
"hashes": [
|
|
196
196
|
{
|
|
197
197
|
"alg": "SHA-256",
|
|
198
|
-
"content": "
|
|
198
|
+
"content": "20974a159438ac73a4c308cdb3710154dd9fe0450ed5184f6f3938792380fece"
|
|
199
199
|
}
|
|
200
200
|
]
|
|
201
201
|
},
|
|
@@ -205,7 +205,7 @@
|
|
|
205
205
|
"hashes": [
|
|
206
206
|
{
|
|
207
207
|
"alg": "SHA-256",
|
|
208
|
-
"content": "
|
|
208
|
+
"content": "bfb9ecebeb1c3cc631f2c8e7f46af4318e3698789c9dda49aaebbe26fe1c7fa7"
|
|
209
209
|
}
|
|
210
210
|
]
|
|
211
211
|
},
|
|
@@ -215,7 +215,7 @@
|
|
|
215
215
|
"hashes": [
|
|
216
216
|
{
|
|
217
217
|
"alg": "SHA-256",
|
|
218
|
-
"content": "
|
|
218
|
+
"content": "aaac0134a074fd06e9b5e09daa952ef6353510e481885da51a6e4f2e92beba35"
|
|
219
219
|
}
|
|
220
220
|
]
|
|
221
221
|
},
|
|
@@ -225,7 +225,7 @@
|
|
|
225
225
|
"hashes": [
|
|
226
226
|
{
|
|
227
227
|
"alg": "SHA-256",
|
|
228
|
-
"content": "
|
|
228
|
+
"content": "459c3381a9fbcd3378b623f9fe103a8d3337e4025a2f546b6d40e4d9dd4e0f70"
|
|
229
229
|
}
|
|
230
230
|
]
|
|
231
231
|
},
|
|
@@ -265,7 +265,7 @@
|
|
|
265
265
|
"hashes": [
|
|
266
266
|
{
|
|
267
267
|
"alg": "SHA-256",
|
|
268
|
-
"content": "
|
|
268
|
+
"content": "c03aafaeaf663bb47b756608049bf77f6460615f1e806054644ec830c524cf72"
|
|
269
269
|
}
|
|
270
270
|
]
|
|
271
271
|
},
|
|
@@ -285,7 +285,7 @@
|
|
|
285
285
|
"hashes": [
|
|
286
286
|
{
|
|
287
287
|
"alg": "SHA-256",
|
|
288
|
-
"content": "
|
|
288
|
+
"content": "6b8d21416576a2744b5d9bf973d7b878c255495829fcf3679b183a977abc55a2"
|
|
289
289
|
}
|
|
290
290
|
]
|
|
291
291
|
},
|
|
@@ -295,7 +295,7 @@
|
|
|
295
295
|
"hashes": [
|
|
296
296
|
{
|
|
297
297
|
"alg": "SHA-256",
|
|
298
|
-
"content": "
|
|
298
|
+
"content": "395b27b317e575b94a1049cdb284db185e0ec382fbae1e588c78ff2398b2563f"
|
|
299
299
|
}
|
|
300
300
|
]
|
|
301
301
|
},
|
|
@@ -305,7 +305,7 @@
|
|
|
305
305
|
"hashes": [
|
|
306
306
|
{
|
|
307
307
|
"alg": "SHA-256",
|
|
308
|
-
"content": "
|
|
308
|
+
"content": "09472c195254c359f14256c39e8c7afd08c0b71aadb57cd077534f0d7c482005"
|
|
309
309
|
}
|
|
310
310
|
]
|
|
311
311
|
},
|
package/src/cli.ts
CHANGED
|
@@ -114,6 +114,15 @@ import {
|
|
|
114
114
|
listSignablePackFiles,
|
|
115
115
|
assertKeyOutsideTree,
|
|
116
116
|
decidePublishGate,
|
|
117
|
+
collectPackageFacts,
|
|
118
|
+
planReleaseGates,
|
|
119
|
+
selectAffectedPackages,
|
|
120
|
+
classifyGateExecutions,
|
|
121
|
+
buildFailureIssue,
|
|
122
|
+
buildReleaseNotes,
|
|
123
|
+
releaseTagName,
|
|
124
|
+
firstOutputLine,
|
|
125
|
+
formatPublishError,
|
|
117
126
|
MANIFEST_NAME,
|
|
118
127
|
SBOM_NAME,
|
|
119
128
|
buildArchitectureMap,
|
|
@@ -170,7 +179,7 @@ import {
|
|
|
170
179
|
} from '@dzhechkov/harness-core';
|
|
171
180
|
import type { Family, ModelRung, Candidate as BtoCandidate, DimScores } from '@dzhechkov/harness-core';
|
|
172
181
|
import type { SetupSpec } from '@dzhechkov/harness-core';
|
|
173
|
-
import type { ProvenanceMode, PackVerdict, ClaudeUsageModel, PatternRecord, TargetName, BookKU, HarmonizeReport, UsageCalibrationPlan, ClaimFinding, RecallUsagePatternRow } from '@dzhechkov/harness-core';
|
|
182
|
+
import type { ProvenanceMode, PackVerdict, ClaudeUsageModel, PatternRecord, TargetName, BookKU, HarmonizeReport, UsageCalibrationPlan, ClaimFinding, RecallUsagePatternRow, GateExecution, GateStep } from '@dzhechkov/harness-core';
|
|
174
183
|
import { getPreset, PRESET_NAMES } from '@dzhechkov/harness-presets';
|
|
175
184
|
import { scanGitHub, analyzeRepo, generateReport, deepAnalyze, scanAllSources, ScoutMemory } from '@dzhechkov/scout';
|
|
176
185
|
|
|
@@ -194,6 +203,7 @@ Usage:
|
|
|
194
203
|
dz sign --pack <dir> --key <path-outside-repo> (Ed25519 manifest + CycloneDX SBOM for a pack)
|
|
195
204
|
dz verify-pack --pack <dir> [--pubkey <path>] (signature check; fail-closed; key from the repo, never the pack)
|
|
196
205
|
dz publish [--filter <name>] [--bump-only] [--claim-check <off|warn|error>] [--require-signing] [--provenance|--no-provenance] (dry-run by default; pass --yes/--confirm/--no-dry-run to go live; claim-check gate default warn — surfaces README claim findings, never blocks; error fails an offending package)
|
|
206
|
+
dz release [--filter <name>] [--tag] [--publish] [--json] [--dry-run] [--no-issue] (VERIFIED release: 4 HARD gates in FRONT of dz publish — full package test suites, audit >=high, node --check of every dist/bin file, bin smoke-boot via "node <bin> --help" — any red gate STOPS the release (exit 1) + best-effort gh issue; all green ⇒ re-sign reminder, then prints the ready dz publish command (or chains with --publish); never duplicates publish's own gates)
|
|
197
207
|
dz setup --target <name> [--preset <name>] [--select id,id,...] [--skills-dir <dir>] [--project <dir>] [--memory agentdb] [--no-memory] [--no-hooks] [--install-driver] [--force] [--enrich]
|
|
198
208
|
dz teach "<pattern>" [--reward <0-1>] [--domain <name>] [--type rule|success-pattern|lesson-learned] [--project <dir>] [--no-mirror] (--project pins the learned store to <dir>/.dz, not the cwd — pin to a canonical brain)
|
|
199
209
|
dz teach --from-json <file> [--project <dir>] [--no-mirror] (bulk-import a 'dz recall --all --json' export — share a learned store across machines)
|
|
@@ -254,8 +264,21 @@ export interface CliIo {
|
|
|
254
264
|
* command that needs it (`brain ground`), and never when stdin is a TTY (nothing piped).
|
|
255
265
|
*/
|
|
256
266
|
readonly stdin?: string;
|
|
267
|
+
/**
|
|
268
|
+
* Test seam for `dz release`: overrides subprocess execution for gate steps and the
|
|
269
|
+
* gh/git side channels (production leaves it unset → real `execSync`, stdio piped).
|
|
270
|
+
* A scripted runner makes failing gates, gh outages, and git-tag failures testable
|
|
271
|
+
* without spawning anything.
|
|
272
|
+
*/
|
|
273
|
+
readonly releaseRunner?: ReleaseExecRunner;
|
|
257
274
|
}
|
|
258
275
|
|
|
276
|
+
/** Injected subprocess runner used by `dz release` (see {@link CliIo.releaseRunner}). */
|
|
277
|
+
export type ReleaseExecRunner = (
|
|
278
|
+
cmd: string,
|
|
279
|
+
opts: { readonly cwd: string; readonly timeoutMs: number },
|
|
280
|
+
) => { exitCode: number; stdout: string; stderr: string; timedOut?: boolean };
|
|
281
|
+
|
|
259
282
|
interface ParsedArgs {
|
|
260
283
|
readonly command: string;
|
|
261
284
|
readonly options: Map<string, string>;
|
|
@@ -3548,6 +3571,294 @@ function cmdPublish(options: Map<string, string>, flags: Set<string>, cwd: strin
|
|
|
3548
3571
|
return report.errors > 0 ? 1 : 0;
|
|
3549
3572
|
}
|
|
3550
3573
|
|
|
3574
|
+
/* ------------------------------------------------------------------ */
|
|
3575
|
+
/* dz release — verified-release conveyor (feature release-verified, */
|
|
3576
|
+
/* ADR-001): 4 HARD gates in FRONT of the untouched dz publish. */
|
|
3577
|
+
/* ------------------------------------------------------------------ */
|
|
3578
|
+
|
|
3579
|
+
/**
|
|
3580
|
+
* The single executor of the pure engine's {@link GateStep} plan. ALL subprocess side effects
|
|
3581
|
+
* of the release live here (engine purity, NFR-1). Contract highlights:
|
|
3582
|
+
*
|
|
3583
|
+
* - ALL four gates execute even after an earlier gate fails (AM-9/FR-6 — no fail-fast hiding);
|
|
3584
|
+
* one red gate still reddens the release.
|
|
3585
|
+
* - Smoke steps run in a THROWAWAY cwd (AM-4): skills bins are installers that mutate
|
|
3586
|
+
* `.claude/` on default action.
|
|
3587
|
+
* - gh issue + git tag are best-effort periphery: loud on failure, NEVER verdict- or
|
|
3588
|
+
* exit-code-affecting (FR-7/FR-8, AM-6).
|
|
3589
|
+
* - On green it never injects `--yes/--confirm/--no-dry-run` into the printed/chained publish
|
|
3590
|
+
* (AM-5): live publish stays the operator's explicit act, and `--publish` chains into
|
|
3591
|
+
* publish's own default (dry-run) protocol with ALL its gates untouched.
|
|
3592
|
+
*/
|
|
3593
|
+
function cmdRelease(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write, runner?: ReleaseExecRunner): number {
|
|
3594
|
+
const json = flags.has('json');
|
|
3595
|
+
const warnings: string[] = [];
|
|
3596
|
+
const say = (line: string): void => {
|
|
3597
|
+
if (!json) write(line);
|
|
3598
|
+
};
|
|
3599
|
+
const loud = (line: string): void => {
|
|
3600
|
+
warnings.push(line);
|
|
3601
|
+
if (!json) write(line);
|
|
3602
|
+
};
|
|
3603
|
+
const emitJson = (obj: Record<string, unknown>): void => {
|
|
3604
|
+
if (json) write(JSON.stringify({ ...obj, warnings }, null, 2));
|
|
3605
|
+
};
|
|
3606
|
+
// G9 reuse-never-copy: the engine's firstOutputLine, not a byte-parallel local copy.
|
|
3607
|
+
const oneLine = firstOutputLine;
|
|
3608
|
+
|
|
3609
|
+
// Strict allowlist (cmdPublish's typo-rejection pattern) — a mistyped flag is NEVER
|
|
3610
|
+
// swallowed, and the rejection honors the --json contract too (AC-7).
|
|
3611
|
+
// `--verified` is accepted but not read: verified is the DEFAULT and only mode of
|
|
3612
|
+
// `dz release` in this slice — the flag exists so the branded invocation is not a typo error.
|
|
3613
|
+
const allowedFlags = new Set(['verified', 'tag', 'publish', 'json', 'dry-run', 'no-issue', 'affected', 'audit-dev', 'help']);
|
|
3614
|
+
const allowedOptions = new Set(['filter']);
|
|
3615
|
+
const allowedHelp = ' allowed: --verified, --filter <substr>, --affected, --audit-dev, --tag, --publish, --json, --dry-run, --no-issue';
|
|
3616
|
+
const rejectUnknown = (key: string): number => {
|
|
3617
|
+
if (json) write(JSON.stringify({ error: `unknown option --${key}`, allowed: allowedHelp.trim(), publishAction: 'blocked', exitCode: 1 }, null, 2));
|
|
3618
|
+
else {
|
|
3619
|
+
write(`dz release: unknown option --${key}`);
|
|
3620
|
+
write(allowedHelp);
|
|
3621
|
+
}
|
|
3622
|
+
return 1;
|
|
3623
|
+
};
|
|
3624
|
+
for (const flag of flags) if (!allowedFlags.has(flag)) return rejectUnknown(flag);
|
|
3625
|
+
for (const key of options.keys()) {
|
|
3626
|
+
if (key.startsWith('_positional_')) continue;
|
|
3627
|
+
if (!allowedOptions.has(key)) return rejectUnknown(key);
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3630
|
+
// --filter mirrors publish semantics: trim + drop empties; empty result is an ERROR,
|
|
3631
|
+
// never "match all" (the publish P0 regression this deliberately copies).
|
|
3632
|
+
const filterStr = options.get('filter');
|
|
3633
|
+
let filter: string[] | undefined;
|
|
3634
|
+
if (filterStr !== undefined) {
|
|
3635
|
+
filter = filterStr.split(',').map((s) => s.trim()).filter((s) => s.length > 0);
|
|
3636
|
+
if (filter.length === 0) {
|
|
3637
|
+
if (json) write(JSON.stringify({ error: '--filter requires a non-empty comma-separated list of package-name substrings', publishAction: 'blocked', exitCode: 1 }, null, 2));
|
|
3638
|
+
else write('dz release: --filter requires a non-empty comma-separated list of package-name substrings');
|
|
3639
|
+
return 1;
|
|
3640
|
+
}
|
|
3641
|
+
}
|
|
3642
|
+
|
|
3643
|
+
// DETECT — real facts via the engine's one fs seam (discoverPackages underneath).
|
|
3644
|
+
let factsList: ReturnType<typeof collectPackageFacts>;
|
|
3645
|
+
try {
|
|
3646
|
+
factsList = collectPackageFacts(cwd, filter);
|
|
3647
|
+
} catch (err) {
|
|
3648
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3649
|
+
if (json) write(JSON.stringify({ error: msg, publishAction: 'blocked', exitCode: 1 }, null, 2));
|
|
3650
|
+
else write(`dz release: ${msg}`);
|
|
3651
|
+
return 1;
|
|
3652
|
+
}
|
|
3653
|
+
if (factsList.length === 0) {
|
|
3654
|
+
// C-6: outside a workspace (or an over-narrow filter) — explain and exit non-zero, never throw.
|
|
3655
|
+
const msg = 'no publishable packages found under packages/@dzhechkov — run from the monorepo root (or widen --filter)';
|
|
3656
|
+
if (json) write(JSON.stringify({ error: msg, packages: [], publishAction: 'blocked', exitCode: 1 }, null, 2));
|
|
3657
|
+
else write(`dz release: ${msg}`);
|
|
3658
|
+
return 1;
|
|
3659
|
+
}
|
|
3660
|
+
|
|
3661
|
+
// Executor: default = real execSync with piped stdio + timeout; injectable for tests.
|
|
3662
|
+
// Defined BEFORE planning because --affected needs read-only git diffs at DETECT time.
|
|
3663
|
+
const run: ReleaseExecRunner =
|
|
3664
|
+
runner ??
|
|
3665
|
+
((cmd, opts) => {
|
|
3666
|
+
try {
|
|
3667
|
+
const stdout = execSync(cmd, { cwd: opts.cwd, stdio: 'pipe', encoding: 'utf-8', timeout: opts.timeoutMs });
|
|
3668
|
+
return { exitCode: 0, stdout: stdout == null ? '' : String(stdout), stderr: '' };
|
|
3669
|
+
} catch (err) {
|
|
3670
|
+
const e = err as Error & { status?: number | null; signal?: string | null; killed?: boolean; stdout?: unknown; stderr?: unknown };
|
|
3671
|
+
// execSync's timeout kills via signal and leaves status null — that IS the timeout shape.
|
|
3672
|
+
const timedOut = (e.status === null || e.status === undefined) && (e.signal != null || e.killed === true);
|
|
3673
|
+
return {
|
|
3674
|
+
exitCode: typeof e.status === 'number' ? e.status : 1,
|
|
3675
|
+
stdout: e.stdout == null ? '' : String(e.stdout),
|
|
3676
|
+
stderr: e.stderr == null || String(e.stderr).trim() === '' ? formatPublishError(e) : String(e.stderr),
|
|
3677
|
+
timedOut,
|
|
3678
|
+
};
|
|
3679
|
+
}
|
|
3680
|
+
});
|
|
3681
|
+
|
|
3682
|
+
// AM-8/--affected: narrow the release set to packages touched by the working tree + last
|
|
3683
|
+
// commit. Selection is the PURE selectAffectedPackages; the two read-only git diffs are the
|
|
3684
|
+
// injected fact source. Any git failure ⇒ null ⇒ FAIL-OPEN to the full set (never zero).
|
|
3685
|
+
if (flags.has('affected')) {
|
|
3686
|
+
const dWork = run('git diff --name-only HEAD', { cwd, timeoutMs: 10_000 });
|
|
3687
|
+
const dLast = run('git diff --name-only HEAD~1..HEAD', { cwd, timeoutMs: 10_000 });
|
|
3688
|
+
const anyOk = dWork.exitCode === 0 || dLast.exitCode === 0;
|
|
3689
|
+
const changed = anyOk
|
|
3690
|
+
? [dWork, dLast]
|
|
3691
|
+
.filter((r) => r.exitCode === 0)
|
|
3692
|
+
.flatMap((r) => r.stdout.split('\n'))
|
|
3693
|
+
.map((s) => s.trim())
|
|
3694
|
+
.filter((s) => s.length > 0)
|
|
3695
|
+
: null;
|
|
3696
|
+
const selected = selectAffectedPackages(changed, factsList);
|
|
3697
|
+
if (changed === null) loud('dz release --affected: ⚠ git diff unavailable — FAIL-OPEN to the full package set');
|
|
3698
|
+
else say(`dz release --affected: ${selected.length}/${factsList.length} package(s) selected from ${changed.length} changed file(s)${selected.length === factsList.length ? ' (no narrowing — full set)' : ''}`);
|
|
3699
|
+
factsList = selected;
|
|
3700
|
+
}
|
|
3701
|
+
|
|
3702
|
+
const plan = planReleaseGates(factsList, {
|
|
3703
|
+
monorepoRoot: cwd,
|
|
3704
|
+
pnpmLockPresent: existsSync(join(cwd, 'pnpm-lock.yaml')),
|
|
3705
|
+
includeDevDeps: flags.has('audit-dev'),
|
|
3706
|
+
});
|
|
3707
|
+
|
|
3708
|
+
// --dry-run: print the full plan, execute NOTHING (deterministic, byte-testable preview).
|
|
3709
|
+
if (flags.has('dry-run')) {
|
|
3710
|
+
if (json) {
|
|
3711
|
+
write(JSON.stringify({ dryRun: true, packages: plan.packages, steps: plan.steps, skips: plan.skips, warnings }, null, 2));
|
|
3712
|
+
return 0;
|
|
3713
|
+
}
|
|
3714
|
+
write(`\ndz release --dry-run — plan only, zero commands executed (${plan.packages.length} package(s))`);
|
|
3715
|
+
for (const gate of ['tests', 'audit', 'syntax', 'smoke'] as const) {
|
|
3716
|
+
const steps = plan.steps.filter((s) => s.gate === gate);
|
|
3717
|
+
write(` ${gate} (${steps.length} step(s)):`);
|
|
3718
|
+
for (const s of steps) {
|
|
3719
|
+
if (s.kind === 'synthetic-fail') write(` ✗ [planned ${s.failClass ?? 'FAIL'}] ${s.pkg ?? ''}: ${s.reason}`);
|
|
3720
|
+
else write(` · ${s.cmd} (cwd=${s.tempCwd === true ? '<temp>' : s.cwd}, timeout=${s.timeoutMs}ms)`);
|
|
3721
|
+
}
|
|
3722
|
+
if (gate === 'tests') for (const sk of plan.skips) write(` ○ [${sk.class}] ${sk.pkg}: ${sk.reason}`);
|
|
3723
|
+
}
|
|
3724
|
+
write(' → run without --dry-run to execute the gates');
|
|
3725
|
+
return 0;
|
|
3726
|
+
}
|
|
3727
|
+
|
|
3728
|
+
// VERIFY — execute EVERY exec step (AM-9: all 4 gates run and report even after a failure).
|
|
3729
|
+
const executions: GateExecution[] = [];
|
|
3730
|
+
let smokeTmp: string | undefined;
|
|
3731
|
+
const execSteps: GateStep[] = plan.steps.filter((s) => s.kind !== 'synthetic-fail');
|
|
3732
|
+
say(`\ndz release — executing ${execSteps.length} gate step(s) across ${plan.packages.length} package(s)…`);
|
|
3733
|
+
for (const step of execSteps) {
|
|
3734
|
+
let stepCwd = step.cwd;
|
|
3735
|
+
if (step.tempCwd === true) {
|
|
3736
|
+
// AM-4: boot bins in a throwaway cwd so an installer-style bin cannot mutate the workspace.
|
|
3737
|
+
if (smokeTmp === undefined) smokeTmp = mkdtempSync(join(tmpdir(), 'dz-release-smoke-'));
|
|
3738
|
+
stepCwd = smokeTmp;
|
|
3739
|
+
}
|
|
3740
|
+
const started = Date.now();
|
|
3741
|
+
const r = run(step.cmd, { cwd: stepCwd, timeoutMs: step.timeoutMs });
|
|
3742
|
+
executions.push({
|
|
3743
|
+
stepId: step.id,
|
|
3744
|
+
exitCode: r.exitCode,
|
|
3745
|
+
stdout: r.stdout,
|
|
3746
|
+
stderr: r.stderr,
|
|
3747
|
+
durationMs: Date.now() - started,
|
|
3748
|
+
timedOut: r.timedOut,
|
|
3749
|
+
});
|
|
3750
|
+
}
|
|
3751
|
+
if (smokeTmp !== undefined) {
|
|
3752
|
+
try { rmSync(smokeTmp, { recursive: true, force: true }); } catch { /* best-effort cleanup */ }
|
|
3753
|
+
}
|
|
3754
|
+
|
|
3755
|
+
const verdict = classifyGateExecutions(plan, executions);
|
|
3756
|
+
|
|
3757
|
+
// Report: per-gate ✓/✗/○ with package granularity + timestamp (NFR-5). Skips are named,
|
|
3758
|
+
// never folded into pass wording (AM-2: "N passed, M skipped", not "all tests passed").
|
|
3759
|
+
for (const g of verdict.gates) {
|
|
3760
|
+
const icon = g.status === 'pass' ? '✓' : g.status === 'fail' ? '✗' : '○';
|
|
3761
|
+
say(` ${icon} ${g.gate.padEnd(7)} ${g.status.toUpperCase().padEnd(4)} ${g.passed} passed, ${g.failures.length} failed, ${g.skips.length} skipped`);
|
|
3762
|
+
for (const f of g.failures) say(` [${f.class}] ${f.pkg !== undefined ? `${f.pkg}: ` : ''}${f.reason}`);
|
|
3763
|
+
for (const sk of g.skips) say(` [${sk.class}] ${sk.pkg}: ${sk.reason}`);
|
|
3764
|
+
}
|
|
3765
|
+
say(` verdict at ${verdict.timestamp}: ${verdict.publishAction === 'proceed' ? '✓ all gates green' : '✗ RELEASE BLOCKED'}`);
|
|
3766
|
+
|
|
3767
|
+
const invocation = `dz release${filterStr !== undefined ? ` --filter ${filterStr}` : ''}`;
|
|
3768
|
+
const shq = (s: string): string => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
3769
|
+
|
|
3770
|
+
if (verdict.publishAction === 'blocked') {
|
|
3771
|
+
say(`dz release: ✗ release STOPPED — ${verdict.blockedBy.join('; ')}`);
|
|
3772
|
+
// FR-7 / AM-6: best-effort issue — a courier, never a judge. Loud on any failure;
|
|
3773
|
+
// the verdict and exit code are ALREADY decided and cannot change here.
|
|
3774
|
+
let issueUrl: string | undefined;
|
|
3775
|
+
if (!flags.has('no-issue')) {
|
|
3776
|
+
const issue = buildFailureIssue(verdict, { invocation });
|
|
3777
|
+
const probe = run('command -v gh', { cwd, timeoutMs: 10_000 });
|
|
3778
|
+
if (probe.exitCode !== 0) {
|
|
3779
|
+
loud(`dz release: ⚠ gh unavailable — file the issue manually: ${issue.title}`);
|
|
3780
|
+
} else {
|
|
3781
|
+
const res = run(`gh issue create --title ${shq(issue.title)} --body ${shq(issue.body)}`, { cwd, timeoutMs: 30_000 });
|
|
3782
|
+
if (res.exitCode === 0) {
|
|
3783
|
+
issueUrl = oneLine(res.stdout) || undefined;
|
|
3784
|
+
say(`dz release: gh issue created${issueUrl !== undefined ? `: ${issueUrl}` : ''}`);
|
|
3785
|
+
} else {
|
|
3786
|
+
loud(`dz release: ⚠ gh issue creation failed (${oneLine(res.stderr, res.stdout) || 'unknown error'}) — file the issue manually: ${issue.title}`);
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
emitJson({
|
|
3791
|
+
gates: verdict.gates,
|
|
3792
|
+
ok: verdict.ok,
|
|
3793
|
+
blockedBy: verdict.blockedBy,
|
|
3794
|
+
skipped: verdict.skipped,
|
|
3795
|
+
publishAction: verdict.publishAction,
|
|
3796
|
+
timestamp: verdict.timestamp,
|
|
3797
|
+
...(issueUrl !== undefined ? { issueUrl } : {}),
|
|
3798
|
+
exitCode: 1,
|
|
3799
|
+
});
|
|
3800
|
+
return 1;
|
|
3801
|
+
}
|
|
3802
|
+
|
|
3803
|
+
// Success path. FR-8: --tag is best-effort trimming — loud on failure, never gate-affecting.
|
|
3804
|
+
let tagName: string | undefined;
|
|
3805
|
+
if (flags.has('tag')) {
|
|
3806
|
+
const sha = run('git rev-parse --short HEAD', { cwd, timeoutMs: 10_000 });
|
|
3807
|
+
const log = run('git log --oneline -n 15 --no-decorate', { cwd, timeoutMs: 10_000 });
|
|
3808
|
+
const notes = buildReleaseNotes(log.exitCode === 0 ? log.stdout.split('\n') : []);
|
|
3809
|
+
tagName = releaseTagName(new Date(), sha.exitCode === 0 ? sha.stdout.trim() : '');
|
|
3810
|
+
const tag = run(`git tag -a ${tagName} -m ${shq(notes)}`, { cwd, timeoutMs: 10_000 });
|
|
3811
|
+
if (tag.exitCode === 0) {
|
|
3812
|
+
say(`dz release: tagged ${tagName} (annotated with short release notes from recent commits)`);
|
|
3813
|
+
say('dz release: note — the tag attests a VERIFIED tree, not a completed publish');
|
|
3814
|
+
} else {
|
|
3815
|
+
loud(`dz release: ⚠ git tag failed (${oneLine(tag.stderr, tag.stdout) || 'unknown error'}) — non-blocking; tag manually: git tag -a ${tagName}`);
|
|
3816
|
+
}
|
|
3817
|
+
}
|
|
3818
|
+
|
|
3819
|
+
// FR-10 / AC-8: re-sign reminder BEFORE the handoff — publish's signature gate is
|
|
3820
|
+
// refuse-unsigned, and gates 3–4 often follow a rebuild.
|
|
3821
|
+
say('dz release: reminder — re-sign any rebuilt packs BEFORE publishing (dz sign --pack <dir> --key <path-outside-repo>); the publish signature gate is refuse-unsigned');
|
|
3822
|
+
|
|
3823
|
+
// FR-9 / AM-5: the handoff NEVER injects --yes/--confirm/--no-dry-run — live publish stays
|
|
3824
|
+
// the operator's explicit, loud-bannered act inside dz publish itself.
|
|
3825
|
+
const publishCommand = `dz publish${filterStr !== undefined ? ` --filter ${filterStr}` : ''}`;
|
|
3826
|
+
let publishExit: number | undefined;
|
|
3827
|
+
let publishOutput: string[] | undefined;
|
|
3828
|
+
if (flags.has('publish')) {
|
|
3829
|
+
say(`dz release: ✓ gates green — chaining into ${publishCommand} in-process (ALL publish gates + its dry-run-by-default protocol run untouched)`);
|
|
3830
|
+
const pubOptions = new Map<string, string>(filterStr !== undefined ? [['filter', filterStr]] : []);
|
|
3831
|
+
// --json contract: the chained publish must NOT print prose before the JSON envelope —
|
|
3832
|
+
// capture its lines and carry them INSIDE the envelope instead (stdout stays one JSON doc).
|
|
3833
|
+
if (json) {
|
|
3834
|
+
publishOutput = [];
|
|
3835
|
+
const captured = publishOutput;
|
|
3836
|
+
publishExit = cmdPublish(pubOptions, new Set<string>(), cwd, (line: string) => {
|
|
3837
|
+
captured.push(line);
|
|
3838
|
+
});
|
|
3839
|
+
} else {
|
|
3840
|
+
publishExit = cmdPublish(pubOptions, new Set<string>(), cwd, write);
|
|
3841
|
+
}
|
|
3842
|
+
} else {
|
|
3843
|
+
say(`dz release: ✓ all gates green — publish when ready: ${publishCommand}`);
|
|
3844
|
+
}
|
|
3845
|
+
|
|
3846
|
+
emitJson({
|
|
3847
|
+
gates: verdict.gates,
|
|
3848
|
+
ok: verdict.ok,
|
|
3849
|
+
blockedBy: verdict.blockedBy,
|
|
3850
|
+
skipped: verdict.skipped,
|
|
3851
|
+
publishAction: verdict.publishAction,
|
|
3852
|
+
timestamp: verdict.timestamp,
|
|
3853
|
+
...(tagName !== undefined ? { tag: tagName } : {}),
|
|
3854
|
+
publishCommand,
|
|
3855
|
+
...(publishExit !== undefined ? { publishExit } : {}),
|
|
3856
|
+
...(publishOutput !== undefined ? { publishOutput } : {}),
|
|
3857
|
+
exitCode: publishExit ?? 0,
|
|
3858
|
+
});
|
|
3859
|
+
return publishExit ?? 0;
|
|
3860
|
+
}
|
|
3861
|
+
|
|
3551
3862
|
function cmdRegistry(options: Map<string, string>, cwd: string, write: Write): number {
|
|
3552
3863
|
const registry = buildRegistry(cwd);
|
|
3553
3864
|
if (registry.totalSkills === 0) {
|
|
@@ -4234,6 +4545,26 @@ function gatherReadmeCounts(root: string): { label: string; a: number; b: number
|
|
|
4234
4545
|
const rootAll = num(rootMd, /## All Commands \((\d+)\)/);
|
|
4235
4546
|
if (cjm !== null && cliAll !== null) pairs.push({ label: 'commands (root CJM header vs cli All Commands)', a: cjm, b: cliAll });
|
|
4236
4547
|
if (rootAll !== null && cliAll !== null) pairs.push({ label: 'All Commands (root vs cli)', a: rootAll, b: cliAll });
|
|
4548
|
+
// sitedoc in the readme-first contour (feature sitedoc-readme-first, ADR-001 + Codex QE AM-3/AM-4):
|
|
4549
|
+
// the docs SITE carries the same command count as the cli README. A MISSING file skips its pair
|
|
4550
|
+
// (target repo without sitedoc — missing-evidence contract). But a file that EXISTS and no longer
|
|
4551
|
+
// matches its anchored pattern emits a MISMATCH pair (a: -1) — silent non-extraction is the exact
|
|
4552
|
+
// disease this contour cures (AM-1 applies to the guard path too, not only the CI test).
|
|
4553
|
+
const sitePair = (rel: string, re: RegExp, label: string): void => {
|
|
4554
|
+
if (cliAll === null || !existsSync(join(root, rel))) return;
|
|
4555
|
+
const found = num(read(rel), re);
|
|
4556
|
+
pairs.push({ label, a: found ?? -1, b: cliAll });
|
|
4557
|
+
};
|
|
4558
|
+
sitePair(
|
|
4559
|
+
'packages/@dzhechkov/sitedoc/src/content/docs/cli/overview.md',
|
|
4560
|
+
/^description: All (\d+) dz commands/m,
|
|
4561
|
+
'commands (docs-site CLI overview description vs cli All Commands)',
|
|
4562
|
+
);
|
|
4563
|
+
sitePair(
|
|
4564
|
+
'packages/@dzhechkov/sitedoc/src/content/docs/index.mdx',
|
|
4565
|
+
/^\s*tagline: "[^"]*?(\d+) commands/m,
|
|
4566
|
+
'commands (docs-site index hero tagline vs cli All Commands)',
|
|
4567
|
+
);
|
|
4237
4568
|
return pairs;
|
|
4238
4569
|
}
|
|
4239
4570
|
|
|
@@ -5399,6 +5730,8 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
|
|
|
5399
5730
|
return await cmdAutoCanonicalize(options, cwd, write);
|
|
5400
5731
|
case 'publish':
|
|
5401
5732
|
return cmdPublish(options, flags, cwd, write);
|
|
5733
|
+
case 'release':
|
|
5734
|
+
return cmdRelease(options, flags, cwd, write, io.releaseRunner);
|
|
5402
5735
|
case 'registry':
|
|
5403
5736
|
return cmdRegistry(options, cwd, write);
|
|
5404
5737
|
case 'benchmark':
|
package/src/index.ts
CHANGED
|
@@ -11,4 +11,4 @@ export const HARNESS_CLI_VERSION: string =
|
|
|
11
11
|
(createRequire(import.meta.url)('../package.json') as { version: string }).version;
|
|
12
12
|
|
|
13
13
|
export { runCli } from './cli.js';
|
|
14
|
-
export type { CliIo } from './cli.js';
|
|
14
|
+
export type { CliIo, ReleaseExecRunner } from './cli.js';
|