@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dzhechkov/harness-cli",
3
- "version": "0.8.19",
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.27",
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-news": "^0.1.16",
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-website-cloner": "^0.1.5",
61
- "@dzhechkov/skills-presentation-storyteller": "^0.1.14"
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": "45af5a47278e24f2049789ac96bba2c18597e29e26f3a9df3ac95fc5b52719c2"
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": "22cddd1ff809cea9301af0905195bc1f2ff29730ce1b8a257c99c518457cff34"
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": "724695fafaa2a68b6c279ea1f55f14732888ee7ba6919f991ace14fa54620b08"
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": "2935abfc89e4a704d9615b0417ff19279487c05a8a97347b2de6c88908276d33"
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": "5f0c597e704b7b515d40f4a95b784b26d4fe2a5adf1f1309a350541506eb14d4"
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": "f0bc9f52737064a3d5b26fb2ef07c0aa79e003dfca5e1277cd0988a65cdeeb8e"
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 m = JSON.parse(readFileSync(join(root, rel), 'utf8')) as Manifest;
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
- // readme-first: from the WORKING-TREE diff (publishes happen pre-commit here), per package: does the
9815
- // change set contain its package.json (the version-bump signal) without its README.md?
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
- const perPack = new Map<string, { pkgJson: boolean; readme: boolean }>();
9819
- for (const line of status.split('\n')) {
9820
- const m = line.match(/packages\/@dzhechkov\/([^/]+)\/(.+)$/);
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]) ?? { pkgJson: false, readme: false };
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
- facts['readmeFirst'] = [...perPack.entries()].map(([name, e]) => ({ name: '@dzhechkov/' + name, versionBumped: e.pkgJson, readmeChanged: e.readme }));
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