@dzhechkov/harness-cli 0.8.19 → 0.8.21
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 +7 -7
- package/README.md +39 -19
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +167 -12
- package/dist/cli.js.map +1 -1
- package/package.json +9 -9
- package/sbom.json +6 -6
- package/src/cli.ts +162 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dzhechkov/harness-cli",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.21",
|
|
4
4
|
"description": "The dz CLI — install AI skills for Claude Code, Codex, OpenCode, Hermes, OpenClaude, GitHub Copilot. 87 commands, 14 presets, 10 platform targets.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,23 +42,23 @@
|
|
|
42
42
|
"src"
|
|
43
43
|
],
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@dzhechkov/harness-core": "^0.8.
|
|
45
|
+
"@dzhechkov/harness-core": "^0.8.29",
|
|
46
46
|
"@dzhechkov/harness-presets": "^0.5.18",
|
|
47
|
-
"@dzhechkov/skills-devops": "^0.3.17",
|
|
48
|
-
"@dzhechkov/scout": "^0.8.11",
|
|
49
47
|
"@dzhechkov/skills-mcp": "^0.3.10",
|
|
48
|
+
"@dzhechkov/scout": "^0.8.11",
|
|
49
|
+
"@dzhechkov/skills-devops": "^0.3.17",
|
|
50
50
|
"@dzhechkov/skills-web3": "^0.2.9",
|
|
51
51
|
"@dzhechkov/skills-meta": "^0.9.55",
|
|
52
|
-
"@dzhechkov/skills-academic": "^0.3.7",
|
|
53
52
|
"@dzhechkov/skills-qe": "^0.3.14",
|
|
54
|
-
"@dzhechkov/skills-
|
|
53
|
+
"@dzhechkov/skills-academic": "^0.3.7",
|
|
55
54
|
"@dzhechkov/skills-ecc": "^0.1.12",
|
|
55
|
+
"@dzhechkov/skills-news": "^0.1.16",
|
|
56
56
|
"@dzhechkov/skills-pm": "^0.1.5",
|
|
57
57
|
"@dzhechkov/skills-taste": "^0.1.5",
|
|
58
58
|
"@dzhechkov/skills-idea2prd": "^0.1.15",
|
|
59
59
|
"@dzhechkov/skills-reverse-engineering": "^0.3.4",
|
|
60
|
-
"@dzhechkov/skills-
|
|
61
|
-
"@dzhechkov/skills-
|
|
60
|
+
"@dzhechkov/skills-presentation-storyteller": "^0.1.14",
|
|
61
|
+
"@dzhechkov/skills-website-cloner": "^0.1.5"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@types/node": "^25.6.0",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
},
|
|
74
74
|
"repository": {
|
|
75
75
|
"type": "git",
|
|
76
|
-
"url": "git+https://github.com/djd1m/dz-harness.git",
|
|
76
|
+
"url": "git+https://github.com/djd1m/dz-harness-hub.git",
|
|
77
77
|
"directory": "packages/@dzhechkov/harness-cli"
|
|
78
78
|
},
|
|
79
79
|
"homepage": "https://github.com/djd1m/dz-harness/tree/main/packages/@dzhechkov/harness-cli#readme",
|
package/sbom.json
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"hashes": [
|
|
26
26
|
{
|
|
27
27
|
"alg": "SHA-256",
|
|
28
|
-
"content": "
|
|
28
|
+
"content": "2a3c1e1f9f922913c45df3482f2055ac2efc726108e1d4377195739e026a4034"
|
|
29
29
|
}
|
|
30
30
|
]
|
|
31
31
|
},
|
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
"hashes": [
|
|
126
126
|
{
|
|
127
127
|
"alg": "SHA-256",
|
|
128
|
-
"content": "
|
|
128
|
+
"content": "ef3852bb748f37d4e78a9a973505c3993eae2b996cc0cdd0659f531aa76621f4"
|
|
129
129
|
}
|
|
130
130
|
]
|
|
131
131
|
},
|
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
"hashes": [
|
|
136
136
|
{
|
|
137
137
|
"alg": "SHA-256",
|
|
138
|
-
"content": "
|
|
138
|
+
"content": "9d0a6cc044810b31d261b36913e3f63445f5e96550176fedd0faf606d8cbcd75"
|
|
139
139
|
}
|
|
140
140
|
]
|
|
141
141
|
},
|
|
@@ -145,7 +145,7 @@
|
|
|
145
145
|
"hashes": [
|
|
146
146
|
{
|
|
147
147
|
"alg": "SHA-256",
|
|
148
|
-
"content": "
|
|
148
|
+
"content": "3049258ffd0972b6a748eda703221911dd33e3450486c1dec0f598a431faa038"
|
|
149
149
|
}
|
|
150
150
|
]
|
|
151
151
|
},
|
|
@@ -379,7 +379,7 @@
|
|
|
379
379
|
},
|
|
380
380
|
{
|
|
381
381
|
"name": "dz:canonical-json-sha256-v2",
|
|
382
|
-
"value": "
|
|
382
|
+
"value": "52e0e71990b14cf8dc5b07046c30d6d9807f7cec1cdef4463ac6bd155b0312d4"
|
|
383
383
|
}
|
|
384
384
|
]
|
|
385
385
|
},
|
|
@@ -409,7 +409,7 @@
|
|
|
409
409
|
"hashes": [
|
|
410
410
|
{
|
|
411
411
|
"alg": "SHA-256",
|
|
412
|
-
"content": "
|
|
412
|
+
"content": "f34208b28b53697118db5370f82c6aaab883bb7be10d99789a96a39a06acfac7"
|
|
413
413
|
}
|
|
414
414
|
]
|
|
415
415
|
},
|
package/src/cli.ts
CHANGED
|
@@ -279,6 +279,8 @@ import {
|
|
|
279
279
|
isInsideTree,
|
|
280
280
|
signManifest,
|
|
281
281
|
verifyManifest,
|
|
282
|
+
hashPackBytes,
|
|
283
|
+
rewriteWorkspaceSpecs,
|
|
282
284
|
listPackFiles,
|
|
283
285
|
listSignablePackFiles,
|
|
284
286
|
assertKeyOutsideTree,
|
|
@@ -7177,6 +7179,8 @@ function cmdPublish(options: Map<string, string>, flags: Set<string>, cwd: strin
|
|
|
7177
7179
|
for (const item of pkg.notVerified) write(` · ${item}`);
|
|
7178
7180
|
}
|
|
7179
7181
|
}
|
|
7182
|
+
for (const warning of report.warnings ?? []) write(` ⚠ warning: ${warning}`);
|
|
7183
|
+
for (const path of report.releaseLineSynced ?? []) write(` ↳ release line synced: ${path}`);
|
|
7180
7184
|
return report.errors > 0 ? 1 : 0;
|
|
7181
7185
|
}
|
|
7182
7186
|
|
|
@@ -9635,21 +9639,52 @@ function gatherGuardFacts(op: string, root: string, text: string | undefined, st
|
|
|
9635
9639
|
// a resolvable workspace dep becomes its real semver (safe → the rule passes); an UNRESOLVABLE one (points at
|
|
9636
9640
|
// no workspace package, or not a pnpm workspace) stays `workspace:*` so the rule catches a dep that WOULD ship
|
|
9637
9641
|
// raw. That is the genuinely dangerous case the rule exists for.
|
|
9638
|
-
type Manifest = { name?: string; version?: string; private?: boolean; license?: string; licenseHold?: unknown; dependencies?: Record<string, string>; devDependencies?: Record<string, string> };
|
|
9642
|
+
type Manifest = { name?: string; version?: string; private?: boolean; license?: string; licenseHold?: unknown; dependencies?: Record<string, string>; devDependencies?: Record<string, string>; peerDependencies?: Record<string, string>; optionalDependencies?: Record<string, string> };
|
|
9639
9643
|
const manifests: Manifest[] = [];
|
|
9640
|
-
const located: { dir: string; m: Manifest }[] = [];
|
|
9644
|
+
const located: { dir: string; m: Manifest; text: string }[] = [];
|
|
9641
9645
|
try {
|
|
9642
9646
|
const out = volumeGitText(root, ['ls-files', 'packages/@dzhechkov/*/package.json']);
|
|
9643
9647
|
for (const rel of out.split('\n').map((s) => s.trim()).filter(Boolean)) {
|
|
9644
9648
|
try {
|
|
9645
|
-
const
|
|
9649
|
+
const packageJsonText = readFileSync(join(root, rel), 'utf8');
|
|
9650
|
+
const m = JSON.parse(packageJsonText) as Manifest;
|
|
9646
9651
|
manifests.push(m);
|
|
9647
|
-
located.push({ dir: rel.replace(/\/package\.json$/, ''), m });
|
|
9652
|
+
located.push({ dir: rel.replace(/\/package\.json$/, ''), m, text: packageJsonText });
|
|
9648
9653
|
} catch { /* skip unreadable */ }
|
|
9649
9654
|
}
|
|
9650
9655
|
} catch { /* not a git repo */ }
|
|
9651
9656
|
const versionByName = new Map<string, string>();
|
|
9652
9657
|
for (const m of manifests) if (m.name && typeof m.version === 'string') versionByName.set(m.name, m.version);
|
|
9658
|
+
// release-line-in-sync: filesystem access belongs in the CLI gatherer. The core evaluator receives
|
|
9659
|
+
// only immutable evidence, so evaluateGuard(facts) stays deterministic and independent of cwd.
|
|
9660
|
+
const coreManifestPath = join(root, 'packages', '@dzhechkov', 'harness-core', 'package.json');
|
|
9661
|
+
const cliManifestPath = join(root, 'packages', '@dzhechkov', 'harness-cli', 'package.json');
|
|
9662
|
+
if (existsSync(coreManifestPath) || existsSync(cliManifestPath)) {
|
|
9663
|
+
const readText = (path: string): string | null => {
|
|
9664
|
+
try { return readFileSync(path, 'utf8'); } catch { return null; }
|
|
9665
|
+
};
|
|
9666
|
+
const readVersion = (path: string): string | null => {
|
|
9667
|
+
const raw = readText(path);
|
|
9668
|
+
if (raw === null) return null;
|
|
9669
|
+
try {
|
|
9670
|
+
const parsed = JSON.parse(raw) as { version?: unknown };
|
|
9671
|
+
return typeof parsed.version === 'string' && /^\d+\.\d+\.\d+$/.test(parsed.version)
|
|
9672
|
+
? parsed.version
|
|
9673
|
+
: null;
|
|
9674
|
+
} catch { return null; }
|
|
9675
|
+
};
|
|
9676
|
+
facts['releaseLines'] = {
|
|
9677
|
+
coreVersion: readVersion(coreManifestPath),
|
|
9678
|
+
cliVersion: readVersion(cliManifestPath),
|
|
9679
|
+
readmes: [
|
|
9680
|
+
{ path: 'README.md', text: readText(join(root, 'README.md')) },
|
|
9681
|
+
{
|
|
9682
|
+
path: 'packages/@dzhechkov/harness-cli/README.md',
|
|
9683
|
+
text: readText(join(root, 'packages', '@dzhechkov', 'harness-cli', 'README.md')),
|
|
9684
|
+
},
|
|
9685
|
+
],
|
|
9686
|
+
};
|
|
9687
|
+
}
|
|
9653
9688
|
publishPackageRoots.push(...located
|
|
9654
9689
|
.filter(({ dir, m }) => m.private !== true && (
|
|
9655
9690
|
publishFilter === undefined
|
|
@@ -9811,22 +9846,136 @@ function gatherGuardFacts(op: string, root: string, text: string | undefined, st
|
|
|
9811
9846
|
}
|
|
9812
9847
|
} catch { /* нечитаемо — правило молчит */ }
|
|
9813
9848
|
facts['counts'] = gatherReadmeCounts(root);
|
|
9814
|
-
//
|
|
9815
|
-
//
|
|
9849
|
+
// One package-scoped WORKING-TREE diff feeds both readme-first and signature-fresh. A second
|
|
9850
|
+
// porcelain call could observe a different tree and let the two publish guards disagree.
|
|
9851
|
+
let changedPackageFiles: string[] | undefined;
|
|
9852
|
+
// readme-first: from the WORKING-TREE diff (publishes happen pre-commit here), per package: did
|
|
9853
|
+
// the version FIELD change from HEAD without its README.md changing? A package.json edit alone
|
|
9854
|
+
// says nothing about version; repository/metadata-only edits must not manufacture a bump.
|
|
9816
9855
|
try {
|
|
9817
|
-
const status = execSync('git status --porcelain -- "packages/@dzhechkov/"', { cwd: root, encoding: 'utf-8' });
|
|
9818
|
-
|
|
9819
|
-
|
|
9820
|
-
|
|
9856
|
+
const status = execSync('git status --porcelain -uall -- "packages/@dzhechkov/"', { cwd: root, encoding: 'utf-8' });
|
|
9857
|
+
changedPackageFiles = status
|
|
9858
|
+
.split('\n')
|
|
9859
|
+
.map((line) => line.slice(3).trim())
|
|
9860
|
+
.map((path) => (path.includes(' -> ') ? path.split(' -> ')[1]!.trim() : path))
|
|
9861
|
+
.filter(Boolean);
|
|
9862
|
+
const perPack = new Map<string, { readme: boolean }>();
|
|
9863
|
+
for (const rel of changedPackageFiles) {
|
|
9864
|
+
const m = rel.match(/^packages\/@dzhechkov\/([^/]+)\/(.+)$/);
|
|
9821
9865
|
if (!m || !m[1] || !m[2]) continue;
|
|
9822
|
-
const e = perPack.get(m[1]) ?? {
|
|
9823
|
-
if (m[2] === 'package.json') e.pkgJson = true;
|
|
9866
|
+
const e = perPack.get(m[1]) ?? { readme: false };
|
|
9824
9867
|
if (m[2] === 'README.md') e.readme = true;
|
|
9825
9868
|
perPack.set(m[1], e);
|
|
9826
9869
|
}
|
|
9827
|
-
|
|
9870
|
+
const readVersion = (raw: string): string => {
|
|
9871
|
+
const version = (JSON.parse(raw) as { version?: unknown }).version;
|
|
9872
|
+
if (typeof version !== 'string') throw new Error('package version is unreadable');
|
|
9873
|
+
return version;
|
|
9874
|
+
};
|
|
9875
|
+
facts['readmeFirst'] = [...perPack.entries()].map(([name, e]) => {
|
|
9876
|
+
const packagePath = `packages/@dzhechkov/${name}/package.json`;
|
|
9877
|
+
let versionBumped = false;
|
|
9878
|
+
let versionUnknown = false;
|
|
9879
|
+
try {
|
|
9880
|
+
const treeVersion = readVersion(readFileSync(join(root, packagePath), 'utf8'));
|
|
9881
|
+
try {
|
|
9882
|
+
const headVersion = readVersion(execFileSync('git', ['show', `HEAD:${packagePath}`], {
|
|
9883
|
+
cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
|
9884
|
+
}));
|
|
9885
|
+
versionBumped = treeVersion !== headVersion;
|
|
9886
|
+
} catch {
|
|
9887
|
+
try {
|
|
9888
|
+
const trackedAtHead = execFileSync('git', ['ls-tree', '--name-only', 'HEAD', '--', packagePath], {
|
|
9889
|
+
cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
|
9890
|
+
}).trim() === packagePath;
|
|
9891
|
+
if (trackedAtHead) versionUnknown = true;
|
|
9892
|
+
else versionBumped = true;
|
|
9893
|
+
} catch {
|
|
9894
|
+
versionUnknown = true;
|
|
9895
|
+
}
|
|
9896
|
+
}
|
|
9897
|
+
} catch {
|
|
9898
|
+
versionUnknown = true;
|
|
9899
|
+
}
|
|
9900
|
+
return {
|
|
9901
|
+
name: '@dzhechkov/' + name,
|
|
9902
|
+
versionBumped,
|
|
9903
|
+
readmeChanged: e.readme,
|
|
9904
|
+
...(versionUnknown ? { versionUnknown: true } : {}),
|
|
9905
|
+
};
|
|
9906
|
+
});
|
|
9828
9907
|
} catch { /* not a git repo — rule skips */ }
|
|
9829
9908
|
|
|
9909
|
+
// signature-fresh: verify only signed packs whose non-signature files changed in the same
|
|
9910
|
+
// porcelain listing above. Manifest/SBOM churn is the remedy, not another reason to verify.
|
|
9911
|
+
// The trust key and every byte read here stay in the CLI; the guard core receives facts only.
|
|
9912
|
+
const trustRootPath = join(root, TRUST_ROOT_REL);
|
|
9913
|
+
if (changedPackageFiles !== undefined && existsSync(trustRootPath)) {
|
|
9914
|
+
try {
|
|
9915
|
+
const trustRootPem = readFileSync(trustRootPath, 'utf8');
|
|
9916
|
+
const signedPacks: {
|
|
9917
|
+
name: string;
|
|
9918
|
+
dir: string;
|
|
9919
|
+
changed: boolean;
|
|
9920
|
+
ok: boolean | null;
|
|
9921
|
+
failures: string[];
|
|
9922
|
+
note?: string;
|
|
9923
|
+
}[] = [];
|
|
9924
|
+
for (const { dir, m, text: packageJsonText } of located) {
|
|
9925
|
+
const manifestPath = join(root, dir, MANIFEST_NAME);
|
|
9926
|
+
if (!existsSync(manifestPath)) continue;
|
|
9927
|
+
const prefix = dir + '/';
|
|
9928
|
+
const changedPackFiles = new Set(changedPackageFiles
|
|
9929
|
+
.filter((rel) => rel.startsWith(prefix))
|
|
9930
|
+
.map((rel) => rel.slice(prefix.length))
|
|
9931
|
+
.filter((packRel) => packRel !== MANIFEST_NAME && packRel !== SBOM_NAME));
|
|
9932
|
+
const changed = changedPackFiles.size > 0;
|
|
9933
|
+
let ok: boolean | null = null;
|
|
9934
|
+
let failures: string[] = [];
|
|
9935
|
+
let note: string | undefined;
|
|
9936
|
+
if (changed) {
|
|
9937
|
+
try {
|
|
9938
|
+
const signed = JSON.parse(readFileSync(manifestPath, 'utf8')) as {
|
|
9939
|
+
manifest?: { files?: readonly { path?: unknown; sha256?: unknown }[] };
|
|
9940
|
+
};
|
|
9941
|
+
const verified = verifyManifest(join(root, dir), signed as never, trustRootPem);
|
|
9942
|
+
// A source checkout contains dev files outside package.json#files. They are absent
|
|
9943
|
+
// from the tarball by design, so an unchanged "present but not signed" source file
|
|
9944
|
+
// is not evidence about signature freshness. A changed/new unsigned file stays loud.
|
|
9945
|
+
const relevantFailures = verified.failures.filter((failure) =>
|
|
9946
|
+
failure.reason !== 'present in the pack but not signed' || changedPackFiles.has(failure.path));
|
|
9947
|
+
ok = verified.ok || relevantFailures.length === 0;
|
|
9948
|
+
if (!ok && relevantFailures.length > 0 && relevantFailures.every(({ path }) => path === 'package.json')) {
|
|
9949
|
+
const signedPackageHash = signed.manifest?.files?.find(({ path }) => path === 'package.json')?.sha256;
|
|
9950
|
+
if (typeof signedPackageHash === 'string') {
|
|
9951
|
+
const rewritten = rewriteWorkspaceSpecs(packageJsonText, versionByName);
|
|
9952
|
+
// MEASURED 2026-09-10 on keysarium/skills-meta/core: pnpm also removes only
|
|
9953
|
+
// prepublishOnly from the packed scripts table. Keep that I/O-adapter concern
|
|
9954
|
+
// outside the workspace-only core helper.
|
|
9955
|
+
const packedShape = JSON.parse(rewritten) as Record<string, unknown>;
|
|
9956
|
+
const scripts = packedShape['scripts'];
|
|
9957
|
+
if (scripts !== null && typeof scripts === 'object' && !Array.isArray(scripts)) {
|
|
9958
|
+
delete (scripts as Record<string, unknown>)['prepublishOnly'];
|
|
9959
|
+
}
|
|
9960
|
+
const rewrittenHash = hashPackBytes('package.json', Buffer.from(JSON.stringify(packedShape, null, 2) + '\n'), (signed.manifest as { version?: number } | undefined)?.version);
|
|
9961
|
+
if (rewrittenHash === signedPackageHash) {
|
|
9962
|
+
ok = true;
|
|
9963
|
+
note = 'package.json matches after workspace rewrite';
|
|
9964
|
+
}
|
|
9965
|
+
}
|
|
9966
|
+
}
|
|
9967
|
+
failures = relevantFailures.slice(0, 3).map((failure) => `${failure.path}: ${failure.reason}`);
|
|
9968
|
+
} catch (error) {
|
|
9969
|
+
ok = false;
|
|
9970
|
+
failures = [`${MANIFEST_NAME}: ${error instanceof Error ? error.message : String(error)}`];
|
|
9971
|
+
}
|
|
9972
|
+
}
|
|
9973
|
+
signedPacks.push({ name: m.name ?? dir, dir, changed, ok, failures, ...(note !== undefined ? { note } : {}) });
|
|
9974
|
+
}
|
|
9975
|
+
facts['signedPacks'] = signedPacks;
|
|
9976
|
+
} catch { /* unreadable trust root — absence of evidence, so the rule is not established */ }
|
|
9977
|
+
}
|
|
9978
|
+
|
|
9830
9979
|
// review-round: the same WORKING-TREE diff, asked a different question — does a package that
|
|
9831
9980
|
// bumps its version and changes SOURCE bring a GRADED QE report with it? Scoped to source so a
|
|
9832
9981
|
// docs-only republish is never blocked (ADR-001, features/publish-needs-a-review). A throw here
|