@harness-engineering/cli 1.10.0 → 1.12.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 (59) hide show
  1. package/dist/agents/skills/claude-code/enforce-architecture/SKILL.md +4 -0
  2. package/dist/agents/skills/claude-code/harness-parallel-agents/SKILL.md +105 -20
  3. package/dist/agents/skills/claude-code/harness-pre-commit-review/SKILL.md +37 -0
  4. package/dist/agents/skills/gemini-cli/enforce-architecture/SKILL.md +4 -0
  5. package/dist/agents/skills/gemini-cli/harness-parallel-agents/SKILL.md +105 -20
  6. package/dist/agents/skills/gemini-cli/harness-pre-commit-review/SKILL.md +37 -0
  7. package/dist/{agents-md-EMRFLNBC.js → agents-md-KIS2RSMG.js} +1 -1
  8. package/dist/{architecture-5JNN5L3M.js → architecture-AJAUDRQQ.js} +2 -2
  9. package/dist/bin/harness-mcp.js +14 -14
  10. package/dist/bin/harness.js +20 -20
  11. package/dist/{check-phase-gate-WOKIYGAM.js → check-phase-gate-K7QCSYRJ.js} +4 -4
  12. package/dist/{chunk-FPIPT36X.js → chunk-2SWJ4VO7.js} +6 -6
  13. package/dist/{chunk-OPXH4CQN.js → chunk-2YPZKGAG.js} +1 -1
  14. package/dist/{chunk-B7HFEHWP.js → chunk-3WGJMBKH.js} +10 -0
  15. package/dist/{chunk-ECUJQS3B.js → chunk-6N4R6FVX.js} +3 -3
  16. package/dist/{chunk-LXU5M77O.js → chunk-747VBPA4.js} +390 -57
  17. package/dist/{chunk-NX6DSZSM.js → chunk-AE2OWWDH.js} +4497 -2640
  18. package/dist/{chunk-PAHHT2IK.js → chunk-B5SBNH4S.js} +2833 -918
  19. package/dist/{chunk-F4PTVZWA.js → chunk-CTTFXXKJ.js} +7 -7
  20. package/dist/{chunk-PSXF277V.js → chunk-EAURF4LH.js} +1 -1
  21. package/dist/chunk-EBJQ6N4M.js +39 -0
  22. package/dist/{chunk-4PFMY3H7.js → chunk-FLOEMHDF.js} +9 -9
  23. package/dist/{chunk-46YA6FI3.js → chunk-GNGELAXY.js} +2 -2
  24. package/dist/{chunk-EOLRW32Q.js → chunk-HD4IBGLA.js} +9 -1
  25. package/dist/{chunk-CWZ4Y2PO.js → chunk-JLXOEO5C.js} +4 -4
  26. package/dist/{chunk-F3YDAJFQ.js → chunk-L2KLU56K.js} +2 -2
  27. package/dist/{chunk-MO4YQOMB.js → chunk-OIGVQF5V.js} +3 -3
  28. package/dist/{chunk-PMTFPOCT.js → chunk-TJVVU3HB.js} +1 -1
  29. package/dist/{chunk-MDUK2J2O.js → chunk-VRFZWGMS.js} +2 -1
  30. package/dist/{chunk-FX7SQHGD.js → chunk-YXOG2277.js} +2 -2
  31. package/dist/{chunk-7X7ZAYMY.js → chunk-ZU2UBYBY.js} +102 -5
  32. package/dist/{ci-workflow-ZBBUNTHQ.js → ci-workflow-NBL4OT4A.js} +1 -1
  33. package/dist/create-skill-WPXHSLX2.js +11 -0
  34. package/dist/{dist-PBTNVK6K.js → dist-IJ4J4C5G.js} +103 -1
  35. package/dist/{dist-I7DB5VKB.js → dist-M6BQODWC.js} +1145 -0
  36. package/dist/{docs-PTJGD6XI.js → docs-CPTMH3VY.js} +2 -2
  37. package/dist/{engine-SCMZ3G3E.js → engine-BUWPAAGD.js} +1 -1
  38. package/dist/{entropy-YIUBGKY7.js → entropy-Z4FYVQ7L.js} +2 -2
  39. package/dist/{feedback-WEVQSLAA.js → feedback-TT6WF5YX.js} +1 -1
  40. package/dist/{generate-agent-definitions-BU5LOJTI.js → generate-agent-definitions-J5HANRNR.js} +4 -4
  41. package/dist/{graph-loader-RLO3KRIX.js → graph-loader-KO4GJ5N2.js} +1 -1
  42. package/dist/index.d.ts +355 -12
  43. package/dist/index.js +33 -21
  44. package/dist/{loader-6S6PVGSF.js → loader-PCU5YWRH.js} +1 -1
  45. package/dist/mcp-YM6QLHLZ.js +34 -0
  46. package/dist/{performance-5TVW6SA6.js → performance-YJVXOKIB.js} +2 -2
  47. package/dist/{review-pipeline-4JTQAWKW.js → review-pipeline-KGMIMLIE.js} +1 -1
  48. package/dist/{runtime-PXIM7UV6.js → runtime-F6R27LD6.js} +1 -1
  49. package/dist/{security-URYTKLGK.js → security-MX5VVXBC.js} +1 -1
  50. package/dist/skill-executor-RG45LUO5.js +8 -0
  51. package/dist/templates/orchestrator/WORKFLOW.md +48 -0
  52. package/dist/templates/orchestrator/template.json +6 -0
  53. package/dist/{validate-KSDUUK2M.js → validate-EFNMSFKD.js} +2 -2
  54. package/dist/{validate-cross-check-WZAX357V.js → validate-cross-check-LJX65SBS.js} +1 -1
  55. package/package.json +10 -6
  56. package/dist/chunk-HIOXKZYF.js +0 -15
  57. package/dist/create-skill-LUWO46WF.js +0 -11
  58. package/dist/mcp-BNLBTCXZ.js +0 -34
  59. package/dist/skill-executor-KVS47DAU.js +0 -8
@@ -5,13 +5,14 @@ import {
5
5
  OutputFormatter,
6
6
  OutputMode,
7
7
  createCheckPhaseGateCommand,
8
+ findConfigFile,
8
9
  findFiles,
9
10
  resolveConfig
10
- } from "./chunk-7X7ZAYMY.js";
11
+ } from "./chunk-ZU2UBYBY.js";
11
12
  import {
12
13
  createGenerateAgentDefinitionsCommand,
13
14
  generateAgentDefinitions
14
- } from "./chunk-46YA6FI3.js";
15
+ } from "./chunk-GNGELAXY.js";
15
16
  import {
16
17
  listPersonas,
17
18
  loadPersona
@@ -21,16 +22,16 @@ import {
21
22
  } from "./chunk-TRAPF4IX.js";
22
23
  import {
23
24
  executeSkill
24
- } from "./chunk-F3YDAJFQ.js";
25
+ } from "./chunk-L2KLU56K.js";
25
26
  import {
26
27
  ALLOWED_PERSONA_COMMANDS
27
28
  } from "./chunk-TEFCFC4H.js";
28
29
  import {
29
30
  createCreateSkillCommand
30
- } from "./chunk-ECUJQS3B.js";
31
+ } from "./chunk-6N4R6FVX.js";
31
32
  import {
32
33
  logger
33
- } from "./chunk-HIOXKZYF.js";
34
+ } from "./chunk-EBJQ6N4M.js";
34
35
  import {
35
36
  generate,
36
37
  validate
@@ -47,24 +48,27 @@ import {
47
48
  import {
48
49
  createGenerateSlashCommandsCommand,
49
50
  generateSlashCommands,
51
+ handleGetImpact,
50
52
  handleOrphanDeletion
51
- } from "./chunk-LXU5M77O.js";
53
+ } from "./chunk-747VBPA4.js";
52
54
  import {
53
55
  VALID_PLATFORMS
54
56
  } from "./chunk-ZOAWBDWU.js";
55
57
  import {
58
+ resolveGlobalSkillsDir,
56
59
  resolvePersonasDir,
60
+ resolveProjectSkillsDir,
57
61
  resolveSkillsDir,
58
62
  resolveTemplatesDir
59
- } from "./chunk-EOLRW32Q.js";
63
+ } from "./chunk-HD4IBGLA.js";
60
64
  import {
61
65
  CLIError,
62
66
  ExitCode,
63
67
  handleError
64
- } from "./chunk-B7HFEHWP.js";
68
+ } from "./chunk-3WGJMBKH.js";
65
69
  import {
66
70
  SkillMetadataSchema
67
- } from "./chunk-MDUK2J2O.js";
71
+ } from "./chunk-VRFZWGMS.js";
68
72
  import {
69
73
  CLI_VERSION
70
74
  } from "./chunk-BM3PWGXQ.js";
@@ -72,11 +76,17 @@ import {
72
76
  TemplateEngine
73
77
  } from "./chunk-C2ERUR3L.js";
74
78
  import {
79
+ ArchBaselineManager,
80
+ ArchConfigSchema,
75
81
  BaselineManager,
82
+ BlueprintGenerator,
83
+ BundleSchema,
76
84
  CriticalPathResolver,
77
85
  EntropyAnalyzer,
86
+ ProjectScanner,
78
87
  SecurityScanner,
79
88
  TypeScriptParser,
89
+ addProvenance,
80
90
  appendLearning,
81
91
  applyFixes,
82
92
  archiveStream,
@@ -84,32 +94,42 @@ import {
84
94
  checkDocCoverage,
85
95
  createFixes,
86
96
  createStream,
97
+ deepMergeConstraints,
87
98
  defineLayer,
88
99
  detectCircularDepsInFiles,
89
100
  detectDeadCode,
90
101
  detectDocDrift,
102
+ diff,
103
+ extractBundle,
91
104
  generateSuggestions,
92
105
  listStreams,
93
106
  loadState,
94
107
  loadStreamIndex,
95
108
  parseDiff,
109
+ parseManifest,
96
110
  parseSecurityConfig,
111
+ readLockfile,
112
+ removeContributions,
113
+ removeProvenance,
97
114
  requestPeerReview,
98
115
  resolveStreamPath,
116
+ runAll,
99
117
  runCIChecks,
100
118
  runReviewPipeline,
101
119
  setActiveStream,
102
120
  validateAgentsMap,
103
121
  validateDependencies,
104
- validateKnowledgeMap
105
- } from "./chunk-NX6DSZSM.js";
122
+ validateKnowledgeMap,
123
+ writeConfig,
124
+ writeLockfile
125
+ } from "./chunk-AE2OWWDH.js";
106
126
  import {
107
127
  Err,
108
128
  Ok
109
129
  } from "./chunk-MHBMTPW7.js";
110
130
 
111
131
  // src/index.ts
112
- import { Command as Command41 } from "commander";
132
+ import { Command as Command53 } from "commander";
113
133
 
114
134
  // src/commands/validate.ts
115
135
  import { Command } from "commander";
@@ -188,7 +208,7 @@ function createValidateCommand() {
188
208
  process.exit(result.error.exitCode);
189
209
  }
190
210
  if (opts.crossCheck) {
191
- const { runCrossCheck: runCrossCheck2 } = await import("./validate-cross-check-WZAX357V.js");
211
+ const { runCrossCheck: runCrossCheck2 } = await import("./validate-cross-check-LJX65SBS.js");
192
212
  const cwd = process.cwd();
193
213
  const specsDir = path.join(cwd, "docs", "specs");
194
214
  const plansDir = path.join(cwd, "docs", "plans");
@@ -314,13 +334,13 @@ function createCheckDepsCommand() {
314
334
  import { Command as Command3 } from "commander";
315
335
  import * as path3 from "path";
316
336
  async function runCheckPerf(cwd, options) {
317
- const runAll = !options.structural && !options.size && !options.coupling;
337
+ const runAll2 = !options.structural && !options.size && !options.coupling;
318
338
  const analyzer = new EntropyAnalyzer({
319
339
  rootDir: path3.resolve(cwd),
320
340
  analyze: {
321
- complexity: runAll || !!options.structural,
322
- coupling: runAll || !!options.coupling,
323
- sizeBudget: runAll || !!options.size
341
+ complexity: runAll2 || !!options.structural,
342
+ coupling: runAll2 || !!options.coupling,
343
+ sizeBudget: runAll2 || !!options.size
324
344
  }
325
345
  });
326
346
  const analysisResult = await analyzer.analyze();
@@ -456,10 +476,10 @@ async function runCheckSecurity(cwd, options) {
456
476
  const projectRoot = path4.resolve(cwd);
457
477
  let configData = {};
458
478
  try {
459
- const fs11 = await import("fs");
479
+ const fs23 = await import("fs");
460
480
  const configPath = path4.join(projectRoot, "harness.config.json");
461
- if (fs11.existsSync(configPath)) {
462
- const raw = fs11.readFileSync(configPath, "utf-8");
481
+ if (fs23.existsSync(configPath)) {
482
+ const raw = fs23.readFileSync(configPath, "utf-8");
463
483
  const parsed = JSON.parse(raw);
464
484
  configData = parsed.security ?? {};
465
485
  }
@@ -546,7 +566,7 @@ function createPerfCommand() {
546
566
  perf.command("bench [glob]").description("Run benchmarks via vitest bench").action(async (glob, _opts, cmd) => {
547
567
  const globalOpts = cmd.optsWithGlobals();
548
568
  const cwd = process.cwd();
549
- const { BenchmarkRunner } = await import("./dist-PBTNVK6K.js");
569
+ const { BenchmarkRunner } = await import("./dist-IJ4J4C5G.js");
550
570
  const runner = new BenchmarkRunner();
551
571
  const benchFiles = runner.discover(cwd, glob);
552
572
  if (benchFiles.length === 0) {
@@ -615,7 +635,7 @@ Results (${result.results.length} benchmarks):`);
615
635
  baselines.command("update").description("Update baselines from latest benchmark run").action(async (_opts, cmd) => {
616
636
  const globalOpts = cmd.optsWithGlobals();
617
637
  const cwd = process.cwd();
618
- const { BenchmarkRunner } = await import("./dist-PBTNVK6K.js");
638
+ const { BenchmarkRunner } = await import("./dist-IJ4J4C5G.js");
619
639
  const runner = new BenchmarkRunner();
620
640
  const manager = new BaselineManager(cwd);
621
641
  logger.info("Running benchmarks to update baselines...");
@@ -628,8 +648,8 @@ Results (${result.results.length} benchmarks):`);
628
648
  }
629
649
  let commitHash = "unknown";
630
650
  try {
631
- const { execSync: execSync3 } = await import("child_process");
632
- commitHash = execSync3("git rev-parse --short HEAD", { cwd, encoding: "utf-8" }).trim();
651
+ const { execSync: execSync5 } = await import("child_process");
652
+ commitHash = execSync5("git rev-parse --short HEAD", { cwd, encoding: "utf-8" }).trim();
633
653
  } catch {
634
654
  }
635
655
  manager.save(benchResult.results, commitHash);
@@ -643,7 +663,7 @@ Results (${result.results.length} benchmarks):`);
643
663
  perf.command("report").description("Full performance report with metrics, trends, and hotspots").action(async (_opts, cmd) => {
644
664
  const globalOpts = cmd.optsWithGlobals();
645
665
  const cwd = process.cwd();
646
- const { EntropyAnalyzer: EntropyAnalyzer2 } = await import("./dist-PBTNVK6K.js");
666
+ const { EntropyAnalyzer: EntropyAnalyzer2 } = await import("./dist-IJ4J4C5G.js");
647
667
  const analyzer = new EntropyAnalyzer2({
648
668
  rootDir: path5.resolve(cwd),
649
669
  analyze: { complexity: true, coupling: true }
@@ -717,7 +737,14 @@ async function runCheckDocs(options) {
717
737
  const coverageResult = await checkDocCoverage("project", {
718
738
  docsDir,
719
739
  sourceDir,
720
- excludePatterns: ["**/*.test.ts", "**/*.spec.ts", "**/node_modules/**"]
740
+ excludePatterns: [
741
+ "**/*.test.ts",
742
+ "**/*.spec.ts",
743
+ "**/node_modules/**",
744
+ "**/dist/**",
745
+ "**/coverage/**",
746
+ "**/.turbo/**"
747
+ ]
721
748
  });
722
749
  if (!coverageResult.ok) {
723
750
  return Err(
@@ -1396,22 +1423,22 @@ async function runAgentReview(options) {
1396
1423
  return configResult;
1397
1424
  }
1398
1425
  const config = configResult.value;
1399
- let diff;
1426
+ let diff2;
1400
1427
  try {
1401
- diff = execSync2("git diff --cached", { encoding: "utf-8" });
1402
- if (!diff) {
1403
- diff = execSync2("git diff", { encoding: "utf-8" });
1428
+ diff2 = execSync2("git diff --cached", { encoding: "utf-8" });
1429
+ if (!diff2) {
1430
+ diff2 = execSync2("git diff", { encoding: "utf-8" });
1404
1431
  }
1405
1432
  } catch {
1406
1433
  return Err(new CLIError("Failed to get git diff", ExitCode.ERROR));
1407
1434
  }
1408
- if (!diff) {
1435
+ if (!diff2) {
1409
1436
  return Ok({
1410
1437
  passed: true,
1411
1438
  checklist: [{ check: "No changes to review", passed: true }]
1412
1439
  });
1413
1440
  }
1414
- const parsedDiffResult = parseDiff(diff);
1441
+ const parsedDiffResult = parseDiff(diff2);
1415
1442
  if (!parsedDiffResult.ok) {
1416
1443
  return Err(new CLIError(parsedDiffResult.error.message, ExitCode.ERROR));
1417
1444
  }
@@ -1425,7 +1452,7 @@ async function runAgentReview(options) {
1425
1452
  changedFiles: codeChanges.files.map((f) => f.path),
1426
1453
  newFiles: codeChanges.files.filter((f) => f.status === "added").map((f) => f.path),
1427
1454
  deletedFiles: codeChanges.files.filter((f) => f.status === "deleted").map((f) => f.path),
1428
- totalDiffLines: diff.split("\n").length,
1455
+ totalDiffLines: diff2.split("\n").length,
1429
1456
  fileDiffs: new Map(codeChanges.files.map((f) => [f.path, ""]))
1430
1457
  };
1431
1458
  const pipelineResult = await runReviewPipeline({
@@ -1592,7 +1619,7 @@ async function runAdd(componentType, name, options) {
1592
1619
  break;
1593
1620
  }
1594
1621
  case "skill": {
1595
- const { generateSkillFiles: generateSkillFiles2 } = await import("./create-skill-LUWO46WF.js");
1622
+ const { generateSkillFiles: generateSkillFiles2 } = await import("./create-skill-WPXHSLX2.js");
1596
1623
  generateSkillFiles2({
1597
1624
  name,
1598
1625
  description: `${name} skill`,
@@ -1857,37 +1884,171 @@ function createPersonaCommand() {
1857
1884
  }
1858
1885
 
1859
1886
  // src/commands/skill/index.ts
1860
- import { Command as Command25 } from "commander";
1887
+ import { Command as Command28 } from "commander";
1861
1888
 
1862
1889
  // src/commands/skill/list.ts
1863
1890
  import { Command as Command21 } from "commander";
1891
+ import * as fs6 from "fs";
1892
+ import * as path15 from "path";
1893
+ import { parse } from "yaml";
1894
+
1895
+ // src/registry/lockfile.ts
1864
1896
  import * as fs5 from "fs";
1865
1897
  import * as path14 from "path";
1866
- import { parse } from "yaml";
1867
- function createListCommand2() {
1868
- return new Command21("list").description("List available skills").action(async (_opts, cmd) => {
1869
- const globalOpts = cmd.optsWithGlobals();
1870
- const skillsDir = resolveSkillsDir();
1871
- if (!fs5.existsSync(skillsDir)) {
1872
- logger.info("No skills directory found.");
1873
- process.exit(ExitCode.SUCCESS);
1874
- return;
1898
+ function createEmptyLockfile() {
1899
+ return { version: 1, skills: {} };
1900
+ }
1901
+ function sortedStringify(obj) {
1902
+ return JSON.stringify(
1903
+ obj,
1904
+ (_key, value) => {
1905
+ if (value && typeof value === "object" && !Array.isArray(value)) {
1906
+ return Object.keys(value).sort().reduce((sorted, k) => {
1907
+ sorted[k] = value[k];
1908
+ return sorted;
1909
+ }, {});
1910
+ }
1911
+ return value;
1912
+ },
1913
+ 2
1914
+ );
1915
+ }
1916
+ function readLockfile2(filePath) {
1917
+ if (!fs5.existsSync(filePath)) {
1918
+ return createEmptyLockfile();
1919
+ }
1920
+ const raw = fs5.readFileSync(filePath, "utf-8");
1921
+ let parsed;
1922
+ try {
1923
+ parsed = JSON.parse(raw);
1924
+ } catch {
1925
+ throw new Error(
1926
+ `Failed to parse lockfile at ${filePath}. The file may be corrupted. Delete it and re-run harness install to regenerate.`
1927
+ );
1928
+ }
1929
+ if (!parsed || typeof parsed !== "object" || !("version" in parsed) || parsed.version !== 1 || !("skills" in parsed) || typeof parsed.skills !== "object") {
1930
+ throw new Error(
1931
+ `Invalid lockfile format at ${filePath}. Expected version 1 with a skills object. Delete it and re-run harness install to regenerate.`
1932
+ );
1933
+ }
1934
+ return parsed;
1935
+ }
1936
+ function writeLockfile2(filePath, lockfile) {
1937
+ const dir = path14.dirname(filePath);
1938
+ fs5.mkdirSync(dir, { recursive: true });
1939
+ fs5.writeFileSync(filePath, sortedStringify(lockfile) + "\n", "utf-8");
1940
+ }
1941
+ function updateLockfileEntry(lockfile, name, entry) {
1942
+ return {
1943
+ ...lockfile,
1944
+ skills: {
1945
+ ...lockfile.skills,
1946
+ [name]: entry
1875
1947
  }
1876
- const entries = fs5.readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
1877
- const skills = [];
1878
- for (const name of entries) {
1879
- const yamlPath = path14.join(skillsDir, name, "skill.yaml");
1880
- if (!fs5.existsSync(yamlPath)) continue;
1881
- try {
1882
- const raw = fs5.readFileSync(yamlPath, "utf-8");
1883
- const parsed = parse(raw);
1884
- const result = SkillMetadataSchema.safeParse(parsed);
1885
- if (result.success) {
1886
- skills.push(result.data);
1887
- }
1888
- } catch {
1948
+ };
1949
+ }
1950
+ function removeLockfileEntry(lockfile, name) {
1951
+ if (!(name in lockfile.skills)) {
1952
+ return lockfile;
1953
+ }
1954
+ const { [name]: _removed, ...rest } = lockfile.skills;
1955
+ return {
1956
+ ...lockfile,
1957
+ skills: rest
1958
+ };
1959
+ }
1960
+
1961
+ // src/commands/skill/list.ts
1962
+ function scanDirectory(dirPath, source) {
1963
+ if (!fs6.existsSync(dirPath)) return [];
1964
+ const entries = fs6.readdirSync(dirPath, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
1965
+ const skills = [];
1966
+ for (const name of entries) {
1967
+ const yamlPath = path15.join(dirPath, name, "skill.yaml");
1968
+ if (!fs6.existsSync(yamlPath)) continue;
1969
+ try {
1970
+ const raw = fs6.readFileSync(yamlPath, "utf-8");
1971
+ const parsed = parse(raw);
1972
+ const result = SkillMetadataSchema.safeParse(parsed);
1973
+ if (result.success) {
1974
+ skills.push({
1975
+ name: result.data.name,
1976
+ description: result.data.description,
1977
+ type: result.data.type,
1978
+ source
1979
+ });
1980
+ }
1981
+ } catch {
1982
+ }
1983
+ }
1984
+ return skills;
1985
+ }
1986
+ function collectSkills(opts) {
1987
+ const seen = /* @__PURE__ */ new Set();
1988
+ const allSkills = [];
1989
+ const addUnique = (entries) => {
1990
+ for (const entry of entries) {
1991
+ if (!seen.has(entry.name)) {
1992
+ seen.add(entry.name);
1993
+ allSkills.push(entry);
1994
+ }
1995
+ }
1996
+ };
1997
+ if (opts.filter === "all" || opts.filter === "local") {
1998
+ const projectDir = resolveProjectSkillsDir();
1999
+ if (projectDir) {
2000
+ addUnique(scanDirectory(projectDir, "local"));
2001
+ }
2002
+ }
2003
+ if (opts.filter === "all" || opts.filter === "installed") {
2004
+ const globalDir = resolveGlobalSkillsDir();
2005
+ const skillsDir = path15.dirname(globalDir);
2006
+ const communityBase = path15.join(skillsDir, "community");
2007
+ const communityPlatformDir = path15.join(communityBase, "claude-code");
2008
+ const lockfilePath = path15.join(communityBase, "skills-lock.json");
2009
+ const lockfile = readLockfile2(lockfilePath);
2010
+ const communitySkills = scanDirectory(communityPlatformDir, "community");
2011
+ for (const skill of communitySkills) {
2012
+ const pkgName = `@harness-skills/${skill.name}`;
2013
+ const lockEntry = lockfile.skills[pkgName];
2014
+ if (lockEntry) {
2015
+ skill.version = lockEntry.version;
2016
+ }
2017
+ }
2018
+ addUnique(communitySkills);
2019
+ for (const [pkgName, entry] of Object.entries(lockfile.skills)) {
2020
+ const shortName = pkgName.replace("@harness-skills/", "");
2021
+ if (!seen.has(shortName)) {
2022
+ seen.add(shortName);
2023
+ allSkills.push({
2024
+ name: shortName,
2025
+ description: "",
2026
+ type: "",
2027
+ source: "community",
2028
+ version: entry.version
2029
+ });
1889
2030
  }
1890
2031
  }
2032
+ }
2033
+ if (opts.filter === "all") {
2034
+ const globalDir = resolveGlobalSkillsDir();
2035
+ addUnique(scanDirectory(globalDir, "bundled"));
2036
+ }
2037
+ if (opts.filter === "installed") {
2038
+ return allSkills.filter((s) => s.source === "community");
2039
+ }
2040
+ if (opts.filter === "local") {
2041
+ return allSkills.filter((s) => s.source === "local");
2042
+ }
2043
+ return allSkills;
2044
+ }
2045
+ function createListCommand2() {
2046
+ return new Command21("list").description("List available skills").option("--installed", "Show only community-installed skills").option("--local", "Show only project-local skills").option("--all", "Show all skills (default)").action(async (opts, cmd) => {
2047
+ const globalOpts = cmd.optsWithGlobals();
2048
+ let filter = "all";
2049
+ if (opts.installed) filter = "installed";
2050
+ else if (opts.local) filter = "local";
2051
+ const skills = collectSkills({ filter });
1891
2052
  if (globalOpts.json) {
1892
2053
  logger.raw(skills);
1893
2054
  } else if (globalOpts.quiet) {
@@ -1898,9 +2059,12 @@ function createListCommand2() {
1898
2059
  } else {
1899
2060
  console.log("Available skills:\n");
1900
2061
  for (const s of skills) {
1901
- console.log(` ${s.name} (${s.type})`);
1902
- console.log(` ${s.description}
1903
- `);
2062
+ const version = s.version ? `@${s.version}` : "";
2063
+ console.log(` ${s.name}${version} [${s.source}] (${s.type || "unknown"})`);
2064
+ if (s.description) {
2065
+ console.log(` ${s.description}`);
2066
+ }
2067
+ console.log();
1904
2068
  }
1905
2069
  }
1906
2070
  }
@@ -1910,8 +2074,8 @@ function createListCommand2() {
1910
2074
 
1911
2075
  // src/commands/skill/run.ts
1912
2076
  import { Command as Command22 } from "commander";
1913
- import * as fs6 from "fs";
1914
- import * as path15 from "path";
2077
+ import * as fs7 from "fs";
2078
+ import * as path16 from "path";
1915
2079
  import { parse as parse2 } from "yaml";
1916
2080
 
1917
2081
  // src/skill/complexity.ts
@@ -2001,17 +2165,17 @@ ${options.priorState}`);
2001
2165
  function createRunCommand2() {
2002
2166
  return new Command22("run").description("Run a skill (outputs SKILL.md content with context preamble)").argument("<name>", "Skill name (e.g., harness-tdd)").option("--path <path>", "Project root path for context injection").option("--complexity <level>", "Complexity: auto, light, full", "auto").option("--phase <name>", "Start at a specific phase (for re-entry)").option("--party", "Enable multi-perspective evaluation").action(async (name, opts, _cmd) => {
2003
2167
  const skillsDir = resolveSkillsDir();
2004
- const skillDir = path15.join(skillsDir, name);
2005
- if (!fs6.existsSync(skillDir)) {
2168
+ const skillDir = path16.join(skillsDir, name);
2169
+ if (!fs7.existsSync(skillDir)) {
2006
2170
  logger.error(`Skill not found: ${name}`);
2007
2171
  process.exit(ExitCode.ERROR);
2008
2172
  return;
2009
2173
  }
2010
- const yamlPath = path15.join(skillDir, "skill.yaml");
2174
+ const yamlPath = path16.join(skillDir, "skill.yaml");
2011
2175
  let metadata = null;
2012
- if (fs6.existsSync(yamlPath)) {
2176
+ if (fs7.existsSync(yamlPath)) {
2013
2177
  try {
2014
- const raw = fs6.readFileSync(yamlPath, "utf-8");
2178
+ const raw = fs7.readFileSync(yamlPath, "utf-8");
2015
2179
  const parsed = parse2(raw);
2016
2180
  const result = SkillMetadataSchema.safeParse(parsed);
2017
2181
  if (result.success) metadata = result.data;
@@ -2022,17 +2186,17 @@ function createRunCommand2() {
2022
2186
  if (metadata?.phases && metadata.phases.length > 0) {
2023
2187
  const requested = opts.complexity ?? "auto";
2024
2188
  if (requested === "auto") {
2025
- const projectPath2 = opts.path ? path15.resolve(opts.path) : process.cwd();
2189
+ const projectPath2 = opts.path ? path16.resolve(opts.path) : process.cwd();
2026
2190
  complexity = detectComplexity(projectPath2);
2027
2191
  } else {
2028
2192
  complexity = requested;
2029
2193
  }
2030
2194
  }
2031
2195
  let principles;
2032
- const projectPath = opts.path ? path15.resolve(opts.path) : process.cwd();
2033
- const principlesPath = path15.join(projectPath, "docs", "principles.md");
2034
- if (fs6.existsSync(principlesPath)) {
2035
- principles = fs6.readFileSync(principlesPath, "utf-8");
2196
+ const projectPath = opts.path ? path16.resolve(opts.path) : process.cwd();
2197
+ const principlesPath = path16.join(projectPath, "docs", "principles.md");
2198
+ if (fs7.existsSync(principlesPath)) {
2199
+ principles = fs7.readFileSync(principlesPath, "utf-8");
2036
2200
  }
2037
2201
  let priorState;
2038
2202
  let stateWarning;
@@ -2047,16 +2211,16 @@ function createRunCommand2() {
2047
2211
  }
2048
2212
  if (metadata?.state.persistent && metadata.state.files.length > 0) {
2049
2213
  for (const stateFilePath of metadata.state.files) {
2050
- const fullPath = path15.join(projectPath, stateFilePath);
2051
- if (fs6.existsSync(fullPath)) {
2052
- const stat = fs6.statSync(fullPath);
2214
+ const fullPath = path16.join(projectPath, stateFilePath);
2215
+ if (fs7.existsSync(fullPath)) {
2216
+ const stat = fs7.statSync(fullPath);
2053
2217
  if (stat.isDirectory()) {
2054
- const files = fs6.readdirSync(fullPath).map((f) => ({ name: f, mtime: fs6.statSync(path15.join(fullPath, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
2218
+ const files = fs7.readdirSync(fullPath).map((f) => ({ name: f, mtime: fs7.statSync(path16.join(fullPath, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
2055
2219
  if (files.length > 0) {
2056
- priorState = fs6.readFileSync(path15.join(fullPath, files[0].name), "utf-8");
2220
+ priorState = fs7.readFileSync(path16.join(fullPath, files[0].name), "utf-8");
2057
2221
  }
2058
2222
  } else {
2059
- priorState = fs6.readFileSync(fullPath, "utf-8");
2223
+ priorState = fs7.readFileSync(fullPath, "utf-8");
2060
2224
  }
2061
2225
  break;
2062
2226
  }
@@ -2075,17 +2239,17 @@ function createRunCommand2() {
2075
2239
  ...stateWarning !== void 0 && { stateWarning },
2076
2240
  party: opts.party
2077
2241
  });
2078
- const skillMdPath = path15.join(skillDir, "SKILL.md");
2079
- if (!fs6.existsSync(skillMdPath)) {
2242
+ const skillMdPath = path16.join(skillDir, "SKILL.md");
2243
+ if (!fs7.existsSync(skillMdPath)) {
2080
2244
  logger.error(`SKILL.md not found for skill: ${name}`);
2081
2245
  process.exit(ExitCode.ERROR);
2082
2246
  return;
2083
2247
  }
2084
- let content = fs6.readFileSync(skillMdPath, "utf-8");
2248
+ let content = fs7.readFileSync(skillMdPath, "utf-8");
2085
2249
  if (metadata?.state.persistent && opts.path) {
2086
- const stateFile = path15.join(projectPath, ".harness", "state.json");
2087
- if (fs6.existsSync(stateFile)) {
2088
- const stateContent = fs6.readFileSync(stateFile, "utf-8");
2250
+ const stateFile = path16.join(projectPath, ".harness", "state.json");
2251
+ if (fs7.existsSync(stateFile)) {
2252
+ const stateContent = fs7.readFileSync(stateFile, "utf-8");
2089
2253
  content += `
2090
2254
 
2091
2255
  ---
@@ -2103,8 +2267,8 @@ ${stateContent}
2103
2267
 
2104
2268
  // src/commands/skill/validate.ts
2105
2269
  import { Command as Command23 } from "commander";
2106
- import * as fs7 from "fs";
2107
- import * as path16 from "path";
2270
+ import * as fs8 from "fs";
2271
+ import * as path17 from "path";
2108
2272
  import { parse as parse3 } from "yaml";
2109
2273
  var REQUIRED_SECTIONS = [
2110
2274
  "## When to Use",
@@ -2117,32 +2281,32 @@ function createValidateCommand3() {
2117
2281
  return new Command23("validate").description("Validate all skill.yaml files and SKILL.md structure").action(async (_opts, cmd) => {
2118
2282
  const globalOpts = cmd.optsWithGlobals();
2119
2283
  const skillsDir = resolveSkillsDir();
2120
- if (!fs7.existsSync(skillsDir)) {
2284
+ if (!fs8.existsSync(skillsDir)) {
2121
2285
  logger.info("No skills directory found.");
2122
2286
  process.exit(ExitCode.SUCCESS);
2123
2287
  return;
2124
2288
  }
2125
- const entries = fs7.readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
2289
+ const entries = fs8.readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
2126
2290
  const errors = [];
2127
2291
  let validated = 0;
2128
2292
  for (const name of entries) {
2129
- const skillDir = path16.join(skillsDir, name);
2130
- const yamlPath = path16.join(skillDir, "skill.yaml");
2131
- const skillMdPath = path16.join(skillDir, "SKILL.md");
2132
- if (!fs7.existsSync(yamlPath)) {
2293
+ const skillDir = path17.join(skillsDir, name);
2294
+ const yamlPath = path17.join(skillDir, "skill.yaml");
2295
+ const skillMdPath = path17.join(skillDir, "SKILL.md");
2296
+ if (!fs8.existsSync(yamlPath)) {
2133
2297
  errors.push(`${name}: missing skill.yaml`);
2134
2298
  continue;
2135
2299
  }
2136
2300
  try {
2137
- const raw = fs7.readFileSync(yamlPath, "utf-8");
2301
+ const raw = fs8.readFileSync(yamlPath, "utf-8");
2138
2302
  const parsed = parse3(raw);
2139
2303
  const result = SkillMetadataSchema.safeParse(parsed);
2140
2304
  if (!result.success) {
2141
2305
  errors.push(`${name}/skill.yaml: ${result.error.message}`);
2142
2306
  continue;
2143
2307
  }
2144
- if (fs7.existsSync(skillMdPath)) {
2145
- const mdContent = fs7.readFileSync(skillMdPath, "utf-8");
2308
+ if (fs8.existsSync(skillMdPath)) {
2309
+ const mdContent = fs8.readFileSync(skillMdPath, "utf-8");
2146
2310
  for (const section of REQUIRED_SECTIONS) {
2147
2311
  if (!mdContent.includes(section)) {
2148
2312
  errors.push(`${name}/SKILL.md: missing section "${section}"`);
@@ -2184,27 +2348,27 @@ function createValidateCommand3() {
2184
2348
 
2185
2349
  // src/commands/skill/info.ts
2186
2350
  import { Command as Command24 } from "commander";
2187
- import * as fs8 from "fs";
2188
- import * as path17 from "path";
2351
+ import * as fs9 from "fs";
2352
+ import * as path18 from "path";
2189
2353
  import { parse as parse4 } from "yaml";
2190
2354
  function createInfoCommand() {
2191
2355
  return new Command24("info").description("Show metadata for a skill").argument("<name>", "Skill name (e.g., harness-tdd)").action(async (name, _opts, cmd) => {
2192
2356
  const globalOpts = cmd.optsWithGlobals();
2193
2357
  const skillsDir = resolveSkillsDir();
2194
- const skillDir = path17.join(skillsDir, name);
2195
- if (!fs8.existsSync(skillDir)) {
2358
+ const skillDir = path18.join(skillsDir, name);
2359
+ if (!fs9.existsSync(skillDir)) {
2196
2360
  logger.error(`Skill not found: ${name}`);
2197
2361
  process.exit(ExitCode.ERROR);
2198
2362
  return;
2199
2363
  }
2200
- const yamlPath = path17.join(skillDir, "skill.yaml");
2201
- if (!fs8.existsSync(yamlPath)) {
2364
+ const yamlPath = path18.join(skillDir, "skill.yaml");
2365
+ if (!fs9.existsSync(yamlPath)) {
2202
2366
  logger.error(`skill.yaml not found for skill: ${name}`);
2203
2367
  process.exit(ExitCode.ERROR);
2204
2368
  return;
2205
2369
  }
2206
2370
  try {
2207
- const raw = fs8.readFileSync(yamlPath, "utf-8");
2371
+ const raw = fs9.readFileSync(yamlPath, "utf-8");
2208
2372
  const parsed = parse4(raw);
2209
2373
  const result = SkillMetadataSchema.safeParse(parsed);
2210
2374
  if (!result.success) {
@@ -2243,974 +2407,2710 @@ function createInfoCommand() {
2243
2407
  });
2244
2408
  }
2245
2409
 
2246
- // src/commands/skill/index.ts
2247
- function createSkillCommand() {
2248
- const command = new Command25("skill").description("Skill management commands");
2249
- command.addCommand(createListCommand2());
2250
- command.addCommand(createRunCommand2());
2251
- command.addCommand(createValidateCommand3());
2252
- command.addCommand(createInfoCommand());
2253
- return command;
2254
- }
2255
-
2256
- // src/commands/state/index.ts
2257
- import { Command as Command30 } from "commander";
2410
+ // src/commands/skill/search.ts
2411
+ import { Command as Command25 } from "commander";
2258
2412
 
2259
- // src/commands/state/show.ts
2260
- import { Command as Command26 } from "commander";
2261
- import * as path18 from "path";
2262
- function createShowCommand() {
2263
- return new Command26("show").description("Show current project state").option("--path <path>", "Project root path", ".").option("--stream <name>", "Target a specific stream").action(async (opts, cmd) => {
2264
- const globalOpts = cmd.optsWithGlobals();
2265
- const projectPath = path18.resolve(opts.path);
2266
- const result = await loadState(projectPath, opts.stream);
2267
- if (!result.ok) {
2268
- logger.error(result.error.message);
2269
- process.exit(ExitCode.ERROR);
2270
- return;
2271
- }
2272
- const state = result.value;
2273
- if (globalOpts.json) {
2274
- logger.raw(state);
2275
- } else if (globalOpts.quiet) {
2276
- console.log(JSON.stringify(state));
2277
- } else {
2278
- if (opts.stream) console.log(`Stream: ${opts.stream}`);
2279
- console.log(`Schema Version: ${state.schemaVersion}`);
2280
- if (state.position.phase) console.log(`Phase: ${state.position.phase}`);
2281
- if (state.position.task) console.log(`Task: ${state.position.task}`);
2282
- if (state.lastSession) {
2283
- console.log(`Last Session: ${state.lastSession.date} \u2014 ${state.lastSession.summary}`);
2284
- }
2285
- if (Object.keys(state.progress).length > 0) {
2286
- console.log("\nProgress:");
2287
- for (const [task, status] of Object.entries(state.progress)) {
2288
- console.log(` ${task}: ${status}`);
2413
+ // src/registry/npm-client.ts
2414
+ import * as fs10 from "fs";
2415
+ import * as path19 from "path";
2416
+ import * as os2 from "os";
2417
+ var NPM_REGISTRY = "https://registry.npmjs.org";
2418
+ var FETCH_TIMEOUT_MS = 3e4;
2419
+ var HARNESS_SKILLS_SCOPE = "@harness-skills/";
2420
+ function resolvePackageName(name) {
2421
+ if (name.startsWith(HARNESS_SKILLS_SCOPE)) {
2422
+ return name;
2423
+ }
2424
+ if (name.startsWith("@")) {
2425
+ throw new Error(`Only @harness-skills/ scoped packages are supported. Got: ${name}`);
2426
+ }
2427
+ return `${HARNESS_SKILLS_SCOPE}${name}`;
2428
+ }
2429
+ function extractSkillName(packageName) {
2430
+ if (packageName.startsWith(HARNESS_SKILLS_SCOPE)) {
2431
+ return packageName.slice(HARNESS_SKILLS_SCOPE.length);
2432
+ }
2433
+ return packageName;
2434
+ }
2435
+ function readNpmrcToken(registryUrl) {
2436
+ const { hostname, pathname } = new URL(registryUrl);
2437
+ const registryPath = `//${hostname}${pathname.replace(/\/$/, "")}/:_authToken=`;
2438
+ const candidates = [path19.join(process.cwd(), ".npmrc"), path19.join(os2.homedir(), ".npmrc")];
2439
+ for (const npmrcPath of candidates) {
2440
+ try {
2441
+ const content = fs10.readFileSync(npmrcPath, "utf-8");
2442
+ for (const line of content.split("\n")) {
2443
+ const trimmed = line.trim();
2444
+ if (trimmed.startsWith(registryPath)) {
2445
+ return trimmed.slice(registryPath.length).trim();
2289
2446
  }
2290
2447
  }
2291
- if (state.decisions.length > 0) {
2292
- console.log(`
2293
- Decisions: ${state.decisions.length}`);
2294
- }
2295
- if (state.blockers.length > 0) {
2296
- const open = state.blockers.filter((b) => b.status === "open").length;
2297
- console.log(`Blockers: ${open} open / ${state.blockers.length} total`);
2298
- }
2448
+ } catch {
2299
2449
  }
2300
- process.exit(ExitCode.SUCCESS);
2301
- });
2450
+ }
2451
+ return null;
2302
2452
  }
2303
-
2304
- // src/commands/state/reset.ts
2305
- import { Command as Command27 } from "commander";
2306
- import * as fs9 from "fs";
2307
- import * as path19 from "path";
2308
- import * as readline from "readline";
2309
- function createResetCommand() {
2310
- return new Command27("reset").description("Reset project state (deletes .harness/state.json)").option("--path <path>", "Project root path", ".").option("--stream <name>", "Target a specific stream").option("--yes", "Skip confirmation prompt").action(async (opts, _cmd) => {
2311
- const projectPath = path19.resolve(opts.path);
2312
- let statePath;
2313
- if (opts.stream) {
2314
- const streamResult = await resolveStreamPath(projectPath, { stream: opts.stream });
2315
- if (!streamResult.ok) {
2316
- logger.error(streamResult.error.message);
2317
- process.exit(ExitCode.ERROR);
2318
- return;
2319
- }
2320
- statePath = path19.join(streamResult.value, "state.json");
2321
- } else {
2322
- statePath = path19.join(projectPath, ".harness", "state.json");
2323
- }
2324
- if (!fs9.existsSync(statePath)) {
2325
- logger.info("No state file found. Nothing to reset.");
2326
- process.exit(ExitCode.SUCCESS);
2327
- return;
2453
+ async function fetchPackageMetadata(packageName, registryUrl) {
2454
+ const registry = registryUrl ?? NPM_REGISTRY;
2455
+ const headers = {};
2456
+ if (registryUrl) {
2457
+ const token = readNpmrcToken(registryUrl);
2458
+ if (token) headers["Authorization"] = `Bearer ${token}`;
2459
+ }
2460
+ const encodedName = encodeURIComponent(packageName);
2461
+ const url = `${registry}/${encodedName}`;
2462
+ let response;
2463
+ try {
2464
+ response = await fetch(url, {
2465
+ headers,
2466
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
2467
+ });
2468
+ } catch {
2469
+ throw new Error("Cannot reach npm registry. Check your network connection.");
2470
+ }
2471
+ if (!response.ok) {
2472
+ if (response.status === 404) {
2473
+ throw new Error(`Package ${packageName} not found on npm registry.`);
2328
2474
  }
2329
- if (!opts.yes) {
2330
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
2331
- const answer = await new Promise((resolve20) => {
2332
- rl.question("Reset project state? This cannot be undone. [y/N] ", resolve20);
2475
+ throw new Error(
2476
+ `npm registry returned ${response.status} ${response.statusText} for ${packageName}.`
2477
+ );
2478
+ }
2479
+ return await response.json();
2480
+ }
2481
+ async function downloadTarball(tarballUrl, authToken) {
2482
+ let lastError;
2483
+ const headers = {};
2484
+ if (authToken) {
2485
+ headers["Authorization"] = `Bearer ${authToken}`;
2486
+ }
2487
+ for (let attempt = 0; attempt < 2; attempt++) {
2488
+ try {
2489
+ const response = await fetch(tarballUrl, {
2490
+ headers,
2491
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
2333
2492
  });
2334
- rl.close();
2335
- if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
2336
- logger.info("Reset cancelled.");
2337
- process.exit(ExitCode.SUCCESS);
2338
- return;
2493
+ if (!response.ok) {
2494
+ throw new Error(`HTTP ${response.status} ${response.statusText}`);
2339
2495
  }
2496
+ const arrayBuffer = await response.arrayBuffer();
2497
+ return Buffer.from(arrayBuffer);
2498
+ } catch (err) {
2499
+ lastError = err instanceof Error ? err : new Error(String(err));
2340
2500
  }
2341
- try {
2342
- fs9.unlinkSync(statePath);
2343
- logger.success("Project state reset.");
2344
- } catch (e) {
2345
- logger.error(`Failed to reset state: ${e instanceof Error ? e.message : String(e)}`);
2346
- process.exit(ExitCode.ERROR);
2347
- return;
2348
- }
2349
- process.exit(ExitCode.SUCCESS);
2350
- });
2501
+ }
2502
+ throw new Error(`Download failed for ${tarballUrl}. Try again. (${lastError?.message})`);
2503
+ }
2504
+ async function searchNpmRegistry(query, registryUrl) {
2505
+ const registry = registryUrl ?? NPM_REGISTRY;
2506
+ const headers = {};
2507
+ if (registryUrl) {
2508
+ const token = readNpmrcToken(registryUrl);
2509
+ if (token) headers["Authorization"] = `Bearer ${token}`;
2510
+ }
2511
+ const searchText = encodeURIComponent(`scope:harness-skills ${query}`);
2512
+ const url = `${registry}/-/v1/search?text=${searchText}&size=20`;
2513
+ let response;
2514
+ try {
2515
+ response = await fetch(url, {
2516
+ headers,
2517
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
2518
+ });
2519
+ } catch {
2520
+ throw new Error("Cannot reach npm registry. Check your network connection.");
2521
+ }
2522
+ if (!response.ok) {
2523
+ throw new Error(`npm registry search returned ${response.status} ${response.statusText}.`);
2524
+ }
2525
+ const data = await response.json();
2526
+ return data.objects.map((obj) => ({
2527
+ name: obj.package.name,
2528
+ version: obj.package.version,
2529
+ description: obj.package.description,
2530
+ keywords: obj.package.keywords || [],
2531
+ date: obj.package.date
2532
+ }));
2351
2533
  }
2352
2534
 
2353
- // src/commands/state/learn.ts
2354
- import { Command as Command28 } from "commander";
2355
- import * as path20 from "path";
2356
- function createLearnCommand() {
2357
- return new Command28("learn").description("Append a learning to .harness/learnings.md").argument("<message>", "The learning to record").option("--path <path>", "Project root path", ".").option("--stream <name>", "Target a specific stream").action(async (message, opts, _cmd) => {
2358
- const projectPath = path20.resolve(opts.path);
2359
- const result = await appendLearning(projectPath, message, void 0, void 0, opts.stream);
2360
- if (!result.ok) {
2361
- logger.error(result.error.message);
2362
- process.exit(ExitCode.ERROR);
2363
- return;
2535
+ // src/commands/skill/search.ts
2536
+ async function runSearch(query, opts) {
2537
+ const results = await searchNpmRegistry(query, opts.registry);
2538
+ return results.filter((r) => {
2539
+ if (opts.platform && !r.keywords.includes(opts.platform)) {
2540
+ return false;
2364
2541
  }
2365
- logger.success(`Learning recorded.`);
2366
- process.exit(ExitCode.SUCCESS);
2542
+ if (opts.trigger && !r.keywords.includes(opts.trigger)) {
2543
+ return false;
2544
+ }
2545
+ return true;
2367
2546
  });
2368
2547
  }
2369
-
2370
- // src/commands/state/streams.ts
2371
- import { Command as Command29 } from "commander";
2372
- import * as path21 from "path";
2373
- function createStreamsCommand() {
2374
- const command = new Command29("streams").description("Manage state streams");
2375
- command.command("list").description("List all known streams").option("--path <path>", "Project root path", ".").action(async (opts, cmd) => {
2548
+ function createSearchCommand() {
2549
+ return new Command25("search").description("Search for community skills on the @harness-skills registry").argument("<query>", "Search query").option("--platform <platform>", "Filter by platform (e.g., claude-code)").option("--trigger <trigger>", "Filter by trigger type (e.g., manual, automatic)").option("--registry <url>", "Use a custom npm registry URL").action(async (query, opts, cmd) => {
2376
2550
  const globalOpts = cmd.optsWithGlobals();
2377
- const projectPath = path21.resolve(opts.path);
2378
- const indexResult = await loadStreamIndex(projectPath);
2379
- const result = await listStreams(projectPath);
2380
- if (!result.ok) {
2381
- logger.error(result.error.message);
2382
- process.exit(ExitCode.ERROR);
2383
- return;
2384
- }
2385
- const active = indexResult.ok ? indexResult.value.activeStream : null;
2386
- if (globalOpts.json) {
2387
- logger.raw({ activeStream: active, streams: result.value });
2388
- } else {
2389
- if (result.value.length === 0) {
2390
- console.log("No streams found.");
2551
+ try {
2552
+ const results = await runSearch(query, opts);
2553
+ if (globalOpts.json) {
2554
+ logger.raw(results);
2555
+ process.exit(ExitCode.SUCCESS);
2556
+ return;
2391
2557
  }
2392
- for (const s of result.value) {
2393
- const marker = s.name === active ? " (active)" : "";
2394
- const branch = s.branch ? ` [${s.branch}]` : "";
2395
- console.log(` ${s.name}${marker}${branch} \u2014 last active: ${s.lastActiveAt}`);
2558
+ if (results.length === 0) {
2559
+ logger.info(`No skills found matching "${query}".`);
2560
+ process.exit(ExitCode.SUCCESS);
2561
+ return;
2562
+ }
2563
+ console.log(`
2564
+ Found ${results.length} skill(s):
2565
+ `);
2566
+ for (const r of results) {
2567
+ const shortName = extractSkillName(r.name);
2568
+ console.log(` ${shortName}@${r.version}`);
2569
+ console.log(` ${r.description}`);
2570
+ if (r.keywords.length > 0) {
2571
+ console.log(` keywords: ${r.keywords.join(", ")}`);
2572
+ }
2573
+ console.log();
2396
2574
  }
2575
+ logger.info(`Install with: harness install <skill-name>`);
2576
+ process.exit(ExitCode.SUCCESS);
2577
+ } catch (err) {
2578
+ logger.error(err instanceof Error ? err.message : String(err));
2579
+ process.exit(ExitCode.VALIDATION_FAILED);
2397
2580
  }
2398
- process.exit(ExitCode.SUCCESS);
2399
2581
  });
2400
- command.command("create <name>").description("Create a new stream").option("--path <path>", "Project root path", ".").option("--branch <branch>", "Associate with a git branch").action(async (name, opts) => {
2401
- const projectPath = path21.resolve(opts.path);
2402
- const result = await createStream(projectPath, name, opts.branch);
2403
- if (!result.ok) {
2404
- logger.error(result.error.message);
2405
- process.exit(ExitCode.ERROR);
2406
- return;
2407
- }
2408
- logger.success(`Stream '${name}' created.`);
2409
- process.exit(ExitCode.SUCCESS);
2410
- });
2411
- command.command("archive <name>").description("Archive a stream").option("--path <path>", "Project root path", ".").action(async (name, opts) => {
2412
- const projectPath = path21.resolve(opts.path);
2413
- const result = await archiveStream(projectPath, name);
2414
- if (!result.ok) {
2415
- logger.error(result.error.message);
2416
- process.exit(ExitCode.ERROR);
2417
- return;
2418
- }
2419
- logger.success(`Stream '${name}' archived.`);
2420
- process.exit(ExitCode.SUCCESS);
2421
- });
2422
- command.command("activate <name>").description("Set the active stream").option("--path <path>", "Project root path", ".").action(async (name, opts) => {
2423
- const projectPath = path21.resolve(opts.path);
2424
- const result = await setActiveStream(projectPath, name);
2425
- if (!result.ok) {
2426
- logger.error(result.error.message);
2427
- process.exit(ExitCode.ERROR);
2428
- return;
2429
- }
2430
- logger.success(`Active stream set to '${name}'.`);
2431
- process.exit(ExitCode.SUCCESS);
2432
- });
2433
- return command;
2434
2582
  }
2435
2583
 
2436
- // src/commands/state/index.ts
2437
- function createStateCommand() {
2438
- const command = new Command30("state").description("Project state management commands");
2439
- command.addCommand(createShowCommand());
2440
- command.addCommand(createResetCommand());
2441
- command.addCommand(createLearnCommand());
2442
- command.addCommand(createStreamsCommand());
2443
- return command;
2444
- }
2584
+ // src/commands/skill/create.ts
2585
+ import { Command as Command26 } from "commander";
2586
+ import * as path20 from "path";
2587
+ import * as fs11 from "fs";
2588
+ import YAML from "yaml";
2589
+ var KEBAB_CASE_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
2590
+ function buildReadme(name, description) {
2591
+ return `# @harness-skills/${name}
2445
2592
 
2446
- // src/commands/ci/index.ts
2447
- import { Command as Command33 } from "commander";
2593
+ ${description}
2448
2594
 
2449
- // src/commands/ci/check.ts
2450
- import { Command as Command31 } from "commander";
2451
- var VALID_CHECKS = ["validate", "deps", "docs", "entropy", "phase-gate"];
2452
- async function runCICheck(options) {
2453
- const configResult = resolveConfig(options.configPath);
2454
- if (!configResult.ok) {
2455
- return configResult;
2456
- }
2457
- const input = {
2458
- projectRoot: process.cwd(),
2459
- config: configResult.value
2595
+ ## Installation
2596
+
2597
+ \`\`\`bash
2598
+ harness install ${name}
2599
+ \`\`\`
2600
+
2601
+ ## Usage
2602
+
2603
+ This skill is automatically available after installation. Invoke it via:
2604
+
2605
+ \`\`\`bash
2606
+ harness skill run ${name}
2607
+ \`\`\`
2608
+
2609
+ Or use the slash command \`/${name}\` in your AI coding assistant.
2610
+
2611
+ ## Development
2612
+
2613
+ Edit \`skill.yaml\` to configure the skill metadata and \`SKILL.md\` to define the skill's behavior.
2614
+
2615
+ ### Validate
2616
+
2617
+ \`\`\`bash
2618
+ harness skill validate ${name}
2619
+ \`\`\`
2620
+
2621
+ ### Publish
2622
+
2623
+ \`\`\`bash
2624
+ harness skills publish
2625
+ \`\`\`
2626
+ `;
2627
+ }
2628
+ function buildSkillYaml(name, opts) {
2629
+ const platforms = opts.platforms ? opts.platforms.split(",").map((p) => p.trim()) : ["claude-code"];
2630
+ const triggers = opts.triggers ? opts.triggers.split(",").map((t) => t.trim()) : ["manual"];
2631
+ return {
2632
+ name,
2633
+ version: "0.1.0",
2634
+ description: opts.description || `A community skill: ${name}`,
2635
+ triggers,
2636
+ platforms,
2637
+ tools: ["Read", "Grep", "Glob", "Edit", "Write", "Bash"],
2638
+ type: opts.type || "flexible",
2639
+ state: {
2640
+ persistent: false,
2641
+ files: []
2642
+ },
2643
+ depends_on: []
2460
2644
  };
2461
- if (options.skip) input.skip = options.skip;
2462
- if (options.failOn) input.failOn = options.failOn;
2463
- const result = await runCIChecks(input);
2464
- if (!result.ok) {
2465
- return {
2466
- ok: false,
2467
- error: new CLIError(result.error.message, ExitCode.ERROR)
2468
- };
2469
- }
2470
- return { ok: true, value: result.value };
2471
2645
  }
2472
- function parseSkip(skip) {
2473
- if (!skip) return [];
2474
- return skip.split(",").map((s) => s.trim()).filter((s) => VALID_CHECKS.includes(s));
2646
+ function buildSkillMd(name, description) {
2647
+ return `# ${name}
2648
+
2649
+ ${description}
2650
+
2651
+ ## When to Use
2652
+
2653
+ - [Describe when this skill should be invoked]
2654
+ - [Describe the trigger conditions]
2655
+
2656
+ ## Process
2657
+
2658
+ 1. [Describe the step-by-step process]
2659
+ 2. [Add additional steps as needed]
2660
+
2661
+ ## Success Criteria
2662
+
2663
+ - [Define what a successful execution looks like]
2664
+ - [Add measurable criteria]
2665
+ `;
2475
2666
  }
2476
- function parseFailOn(failOn) {
2477
- if (failOn === "warning") return "warning";
2478
- return "error";
2667
+ function runCreate(name, opts) {
2668
+ if (!KEBAB_CASE_RE.test(name)) {
2669
+ throw new Error(`Invalid skill name "${name}". Must be kebab-case (e.g., my-skill).`);
2670
+ }
2671
+ const baseDir = opts.outputDir ?? path20.join(process.cwd(), "agents", "skills", "claude-code");
2672
+ const skillDir = path20.join(baseDir, name);
2673
+ if (fs11.existsSync(skillDir)) {
2674
+ throw new Error(`Skill directory already exists: ${skillDir}`);
2675
+ }
2676
+ fs11.mkdirSync(skillDir, { recursive: true });
2677
+ const description = opts.description || `A community skill: ${name}`;
2678
+ const skillYaml = buildSkillYaml(name, opts);
2679
+ const skillYamlPath = path20.join(skillDir, "skill.yaml");
2680
+ fs11.writeFileSync(skillYamlPath, YAML.stringify(skillYaml));
2681
+ const skillMd = buildSkillMd(name, description);
2682
+ const skillMdPath = path20.join(skillDir, "SKILL.md");
2683
+ fs11.writeFileSync(skillMdPath, skillMd);
2684
+ const readme = buildReadme(name, description);
2685
+ const readmePath = path20.join(skillDir, "README.md");
2686
+ fs11.writeFileSync(readmePath, readme);
2687
+ return {
2688
+ name,
2689
+ directory: skillDir,
2690
+ files: [skillYamlPath, skillMdPath, readmePath]
2691
+ };
2479
2692
  }
2480
- function createCheckCommand() {
2481
- return new Command31("check").description("Run all harness checks for CI (validate, deps, docs, entropy, phase-gate)").option("--skip <checks>", "Comma-separated checks to skip (e.g., entropy,docs)").option("--fail-on <severity>", "Fail on severity level: error (default) or warning", "error").action(async (opts, cmd) => {
2693
+ function createCreateCommand() {
2694
+ return new Command26("create").description("Scaffold a new community skill").argument("<name>", "Skill name (kebab-case)").option("--description <desc>", "Skill description").option("--type <type>", "Skill type: rigid or flexible", "flexible").option("--platforms <platforms>", "Comma-separated platforms (default: claude-code)").option("--triggers <triggers>", "Comma-separated triggers (default: manual)").option("--output-dir <dir>", "Output directory (default: agents/skills/claude-code/)").action(async (name, opts, cmd) => {
2482
2695
  const globalOpts = cmd.optsWithGlobals();
2483
- const mode = globalOpts.json ? OutputMode.JSON : globalOpts.quiet ? OutputMode.QUIET : globalOpts.verbose ? OutputMode.VERBOSE : OutputMode.TEXT;
2484
- const skip = parseSkip(opts.skip);
2485
- const failOn = parseFailOn(opts.failOn);
2486
- const result = await runCICheck({
2487
- configPath: globalOpts.config,
2488
- skip,
2489
- failOn
2490
- });
2491
- if (!result.ok) {
2492
- if (mode === OutputMode.JSON) {
2493
- console.log(JSON.stringify({ error: result.error.message }));
2696
+ try {
2697
+ const result = runCreate(name, {
2698
+ description: opts.description,
2699
+ type: opts.type,
2700
+ platforms: opts.platforms,
2701
+ triggers: opts.triggers,
2702
+ outputDir: opts.outputDir
2703
+ });
2704
+ if (globalOpts.json) {
2705
+ logger.raw(result);
2494
2706
  } else {
2495
- logger.error(result.error.message);
2496
- }
2497
- process.exit(ExitCode.ERROR);
2498
- }
2499
- const report = result.value;
2500
- if (mode === OutputMode.JSON) {
2501
- console.log(JSON.stringify(report, null, 2));
2502
- } else if (mode !== OutputMode.QUIET) {
2503
- for (const check of report.checks) {
2504
- const logFn = check.status === "pass" ? logger.success : check.status === "fail" ? logger.error : check.status === "warn" ? logger.warn : logger.dim;
2505
- logFn(`${check.name}: ${check.status} (${check.durationMs}ms)`);
2506
- for (const issue of check.issues) {
2507
- const prefix = issue.severity === "error" ? " x" : " !";
2508
- console.log(`${prefix} ${issue.message}${issue.file ? ` (${issue.file})` : ""}`);
2707
+ logger.success(`Created skill "${name}"`);
2708
+ for (const f of result.files) {
2709
+ logger.info(` ${f}`);
2509
2710
  }
2510
- }
2511
- console.log("");
2512
- if (report.exitCode === 0) {
2513
- logger.success(`All checks passed (${report.summary.passed}/${report.summary.total})`);
2514
- } else {
2515
- logger.error(
2516
- `${report.summary.failed} failed, ${report.summary.warnings} warnings, ${report.summary.passed} passed`
2711
+ logger.info(`
2712
+ Next steps:`);
2713
+ logger.info(
2714
+ ` 1. Edit ${path20.join(result.directory, "SKILL.md")} with your skill content`
2517
2715
  );
2716
+ logger.info(` 2. Run: harness skill validate ${name}`);
2717
+ logger.info(` 3. Run: harness skills publish`);
2518
2718
  }
2719
+ } catch (err) {
2720
+ logger.error(err instanceof Error ? err.message : String(err));
2721
+ process.exit(ExitCode.VALIDATION_FAILED);
2519
2722
  }
2520
- process.exit(report.exitCode);
2521
2723
  });
2522
2724
  }
2523
2725
 
2524
- // src/commands/ci/init.ts
2525
- import { Command as Command32 } from "commander";
2526
- import * as fs10 from "fs";
2527
- import * as path22 from "path";
2528
- var ALL_CHECKS = ["validate", "deps", "docs", "entropy", "phase-gate"];
2529
- function buildSkipFlag(checks) {
2530
- if (!checks) return "";
2531
- const skipChecks = ALL_CHECKS.filter((c) => !checks.includes(c));
2532
- if (skipChecks.length === 0) return "";
2533
- return ` --skip ${skipChecks.join(",")}`;
2534
- }
2535
- function generateGitHubActions(skipFlag) {
2536
- return `name: Harness Checks
2726
+ // src/commands/skill/publish.ts
2727
+ import { Command as Command27 } from "commander";
2728
+ import * as fs14 from "fs";
2729
+ import * as path23 from "path";
2730
+ import { execFileSync as execFileSync3 } from "child_process";
2537
2731
 
2538
- on:
2539
- push:
2540
- branches: [main]
2541
- pull_request:
2542
- branches: [main]
2732
+ // src/registry/validator.ts
2733
+ import * as fs13 from "fs";
2734
+ import * as path22 from "path";
2735
+ import { parse as parse5 } from "yaml";
2736
+ import semver from "semver";
2543
2737
 
2544
- concurrency:
2545
- group: harness-\${{ github.ref }}
2546
- cancel-in-progress: true
2738
+ // src/registry/bundled-skills.ts
2739
+ import * as fs12 from "fs";
2740
+ import * as path21 from "path";
2741
+ function getBundledSkillNames(bundledSkillsDir) {
2742
+ if (!fs12.existsSync(bundledSkillsDir)) {
2743
+ return /* @__PURE__ */ new Set();
2744
+ }
2745
+ const entries = fs12.readdirSync(bundledSkillsDir);
2746
+ const names = /* @__PURE__ */ new Set();
2747
+ for (const entry of entries) {
2748
+ try {
2749
+ const stat = fs12.statSync(path21.join(bundledSkillsDir, String(entry)));
2750
+ if (stat.isDirectory()) {
2751
+ names.add(String(entry));
2752
+ }
2753
+ } catch {
2754
+ }
2755
+ }
2756
+ return names;
2757
+ }
2547
2758
 
2548
- jobs:
2549
- harness:
2550
- runs-on: ubuntu-latest
2551
- steps:
2552
- - uses: actions/checkout@v4
2553
- - uses: actions/setup-node@v4
2554
- with:
2555
- node-version: '22'
2556
- - run: npm install -g @harness-engineering/cli
2557
- - name: Run harness checks
2558
- run: harness ci check --json${skipFlag}
2559
- `;
2759
+ // src/registry/validator.ts
2760
+ async function validateForPublish(skillDir, registryUrl) {
2761
+ const errors = [];
2762
+ const skillYamlPath = path22.join(skillDir, "skill.yaml");
2763
+ if (!fs13.existsSync(skillYamlPath)) {
2764
+ errors.push("skill.yaml not found. Create one with: harness skill create <name>");
2765
+ return { valid: false, errors };
2766
+ }
2767
+ let skillMeta;
2768
+ try {
2769
+ const raw = fs13.readFileSync(skillYamlPath, "utf-8");
2770
+ const parsed = parse5(raw);
2771
+ const result = SkillMetadataSchema.safeParse(parsed);
2772
+ if (!result.success) {
2773
+ const issues = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
2774
+ errors.push(`skill.yaml validation failed: ${issues}`);
2775
+ return { valid: false, errors };
2776
+ }
2777
+ skillMeta = result.data;
2778
+ } catch (err) {
2779
+ errors.push(`Failed to parse skill.yaml: ${err instanceof Error ? err.message : String(err)}`);
2780
+ return { valid: false, errors };
2781
+ }
2782
+ if (!skillMeta.description || skillMeta.description.trim().length === 0) {
2783
+ errors.push("description must not be empty. Add a meaningful description to skill.yaml.");
2784
+ }
2785
+ if (!skillMeta.platforms || skillMeta.platforms.length === 0) {
2786
+ errors.push("At least one platform is required. Add platforms to skill.yaml.");
2787
+ }
2788
+ if (!skillMeta.triggers || skillMeta.triggers.length === 0) {
2789
+ errors.push("At least one trigger is required. Add triggers to skill.yaml.");
2790
+ }
2791
+ const skillMdPath = path22.join(skillDir, "SKILL.md");
2792
+ if (!fs13.existsSync(skillMdPath)) {
2793
+ errors.push("SKILL.md not found. Create it with content describing your skill.");
2794
+ } else {
2795
+ const content = fs13.readFileSync(skillMdPath, "utf-8");
2796
+ if (!content.includes("## When to Use")) {
2797
+ errors.push('SKILL.md must contain a "## When to Use" section.');
2798
+ }
2799
+ if (!content.includes("## Process")) {
2800
+ errors.push('SKILL.md must contain a "## Process" section.');
2801
+ }
2802
+ }
2803
+ const globalSkillsDir = resolveGlobalSkillsDir();
2804
+ const bundledNames = getBundledSkillNames(globalSkillsDir);
2805
+ if (bundledNames.has(skillMeta.name)) {
2806
+ errors.push(
2807
+ `Skill name "${skillMeta.name}" conflicts with a bundled skill. Choose a different name.`
2808
+ );
2809
+ }
2810
+ try {
2811
+ const packageName = resolvePackageName(skillMeta.name);
2812
+ const metadata = await fetchPackageMetadata(packageName, registryUrl);
2813
+ const publishedVersion = metadata["dist-tags"]?.latest;
2814
+ if (publishedVersion && !semver.gt(skillMeta.version, publishedVersion)) {
2815
+ errors.push(
2816
+ `Version ${skillMeta.version} must be greater than published version ${publishedVersion}. Bump the version in skill.yaml.`
2817
+ );
2818
+ }
2819
+ } catch (err) {
2820
+ const msg = err instanceof Error ? err.message : String(err);
2821
+ if (!msg.includes("not found")) {
2822
+ errors.push(`Cannot verify version against npm registry: ${msg}`);
2823
+ }
2824
+ }
2825
+ if (skillMeta.depends_on && skillMeta.depends_on.length > 0) {
2826
+ for (const dep of skillMeta.depends_on) {
2827
+ if (bundledNames.has(dep)) continue;
2828
+ try {
2829
+ const depPkg = resolvePackageName(dep);
2830
+ await fetchPackageMetadata(depPkg, registryUrl);
2831
+ } catch {
2832
+ errors.push(
2833
+ `Dependency "${dep}" not found on npm or as a bundled skill. Publish it first or remove from depends_on.`
2834
+ );
2835
+ }
2836
+ }
2837
+ }
2838
+ return {
2839
+ valid: errors.length === 0,
2840
+ errors,
2841
+ ...skillMeta ? { skillMeta } : {}
2842
+ };
2560
2843
  }
2561
- function generateGitLabCI(skipFlag) {
2562
- return `harness:
2563
- stage: test
2564
- image: node:22
2565
- before_script:
2566
- - npm install -g @harness-engineering/cli
2567
- script:
2568
- - harness ci check --json${skipFlag}
2569
- rules:
2570
- - if: $CI_PIPELINE_SOURCE == "merge_request_event"
2571
- - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
2572
- `;
2844
+
2845
+ // src/skill/package-json.ts
2846
+ function derivePackageJson(skill) {
2847
+ const keywords = ["harness-skill", ...skill.platforms, ...skill.triggers];
2848
+ const pkg = {
2849
+ name: `@harness-skills/${skill.name}`,
2850
+ version: skill.version,
2851
+ description: skill.description,
2852
+ keywords,
2853
+ files: ["skill.yaml", "SKILL.md", "README.md"],
2854
+ license: "MIT"
2855
+ };
2856
+ if (skill.repository) {
2857
+ pkg.repository = {
2858
+ type: "git",
2859
+ url: skill.repository
2860
+ };
2861
+ }
2862
+ return pkg;
2573
2863
  }
2574
- function generateGenericScript(skipFlag) {
2575
- return `#!/usr/bin/env bash
2576
- set -euo pipefail
2577
2864
 
2578
- # Harness CI Check Script
2579
- # Generated by: harness ci init --platform generic
2865
+ // src/commands/skill/publish.ts
2866
+ async function runPublish(skillDir, opts) {
2867
+ const validation = await validateForPublish(skillDir, opts.registry);
2868
+ if (!validation.valid) {
2869
+ const errorList = validation.errors.map((e) => ` - ${e}`).join("\n");
2870
+ throw new Error(`Pre-publish validation failed:
2871
+ ${errorList}`);
2872
+ }
2873
+ const meta = validation.skillMeta;
2874
+ const pkg = derivePackageJson(meta);
2875
+ const pkgPath = path23.join(skillDir, "package.json");
2876
+ fs14.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
2877
+ const readmePath = path23.join(skillDir, "README.md");
2878
+ if (!fs14.existsSync(readmePath)) {
2879
+ const skillMdContent = fs14.readFileSync(path23.join(skillDir, "SKILL.md"), "utf-8");
2880
+ const readme = `# ${pkg.name}
2580
2881
 
2581
- if ! command -v harness &> /dev/null; then
2582
- echo "Installing @harness-engineering/cli..."
2583
- npm install -g @harness-engineering/cli
2584
- fi
2882
+ ${meta.description}
2585
2883
 
2586
- echo "Running harness checks..."
2587
- harness ci check --json${skipFlag}
2588
- EXIT_CODE=$?
2884
+ ## Installation
2589
2885
 
2590
- if [ $EXIT_CODE -eq 0 ]; then
2591
- echo "All harness checks passed."
2592
- elif [ $EXIT_CODE -eq 1 ]; then
2593
- echo "Harness checks failed. See report above."
2594
- else
2595
- echo "Harness internal error."
2596
- fi
2886
+ \`\`\`bash
2887
+ harness install ${meta.name}
2888
+ \`\`\`
2597
2889
 
2598
- exit $EXIT_CODE
2599
- `;
2600
- }
2601
- function generateCIConfig(options) {
2602
- const { platform, checks } = options;
2603
- const skipFlag = buildSkipFlag(checks);
2604
- const generators = {
2605
- github: { filename: ".github/workflows/harness.yml", generate: generateGitHubActions },
2606
- gitlab: { filename: ".gitlab-ci-harness.yml", generate: generateGitLabCI },
2607
- generic: { filename: "harness-ci.sh", generate: generateGenericScript }
2608
- };
2609
- const entry = generators[platform];
2610
- if (!entry) {
2611
- return Err(new CLIError(`Unknown platform: ${platform}`, ExitCode.ERROR));
2890
+ ---
2891
+
2892
+ ${skillMdContent}`;
2893
+ fs14.writeFileSync(readmePath, readme);
2612
2894
  }
2613
- return Ok({
2614
- filename: entry.filename,
2615
- content: entry.generate(skipFlag)
2895
+ if (opts.dryRun) {
2896
+ return {
2897
+ name: pkg.name,
2898
+ version: pkg.version,
2899
+ published: false,
2900
+ dryRun: true
2901
+ };
2902
+ }
2903
+ const publishArgs = ["publish", "--access", "public"];
2904
+ if (opts.registry) {
2905
+ publishArgs.push("--registry", opts.registry);
2906
+ }
2907
+ execFileSync3("npm", publishArgs, {
2908
+ cwd: skillDir,
2909
+ stdio: "pipe",
2910
+ timeout: 6e4
2616
2911
  });
2912
+ return {
2913
+ name: pkg.name,
2914
+ version: pkg.version,
2915
+ published: true
2916
+ };
2617
2917
  }
2618
- function detectPlatform() {
2619
- if (fs10.existsSync(".github")) return "github";
2620
- if (fs10.existsSync(".gitlab-ci.yml")) return "gitlab";
2621
- return null;
2918
+ function createPublishCommand() {
2919
+ return new Command27("publish").description("Validate and publish a skill to @harness-skills on npm").option("--dry-run", "Run validation and generate package.json without publishing").option("--dir <dir>", "Skill directory (default: current directory)").option("--registry <url>", "Use a custom npm registry URL").action(async (opts, cmd) => {
2920
+ const globalOpts = cmd.optsWithGlobals();
2921
+ const skillDir = opts.dir || process.cwd();
2922
+ try {
2923
+ const result = await runPublish(skillDir, {
2924
+ dryRun: opts.dryRun,
2925
+ registry: opts.registry
2926
+ });
2927
+ if (globalOpts.json) {
2928
+ logger.raw(result);
2929
+ } else if (result.dryRun) {
2930
+ logger.success(`Validation passed. Would publish: ${result.name}@${result.version}`);
2931
+ logger.info("Run without --dry-run to publish.");
2932
+ } else {
2933
+ logger.success(`Published ${result.name}@${result.version}`);
2934
+ }
2935
+ } catch (err) {
2936
+ logger.error(err instanceof Error ? err.message : String(err));
2937
+ process.exit(ExitCode.VALIDATION_FAILED);
2938
+ }
2939
+ });
2622
2940
  }
2623
- function createInitCommand2() {
2624
- return new Command32("init").description("Generate CI configuration for harness checks").option("--platform <platform>", "CI platform: github, gitlab, or generic").option("--checks <list>", "Comma-separated list of checks to include").action(async (opts, cmd) => {
2941
+
2942
+ // src/commands/skill/index.ts
2943
+ function createSkillCommand() {
2944
+ const command = new Command28("skill").description("Skill management commands");
2945
+ command.addCommand(createListCommand2());
2946
+ command.addCommand(createRunCommand2());
2947
+ command.addCommand(createValidateCommand3());
2948
+ command.addCommand(createInfoCommand());
2949
+ command.addCommand(createSearchCommand());
2950
+ command.addCommand(createCreateCommand());
2951
+ command.addCommand(createPublishCommand());
2952
+ return command;
2953
+ }
2954
+
2955
+ // src/commands/state/index.ts
2956
+ import { Command as Command33 } from "commander";
2957
+
2958
+ // src/commands/state/show.ts
2959
+ import { Command as Command29 } from "commander";
2960
+ import * as path24 from "path";
2961
+ function createShowCommand() {
2962
+ return new Command29("show").description("Show current project state").option("--path <path>", "Project root path", ".").option("--stream <name>", "Target a specific stream").action(async (opts, cmd) => {
2625
2963
  const globalOpts = cmd.optsWithGlobals();
2626
- const platform = opts.platform ?? detectPlatform() ?? "generic";
2627
- const checks = opts.checks ? opts.checks.split(",").map((s) => s.trim()) : void 0;
2628
- const opts2 = { platform };
2629
- if (checks) opts2.checks = checks;
2630
- const result = generateCIConfig(opts2);
2964
+ const projectPath = path24.resolve(opts.path);
2965
+ const result = await loadState(projectPath, opts.stream);
2631
2966
  if (!result.ok) {
2632
2967
  logger.error(result.error.message);
2633
- process.exit(result.error.exitCode);
2968
+ process.exit(ExitCode.ERROR);
2969
+ return;
2970
+ }
2971
+ const state = result.value;
2972
+ if (globalOpts.json) {
2973
+ logger.raw(state);
2974
+ } else if (globalOpts.quiet) {
2975
+ console.log(JSON.stringify(state));
2976
+ } else {
2977
+ if (opts.stream) console.log(`Stream: ${opts.stream}`);
2978
+ console.log(`Schema Version: ${state.schemaVersion}`);
2979
+ if (state.position.phase) console.log(`Phase: ${state.position.phase}`);
2980
+ if (state.position.task) console.log(`Task: ${state.position.task}`);
2981
+ if (state.lastSession) {
2982
+ console.log(`Last Session: ${state.lastSession.date} \u2014 ${state.lastSession.summary}`);
2983
+ }
2984
+ if (Object.keys(state.progress).length > 0) {
2985
+ console.log("\nProgress:");
2986
+ for (const [task, status] of Object.entries(state.progress)) {
2987
+ console.log(` ${task}: ${status}`);
2988
+ }
2989
+ }
2990
+ if (state.decisions.length > 0) {
2991
+ console.log(`
2992
+ Decisions: ${state.decisions.length}`);
2993
+ }
2994
+ if (state.blockers.length > 0) {
2995
+ const open = state.blockers.filter((b) => b.status === "open").length;
2996
+ console.log(`Blockers: ${open} open / ${state.blockers.length} total`);
2997
+ }
2998
+ }
2999
+ process.exit(ExitCode.SUCCESS);
3000
+ });
3001
+ }
3002
+
3003
+ // src/commands/state/reset.ts
3004
+ import { Command as Command30 } from "commander";
3005
+ import * as fs15 from "fs";
3006
+ import * as path25 from "path";
3007
+ import * as readline from "readline";
3008
+ function createResetCommand() {
3009
+ return new Command30("reset").description("Reset project state (deletes .harness/state.json)").option("--path <path>", "Project root path", ".").option("--stream <name>", "Target a specific stream").option("--yes", "Skip confirmation prompt").action(async (opts, _cmd) => {
3010
+ const projectPath = path25.resolve(opts.path);
3011
+ let statePath;
3012
+ if (opts.stream) {
3013
+ const streamResult = await resolveStreamPath(projectPath, { stream: opts.stream });
3014
+ if (!streamResult.ok) {
3015
+ logger.error(streamResult.error.message);
3016
+ process.exit(ExitCode.ERROR);
3017
+ return;
3018
+ }
3019
+ statePath = path25.join(streamResult.value, "state.json");
3020
+ } else {
3021
+ statePath = path25.join(projectPath, ".harness", "state.json");
3022
+ }
3023
+ if (!fs15.existsSync(statePath)) {
3024
+ logger.info("No state file found. Nothing to reset.");
3025
+ process.exit(ExitCode.SUCCESS);
3026
+ return;
3027
+ }
3028
+ if (!opts.yes) {
3029
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
3030
+ const answer = await new Promise((resolve27) => {
3031
+ rl.question("Reset project state? This cannot be undone. [y/N] ", resolve27);
3032
+ });
3033
+ rl.close();
3034
+ if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
3035
+ logger.info("Reset cancelled.");
3036
+ process.exit(ExitCode.SUCCESS);
3037
+ return;
3038
+ }
3039
+ }
3040
+ try {
3041
+ fs15.unlinkSync(statePath);
3042
+ logger.success("Project state reset.");
3043
+ } catch (e) {
3044
+ logger.error(`Failed to reset state: ${e instanceof Error ? e.message : String(e)}`);
3045
+ process.exit(ExitCode.ERROR);
3046
+ return;
3047
+ }
3048
+ process.exit(ExitCode.SUCCESS);
3049
+ });
3050
+ }
3051
+
3052
+ // src/commands/state/learn.ts
3053
+ import { Command as Command31 } from "commander";
3054
+ import * as path26 from "path";
3055
+ function createLearnCommand() {
3056
+ return new Command31("learn").description("Append a learning to .harness/learnings.md").argument("<message>", "The learning to record").option("--path <path>", "Project root path", ".").option("--stream <name>", "Target a specific stream").action(async (message, opts, _cmd) => {
3057
+ const projectPath = path26.resolve(opts.path);
3058
+ const result = await appendLearning(projectPath, message, void 0, void 0, opts.stream);
3059
+ if (!result.ok) {
3060
+ logger.error(result.error.message);
3061
+ process.exit(ExitCode.ERROR);
3062
+ return;
3063
+ }
3064
+ logger.success(`Learning recorded.`);
3065
+ process.exit(ExitCode.SUCCESS);
3066
+ });
3067
+ }
3068
+
3069
+ // src/commands/state/streams.ts
3070
+ import { Command as Command32 } from "commander";
3071
+ import * as path27 from "path";
3072
+ function createStreamsCommand() {
3073
+ const command = new Command32("streams").description("Manage state streams");
3074
+ command.command("list").description("List all known streams").option("--path <path>", "Project root path", ".").action(async (opts, cmd) => {
3075
+ const globalOpts = cmd.optsWithGlobals();
3076
+ const projectPath = path27.resolve(opts.path);
3077
+ const indexResult = await loadStreamIndex(projectPath);
3078
+ const result = await listStreams(projectPath);
3079
+ if (!result.ok) {
3080
+ logger.error(result.error.message);
3081
+ process.exit(ExitCode.ERROR);
3082
+ return;
3083
+ }
3084
+ const active = indexResult.ok ? indexResult.value.activeStream : null;
3085
+ if (globalOpts.json) {
3086
+ logger.raw({ activeStream: active, streams: result.value });
3087
+ } else {
3088
+ if (result.value.length === 0) {
3089
+ console.log("No streams found.");
3090
+ }
3091
+ for (const s of result.value) {
3092
+ const marker = s.name === active ? " (active)" : "";
3093
+ const branch = s.branch ? ` [${s.branch}]` : "";
3094
+ console.log(` ${s.name}${marker}${branch} \u2014 last active: ${s.lastActiveAt}`);
3095
+ }
3096
+ }
3097
+ process.exit(ExitCode.SUCCESS);
3098
+ });
3099
+ command.command("create <name>").description("Create a new stream").option("--path <path>", "Project root path", ".").option("--branch <branch>", "Associate with a git branch").action(async (name, opts) => {
3100
+ const projectPath = path27.resolve(opts.path);
3101
+ const result = await createStream(projectPath, name, opts.branch);
3102
+ if (!result.ok) {
3103
+ logger.error(result.error.message);
3104
+ process.exit(ExitCode.ERROR);
3105
+ return;
3106
+ }
3107
+ logger.success(`Stream '${name}' created.`);
3108
+ process.exit(ExitCode.SUCCESS);
3109
+ });
3110
+ command.command("archive <name>").description("Archive a stream").option("--path <path>", "Project root path", ".").action(async (name, opts) => {
3111
+ const projectPath = path27.resolve(opts.path);
3112
+ const result = await archiveStream(projectPath, name);
3113
+ if (!result.ok) {
3114
+ logger.error(result.error.message);
3115
+ process.exit(ExitCode.ERROR);
3116
+ return;
3117
+ }
3118
+ logger.success(`Stream '${name}' archived.`);
3119
+ process.exit(ExitCode.SUCCESS);
3120
+ });
3121
+ command.command("activate <name>").description("Set the active stream").option("--path <path>", "Project root path", ".").action(async (name, opts) => {
3122
+ const projectPath = path27.resolve(opts.path);
3123
+ const result = await setActiveStream(projectPath, name);
3124
+ if (!result.ok) {
3125
+ logger.error(result.error.message);
3126
+ process.exit(ExitCode.ERROR);
3127
+ return;
3128
+ }
3129
+ logger.success(`Active stream set to '${name}'.`);
3130
+ process.exit(ExitCode.SUCCESS);
3131
+ });
3132
+ return command;
3133
+ }
3134
+
3135
+ // src/commands/state/index.ts
3136
+ function createStateCommand() {
3137
+ const command = new Command33("state").description("Project state management commands");
3138
+ command.addCommand(createShowCommand());
3139
+ command.addCommand(createResetCommand());
3140
+ command.addCommand(createLearnCommand());
3141
+ command.addCommand(createStreamsCommand());
3142
+ return command;
3143
+ }
3144
+
3145
+ // src/commands/ci/index.ts
3146
+ import { Command as Command36 } from "commander";
3147
+
3148
+ // src/commands/ci/check.ts
3149
+ import { Command as Command34 } from "commander";
3150
+ var VALID_CHECKS = [
3151
+ "validate",
3152
+ "deps",
3153
+ "docs",
3154
+ "entropy",
3155
+ "security",
3156
+ "perf",
3157
+ "phase-gate",
3158
+ "arch"
3159
+ ];
3160
+ async function runCICheck(options) {
3161
+ const configResult = resolveConfig(options.configPath);
3162
+ if (!configResult.ok) {
3163
+ return configResult;
3164
+ }
3165
+ const input = {
3166
+ projectRoot: process.cwd(),
3167
+ config: configResult.value
3168
+ };
3169
+ if (options.skip) input.skip = options.skip;
3170
+ if (options.failOn) input.failOn = options.failOn;
3171
+ const result = await runCIChecks(input);
3172
+ if (!result.ok) {
3173
+ return {
3174
+ ok: false,
3175
+ error: new CLIError(result.error.message, ExitCode.ERROR)
3176
+ };
3177
+ }
3178
+ return { ok: true, value: result.value };
3179
+ }
3180
+ function parseSkip(skip) {
3181
+ if (!skip) return [];
3182
+ return skip.split(",").map((s) => s.trim()).filter((s) => VALID_CHECKS.includes(s));
3183
+ }
3184
+ function parseFailOn(failOn) {
3185
+ if (failOn === "warning") return "warning";
3186
+ return "error";
3187
+ }
3188
+ function createCheckCommand() {
3189
+ return new Command34("check").description("Run all harness checks for CI (validate, deps, docs, entropy, phase-gate, arch)").option("--skip <checks>", "Comma-separated checks to skip (e.g., entropy,docs)").option("--fail-on <severity>", "Fail on severity level: error (default) or warning", "error").action(async (opts, cmd) => {
3190
+ const globalOpts = cmd.optsWithGlobals();
3191
+ const mode = globalOpts.json ? OutputMode.JSON : globalOpts.quiet ? OutputMode.QUIET : globalOpts.verbose ? OutputMode.VERBOSE : OutputMode.TEXT;
3192
+ const skip = parseSkip(opts.skip);
3193
+ const failOn = parseFailOn(opts.failOn);
3194
+ const result = await runCICheck({
3195
+ configPath: globalOpts.config,
3196
+ skip,
3197
+ failOn
3198
+ });
3199
+ if (!result.ok) {
3200
+ if (mode === OutputMode.JSON) {
3201
+ console.log(JSON.stringify({ error: result.error.message }));
3202
+ } else {
3203
+ logger.error(result.error.message);
3204
+ }
3205
+ process.exit(ExitCode.ERROR);
3206
+ }
3207
+ const report = result.value;
3208
+ if (mode === OutputMode.JSON) {
3209
+ console.log(JSON.stringify(report, null, 2));
3210
+ } else if (mode !== OutputMode.QUIET) {
3211
+ for (const check of report.checks) {
3212
+ const logFn = check.status === "pass" ? logger.success : check.status === "fail" ? logger.error : check.status === "warn" ? logger.warn : logger.dim;
3213
+ logFn(`${check.name}: ${check.status} (${check.durationMs}ms)`);
3214
+ for (const issue of check.issues) {
3215
+ const prefix = issue.severity === "error" ? " x" : " !";
3216
+ console.log(`${prefix} ${issue.message}${issue.file ? ` (${issue.file})` : ""}`);
3217
+ }
3218
+ }
3219
+ console.log("");
3220
+ if (report.exitCode === 0) {
3221
+ logger.success(`All checks passed (${report.summary.passed}/${report.summary.total})`);
3222
+ } else {
3223
+ logger.error(
3224
+ `${report.summary.failed} failed, ${report.summary.warnings} warnings, ${report.summary.passed} passed`
3225
+ );
3226
+ }
3227
+ }
3228
+ process.exit(report.exitCode);
3229
+ });
3230
+ }
3231
+
3232
+ // src/commands/ci/init.ts
3233
+ import { Command as Command35 } from "commander";
3234
+ import * as fs16 from "fs";
3235
+ import * as path28 from "path";
3236
+ var ALL_CHECKS = [
3237
+ "validate",
3238
+ "deps",
3239
+ "docs",
3240
+ "entropy",
3241
+ "security",
3242
+ "perf",
3243
+ "phase-gate",
3244
+ "arch"
3245
+ ];
3246
+ function buildSkipFlag(checks) {
3247
+ if (!checks) return "";
3248
+ const skipChecks = ALL_CHECKS.filter((c) => !checks.includes(c));
3249
+ if (skipChecks.length === 0) return "";
3250
+ return ` --skip ${skipChecks.join(",")}`;
3251
+ }
3252
+ function generateGitHubActions(skipFlag) {
3253
+ return `name: Harness Checks
3254
+
3255
+ on:
3256
+ push:
3257
+ branches: [main]
3258
+ pull_request:
3259
+ branches: [main]
3260
+
3261
+ concurrency:
3262
+ group: harness-\${{ github.ref }}
3263
+ cancel-in-progress: true
3264
+
3265
+ jobs:
3266
+ harness:
3267
+ runs-on: ubuntu-latest
3268
+ steps:
3269
+ - uses: actions/checkout@v4
3270
+ - uses: actions/setup-node@v4
3271
+ with:
3272
+ node-version: '22'
3273
+ - run: npm install -g @harness-engineering/cli
3274
+ - name: Run harness checks
3275
+ run: harness ci check --json${skipFlag}
3276
+ `;
3277
+ }
3278
+ function generateGitLabCI(skipFlag) {
3279
+ return `harness:
3280
+ stage: test
3281
+ image: node:22
3282
+ before_script:
3283
+ - npm install -g @harness-engineering/cli
3284
+ script:
3285
+ - harness ci check --json${skipFlag}
3286
+ rules:
3287
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
3288
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
3289
+ `;
3290
+ }
3291
+ function generateGenericScript(skipFlag) {
3292
+ return `#!/usr/bin/env bash
3293
+ set -euo pipefail
3294
+
3295
+ # Harness CI Check Script
3296
+ # Generated by: harness ci init --platform generic
3297
+
3298
+ if ! command -v harness &> /dev/null; then
3299
+ echo "Installing @harness-engineering/cli..."
3300
+ npm install -g @harness-engineering/cli
3301
+ fi
3302
+
3303
+ echo "Running harness checks..."
3304
+ harness ci check --json${skipFlag}
3305
+ EXIT_CODE=$?
3306
+
3307
+ if [ $EXIT_CODE -eq 0 ]; then
3308
+ echo "All harness checks passed."
3309
+ elif [ $EXIT_CODE -eq 1 ]; then
3310
+ echo "Harness checks failed. See report above."
3311
+ else
3312
+ echo "Harness internal error."
3313
+ fi
3314
+
3315
+ exit $EXIT_CODE
3316
+ `;
3317
+ }
3318
+ function generateCIConfig(options) {
3319
+ const { platform, checks } = options;
3320
+ const skipFlag = buildSkipFlag(checks);
3321
+ const generators = {
3322
+ github: { filename: ".github/workflows/harness.yml", generate: generateGitHubActions },
3323
+ gitlab: { filename: ".gitlab-ci-harness.yml", generate: generateGitLabCI },
3324
+ generic: { filename: "harness-ci.sh", generate: generateGenericScript }
3325
+ };
3326
+ const entry = generators[platform];
3327
+ if (!entry) {
3328
+ return Err(new CLIError(`Unknown platform: ${platform}`, ExitCode.ERROR));
3329
+ }
3330
+ return Ok({
3331
+ filename: entry.filename,
3332
+ content: entry.generate(skipFlag)
3333
+ });
3334
+ }
3335
+ function detectPlatform() {
3336
+ if (fs16.existsSync(".github")) return "github";
3337
+ if (fs16.existsSync(".gitlab-ci.yml")) return "gitlab";
3338
+ return null;
3339
+ }
3340
+ function createInitCommand2() {
3341
+ return new Command35("init").description("Generate CI configuration for harness checks").option("--platform <platform>", "CI platform: github, gitlab, or generic").option("--checks <list>", "Comma-separated list of checks to include").action(async (opts, cmd) => {
3342
+ const globalOpts = cmd.optsWithGlobals();
3343
+ const platform = opts.platform ?? detectPlatform() ?? "generic";
3344
+ const checks = opts.checks ? opts.checks.split(",").map((s) => s.trim()) : void 0;
3345
+ const opts2 = { platform };
3346
+ if (checks) opts2.checks = checks;
3347
+ const result = generateCIConfig(opts2);
3348
+ if (!result.ok) {
3349
+ logger.error(result.error.message);
3350
+ process.exit(result.error.exitCode);
3351
+ }
3352
+ const { filename, content } = result.value;
3353
+ const targetPath = path28.resolve(filename);
3354
+ const dir = path28.dirname(targetPath);
3355
+ fs16.mkdirSync(dir, { recursive: true });
3356
+ fs16.writeFileSync(targetPath, content);
3357
+ if (platform === "generic" && process.platform !== "win32") {
3358
+ fs16.chmodSync(targetPath, "755");
3359
+ }
3360
+ if (globalOpts.json) {
3361
+ console.log(JSON.stringify({ file: filename, platform }));
3362
+ } else {
3363
+ logger.success(`Generated ${filename} for ${platform}`);
3364
+ logger.dim("Run 'harness ci check' to test locally");
3365
+ }
3366
+ });
3367
+ }
3368
+
3369
+ // src/commands/ci/index.ts
3370
+ function createCICommand() {
3371
+ const command = new Command36("ci").description("CI/CD integration commands");
3372
+ command.addCommand(createCheckCommand());
3373
+ command.addCommand(createInitCommand2());
3374
+ return command;
3375
+ }
3376
+
3377
+ // src/commands/update.ts
3378
+ import { Command as Command37 } from "commander";
3379
+ import { execFileSync as execFileSync4 } from "child_process";
3380
+ import { realpathSync } from "fs";
3381
+ import readline2 from "readline";
3382
+ import chalk3 from "chalk";
3383
+ function detectPackageManager() {
3384
+ try {
3385
+ const argv1 = process.argv[1];
3386
+ if (!argv1) return "npm";
3387
+ const binPath = realpathSync(argv1);
3388
+ const normalizedBin = binPath.replace(/\\/g, "/");
3389
+ if (normalizedBin.includes("pnpm/global/") || // eslint-disable-line @harness-engineering/no-hardcoded-path-separator -- platform-safe
3390
+ normalizedBin.includes("pnpm-global/")) {
3391
+ return "pnpm";
3392
+ }
3393
+ if (normalizedBin.includes(".yarn/")) {
3394
+ return "yarn";
3395
+ }
3396
+ } catch {
3397
+ }
3398
+ return "npm";
3399
+ }
3400
+ function getLatestVersion(pkg = "@harness-engineering/cli") {
3401
+ const output = execFileSync4("npm", ["view", pkg, "dist-tags.latest"], {
3402
+ encoding: "utf-8",
3403
+ timeout: 15e3
3404
+ });
3405
+ return output.trim();
3406
+ }
3407
+ function getInstalledVersion(pm) {
3408
+ try {
3409
+ const output = execFileSync4(pm, ["list", "-g", "@harness-engineering/cli", "--json"], {
3410
+ encoding: "utf-8",
3411
+ timeout: 15e3
3412
+ });
3413
+ const data = JSON.parse(output);
3414
+ const deps = data.dependencies ?? {};
3415
+ return deps["@harness-engineering/cli"]?.version ?? null;
3416
+ } catch {
3417
+ return null;
3418
+ }
3419
+ }
3420
+ function getInstalledPackages(pm) {
3421
+ try {
3422
+ const output = execFileSync4(pm, ["list", "-g", "--json"], {
3423
+ encoding: "utf-8",
3424
+ timeout: 15e3
3425
+ });
3426
+ const data = JSON.parse(output);
3427
+ const deps = data.dependencies ?? {};
3428
+ return Object.keys(deps).filter((name) => name.startsWith("@harness-engineering/"));
3429
+ } catch {
3430
+ return ["@harness-engineering/cli", "@harness-engineering/core"];
3431
+ }
3432
+ }
3433
+ function prompt(question) {
3434
+ const rl = readline2.createInterface({
3435
+ input: process.stdin,
3436
+ output: process.stdout
3437
+ });
3438
+ return new Promise((resolve27) => {
3439
+ rl.question(question, (answer) => {
3440
+ rl.close();
3441
+ resolve27(answer.trim().toLowerCase());
3442
+ });
3443
+ });
3444
+ }
3445
+ function createUpdateCommand() {
3446
+ return new Command37("update").description("Update all @harness-engineering packages to the latest version").option("--version <semver>", "Pin @harness-engineering/cli to a specific version").action(async (opts, cmd) => {
3447
+ const globalOpts = cmd.optsWithGlobals();
3448
+ const pm = detectPackageManager();
3449
+ if (globalOpts.verbose) {
3450
+ logger.info(`Detected package manager: ${pm}`);
3451
+ }
3452
+ const currentVersion = getInstalledVersion(pm);
3453
+ let latestCliVersion;
3454
+ if (!opts.version) {
3455
+ logger.info("Checking for updates...");
3456
+ try {
3457
+ latestCliVersion = getLatestVersion();
3458
+ } catch {
3459
+ logger.error("Failed to fetch latest version from npm registry");
3460
+ return process.exit(ExitCode.ERROR);
3461
+ }
3462
+ if (currentVersion && currentVersion === latestCliVersion) {
3463
+ logger.success(`Already up to date (v${currentVersion})`);
3464
+ process.exit(ExitCode.SUCCESS);
3465
+ }
3466
+ if (currentVersion) {
3467
+ console.log("");
3468
+ logger.info(`Current CLI version: ${chalk3.dim(`v${currentVersion}`)}`);
3469
+ logger.info(`Latest CLI version: ${chalk3.green(`v${latestCliVersion}`)}`);
3470
+ console.log("");
3471
+ }
3472
+ }
3473
+ const packages = getInstalledPackages(pm);
3474
+ if (globalOpts.verbose) {
3475
+ logger.info(`Installed packages: ${packages.join(", ")}`);
3476
+ }
3477
+ const installPkgs = packages.map((pkg) => {
3478
+ if (opts.version && pkg === "@harness-engineering/cli") {
3479
+ return `${pkg}@${opts.version}`;
3480
+ }
3481
+ return `${pkg}@latest`;
3482
+ });
3483
+ const installCmd = `${pm} install -g ${installPkgs.join(" ")}`;
3484
+ if (globalOpts.verbose) {
3485
+ logger.info(`Running: ${installCmd}`);
3486
+ }
3487
+ try {
3488
+ logger.info("Updating packages...");
3489
+ execFileSync4(pm, ["install", "-g", ...installPkgs], { stdio: "inherit", timeout: 12e4 });
3490
+ console.log("");
3491
+ logger.success("Update complete");
3492
+ } catch {
3493
+ console.log("");
3494
+ logger.error("Update failed. You can try manually:");
3495
+ console.log(` ${chalk3.cyan(installCmd)}`);
3496
+ process.exit(ExitCode.ERROR);
3497
+ }
3498
+ console.log("");
3499
+ const regenAnswer = await prompt("Regenerate slash commands and agent definitions? (Y/n) ");
3500
+ if (regenAnswer !== "n" && regenAnswer !== "no") {
3501
+ const scopeAnswer = await prompt("Generate for (G)lobal or (l)ocal project? (G/l) ");
3502
+ const isGlobal = scopeAnswer !== "l" && scopeAnswer !== "local";
3503
+ try {
3504
+ execFileSync4("harness", ["generate", ...isGlobal ? ["--global"] : []], {
3505
+ stdio: "inherit"
3506
+ });
3507
+ } catch {
3508
+ logger.warn("Generation failed. Run manually:");
3509
+ console.log(` ${chalk3.cyan(`harness generate${isGlobal ? " --global" : ""}`)}`);
3510
+ }
3511
+ }
3512
+ process.exit(ExitCode.SUCCESS);
3513
+ });
3514
+ }
3515
+
3516
+ // src/commands/generate.ts
3517
+ import { Command as Command38 } from "commander";
3518
+ function createGenerateCommand3() {
3519
+ return new Command38("generate").description("Generate all platform integrations (slash commands + agent definitions)").option("--platforms <list>", "Target platforms (comma-separated)", "claude-code,gemini-cli").option("--global", "Write to global directories", false).option("--include-global", "Include built-in global skills", false).option("--output <dir>", "Custom output directory").option("--dry-run", "Show what would change without writing", false).option("--yes", "Skip deletion confirmation prompts", false).action(async (opts, cmd) => {
3520
+ const globalOpts = cmd.optsWithGlobals();
3521
+ const platforms = opts.platforms.split(",").map((p) => p.trim());
3522
+ for (const p of platforms) {
3523
+ if (!VALID_PLATFORMS.includes(p)) {
3524
+ throw new CLIError(
3525
+ `Invalid platform "${p}". Valid platforms: ${VALID_PLATFORMS.join(", ")}`,
3526
+ ExitCode.VALIDATION_FAILED
3527
+ );
3528
+ }
3529
+ }
3530
+ try {
3531
+ console.log("Generating slash commands...");
3532
+ const slashResults = generateSlashCommands({
3533
+ platforms,
3534
+ global: opts.global,
3535
+ includeGlobal: opts.includeGlobal,
3536
+ output: opts.output,
3537
+ skillsDir: "",
3538
+ dryRun: opts.dryRun,
3539
+ yes: opts.yes
3540
+ });
3541
+ for (const result of slashResults) {
3542
+ const total = result.added.length + result.updated.length + result.unchanged.length;
3543
+ console.log(
3544
+ ` ${result.platform}: ${total} commands (${result.added.length} new, ${result.updated.length} updated)`
3545
+ );
3546
+ }
3547
+ await handleOrphanDeletion(slashResults, { yes: opts.yes, dryRun: opts.dryRun });
3548
+ console.log("\nGenerating agent definitions...");
3549
+ const agentResults = generateAgentDefinitions({
3550
+ platforms,
3551
+ global: opts.global,
3552
+ output: opts.output,
3553
+ dryRun: opts.dryRun
3554
+ });
3555
+ for (const result of agentResults) {
3556
+ const total = result.added.length + result.updated.length + result.unchanged.length;
3557
+ console.log(
3558
+ ` ${result.platform}: ${total} agents (${result.added.length} new, ${result.updated.length} updated)`
3559
+ );
3560
+ }
3561
+ if (opts.dryRun) {
3562
+ console.log("\n(dry run \u2014 no files written)");
3563
+ } else {
3564
+ console.log("\nDone.");
3565
+ }
3566
+ if (globalOpts.json) {
3567
+ console.log(
3568
+ JSON.stringify({ slashCommands: slashResults, agentDefinitions: agentResults }, null, 2)
3569
+ );
3570
+ }
3571
+ } catch (error) {
3572
+ handleError(error);
3573
+ }
3574
+ });
3575
+ }
3576
+
3577
+ // src/commands/graph/scan.ts
3578
+ import { Command as Command39 } from "commander";
3579
+ import * as path29 from "path";
3580
+ async function runScan(projectPath) {
3581
+ const { GraphStore, CodeIngestor, TopologicalLinker, KnowledgeIngestor, GitIngestor } = await import("./dist-M6BQODWC.js");
3582
+ const store = new GraphStore();
3583
+ const start = Date.now();
3584
+ await new CodeIngestor(store).ingest(projectPath);
3585
+ new TopologicalLinker(store).link();
3586
+ const knowledgeIngestor = new KnowledgeIngestor(store);
3587
+ await knowledgeIngestor.ingestAll(projectPath);
3588
+ try {
3589
+ await new GitIngestor(store).ingest(projectPath);
3590
+ } catch {
3591
+ }
3592
+ const graphDir = path29.join(projectPath, ".harness", "graph");
3593
+ await store.save(graphDir);
3594
+ return { nodeCount: store.nodeCount, edgeCount: store.edgeCount, durationMs: Date.now() - start };
3595
+ }
3596
+ function createScanCommand() {
3597
+ return new Command39("scan").description("Scan project and build knowledge graph").argument("[path]", "Project root path", ".").action(async (inputPath, _opts, cmd) => {
3598
+ const projectPath = path29.resolve(inputPath);
3599
+ const globalOpts = cmd.optsWithGlobals();
3600
+ try {
3601
+ const result = await runScan(projectPath);
3602
+ if (globalOpts.json) {
3603
+ console.log(JSON.stringify(result));
3604
+ } else {
3605
+ console.log(
3606
+ `Graph built: ${result.nodeCount} nodes, ${result.edgeCount} edges (${result.durationMs}ms)`
3607
+ );
3608
+ }
3609
+ } catch (err) {
3610
+ console.error("Scan failed:", err instanceof Error ? err.message : err);
3611
+ process.exit(2);
3612
+ }
3613
+ });
3614
+ }
3615
+
3616
+ // src/commands/graph/ingest.ts
3617
+ import { Command as Command40 } from "commander";
3618
+ import * as path30 from "path";
3619
+ async function loadConnectorConfig(projectPath, source) {
3620
+ try {
3621
+ const fs23 = await import("fs/promises");
3622
+ const configPath = path30.join(projectPath, "harness.config.json");
3623
+ const config = JSON.parse(await fs23.readFile(configPath, "utf-8"));
3624
+ const connector = config.graph?.connectors?.find(
3625
+ (c) => c.source === source
3626
+ );
3627
+ return connector?.config ?? {};
3628
+ } catch {
3629
+ return {};
3630
+ }
3631
+ }
3632
+ function mergeResults(...results) {
3633
+ return results.reduce(
3634
+ (acc, r) => ({
3635
+ nodesAdded: acc.nodesAdded + r.nodesAdded,
3636
+ nodesUpdated: acc.nodesUpdated + r.nodesUpdated,
3637
+ edgesAdded: acc.edgesAdded + r.edgesAdded,
3638
+ edgesUpdated: acc.edgesUpdated + r.edgesUpdated,
3639
+ errors: [...acc.errors, ...r.errors],
3640
+ durationMs: acc.durationMs + r.durationMs
3641
+ }),
3642
+ {
3643
+ nodesAdded: 0,
3644
+ nodesUpdated: 0,
3645
+ edgesAdded: 0,
3646
+ edgesUpdated: 0,
3647
+ errors: [],
3648
+ durationMs: 0
3649
+ }
3650
+ );
3651
+ }
3652
+ async function runIngest(projectPath, source, opts) {
3653
+ const {
3654
+ GraphStore,
3655
+ CodeIngestor,
3656
+ TopologicalLinker,
3657
+ KnowledgeIngestor,
3658
+ GitIngestor,
3659
+ SyncManager,
3660
+ JiraConnector,
3661
+ SlackConnector
3662
+ } = await import("./dist-M6BQODWC.js");
3663
+ const graphDir = path30.join(projectPath, ".harness", "graph");
3664
+ const store = new GraphStore();
3665
+ await store.load(graphDir);
3666
+ if (opts?.all) {
3667
+ const startMs = Date.now();
3668
+ const codeResult = await new CodeIngestor(store).ingest(projectPath);
3669
+ new TopologicalLinker(store).link();
3670
+ const knowledgeResult = await new KnowledgeIngestor(store).ingestAll(projectPath);
3671
+ const gitResult = await new GitIngestor(store).ingest(projectPath);
3672
+ const syncManager = new SyncManager(store, graphDir);
3673
+ const connectorMap = {
3674
+ jira: () => new JiraConnector(),
3675
+ slack: () => new SlackConnector()
3676
+ };
3677
+ for (const [name, factory] of Object.entries(connectorMap)) {
3678
+ const config = await loadConnectorConfig(projectPath, name);
3679
+ syncManager.registerConnector(factory(), config);
3680
+ }
3681
+ const connectorResult = await syncManager.syncAll();
3682
+ await store.save(graphDir);
3683
+ const merged = mergeResults(codeResult, knowledgeResult, gitResult, connectorResult);
3684
+ return { ...merged, durationMs: Date.now() - startMs };
3685
+ }
3686
+ let result;
3687
+ switch (source) {
3688
+ case "code":
3689
+ result = await new CodeIngestor(store).ingest(projectPath);
3690
+ new TopologicalLinker(store).link();
3691
+ break;
3692
+ case "knowledge":
3693
+ result = await new KnowledgeIngestor(store).ingestAll(projectPath);
3694
+ break;
3695
+ case "git":
3696
+ result = await new GitIngestor(store).ingest(projectPath);
3697
+ break;
3698
+ default: {
3699
+ const knownConnectors = ["jira", "slack"];
3700
+ if (!knownConnectors.includes(source)) {
3701
+ throw new Error(`Unknown source: ${source}. Available: code, knowledge, git, jira, slack`);
3702
+ }
3703
+ if (!SyncManager) {
3704
+ throw new Error(
3705
+ `Connector support not available. Ensure @harness-engineering/graph is built with connector support.`
3706
+ );
3707
+ }
3708
+ const syncManager = new SyncManager(store, graphDir);
3709
+ const extConnectorMap = {
3710
+ jira: () => new JiraConnector(),
3711
+ slack: () => new SlackConnector()
3712
+ };
3713
+ const factory = extConnectorMap[source];
3714
+ const config = await loadConnectorConfig(projectPath, source);
3715
+ syncManager.registerConnector(factory(), config);
3716
+ result = await syncManager.sync(source);
3717
+ break;
3718
+ }
3719
+ }
3720
+ await store.save(graphDir);
3721
+ return result;
3722
+ }
3723
+ function createIngestCommand() {
3724
+ return new Command40("ingest").description("Ingest data into the knowledge graph").option("--source <name>", "Source to ingest (code, knowledge, git, jira, slack)").option("--all", "Run all sources (code, knowledge, git, and configured connectors)").option("--full", "Force full re-ingestion").action(async (opts, cmd) => {
3725
+ if (!opts.source && !opts.all) {
3726
+ console.error("Error: --source or --all is required");
3727
+ process.exit(1);
3728
+ }
3729
+ const globalOpts = cmd.optsWithGlobals();
3730
+ const projectPath = path30.resolve(globalOpts.config ? path30.dirname(globalOpts.config) : ".");
3731
+ try {
3732
+ const result = await runIngest(projectPath, opts.source ?? "", {
3733
+ full: opts.full,
3734
+ all: opts.all
3735
+ });
3736
+ if (globalOpts.json) {
3737
+ console.log(JSON.stringify(result));
3738
+ } else {
3739
+ const label = opts.all ? "all" : opts.source;
3740
+ console.log(
3741
+ `Ingested (${label}): +${result.nodesAdded} nodes, +${result.edgesAdded} edges (${result.durationMs}ms)`
3742
+ );
3743
+ }
3744
+ } catch (err) {
3745
+ console.error("Ingest failed:", err instanceof Error ? err.message : err);
3746
+ process.exit(2);
3747
+ }
3748
+ });
3749
+ }
3750
+
3751
+ // src/commands/graph/query.ts
3752
+ import { Command as Command41 } from "commander";
3753
+ import * as path31 from "path";
3754
+ async function runQuery(projectPath, rootNodeId, opts) {
3755
+ const { GraphStore, ContextQL } = await import("./dist-M6BQODWC.js");
3756
+ const store = new GraphStore();
3757
+ const graphDir = path31.join(projectPath, ".harness", "graph");
3758
+ const loaded = await store.load(graphDir);
3759
+ if (!loaded) throw new Error("No graph found. Run `harness scan` first.");
3760
+ const params = {
3761
+ rootNodeIds: [rootNodeId],
3762
+ maxDepth: opts.depth ?? 3,
3763
+ bidirectional: opts.bidirectional ?? false,
3764
+ ...opts.types ? { includeTypes: opts.types.split(",") } : {},
3765
+ ...opts.edges ? { includeEdges: opts.edges.split(",") } : {}
3766
+ };
3767
+ const cql = new ContextQL(store);
3768
+ return cql.execute(params);
3769
+ }
3770
+ function createQueryCommand() {
3771
+ return new Command41("query").description("Query the knowledge graph").argument("<rootNodeId>", "Starting node ID").option("--depth <n>", "Max traversal depth", "3").option("--types <types>", "Comma-separated node types to include").option("--edges <edges>", "Comma-separated edge types to include").option("--bidirectional", "Traverse both directions").action(async (rootNodeId, opts, cmd) => {
3772
+ const globalOpts = cmd.optsWithGlobals();
3773
+ const projectPath = path31.resolve(globalOpts.config ? path31.dirname(globalOpts.config) : ".");
3774
+ try {
3775
+ const result = await runQuery(projectPath, rootNodeId, {
3776
+ depth: parseInt(opts.depth),
3777
+ types: opts.types,
3778
+ edges: opts.edges,
3779
+ bidirectional: opts.bidirectional
3780
+ });
3781
+ if (globalOpts.json) {
3782
+ console.log(JSON.stringify(result, null, 2));
3783
+ } else {
3784
+ console.log(
3785
+ `Found ${result.nodes.length} nodes, ${result.edges.length} edges (depth ${result.stats.depthReached}, pruned ${result.stats.pruned})`
3786
+ );
3787
+ for (const node of result.nodes) {
3788
+ console.log(` ${node.type.padEnd(12)} ${node.id}`);
3789
+ }
3790
+ }
3791
+ } catch (err) {
3792
+ console.error("Query failed:", err instanceof Error ? err.message : err);
3793
+ process.exit(2);
3794
+ }
3795
+ });
3796
+ }
3797
+
3798
+ // src/commands/graph/index.ts
3799
+ import { Command as Command42 } from "commander";
3800
+
3801
+ // src/commands/graph/status.ts
3802
+ import * as path32 from "path";
3803
+ async function runGraphStatus(projectPath) {
3804
+ const { GraphStore } = await import("./dist-M6BQODWC.js");
3805
+ const graphDir = path32.join(projectPath, ".harness", "graph");
3806
+ const store = new GraphStore();
3807
+ const loaded = await store.load(graphDir);
3808
+ if (!loaded) return { status: "no_graph", message: "No graph found. Run `harness scan` first." };
3809
+ const fs23 = await import("fs/promises");
3810
+ const metaPath = path32.join(graphDir, "metadata.json");
3811
+ let lastScan = "unknown";
3812
+ try {
3813
+ const meta = JSON.parse(await fs23.readFile(metaPath, "utf-8"));
3814
+ lastScan = meta.lastScanTimestamp;
3815
+ } catch {
3816
+ }
3817
+ const allNodes = store.findNodes({});
3818
+ const nodesByType = {};
3819
+ for (const node of allNodes) {
3820
+ nodesByType[node.type] = (nodesByType[node.type] ?? 0) + 1;
3821
+ }
3822
+ let connectorSyncStatus = {};
3823
+ try {
3824
+ const syncMetaPath = path32.join(graphDir, "sync-metadata.json");
3825
+ const syncMeta = JSON.parse(await fs23.readFile(syncMetaPath, "utf-8"));
3826
+ for (const [name, data] of Object.entries(syncMeta.connectors ?? {})) {
3827
+ connectorSyncStatus[name] = data.lastSyncTimestamp;
3828
+ }
3829
+ } catch {
3830
+ }
3831
+ return {
3832
+ status: "ok",
3833
+ nodeCount: store.nodeCount,
3834
+ edgeCount: store.edgeCount,
3835
+ nodesByType,
3836
+ lastScanTimestamp: lastScan,
3837
+ ...Object.keys(connectorSyncStatus).length > 0 ? { connectorSyncStatus } : {}
3838
+ };
3839
+ }
3840
+
3841
+ // src/commands/graph/export.ts
3842
+ import * as path33 from "path";
3843
+ async function runGraphExport(projectPath, format) {
3844
+ const { GraphStore } = await import("./dist-M6BQODWC.js");
3845
+ const graphDir = path33.join(projectPath, ".harness", "graph");
3846
+ const store = new GraphStore();
3847
+ const loaded = await store.load(graphDir);
3848
+ if (!loaded) throw new Error("No graph found. Run `harness scan` first.");
3849
+ if (format === "json") {
3850
+ const nodes = store.findNodes({});
3851
+ const edges = store.getEdges({});
3852
+ return JSON.stringify({ nodes, edges }, null, 2);
3853
+ }
3854
+ if (format === "mermaid") {
3855
+ const nodes = store.findNodes({});
3856
+ const edges = store.getEdges({});
3857
+ const lines = ["graph TD"];
3858
+ for (const node of nodes.slice(0, 200)) {
3859
+ const safeId = node.id.replace(/[^a-zA-Z0-9]/g, "_");
3860
+ const safeName = node.name.replace(/"/g, "#quot;");
3861
+ lines.push(` ${safeId}["${safeName}"]`);
3862
+ }
3863
+ for (const edge of edges.slice(0, 500)) {
3864
+ const safeFrom = edge.from.replace(/[^a-zA-Z0-9]/g, "_");
3865
+ const safeTo = edge.to.replace(/[^a-zA-Z0-9]/g, "_");
3866
+ lines.push(` ${safeFrom} -->|${edge.type}| ${safeTo}`);
2634
3867
  }
2635
- const { filename, content } = result.value;
2636
- const targetPath = path22.resolve(filename);
2637
- const dir = path22.dirname(targetPath);
2638
- fs10.mkdirSync(dir, { recursive: true });
2639
- fs10.writeFileSync(targetPath, content);
2640
- if (platform === "generic" && process.platform !== "win32") {
2641
- fs10.chmodSync(targetPath, "755");
3868
+ return lines.join("\n");
3869
+ }
3870
+ throw new Error(`Unknown format: ${format}. Available: json, mermaid`);
3871
+ }
3872
+
3873
+ // src/commands/graph/index.ts
3874
+ import * as path34 from "path";
3875
+ function createGraphCommand() {
3876
+ const graph = new Command42("graph").description("Knowledge graph management");
3877
+ graph.command("status").description("Show graph statistics").action(async (_opts, cmd) => {
3878
+ try {
3879
+ const globalOpts = cmd.optsWithGlobals();
3880
+ const projectPath = path34.resolve(globalOpts.config ? path34.dirname(globalOpts.config) : ".");
3881
+ const result = await runGraphStatus(projectPath);
3882
+ if (globalOpts.json) {
3883
+ console.log(JSON.stringify(result, null, 2));
3884
+ } else if (result.status === "no_graph") {
3885
+ console.log(result.message);
3886
+ } else {
3887
+ console.log(`Graph: ${result.nodeCount} nodes, ${result.edgeCount} edges`);
3888
+ console.log(`Last scan: ${result.lastScanTimestamp}`);
3889
+ console.log("Nodes by type:");
3890
+ for (const [type, count] of Object.entries(result.nodesByType)) {
3891
+ console.log(` ${type}: ${count}`);
3892
+ }
3893
+ if (result.connectorSyncStatus) {
3894
+ console.log("Connector sync status:");
3895
+ for (const [name, timestamp] of Object.entries(result.connectorSyncStatus)) {
3896
+ console.log(` ${name}: last synced ${timestamp}`);
3897
+ }
3898
+ }
3899
+ }
3900
+ } catch (err) {
3901
+ console.error("Status failed:", err instanceof Error ? err.message : err);
3902
+ process.exit(2);
2642
3903
  }
2643
- if (globalOpts.json) {
2644
- console.log(JSON.stringify({ file: filename, platform }));
2645
- } else {
2646
- logger.success(`Generated ${filename} for ${platform}`);
2647
- logger.dim("Run 'harness ci check' to test locally");
3904
+ });
3905
+ graph.command("export").description("Export graph").requiredOption("--format <format>", "Output format (json, mermaid)").action(async (opts, cmd) => {
3906
+ const globalOpts = cmd.optsWithGlobals();
3907
+ const projectPath = path34.resolve(globalOpts.config ? path34.dirname(globalOpts.config) : ".");
3908
+ try {
3909
+ const output = await runGraphExport(projectPath, opts.format);
3910
+ console.log(output);
3911
+ } catch (err) {
3912
+ console.error("Export failed:", err instanceof Error ? err.message : err);
3913
+ process.exit(2);
2648
3914
  }
2649
3915
  });
3916
+ return graph;
2650
3917
  }
2651
3918
 
2652
- // src/commands/ci/index.ts
2653
- function createCICommand() {
2654
- const command = new Command33("ci").description("CI/CD integration commands");
2655
- command.addCommand(createCheckCommand());
2656
- command.addCommand(createInitCommand2());
2657
- return command;
3919
+ // src/commands/mcp.ts
3920
+ import { Command as Command43 } from "commander";
3921
+ function createMcpCommand() {
3922
+ return new Command43("mcp").description("Start the MCP (Model Context Protocol) server on stdio").action(async () => {
3923
+ const { startServer: startServer2 } = await import("./mcp-YM6QLHLZ.js");
3924
+ await startServer2();
3925
+ });
2658
3926
  }
2659
3927
 
2660
- // src/commands/update.ts
2661
- import { Command as Command34 } from "commander";
2662
- import { execFileSync as execFileSync3 } from "child_process";
2663
- import { realpathSync } from "fs";
2664
- import readline2 from "readline";
2665
- import chalk3 from "chalk";
2666
- function detectPackageManager() {
3928
+ // src/commands/impact-preview.ts
3929
+ import { Command as Command44 } from "commander";
3930
+ import { execSync as execSync3 } from "child_process";
3931
+ import * as path35 from "path";
3932
+ import * as fs17 from "fs";
3933
+ function getStagedFiles(cwd) {
2667
3934
  try {
2668
- const argv1 = process.argv[1];
2669
- if (!argv1) return "npm";
2670
- const binPath = realpathSync(argv1);
2671
- const normalizedBin = binPath.replace(/\\/g, "/");
2672
- if (normalizedBin.includes("pnpm/global/") || // eslint-disable-line @harness-engineering/no-hardcoded-path-separator -- platform-safe
2673
- normalizedBin.includes("pnpm-global/")) {
2674
- return "pnpm";
2675
- }
2676
- if (normalizedBin.includes(".yarn/")) {
2677
- return "yarn";
2678
- }
3935
+ const output = execSync3("git diff --cached --name-only", {
3936
+ cwd,
3937
+ encoding: "utf-8"
3938
+ });
3939
+ return output.trim().split("\n").filter((f) => f.length > 0);
2679
3940
  } catch {
3941
+ return [];
2680
3942
  }
2681
- return "npm";
2682
- }
2683
- function getLatestVersion(pkg = "@harness-engineering/cli") {
2684
- const output = execFileSync3("npm", ["view", pkg, "dist-tags.latest"], {
2685
- encoding: "utf-8",
2686
- timeout: 15e3
2687
- });
2688
- return output.trim();
2689
3943
  }
2690
- function getInstalledVersion(pm) {
3944
+ function graphExists(projectPath) {
2691
3945
  try {
2692
- const output = execFileSync3(pm, ["list", "-g", "@harness-engineering/cli", "--json"], {
2693
- encoding: "utf-8",
2694
- timeout: 15e3
2695
- });
2696
- const data = JSON.parse(output);
2697
- const deps = data.dependencies ?? {};
2698
- return deps["@harness-engineering/cli"]?.version ?? null;
3946
+ return fs17.existsSync(path35.join(projectPath, ".harness", "graph", "graph.json"));
2699
3947
  } catch {
2700
- return null;
3948
+ return false;
2701
3949
  }
2702
3950
  }
2703
- function getInstalledPackages(pm) {
3951
+ function extractNodeName(id) {
3952
+ const parts = id.split(":");
3953
+ if (parts.length > 1) {
3954
+ const fullPath = parts.slice(1).join(":");
3955
+ return path35.basename(fullPath);
3956
+ }
3957
+ return id;
3958
+ }
3959
+ var TEST_NODE_TYPES = /* @__PURE__ */ new Set(["test_result"]);
3960
+ var DOC_NODE_TYPES = /* @__PURE__ */ new Set(["adr", "decision", "document", "learning"]);
3961
+ function parseImpactResponse(response) {
3962
+ if (response.isError) return null;
3963
+ const text = response.content[0]?.text;
3964
+ if (!text) return null;
2704
3965
  try {
2705
- const output = execFileSync3(pm, ["list", "-g", "--json"], {
2706
- encoding: "utf-8",
2707
- timeout: 15e3
2708
- });
2709
- const data = JSON.parse(output);
2710
- const deps = data.dependencies ?? {};
2711
- return Object.keys(deps).filter((name) => name.startsWith("@harness-engineering/"));
3966
+ const data = JSON.parse(text);
3967
+ if (data.mode === "summary") {
3968
+ const items = { code: [], tests: [], docs: [], other: [] };
3969
+ for (const item of data.highestRiskItems ?? []) {
3970
+ if (TEST_NODE_TYPES.has(item.type)) items.tests.push(item);
3971
+ else if (DOC_NODE_TYPES.has(item.type)) items.docs.push(item);
3972
+ else items.code.push(item);
3973
+ }
3974
+ return { counts: data.impactCounts, items };
3975
+ } else {
3976
+ const impact = data.impact ?? {};
3977
+ const items = {
3978
+ code: (impact.code ?? []).map((n) => ({
3979
+ id: n.id,
3980
+ type: n.type
3981
+ })),
3982
+ tests: (impact.tests ?? []).map((n) => ({
3983
+ id: n.id,
3984
+ type: n.type
3985
+ })),
3986
+ docs: (impact.docs ?? []).map((n) => ({
3987
+ id: n.id,
3988
+ type: n.type
3989
+ })),
3990
+ other: (impact.other ?? []).map((n) => ({
3991
+ id: n.id,
3992
+ type: n.type
3993
+ }))
3994
+ };
3995
+ return {
3996
+ counts: {
3997
+ code: items.code.length,
3998
+ tests: items.tests.length,
3999
+ docs: items.docs.length,
4000
+ other: items.other.length
4001
+ },
4002
+ items
4003
+ };
4004
+ }
2712
4005
  } catch {
2713
- return ["@harness-engineering/cli", "@harness-engineering/core"];
4006
+ return null;
2714
4007
  }
2715
4008
  }
2716
- function prompt(question) {
2717
- const rl = readline2.createInterface({
2718
- input: process.stdin,
2719
- output: process.stdout
2720
- });
2721
- return new Promise((resolve20) => {
2722
- rl.question(question, (answer) => {
2723
- rl.close();
2724
- resolve20(answer.trim().toLowerCase());
2725
- });
2726
- });
2727
- }
2728
- function createUpdateCommand() {
2729
- return new Command34("update").description("Update all @harness-engineering packages to the latest version").option("--version <semver>", "Pin @harness-engineering/cli to a specific version").action(async (opts, cmd) => {
2730
- const globalOpts = cmd.optsWithGlobals();
2731
- const pm = detectPackageManager();
2732
- if (globalOpts.verbose) {
2733
- logger.info(`Detected package manager: ${pm}`);
2734
- }
2735
- const currentVersion = getInstalledVersion(pm);
2736
- let latestCliVersion;
2737
- if (!opts.version) {
2738
- logger.info("Checking for updates...");
2739
- try {
2740
- latestCliVersion = getLatestVersion();
2741
- } catch {
2742
- logger.error("Failed to fetch latest version from npm registry");
2743
- return process.exit(ExitCode.ERROR);
2744
- }
2745
- if (currentVersion && currentVersion === latestCliVersion) {
2746
- logger.success(`Already up to date (v${currentVersion})`);
2747
- process.exit(ExitCode.SUCCESS);
2748
- }
2749
- if (currentVersion) {
2750
- console.log("");
2751
- logger.info(`Current CLI version: ${chalk3.dim(`v${currentVersion}`)}`);
2752
- logger.info(`Latest CLI version: ${chalk3.green(`v${latestCliVersion}`)}`);
2753
- console.log("");
4009
+ function mergeImpactGroups(groups) {
4010
+ const seen = /* @__PURE__ */ new Set();
4011
+ const merged = { code: [], tests: [], docs: [], other: [] };
4012
+ for (const group of groups) {
4013
+ for (const category of ["code", "tests", "docs", "other"]) {
4014
+ for (const item of group[category]) {
4015
+ if (!seen.has(item.id)) {
4016
+ seen.add(item.id);
4017
+ merged[category].push(item);
4018
+ }
2754
4019
  }
2755
4020
  }
2756
- const packages = getInstalledPackages(pm);
2757
- if (globalOpts.verbose) {
2758
- logger.info(`Installed packages: ${packages.join(", ")}`);
4021
+ }
4022
+ return merged;
4023
+ }
4024
+ function formatCompactLine(label, count, unit, items, maxItems) {
4025
+ if (count === 0) return "";
4026
+ const labelPad = label.padEnd(6);
4027
+ const countStr = String(count).padStart(3);
4028
+ const topNames = items.slice(0, maxItems).map((i) => extractNodeName(i.id));
4029
+ const remaining = count - topNames.length;
4030
+ const namePart = remaining > 0 ? `(${topNames.join(", ")}, +${remaining})` : topNames.length > 0 ? `(${topNames.join(", ")})` : "";
4031
+ return ` ${labelPad}${countStr} ${unit.padEnd(7)} ${namePart}`;
4032
+ }
4033
+ function formatCompact(stagedCount, merged, counts) {
4034
+ const lines = [];
4035
+ lines.push(`Impact Preview (${stagedCount} staged file${stagedCount === 1 ? "" : "s"})`);
4036
+ const codeLine = formatCompactLine("Code:", counts.code, "files", merged.code, 2);
4037
+ const testsLine = formatCompactLine("Tests:", counts.tests, "tests", merged.tests, 2);
4038
+ const docsLine = formatCompactLine("Docs:", counts.docs, "docs", merged.docs, 2);
4039
+ if (codeLine) lines.push(codeLine);
4040
+ if (testsLine) lines.push(testsLine);
4041
+ if (docsLine) lines.push(docsLine);
4042
+ const total = counts.code + counts.tests + counts.docs + counts.other;
4043
+ lines.push(` Total: ${total} affected`);
4044
+ return lines.join("\n");
4045
+ }
4046
+ function formatDetailed(stagedCount, merged, counts) {
4047
+ const lines = [];
4048
+ lines.push(`Impact Preview (${stagedCount} staged file${stagedCount === 1 ? "" : "s"})`);
4049
+ const sections = [
4050
+ { label: "Code", count: counts.code, unit: "files", items: merged.code },
4051
+ { label: "Tests", count: counts.tests, unit: "tests", items: merged.tests },
4052
+ { label: "Docs", count: counts.docs, unit: "docs", items: merged.docs }
4053
+ ];
4054
+ for (const section of sections) {
4055
+ if (section.count === 0 && section.items.length === 0) continue;
4056
+ lines.push(` ${section.label}: ${section.count} ${section.unit}`);
4057
+ for (const item of section.items) {
4058
+ lines.push(` ${extractNodeName(item.id)}`);
2759
4059
  }
2760
- const installPkgs = packages.map((pkg) => {
2761
- if (opts.version && pkg === "@harness-engineering/cli") {
2762
- return `${pkg}@${opts.version}`;
2763
- }
2764
- return `${pkg}@latest`;
4060
+ }
4061
+ const total = counts.code + counts.tests + counts.docs + counts.other;
4062
+ lines.push(` Total: ${total} affected`);
4063
+ return lines.join("\n");
4064
+ }
4065
+ function formatPerFile(perFileResults) {
4066
+ const lines = [];
4067
+ lines.push(
4068
+ `Impact Preview (${perFileResults.length} staged file${perFileResults.length === 1 ? "" : "s"})`
4069
+ );
4070
+ const maxLen = Math.max(...perFileResults.map((r) => r.file.length));
4071
+ for (const result of perFileResults) {
4072
+ const padded = result.file.padEnd(maxLen);
4073
+ lines.push(` ${padded} -> ${result.code} files, ${result.tests} tests, ${result.docs} docs`);
4074
+ }
4075
+ return lines.join("\n");
4076
+ }
4077
+ async function runImpactPreview(options) {
4078
+ const projectPath = path35.resolve(options.path ?? process.cwd());
4079
+ const stagedFiles = getStagedFiles(projectPath);
4080
+ if (stagedFiles.length === 0) {
4081
+ return "Impact Preview: no staged changes";
4082
+ }
4083
+ if (!graphExists(projectPath)) {
4084
+ return "Impact Preview: skipped (no graph \u2014 run `harness scan` to enable)";
4085
+ }
4086
+ const mode = options.detailed ? "detailed" : "summary";
4087
+ const perFileResults = [];
4088
+ const allGroups = [];
4089
+ const aggregateCounts = { code: 0, tests: 0, docs: 0, other: 0 };
4090
+ for (const file of stagedFiles) {
4091
+ const response = await handleGetImpact({
4092
+ path: projectPath,
4093
+ filePath: file,
4094
+ mode: options.perFile ? "summary" : mode
2765
4095
  });
2766
- const installCmd = `${pm} install -g ${installPkgs.join(" ")}`;
2767
- if (globalOpts.verbose) {
2768
- logger.info(`Running: ${installCmd}`);
2769
- }
2770
- try {
2771
- logger.info("Updating packages...");
2772
- execFileSync3(pm, ["install", "-g", ...installPkgs], { stdio: "inherit", timeout: 12e4 });
2773
- console.log("");
2774
- logger.success("Update complete");
2775
- } catch {
2776
- console.log("");
2777
- logger.error("Update failed. You can try manually:");
2778
- console.log(` ${chalk3.cyan(installCmd)}`);
2779
- process.exit(ExitCode.ERROR);
4096
+ const parsed = parseImpactResponse(response);
4097
+ if (!parsed) continue;
4098
+ aggregateCounts.code += parsed.counts.code;
4099
+ aggregateCounts.tests += parsed.counts.tests;
4100
+ aggregateCounts.docs += parsed.counts.docs;
4101
+ aggregateCounts.other += parsed.counts.other;
4102
+ if (options.perFile) {
4103
+ perFileResults.push({
4104
+ file,
4105
+ code: parsed.counts.code,
4106
+ tests: parsed.counts.tests,
4107
+ docs: parsed.counts.docs
4108
+ });
2780
4109
  }
2781
- console.log("");
2782
- const regenAnswer = await prompt("Regenerate slash commands and agent definitions? (y/N) ");
2783
- if (regenAnswer === "y" || regenAnswer === "yes") {
2784
- const scopeAnswer = await prompt("Generate for (g)lobal or (l)ocal project? (g/l) ");
2785
- const isGlobal = scopeAnswer === "g" || scopeAnswer === "global";
2786
- try {
2787
- execFileSync3("harness", ["generate", ...isGlobal ? ["--global"] : []], {
2788
- stdio: "inherit"
2789
- });
2790
- } catch {
2791
- logger.warn("Generation failed. Run manually:");
2792
- console.log(` ${chalk3.cyan(`harness generate${isGlobal ? " --global" : ""}`)}`);
2793
- }
4110
+ allGroups.push(parsed.items);
4111
+ }
4112
+ if (options.perFile) {
4113
+ if (perFileResults.length === 0) {
4114
+ return `Impact Preview (${stagedFiles.length} staged file${stagedFiles.length === 1 ? "" : "s"}): no impact data`;
2794
4115
  }
2795
- process.exit(ExitCode.SUCCESS);
4116
+ return formatPerFile(perFileResults);
4117
+ }
4118
+ const merged = mergeImpactGroups(allGroups);
4119
+ if (options.detailed) {
4120
+ return formatDetailed(stagedFiles.length, merged, aggregateCounts);
4121
+ }
4122
+ return formatCompact(stagedFiles.length, merged, aggregateCounts);
4123
+ }
4124
+ function createImpactPreviewCommand() {
4125
+ const command = new Command44("impact-preview").description("Show blast radius of staged changes using the knowledge graph").option("--detailed", "Show all affected files instead of top items").option("--per-file", "Show impact per staged file instead of aggregate").option("--path <dir>", "Project root (default: cwd)").action(async (opts) => {
4126
+ const output = await runImpactPreview({
4127
+ detailed: opts.detailed,
4128
+ perFile: opts.perFile,
4129
+ path: opts.path
4130
+ });
4131
+ console.log(output);
4132
+ process.exit(0);
2796
4133
  });
4134
+ return command;
2797
4135
  }
2798
4136
 
2799
- // src/commands/generate.ts
2800
- import { Command as Command35 } from "commander";
2801
- function createGenerateCommand3() {
2802
- return new Command35("generate").description("Generate all platform integrations (slash commands + agent definitions)").option("--platforms <list>", "Target platforms (comma-separated)", "claude-code,gemini-cli").option("--global", "Write to global directories", false).option("--include-global", "Include built-in global skills", false).option("--output <dir>", "Custom output directory").option("--dry-run", "Show what would change without writing", false).option("--yes", "Skip deletion confirmation prompts", false).action(async (opts, cmd) => {
2803
- const globalOpts = cmd.optsWithGlobals();
2804
- const platforms = opts.platforms.split(",").map((p) => p.trim());
2805
- for (const p of platforms) {
2806
- if (!VALID_PLATFORMS.includes(p)) {
2807
- throw new CLIError(
2808
- `Invalid platform "${p}". Valid platforms: ${VALID_PLATFORMS.join(", ")}`,
2809
- ExitCode.VALIDATION_FAILED
2810
- );
4137
+ // src/commands/check-arch.ts
4138
+ import { Command as Command45 } from "commander";
4139
+ import { execSync as execSync4 } from "child_process";
4140
+ function getCommitHash(cwd) {
4141
+ try {
4142
+ return execSync4("git rev-parse --short HEAD", { cwd, encoding: "utf-8" }).toString().trim();
4143
+ } catch {
4144
+ return "unknown";
4145
+ }
4146
+ }
4147
+ function filterByModule(results, modulePath) {
4148
+ const normalized = modulePath.replace(/\/+$/, "");
4149
+ return results.filter((r) => r.scope === normalized || r.scope.startsWith(normalized + "/"));
4150
+ }
4151
+ function findThresholdViolations(results) {
4152
+ const violations = [];
4153
+ for (const result of results) {
4154
+ for (const v of result.violations) {
4155
+ if (v.severity === "error") {
4156
+ violations.push(v);
2811
4157
  }
2812
4158
  }
2813
- try {
2814
- console.log("Generating slash commands...");
2815
- const slashResults = generateSlashCommands({
2816
- platforms,
2817
- global: opts.global,
2818
- includeGlobal: opts.includeGlobal,
2819
- output: opts.output,
2820
- skillsDir: "",
2821
- dryRun: opts.dryRun,
2822
- yes: opts.yes
2823
- });
2824
- for (const result of slashResults) {
2825
- const total = result.added.length + result.updated.length + result.unchanged.length;
2826
- console.log(
2827
- ` ${result.platform}: ${total} commands (${result.added.length} new, ${result.updated.length} updated)`
2828
- );
2829
- }
2830
- await handleOrphanDeletion(slashResults, { yes: opts.yes, dryRun: opts.dryRun });
2831
- console.log("\nGenerating agent definitions...");
2832
- const agentResults = generateAgentDefinitions({
2833
- platforms,
2834
- global: opts.global,
2835
- output: opts.output,
2836
- dryRun: opts.dryRun
2837
- });
2838
- for (const result of agentResults) {
2839
- const total = result.added.length + result.updated.length + result.unchanged.length;
2840
- console.log(
2841
- ` ${result.platform}: ${total} agents (${result.added.length} new, ${result.updated.length} updated)`
2842
- );
4159
+ }
4160
+ return violations;
4161
+ }
4162
+ async function runCheckArch(options) {
4163
+ const cwd = options.cwd ?? process.cwd();
4164
+ const configResult = resolveConfig(options.configPath);
4165
+ if (!configResult.ok) {
4166
+ return configResult;
4167
+ }
4168
+ const config = configResult.value;
4169
+ const archConfig = config.architecture ?? ArchConfigSchema.parse({});
4170
+ if (!archConfig.enabled) {
4171
+ return Ok({
4172
+ passed: true,
4173
+ mode: "threshold-only",
4174
+ totalViolations: 0,
4175
+ newViolations: [],
4176
+ resolvedViolations: [],
4177
+ preExisting: [],
4178
+ regressions: [],
4179
+ thresholdViolations: []
4180
+ });
4181
+ }
4182
+ let results = await runAll(archConfig, cwd);
4183
+ if (options.module) {
4184
+ results = filterByModule(results, options.module);
4185
+ }
4186
+ const manager = new ArchBaselineManager(cwd, archConfig.baselinePath);
4187
+ if (options.updateBaseline) {
4188
+ const commitHash = getCommitHash(cwd);
4189
+ const baseline2 = manager.capture(results, commitHash);
4190
+ manager.save(baseline2);
4191
+ return Ok({
4192
+ passed: true,
4193
+ mode: "baseline",
4194
+ totalViolations: 0,
4195
+ newViolations: [],
4196
+ resolvedViolations: [],
4197
+ preExisting: [],
4198
+ regressions: [],
4199
+ thresholdViolations: [],
4200
+ baselineUpdated: true
4201
+ });
4202
+ }
4203
+ const thresholdViolations = findThresholdViolations(results);
4204
+ const baseline = manager.load();
4205
+ if (!baseline) {
4206
+ const passed2 = thresholdViolations.length === 0;
4207
+ return Ok({
4208
+ passed: passed2,
4209
+ mode: "threshold-only",
4210
+ totalViolations: thresholdViolations.length,
4211
+ newViolations: [],
4212
+ resolvedViolations: [],
4213
+ preExisting: [],
4214
+ regressions: [],
4215
+ thresholdViolations,
4216
+ warning: "No baseline found. Running in threshold-only mode. Run with --update-baseline to capture current state."
4217
+ });
4218
+ }
4219
+ const diffResult = diff(results, baseline);
4220
+ const passed = diffResult.passed && thresholdViolations.length === 0;
4221
+ return Ok({
4222
+ passed,
4223
+ mode: "baseline",
4224
+ totalViolations: diffResult.newViolations.length + thresholdViolations.length,
4225
+ newViolations: diffResult.newViolations,
4226
+ resolvedViolations: diffResult.resolvedViolations,
4227
+ preExisting: diffResult.preExisting,
4228
+ regressions: diffResult.regressions,
4229
+ thresholdViolations
4230
+ });
4231
+ }
4232
+ function createCheckArchCommand() {
4233
+ const command = new Command45("check-arch").description("Check architecture assertions against baseline and thresholds").option("--update-baseline", "Capture current state as new baseline").option("--module <path>", "Check a single module").action(async (opts, cmd) => {
4234
+ const globalOpts = cmd.optsWithGlobals();
4235
+ const mode = globalOpts.json ? OutputMode.JSON : globalOpts.quiet ? OutputMode.QUIET : globalOpts.verbose ? OutputMode.VERBOSE : OutputMode.TEXT;
4236
+ const formatter = new OutputFormatter(mode);
4237
+ const result = await runCheckArch({
4238
+ configPath: globalOpts.config,
4239
+ updateBaseline: opts.updateBaseline,
4240
+ json: globalOpts.json,
4241
+ module: opts.module
4242
+ });
4243
+ if (!result.ok) {
4244
+ if (mode === OutputMode.JSON) {
4245
+ console.log(JSON.stringify({ error: result.error.message }));
4246
+ } else {
4247
+ logger.error(result.error.message);
2843
4248
  }
2844
- if (opts.dryRun) {
2845
- console.log("\n(dry run \u2014 no files written)");
4249
+ process.exit(result.error.exitCode);
4250
+ }
4251
+ const value = result.value;
4252
+ if (value.warning && mode !== OutputMode.JSON) {
4253
+ logger.warn(value.warning);
4254
+ }
4255
+ if (value.baselineUpdated) {
4256
+ if (mode === OutputMode.JSON) {
4257
+ console.log(JSON.stringify({ baselineUpdated: true }));
2846
4258
  } else {
2847
- console.log("\nDone.");
4259
+ logger.success("Baseline updated successfully.");
2848
4260
  }
2849
- if (globalOpts.json) {
2850
- console.log(
2851
- JSON.stringify({ slashCommands: slashResults, agentDefinitions: agentResults }, null, 2)
4261
+ process.exit(ExitCode.SUCCESS);
4262
+ return;
4263
+ }
4264
+ const issues = [
4265
+ ...value.newViolations.map((v) => ({
4266
+ file: v.file,
4267
+ message: `New violation [${v.severity}]: ${v.detail}`
4268
+ })),
4269
+ ...value.thresholdViolations.map((v) => ({
4270
+ file: v.file,
4271
+ message: `Threshold exceeded: ${v.detail}`
4272
+ })),
4273
+ ...value.regressions.map((r) => ({
4274
+ message: `Regression in ${r.category}: ${r.baselineValue} -> ${r.currentValue} (+${r.delta})`
4275
+ }))
4276
+ ];
4277
+ if (mode === OutputMode.JSON) {
4278
+ console.log(JSON.stringify(value, null, 2));
4279
+ } else {
4280
+ if (value.resolvedViolations.length > 0 && mode !== OutputMode.QUIET) {
4281
+ logger.success(
4282
+ `${value.resolvedViolations.length} violation(s) resolved since baseline.`
2852
4283
  );
2853
4284
  }
2854
- } catch (error) {
2855
- handleError(error);
4285
+ const output = formatter.formatValidation({
4286
+ valid: value.passed,
4287
+ issues
4288
+ });
4289
+ if (output) {
4290
+ console.log(output);
4291
+ }
2856
4292
  }
4293
+ process.exit(value.passed ? ExitCode.SUCCESS : ExitCode.VALIDATION_FAILED);
2857
4294
  });
4295
+ return command;
2858
4296
  }
2859
4297
 
2860
- // src/commands/graph/scan.ts
2861
- import { Command as Command36 } from "commander";
2862
- import * as path23 from "path";
2863
- async function runScan(projectPath) {
2864
- const { GraphStore, CodeIngestor, TopologicalLinker, KnowledgeIngestor, GitIngestor } = await import("./dist-I7DB5VKB.js");
2865
- const store = new GraphStore();
2866
- const start = Date.now();
2867
- await new CodeIngestor(store).ingest(projectPath);
2868
- new TopologicalLinker(store).link();
2869
- const knowledgeIngestor = new KnowledgeIngestor(store);
2870
- await knowledgeIngestor.ingestAll(projectPath);
2871
- try {
2872
- await new GitIngestor(store).ingest(projectPath);
2873
- } catch {
2874
- }
2875
- const graphDir = path23.join(projectPath, ".harness", "graph");
2876
- await store.save(graphDir);
2877
- return { nodeCount: store.nodeCount, edgeCount: store.edgeCount, durationMs: Date.now() - start };
4298
+ // src/commands/blueprint.ts
4299
+ import { Command as Command46 } from "commander";
4300
+ import * as path36 from "path";
4301
+ function createBlueprintCommand() {
4302
+ return new Command46("blueprint").description("Generate a self-contained, interactive blueprint of the codebase").argument("[path]", "Path to the project root", ".").option("-o, --output <dir>", "Output directory", "docs/blueprint").action(async (projectPath, options) => {
4303
+ try {
4304
+ const rootDir = path36.resolve(projectPath);
4305
+ const outputDir = path36.resolve(options.output);
4306
+ logger.info(`Scanning project at ${rootDir}...`);
4307
+ const scanner = new ProjectScanner(rootDir);
4308
+ const data = await scanner.scan();
4309
+ logger.info(`Generating blueprint to ${outputDir}...`);
4310
+ const generator = new BlueprintGenerator();
4311
+ await generator.generate(data, { outputDir });
4312
+ logger.success(`Blueprint generated successfully at ${path36.join(outputDir, "index.html")}`);
4313
+ } catch (error) {
4314
+ logger.error(
4315
+ `Failed to generate blueprint: ${error instanceof Error ? error.message : String(error)}`
4316
+ );
4317
+ process.exit(1);
4318
+ }
4319
+ });
2878
4320
  }
2879
- function createScanCommand() {
2880
- return new Command36("scan").description("Scan project and build knowledge graph").argument("[path]", "Project root path", ".").action(async (inputPath, _opts, cmd) => {
2881
- const projectPath = path23.resolve(inputPath);
2882
- const globalOpts = cmd.optsWithGlobals();
4321
+
4322
+ // src/commands/share.ts
4323
+ import { Command as Command47 } from "commander";
4324
+ import * as fs18 from "fs";
4325
+ import * as path37 from "path";
4326
+ import { parse as parseYaml } from "yaml";
4327
+ var MANIFEST_FILENAME = "constraints.yaml";
4328
+ function createShareCommand() {
4329
+ return new Command47("share").description("Extract and publish a constraints bundle from constraints.yaml").argument("[path]", "Path to the project root", ".").option("-o, --output <dir>", "Output directory for the bundle", ".").action(async (projectPath, options) => {
4330
+ const rootDir = path37.resolve(projectPath);
4331
+ const manifestPath = path37.join(rootDir, MANIFEST_FILENAME);
4332
+ if (!fs18.existsSync(manifestPath)) {
4333
+ logger.error(
4334
+ `No ${MANIFEST_FILENAME} found at ${manifestPath}.
4335
+ Create a constraints.yaml in your project root to define what to share.`
4336
+ );
4337
+ process.exit(1);
4338
+ }
4339
+ let parsed;
2883
4340
  try {
2884
- const result = await runScan(projectPath);
2885
- if (globalOpts.json) {
2886
- console.log(JSON.stringify(result));
2887
- } else {
2888
- console.log(
2889
- `Graph built: ${result.nodeCount} nodes, ${result.edgeCount} edges (${result.durationMs}ms)`
2890
- );
2891
- }
4341
+ const raw = fs18.readFileSync(manifestPath, "utf-8");
4342
+ parsed = parseYaml(raw);
2892
4343
  } catch (err) {
2893
- console.error("Scan failed:", err instanceof Error ? err.message : err);
2894
- process.exit(2);
4344
+ logger.error(
4345
+ `Failed to read ${MANIFEST_FILENAME}: ${err instanceof Error ? err.message : String(err)}`
4346
+ );
4347
+ process.exit(1);
4348
+ }
4349
+ const manifestResult = parseManifest(parsed);
4350
+ if (!manifestResult.ok) {
4351
+ logger.error(`Invalid ${MANIFEST_FILENAME}: ${manifestResult.error}`);
4352
+ process.exit(1);
4353
+ }
4354
+ const manifest = manifestResult.value;
4355
+ const configResult = resolveConfig(path37.join(rootDir, "harness.config.json"));
4356
+ if (!configResult.ok) {
4357
+ logger.error(configResult.error.message);
4358
+ process.exit(1);
2895
4359
  }
4360
+ const config = configResult.value;
4361
+ const bundleResult = extractBundle(manifest, config);
4362
+ if (!bundleResult.ok) {
4363
+ logger.error(`Failed to extract bundle: ${bundleResult.error}`);
4364
+ process.exit(1);
4365
+ }
4366
+ const bundle = bundleResult.value;
4367
+ if (Object.keys(bundle.constraints).length === 0) {
4368
+ logger.error(
4369
+ "No constraints found for the include paths in constraints.yaml.\nCheck that your harness config contains the declared sections."
4370
+ );
4371
+ process.exit(1);
4372
+ }
4373
+ const outputDir = path37.resolve(options.output);
4374
+ const outputPath = path37.join(outputDir, `${manifest.name}.harness-constraints.json`);
4375
+ const writeResult = await writeConfig(outputPath, bundle);
4376
+ if (!writeResult.ok) {
4377
+ logger.error(`Failed to write bundle: ${writeResult.error.message}`);
4378
+ process.exit(1);
4379
+ }
4380
+ logger.success(`Bundle written to ${outputPath}`);
2896
4381
  });
2897
4382
  }
2898
4383
 
2899
- // src/commands/graph/ingest.ts
2900
- import { Command as Command37 } from "commander";
2901
- import * as path24 from "path";
2902
- async function loadConnectorConfig(projectPath, source) {
4384
+ // src/commands/install.ts
4385
+ import * as fs20 from "fs";
4386
+ import * as path39 from "path";
4387
+ import { Command as Command48 } from "commander";
4388
+ import { parse as yamlParse } from "yaml";
4389
+
4390
+ // src/registry/tarball.ts
4391
+ import * as fs19 from "fs";
4392
+ import * as path38 from "path";
4393
+ import * as os3 from "os";
4394
+ import { execFileSync as execFileSync5 } from "child_process";
4395
+ function extractTarball(tarballBuffer) {
4396
+ const tmpDir = fs19.mkdtempSync(path38.join(os3.tmpdir(), "harness-skill-install-"));
4397
+ const tarballPath = path38.join(tmpDir, "package.tgz");
2903
4398
  try {
2904
- const fs11 = await import("fs/promises");
2905
- const configPath = path24.join(projectPath, "harness.config.json");
2906
- const config = JSON.parse(await fs11.readFile(configPath, "utf-8"));
2907
- const connector = config.graph?.connectors?.find(
2908
- (c) => c.source === source
4399
+ fs19.writeFileSync(tarballPath, tarballBuffer);
4400
+ execFileSync5("tar", ["-xzf", tarballPath, "-C", tmpDir], {
4401
+ timeout: 3e4
4402
+ });
4403
+ fs19.unlinkSync(tarballPath);
4404
+ } catch (err) {
4405
+ cleanupTempDir(tmpDir);
4406
+ throw new Error(
4407
+ `Failed to extract tarball: ${err instanceof Error ? err.message : String(err)}`,
4408
+ { cause: err }
2909
4409
  );
2910
- return connector?.config ?? {};
2911
- } catch {
2912
- return {};
2913
4410
  }
4411
+ return tmpDir;
2914
4412
  }
2915
- function mergeResults(...results) {
2916
- return results.reduce(
2917
- (acc, r) => ({
2918
- nodesAdded: acc.nodesAdded + r.nodesAdded,
2919
- nodesUpdated: acc.nodesUpdated + r.nodesUpdated,
2920
- edgesAdded: acc.edgesAdded + r.edgesAdded,
2921
- edgesUpdated: acc.edgesUpdated + r.edgesUpdated,
2922
- errors: [...acc.errors, ...r.errors],
2923
- durationMs: acc.durationMs + r.durationMs
2924
- }),
2925
- {
2926
- nodesAdded: 0,
2927
- nodesUpdated: 0,
2928
- edgesAdded: 0,
2929
- edgesUpdated: 0,
2930
- errors: [],
2931
- durationMs: 0
4413
+ function placeSkillContent(extractedPkgDir, communityBaseDir, skillName, platforms) {
4414
+ const files = fs19.readdirSync(extractedPkgDir);
4415
+ for (const platform of platforms) {
4416
+ const targetDir = path38.join(communityBaseDir, platform, skillName);
4417
+ if (fs19.existsSync(targetDir)) {
4418
+ fs19.rmSync(targetDir, { recursive: true, force: true });
2932
4419
  }
2933
- );
4420
+ fs19.mkdirSync(targetDir, { recursive: true });
4421
+ for (const file of files) {
4422
+ if (file === "package.json" || file === "node_modules") continue;
4423
+ const srcPath = path38.join(extractedPkgDir, file);
4424
+ const destPath = path38.join(targetDir, file);
4425
+ const stat = fs19.statSync(srcPath);
4426
+ if (stat.isDirectory()) {
4427
+ fs19.cpSync(srcPath, destPath, { recursive: true });
4428
+ } else {
4429
+ fs19.copyFileSync(srcPath, destPath);
4430
+ }
4431
+ }
4432
+ }
2934
4433
  }
2935
- async function runIngest(projectPath, source, opts) {
2936
- const {
2937
- GraphStore,
2938
- CodeIngestor,
2939
- TopologicalLinker,
2940
- KnowledgeIngestor,
2941
- GitIngestor,
2942
- SyncManager,
2943
- JiraConnector,
2944
- SlackConnector
2945
- } = await import("./dist-I7DB5VKB.js");
2946
- const graphDir = path24.join(projectPath, ".harness", "graph");
2947
- const store = new GraphStore();
2948
- await store.load(graphDir);
2949
- if (opts?.all) {
2950
- const startMs = Date.now();
2951
- const codeResult = await new CodeIngestor(store).ingest(projectPath);
2952
- new TopologicalLinker(store).link();
2953
- const knowledgeResult = await new KnowledgeIngestor(store).ingestAll(projectPath);
2954
- const gitResult = await new GitIngestor(store).ingest(projectPath);
2955
- const syncManager = new SyncManager(store, graphDir);
2956
- const connectorMap = {
2957
- jira: () => new JiraConnector(),
2958
- slack: () => new SlackConnector()
2959
- };
2960
- for (const [name, factory] of Object.entries(connectorMap)) {
2961
- const config = await loadConnectorConfig(projectPath, name);
2962
- syncManager.registerConnector(factory(), config);
4434
+ function removeSkillContent(communityBaseDir, skillName, platforms) {
4435
+ for (const platform of platforms) {
4436
+ const targetDir = path38.join(communityBaseDir, platform, skillName);
4437
+ if (fs19.existsSync(targetDir)) {
4438
+ fs19.rmSync(targetDir, { recursive: true, force: true });
2963
4439
  }
2964
- const connectorResult = await syncManager.syncAll();
2965
- await store.save(graphDir);
2966
- const merged = mergeResults(codeResult, knowledgeResult, gitResult, connectorResult);
2967
- return { ...merged, durationMs: Date.now() - startMs };
2968
4440
  }
2969
- let result;
2970
- switch (source) {
2971
- case "code":
2972
- result = await new CodeIngestor(store).ingest(projectPath);
2973
- new TopologicalLinker(store).link();
2974
- break;
2975
- case "knowledge":
2976
- result = await new KnowledgeIngestor(store).ingestAll(projectPath);
2977
- break;
2978
- case "git":
2979
- result = await new GitIngestor(store).ingest(projectPath);
2980
- break;
2981
- default: {
2982
- const knownConnectors = ["jira", "slack"];
2983
- if (!knownConnectors.includes(source)) {
2984
- throw new Error(`Unknown source: ${source}. Available: code, knowledge, git, jira, slack`);
2985
- }
2986
- if (!SyncManager) {
2987
- throw new Error(
2988
- `Connector support not available. Ensure @harness-engineering/graph is built with connector support.`
2989
- );
2990
- }
2991
- const syncManager = new SyncManager(store, graphDir);
2992
- const extConnectorMap = {
2993
- jira: () => new JiraConnector(),
2994
- slack: () => new SlackConnector()
2995
- };
2996
- const factory = extConnectorMap[source];
2997
- const config = await loadConnectorConfig(projectPath, source);
2998
- syncManager.registerConnector(factory(), config);
2999
- result = await syncManager.sync(source);
3000
- break;
4441
+ }
4442
+ function cleanupTempDir(dirPath) {
4443
+ try {
4444
+ fs19.rmSync(dirPath, { recursive: true, force: true });
4445
+ } catch {
4446
+ }
4447
+ }
4448
+
4449
+ // src/registry/resolver.ts
4450
+ import semver2 from "semver";
4451
+ function resolveVersion(metadata, versionRange) {
4452
+ const versions = Object.keys(metadata.versions);
4453
+ if (versions.length === 0) {
4454
+ throw new Error(`No versions available for ${metadata.name}.`);
4455
+ }
4456
+ if (!versionRange) {
4457
+ const latestTag = metadata["dist-tags"].latest;
4458
+ if (latestTag) {
4459
+ const latestInfo = metadata.versions[latestTag];
4460
+ if (latestInfo) return latestInfo;
3001
4461
  }
4462
+ const highest = semver2.maxSatisfying(versions, "*");
4463
+ if (!highest || !metadata.versions[highest]) {
4464
+ throw new Error(`No versions available for ${metadata.name}.`);
4465
+ }
4466
+ return metadata.versions[highest];
3002
4467
  }
3003
- await store.save(graphDir);
3004
- return result;
4468
+ const matched = semver2.maxSatisfying(versions, versionRange);
4469
+ if (!matched || !metadata.versions[matched]) {
4470
+ throw new Error(
4471
+ `No version of ${metadata.name} matches range ${versionRange}. Available: ${versions.join(", ")}`
4472
+ );
4473
+ }
4474
+ return metadata.versions[matched];
3005
4475
  }
3006
- function createIngestCommand() {
3007
- return new Command37("ingest").description("Ingest data into the knowledge graph").option("--source <name>", "Source to ingest (code, knowledge, git, jira, slack)").option("--all", "Run all sources (code, knowledge, git, and configured connectors)").option("--full", "Force full re-ingestion").action(async (opts, cmd) => {
3008
- if (!opts.source && !opts.all) {
3009
- console.error("Error: --source or --all is required");
3010
- process.exit(1);
4476
+ function findDependentsOf(lockfile, targetPackageName) {
4477
+ const dependents = [];
4478
+ const targetEntry = lockfile.skills[targetPackageName];
4479
+ if (targetEntry?.dependencyOf) {
4480
+ dependents.push(targetEntry.dependencyOf);
4481
+ }
4482
+ return dependents;
4483
+ }
4484
+
4485
+ // src/commands/install.ts
4486
+ function validateSkillYaml(parsed) {
4487
+ const result = SkillMetadataSchema.safeParse(parsed);
4488
+ if (!result.success) {
4489
+ const issues = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
4490
+ throw new Error(`contains invalid skill.yaml: ${issues}`);
4491
+ }
4492
+ return {
4493
+ name: result.data.name,
4494
+ version: result.data.version,
4495
+ platforms: result.data.platforms,
4496
+ depends_on: result.data.depends_on ?? []
4497
+ };
4498
+ }
4499
+ async function runLocalInstall(fromPath, options) {
4500
+ const resolvedPath = path39.resolve(fromPath);
4501
+ if (!fs20.existsSync(resolvedPath)) {
4502
+ throw new Error(`--from path does not exist: ${resolvedPath}`);
4503
+ }
4504
+ const stat = fs20.statSync(resolvedPath);
4505
+ let extractDir = null;
4506
+ let pkgDir;
4507
+ if (stat.isDirectory()) {
4508
+ pkgDir = resolvedPath;
4509
+ } else if (resolvedPath.endsWith(".tgz") || resolvedPath.endsWith(".tar.gz")) {
4510
+ const tarballBuffer = fs20.readFileSync(resolvedPath);
4511
+ extractDir = extractTarball(tarballBuffer);
4512
+ pkgDir = path39.join(extractDir, "package");
4513
+ } else {
4514
+ throw new Error(`--from path must be a directory or .tgz file. Got: ${resolvedPath}`);
4515
+ }
4516
+ try {
4517
+ const skillYamlPath = path39.join(pkgDir, "skill.yaml");
4518
+ if (!fs20.existsSync(skillYamlPath)) {
4519
+ throw new Error(`No skill.yaml found at ${skillYamlPath}`);
3011
4520
  }
3012
- const globalOpts = cmd.optsWithGlobals();
3013
- const projectPath = path24.resolve(globalOpts.config ? path24.dirname(globalOpts.config) : ".");
3014
- try {
3015
- const result = await runIngest(projectPath, opts.source ?? "", {
3016
- full: opts.full,
3017
- all: opts.all
3018
- });
3019
- if (globalOpts.json) {
3020
- console.log(JSON.stringify(result));
3021
- } else {
3022
- const label = opts.all ? "all" : opts.source;
3023
- console.log(
3024
- `Ingested (${label}): +${result.nodesAdded} nodes, +${result.edgesAdded} edges (${result.durationMs}ms)`
3025
- );
3026
- }
3027
- } catch (err) {
3028
- console.error("Ingest failed:", err instanceof Error ? err.message : err);
3029
- process.exit(2);
4521
+ const rawYaml = fs20.readFileSync(skillYamlPath, "utf-8");
4522
+ const parsed = yamlParse(rawYaml);
4523
+ const skillYaml = validateSkillYaml(parsed);
4524
+ const shortName = skillYaml.name;
4525
+ const globalDir = resolveGlobalSkillsDir();
4526
+ const skillsDir = path39.dirname(globalDir);
4527
+ const communityBase = path39.join(skillsDir, "community");
4528
+ const lockfilePath = path39.join(communityBase, "skills-lock.json");
4529
+ const bundledNames = getBundledSkillNames(globalDir);
4530
+ if (bundledNames.has(shortName)) {
4531
+ throw new Error(
4532
+ `'${shortName}' is a bundled skill and cannot be overridden by community installs.`
4533
+ );
4534
+ }
4535
+ placeSkillContent(pkgDir, communityBase, shortName, skillYaml.platforms);
4536
+ const packageName = `@harness-skills/${shortName}`;
4537
+ const lockfile = readLockfile2(lockfilePath);
4538
+ const entry = {
4539
+ version: skillYaml.version,
4540
+ resolved: `local:${resolvedPath}`,
4541
+ integrity: "",
4542
+ platforms: skillYaml.platforms,
4543
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
4544
+ dependencyOf: options._dependencyOf ?? null
4545
+ };
4546
+ const updatedLockfile = updateLockfileEntry(lockfile, packageName, entry);
4547
+ writeLockfile2(lockfilePath, updatedLockfile);
4548
+ return {
4549
+ installed: true,
4550
+ name: packageName,
4551
+ version: skillYaml.version
4552
+ };
4553
+ } finally {
4554
+ if (extractDir) {
4555
+ cleanupTempDir(extractDir);
4556
+ }
4557
+ }
4558
+ }
4559
+ async function runInstall(skillName, options) {
4560
+ if (options.from && options.registry) {
4561
+ throw new Error("--from and --registry cannot be used together");
4562
+ }
4563
+ if (options.from) {
4564
+ return runLocalInstall(options.from, options);
4565
+ }
4566
+ const packageName = resolvePackageName(skillName);
4567
+ const shortName = extractSkillName(packageName);
4568
+ const globalDir = resolveGlobalSkillsDir();
4569
+ const skillsDir = path39.dirname(globalDir);
4570
+ const communityBase = path39.join(skillsDir, "community");
4571
+ const lockfilePath = path39.join(communityBase, "skills-lock.json");
4572
+ const bundledNames = getBundledSkillNames(globalDir);
4573
+ if (bundledNames.has(shortName)) {
4574
+ throw new Error(
4575
+ `'${shortName}' is a bundled skill and cannot be overridden by community installs.`
4576
+ );
4577
+ }
4578
+ const metadata = await fetchPackageMetadata(packageName, options.registry);
4579
+ const versionInfo = resolveVersion(metadata, options.version);
4580
+ const resolvedVersion = versionInfo.version;
4581
+ const lockfile = readLockfile2(lockfilePath);
4582
+ const existingEntry = lockfile.skills[packageName];
4583
+ const previousVersion = existingEntry?.version;
4584
+ if (existingEntry && existingEntry.version === resolvedVersion && !options.force) {
4585
+ return {
4586
+ installed: false,
4587
+ skipped: true,
4588
+ name: packageName,
4589
+ version: resolvedVersion
4590
+ };
4591
+ }
4592
+ const authToken = options.registry ? readNpmrcToken(options.registry) ?? void 0 : void 0;
4593
+ const tarballBuffer = await downloadTarball(versionInfo.dist.tarball, authToken);
4594
+ const extractDir = extractTarball(tarballBuffer);
4595
+ let skillYaml;
4596
+ try {
4597
+ const extractedPkgDir = path39.join(extractDir, "package");
4598
+ const skillYamlPath = path39.join(extractedPkgDir, "skill.yaml");
4599
+ if (!fs20.existsSync(skillYamlPath)) {
4600
+ throw new Error(`contains invalid skill.yaml: file not found in package`);
3030
4601
  }
3031
- });
3032
- }
3033
-
3034
- // src/commands/graph/query.ts
3035
- import { Command as Command38 } from "commander";
3036
- import * as path25 from "path";
3037
- async function runQuery(projectPath, rootNodeId, opts) {
3038
- const { GraphStore, ContextQL } = await import("./dist-I7DB5VKB.js");
3039
- const store = new GraphStore();
3040
- const graphDir = path25.join(projectPath, ".harness", "graph");
3041
- const loaded = await store.load(graphDir);
3042
- if (!loaded) throw new Error("No graph found. Run `harness scan` first.");
3043
- const params = {
3044
- rootNodeIds: [rootNodeId],
3045
- maxDepth: opts.depth ?? 3,
3046
- bidirectional: opts.bidirectional ?? false,
3047
- ...opts.types ? { includeTypes: opts.types.split(",") } : {},
3048
- ...opts.edges ? { includeEdges: opts.edges.split(",") } : {}
4602
+ const rawYaml = fs20.readFileSync(skillYamlPath, "utf-8");
4603
+ const parsed = yamlParse(rawYaml);
4604
+ skillYaml = validateSkillYaml(parsed);
4605
+ placeSkillContent(extractedPkgDir, communityBase, shortName, skillYaml.platforms);
4606
+ } catch (err) {
4607
+ cleanupTempDir(extractDir);
4608
+ throw err;
4609
+ }
4610
+ cleanupTempDir(extractDir);
4611
+ const entry = {
4612
+ version: resolvedVersion,
4613
+ resolved: versionInfo.dist.tarball,
4614
+ integrity: versionInfo.dist.integrity,
4615
+ platforms: skillYaml.platforms,
4616
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
4617
+ dependencyOf: options._dependencyOf ?? null
3049
4618
  };
3050
- const cql = new ContextQL(store);
3051
- return cql.execute(params);
4619
+ let updatedLockfile = updateLockfileEntry(lockfile, packageName, entry);
4620
+ writeLockfile2(lockfilePath, updatedLockfile);
4621
+ const result = {
4622
+ installed: true,
4623
+ name: packageName,
4624
+ version: resolvedVersion
4625
+ };
4626
+ if (previousVersion && previousVersion !== resolvedVersion) {
4627
+ result.upgraded = true;
4628
+ result.previousVersion = previousVersion;
4629
+ }
4630
+ const deps = skillYaml.depends_on ?? [];
4631
+ for (const dep of deps) {
4632
+ logger.info(`Installing dependency: ${dep} (required by ${shortName})`);
4633
+ await runInstall(dep, {
4634
+ _dependencyOf: packageName,
4635
+ ...options.registry !== void 0 ? { registry: options.registry } : {}
4636
+ });
4637
+ }
4638
+ return result;
3052
4639
  }
3053
- function createQueryCommand() {
3054
- return new Command38("query").description("Query the knowledge graph").argument("<rootNodeId>", "Starting node ID").option("--depth <n>", "Max traversal depth", "3").option("--types <types>", "Comma-separated node types to include").option("--edges <edges>", "Comma-separated edge types to include").option("--bidirectional", "Traverse both directions").action(async (rootNodeId, opts, cmd) => {
3055
- const globalOpts = cmd.optsWithGlobals();
3056
- const projectPath = path25.resolve(globalOpts.config ? path25.dirname(globalOpts.config) : ".");
4640
+ function createInstallCommand() {
4641
+ const cmd = new Command48("install");
4642
+ cmd.description("Install a community skill from the @harness-skills registry").argument("<skill>", "Skill name or @harness-skills/scoped package name").option("--version <range>", "Semver range or exact version to install").option("--force", "Force reinstall even if same version is already installed").option("--from <path>", "Install from a local directory or .tgz file").option("--registry <url>", "Use a custom npm registry URL").action(async (skill, opts) => {
3057
4643
  try {
3058
- const result = await runQuery(projectPath, rootNodeId, {
3059
- depth: parseInt(opts.depth),
3060
- types: opts.types,
3061
- edges: opts.edges,
3062
- bidirectional: opts.bidirectional
3063
- });
3064
- if (globalOpts.json) {
3065
- console.log(JSON.stringify(result, null, 2));
3066
- } else {
3067
- console.log(
3068
- `Found ${result.nodes.length} nodes, ${result.edges.length} edges (depth ${result.stats.depthReached}, pruned ${result.stats.pruned})`
4644
+ const result = await runInstall(skill, opts);
4645
+ if (result.skipped) {
4646
+ logger.info(
4647
+ `${result.name}@${result.version} is already installed. Use --force to reinstall.`
3069
4648
  );
3070
- for (const node of result.nodes) {
3071
- console.log(` ${node.type.padEnd(12)} ${node.id}`);
3072
- }
4649
+ } else if (result.upgraded) {
4650
+ logger.success(
4651
+ `Upgraded ${result.name} from ${result.previousVersion} to ${result.version}`
4652
+ );
4653
+ } else {
4654
+ logger.success(`Installed ${result.name}@${result.version}`);
3073
4655
  }
3074
4656
  } catch (err) {
3075
- console.error("Query failed:", err instanceof Error ? err.message : err);
3076
- process.exit(2);
4657
+ logger.error(err instanceof Error ? err.message : String(err));
4658
+ process.exit(1);
3077
4659
  }
3078
4660
  });
4661
+ return cmd;
3079
4662
  }
3080
4663
 
3081
- // src/commands/graph/index.ts
3082
- import { Command as Command39 } from "commander";
3083
-
3084
- // src/commands/graph/status.ts
3085
- import * as path26 from "path";
3086
- async function runGraphStatus(projectPath) {
3087
- const { GraphStore } = await import("./dist-I7DB5VKB.js");
3088
- const graphDir = path26.join(projectPath, ".harness", "graph");
3089
- const store = new GraphStore();
3090
- const loaded = await store.load(graphDir);
3091
- if (!loaded) return { status: "no_graph", message: "No graph found. Run `harness scan` first." };
3092
- const fs11 = await import("fs/promises");
3093
- const metaPath = path26.join(graphDir, "metadata.json");
3094
- let lastScan = "unknown";
4664
+ // src/commands/install-constraints.ts
4665
+ import * as fs21 from "fs/promises";
4666
+ import * as path40 from "path";
4667
+ import { Command as Command49 } from "commander";
4668
+ import semver3 from "semver";
4669
+ async function runInstallConstraints(options) {
4670
+ const { source, configPath, lockfilePath } = options;
4671
+ let rawBundle;
3095
4672
  try {
3096
- const meta = JSON.parse(await fs11.readFile(metaPath, "utf-8"));
3097
- lastScan = meta.lastScanTimestamp;
4673
+ rawBundle = await fs21.readFile(source, "utf-8");
4674
+ } catch (err) {
4675
+ if (isNodeError(err) && err.code === "ENOENT") {
4676
+ return { ok: false, error: `Bundle file not found: ${source}` };
4677
+ }
4678
+ return {
4679
+ ok: false,
4680
+ error: `Failed to read bundle: ${err instanceof Error ? err.message : String(err)}`
4681
+ };
4682
+ }
4683
+ let parsedJson;
4684
+ try {
4685
+ parsedJson = JSON.parse(rawBundle);
3098
4686
  } catch {
4687
+ return { ok: false, error: `Bundle file contains invalid JSON: ${source}` };
3099
4688
  }
3100
- const allNodes = store.findNodes({});
3101
- const nodesByType = {};
3102
- for (const node of allNodes) {
3103
- nodesByType[node.type] = (nodesByType[node.type] ?? 0) + 1;
4689
+ const bundleResult = BundleSchema.safeParse(parsedJson);
4690
+ if (!bundleResult.success) {
4691
+ const issues = bundleResult.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
4692
+ return { ok: false, error: `Bundle schema validation failed: ${issues}` };
3104
4693
  }
3105
- let connectorSyncStatus = {};
4694
+ const bundle = bundleResult.data;
4695
+ if (bundle.minHarnessVersion) {
4696
+ const installed = semver3.valid(semver3.coerce(CLI_VERSION));
4697
+ const required = semver3.valid(semver3.coerce(bundle.minHarnessVersion));
4698
+ if (installed && required && semver3.lt(installed, required)) {
4699
+ return {
4700
+ ok: false,
4701
+ error: `Bundle requires harness version >= ${bundle.minHarnessVersion}, but installed version is ${CLI_VERSION}. Please upgrade.`
4702
+ };
4703
+ }
4704
+ }
4705
+ const constraintKeys = Object.keys(bundle.constraints).filter(
4706
+ (k) => bundle.constraints[k] !== void 0
4707
+ );
4708
+ if (constraintKeys.length === 0) {
4709
+ return {
4710
+ ok: false,
4711
+ error: "Bundle contains no constraints. Nothing to install."
4712
+ };
4713
+ }
4714
+ let localConfig;
3106
4715
  try {
3107
- const syncMetaPath = path26.join(graphDir, "sync-metadata.json");
3108
- const syncMeta = JSON.parse(await fs11.readFile(syncMetaPath, "utf-8"));
3109
- for (const [name, data] of Object.entries(syncMeta.connectors ?? {})) {
3110
- connectorSyncStatus[name] = data.lastSyncTimestamp;
4716
+ const raw = await fs21.readFile(configPath, "utf-8");
4717
+ localConfig = JSON.parse(raw);
4718
+ } catch (err) {
4719
+ return {
4720
+ ok: false,
4721
+ error: `Failed to read local config at ${configPath}: ${err instanceof Error ? err.message : String(err)}`
4722
+ };
4723
+ }
4724
+ const lockfileResult = await readLockfile(lockfilePath);
4725
+ if (!lockfileResult.ok) {
4726
+ return { ok: false, error: lockfileResult.error };
4727
+ }
4728
+ const existingLockfile = lockfileResult.value ?? {
4729
+ version: 1,
4730
+ packages: {}
4731
+ };
4732
+ const existingEntry = existingLockfile.packages[bundle.name];
4733
+ if (existingEntry && existingEntry.version === bundle.version) {
4734
+ return {
4735
+ ok: true,
4736
+ value: {
4737
+ installed: false,
4738
+ packageName: bundle.name,
4739
+ version: bundle.version,
4740
+ contributionsCount: 0,
4741
+ conflicts: [],
4742
+ alreadyInstalled: true
4743
+ }
4744
+ };
4745
+ }
4746
+ if (existingEntry) {
4747
+ const oldContributions = existingEntry.contributions ?? {};
4748
+ localConfig = removeContributions(localConfig, oldContributions);
4749
+ }
4750
+ const mergeResult = deepMergeConstraints(localConfig, bundle.constraints);
4751
+ if (mergeResult.conflicts.length > 0) {
4752
+ if (options.forceLocal) {
4753
+ } else if (options.forcePackage) {
4754
+ for (const conflict of mergeResult.conflicts) {
4755
+ applyPackageValue(mergeResult.config, conflict);
4756
+ addConflictContribution(mergeResult.contributions, conflict);
4757
+ }
4758
+ } else if (!options.dryRun) {
4759
+ return {
4760
+ ok: false,
4761
+ error: formatConflictsError(mergeResult.conflicts)
4762
+ };
3111
4763
  }
3112
- } catch {
4764
+ }
4765
+ if (options.dryRun) {
4766
+ return {
4767
+ ok: true,
4768
+ value: {
4769
+ installed: false,
4770
+ packageName: bundle.name,
4771
+ version: bundle.version,
4772
+ contributionsCount: Object.keys(mergeResult.contributions).length,
4773
+ conflicts: mergeResult.conflicts,
4774
+ dryRun: true
4775
+ }
4776
+ };
4777
+ }
4778
+ const writeResult = await writeConfig(configPath, mergeResult.config);
4779
+ if (!writeResult.ok) {
4780
+ return {
4781
+ ok: false,
4782
+ error: `Failed to write config: ${writeResult.error instanceof Error ? writeResult.error.message : String(writeResult.error)}`
4783
+ };
4784
+ }
4785
+ const lockfileEntry = {
4786
+ version: bundle.version,
4787
+ source,
4788
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
4789
+ contributions: mergeResult.contributions
4790
+ };
4791
+ const updatedLockfile = addProvenance(existingLockfile, bundle.name, lockfileEntry);
4792
+ const lockfileWriteResult = await writeLockfile(lockfilePath, updatedLockfile);
4793
+ if (!lockfileWriteResult.ok) {
4794
+ return {
4795
+ ok: false,
4796
+ error: `Config was written but lockfile write failed: ${lockfileWriteResult.error.message}. Lockfile may be out of sync.`
4797
+ };
3113
4798
  }
3114
4799
  return {
3115
- status: "ok",
3116
- nodeCount: store.nodeCount,
3117
- edgeCount: store.edgeCount,
3118
- nodesByType,
3119
- lastScanTimestamp: lastScan,
3120
- ...Object.keys(connectorSyncStatus).length > 0 ? { connectorSyncStatus } : {}
4800
+ ok: true,
4801
+ value: {
4802
+ installed: true,
4803
+ packageName: bundle.name,
4804
+ version: bundle.version,
4805
+ contributionsCount: Object.keys(mergeResult.contributions).length,
4806
+ conflicts: mergeResult.conflicts
4807
+ }
3121
4808
  };
3122
4809
  }
3123
-
3124
- // src/commands/graph/export.ts
3125
- import * as path27 from "path";
3126
- async function runGraphExport(projectPath, format) {
3127
- const { GraphStore } = await import("./dist-I7DB5VKB.js");
3128
- const graphDir = path27.join(projectPath, ".harness", "graph");
3129
- const store = new GraphStore();
3130
- const loaded = await store.load(graphDir);
3131
- if (!loaded) throw new Error("No graph found. Run `harness scan` first.");
3132
- if (format === "json") {
3133
- const nodes = store.findNodes({});
3134
- const edges = store.getEdges({});
3135
- return JSON.stringify({ nodes, edges }, null, 2);
3136
- }
3137
- if (format === "mermaid") {
3138
- const nodes = store.findNodes({});
3139
- const edges = store.getEdges({});
3140
- const lines = ["graph TD"];
3141
- for (const node of nodes.slice(0, 200)) {
3142
- const safeId = node.id.replace(/[^a-zA-Z0-9]/g, "_");
3143
- const safeName = node.name.replace(/"/g, "#quot;");
3144
- lines.push(` ${safeId}["${safeName}"]`);
4810
+ var sectionAppliers = {
4811
+ layers(config, key, value) {
4812
+ const layers = config.layers;
4813
+ const idx = layers.findIndex((l) => l.name === key);
4814
+ if (idx >= 0) layers[idx] = value;
4815
+ },
4816
+ forbiddenImports(config, key, value) {
4817
+ const rules = config.forbiddenImports;
4818
+ const idx = rules.findIndex((r) => r.from === key);
4819
+ if (idx >= 0) rules[idx] = value;
4820
+ },
4821
+ "architecture.thresholds"(config, key, value) {
4822
+ const arch = config.architecture;
4823
+ if (arch?.thresholds) arch.thresholds[key] = value;
4824
+ },
4825
+ "architecture.modules"(config, key, value) {
4826
+ const arch = config.architecture;
4827
+ const [modulePath, category] = key.split(":");
4828
+ if (arch?.modules && modulePath && category && arch.modules[modulePath]) {
4829
+ arch.modules[modulePath][category] = value;
3145
4830
  }
3146
- for (const edge of edges.slice(0, 500)) {
3147
- const safeFrom = edge.from.replace(/[^a-zA-Z0-9]/g, "_");
3148
- const safeTo = edge.to.replace(/[^a-zA-Z0-9]/g, "_");
3149
- lines.push(` ${safeFrom} -->|${edge.type}| ${safeTo}`);
4831
+ },
4832
+ "security.rules"(config, key, value) {
4833
+ const security = config.security;
4834
+ if (security?.rules) security.rules[key] = value;
4835
+ }
4836
+ };
4837
+ function applyPackageValue(config, conflict) {
4838
+ const applier = sectionAppliers[conflict.section];
4839
+ if (applier) applier(config, conflict.key, conflict.packageValue);
4840
+ }
4841
+ function addConflictContribution(contributions, conflict) {
4842
+ const section = conflict.section;
4843
+ const existing = contributions[section] ?? [];
4844
+ existing.push(conflict.key);
4845
+ contributions[section] = existing;
4846
+ }
4847
+ function formatConflictsError(conflicts) {
4848
+ const lines = [
4849
+ `${conflicts.length} conflict(s) detected. Resolve with --force-local or --force-package:`,
4850
+ ""
4851
+ ];
4852
+ for (const c of conflicts) {
4853
+ lines.push(` [${c.section}] ${c.key}: ${c.description}`);
4854
+ lines.push(` Local: ${JSON.stringify(c.localValue)}`);
4855
+ lines.push(` Package: ${JSON.stringify(c.packageValue)}`);
4856
+ lines.push("");
4857
+ }
4858
+ return lines.join("\n");
4859
+ }
4860
+ function isNodeError(err) {
4861
+ return err instanceof Error && "code" in err;
4862
+ }
4863
+ function resolveConfigPath(opts) {
4864
+ if (opts.config) return path40.resolve(opts.config);
4865
+ const found = findConfigFile();
4866
+ if (!found.ok) {
4867
+ logger.error(found.error.message);
4868
+ process.exit(1);
4869
+ }
4870
+ return found.value;
4871
+ }
4872
+ function logInstallResult(val, opts) {
4873
+ if (val.dryRun) {
4874
+ logger.info(`[dry-run] Would install ${val.packageName}@${val.version}`);
4875
+ logger.info(`[dry-run] ${val.contributionsCount} section(s) would be added`);
4876
+ if (val.conflicts.length > 0) {
4877
+ logger.warn(`[dry-run] ${val.conflicts.length} conflict(s) detected`);
4878
+ for (const c of val.conflicts) {
4879
+ logger.warn(` [${c.section}] ${c.key}: ${c.description}`);
4880
+ }
3150
4881
  }
3151
- return lines.join("\n");
4882
+ return;
3152
4883
  }
3153
- throw new Error(`Unknown format: ${format}. Available: json, mermaid`);
4884
+ if (val.alreadyInstalled) {
4885
+ logger.info(`${val.packageName}@${val.version} is already installed. No changes made.`);
4886
+ return;
4887
+ }
4888
+ logger.success(
4889
+ `Installed ${val.packageName}@${val.version} (${val.contributionsCount} section(s) merged)`
4890
+ );
4891
+ if (val.conflicts.length > 0) {
4892
+ logger.warn(
4893
+ `${val.conflicts.length} conflict(s) resolved with ${opts.forceLocal ? "--force-local" : "--force-package"}`
4894
+ );
4895
+ }
4896
+ }
4897
+ async function handleInstallConstraints(source, opts) {
4898
+ const configPath = resolveConfigPath(opts);
4899
+ const projectRoot = path40.dirname(configPath);
4900
+ const lockfilePath = path40.join(projectRoot, ".harness", "constraints.lock.json");
4901
+ const resolvedSource = path40.resolve(source);
4902
+ if (opts.forceLocal && opts.forcePackage) {
4903
+ logger.error("Cannot use both --force-local and --force-package.");
4904
+ process.exit(1);
4905
+ }
4906
+ const result = await runInstallConstraints({
4907
+ source: resolvedSource,
4908
+ configPath,
4909
+ lockfilePath,
4910
+ ...opts.forceLocal && { forceLocal: true },
4911
+ ...opts.forcePackage && { forcePackage: true },
4912
+ ...opts.dryRun && { dryRun: true }
4913
+ });
4914
+ if (!result.ok) {
4915
+ logger.error(result.error);
4916
+ process.exit(1);
4917
+ }
4918
+ logInstallResult(result.value, opts);
4919
+ }
4920
+ function createInstallConstraintsCommand() {
4921
+ const cmd = new Command49("install-constraints");
4922
+ cmd.description("Install a constraints bundle into the local harness config").argument("<source>", "Path to a .harness-constraints.json bundle file").option("--force-local", "Resolve all conflicts by keeping local values").option("--force-package", "Resolve all conflicts by using package values").option("--dry-run", "Show what would change without writing files").option("-c, --config <path>", "Path to harness.config.json").action(handleInstallConstraints);
4923
+ return cmd;
3154
4924
  }
3155
4925
 
3156
- // src/commands/graph/index.ts
3157
- import * as path28 from "path";
3158
- function createGraphCommand() {
3159
- const graph = new Command39("graph").description("Knowledge graph management");
3160
- graph.command("status").description("Show graph statistics").action(async (_opts, cmd) => {
3161
- try {
3162
- const globalOpts = cmd.optsWithGlobals();
3163
- const projectPath = path28.resolve(globalOpts.config ? path28.dirname(globalOpts.config) : ".");
3164
- const result = await runGraphStatus(projectPath);
3165
- if (globalOpts.json) {
3166
- console.log(JSON.stringify(result, null, 2));
3167
- } else if (result.status === "no_graph") {
3168
- console.log(result.message);
3169
- } else {
3170
- console.log(`Graph: ${result.nodeCount} nodes, ${result.edgeCount} edges`);
3171
- console.log(`Last scan: ${result.lastScanTimestamp}`);
3172
- console.log("Nodes by type:");
3173
- for (const [type, count] of Object.entries(result.nodesByType)) {
3174
- console.log(` ${type}: ${count}`);
3175
- }
3176
- if (result.connectorSyncStatus) {
3177
- console.log("Connector sync status:");
3178
- for (const [name, timestamp] of Object.entries(result.connectorSyncStatus)) {
3179
- console.log(` ${name}: last synced ${timestamp}`);
3180
- }
3181
- }
4926
+ // src/commands/uninstall-constraints.ts
4927
+ import * as fs22 from "fs/promises";
4928
+ import * as path41 from "path";
4929
+ import { Command as Command50 } from "commander";
4930
+ async function runUninstallConstraints(options) {
4931
+ const { packageName, configPath, lockfilePath } = options;
4932
+ const lockfileResult = await readLockfile(lockfilePath);
4933
+ if (!lockfileResult.ok) {
4934
+ return { ok: false, error: lockfileResult.error };
4935
+ }
4936
+ if (lockfileResult.value === null) {
4937
+ return { ok: false, error: "No lockfile found. No constraint packages are installed." };
4938
+ }
4939
+ const lockfile = lockfileResult.value;
4940
+ const entry = lockfile.packages[packageName];
4941
+ if (!entry) {
4942
+ return {
4943
+ ok: false,
4944
+ error: `Package '${packageName}' is not installed.`
4945
+ };
4946
+ }
4947
+ let localConfig;
4948
+ try {
4949
+ const raw = await fs22.readFile(configPath, "utf-8");
4950
+ localConfig = JSON.parse(raw);
4951
+ } catch (err) {
4952
+ return {
4953
+ ok: false,
4954
+ error: `Failed to read local config at ${configPath}: ${err instanceof Error ? err.message : String(err)}`
4955
+ };
4956
+ }
4957
+ const contributions = entry.contributions ?? {};
4958
+ const sectionsRemoved = Object.keys(contributions);
4959
+ const updatedConfig = removeContributions(localConfig, contributions);
4960
+ const { lockfile: updatedLockfile } = removeProvenance(lockfile, packageName);
4961
+ const writeResult = await writeConfig(configPath, updatedConfig);
4962
+ if (!writeResult.ok) {
4963
+ return {
4964
+ ok: false,
4965
+ error: `Failed to write config: ${writeResult.error instanceof Error ? writeResult.error.message : String(writeResult.error)}`
4966
+ };
4967
+ }
4968
+ const lockfileWriteResult = await writeLockfile(lockfilePath, updatedLockfile);
4969
+ if (!lockfileWriteResult.ok) {
4970
+ return {
4971
+ ok: false,
4972
+ error: `Config was written but lockfile write failed: ${lockfileWriteResult.error.message}. Lockfile may be out of sync.`
4973
+ };
4974
+ }
4975
+ return {
4976
+ ok: true,
4977
+ value: {
4978
+ removed: true,
4979
+ packageName,
4980
+ version: entry.version,
4981
+ sectionsRemoved
4982
+ }
4983
+ };
4984
+ }
4985
+ function createUninstallConstraintsCommand() {
4986
+ const cmd = new Command50("uninstall-constraints");
4987
+ cmd.description("Remove a previously installed constraints package").argument("<name>", "Name of the constraint package to uninstall").option("-c, --config <path>", "Path to harness.config.json").action(async (name, opts) => {
4988
+ let configPath;
4989
+ if (opts.config) {
4990
+ configPath = path41.resolve(opts.config);
4991
+ } else {
4992
+ const found = findConfigFile();
4993
+ if (!found.ok) {
4994
+ logger.error(found.error.message);
4995
+ process.exit(1);
3182
4996
  }
3183
- } catch (err) {
3184
- console.error("Status failed:", err instanceof Error ? err.message : err);
3185
- process.exit(2);
4997
+ configPath = found.value;
4998
+ }
4999
+ const projectRoot = path41.dirname(configPath);
5000
+ const lockfilePath = path41.join(projectRoot, ".harness", "constraints.lock.json");
5001
+ const result = await runUninstallConstraints({
5002
+ packageName: name,
5003
+ configPath,
5004
+ lockfilePath
5005
+ });
5006
+ if (!result.ok) {
5007
+ logger.error(result.error);
5008
+ process.exit(1);
5009
+ }
5010
+ const val = result.value;
5011
+ if (val.sectionsRemoved.length === 0) {
5012
+ logger.success(
5013
+ `Removed ${val.packageName}@${val.version} (no contributed rules to remove)`
5014
+ );
5015
+ } else {
5016
+ logger.success(
5017
+ `Removed ${val.packageName}@${val.version} (${val.sectionsRemoved.length} section(s): ${val.sectionsRemoved.join(", ")})`
5018
+ );
3186
5019
  }
3187
5020
  });
3188
- graph.command("export").description("Export graph").requiredOption("--format <format>", "Output format (json, mermaid)").action(async (opts, cmd) => {
3189
- const globalOpts = cmd.optsWithGlobals();
3190
- const projectPath = path28.resolve(globalOpts.config ? path28.dirname(globalOpts.config) : ".");
5021
+ return cmd;
5022
+ }
5023
+
5024
+ // src/commands/uninstall.ts
5025
+ import * as path42 from "path";
5026
+ import { Command as Command51 } from "commander";
5027
+ async function runUninstall(skillName, options) {
5028
+ const packageName = resolvePackageName(skillName);
5029
+ const shortName = extractSkillName(packageName);
5030
+ const globalDir = resolveGlobalSkillsDir();
5031
+ const skillsDir = path42.dirname(globalDir);
5032
+ const communityBase = path42.join(skillsDir, "community");
5033
+ const lockfilePath = path42.join(communityBase, "skills-lock.json");
5034
+ const lockfile = readLockfile2(lockfilePath);
5035
+ const entry = lockfile.skills[packageName];
5036
+ if (!entry) {
5037
+ throw new Error(`Skill '${shortName}' is not installed.`);
5038
+ }
5039
+ const dependents = findDependentsOf(lockfile, packageName);
5040
+ const warnings = [];
5041
+ if (dependents.length > 0) {
5042
+ if (!options.force) {
5043
+ throw new Error(
5044
+ `Cannot uninstall '${shortName}' because it is required by: ${dependents.join(", ")}. Use --force to remove anyway.`
5045
+ );
5046
+ }
5047
+ warnings.push(`Forced removal despite dependents: ${dependents.join(", ")}`);
5048
+ }
5049
+ removeSkillContent(communityBase, shortName, entry.platforms);
5050
+ const updatedLockfile = removeLockfileEntry(lockfile, packageName);
5051
+ writeLockfile2(lockfilePath, updatedLockfile);
5052
+ const result = {
5053
+ removed: true,
5054
+ name: packageName,
5055
+ version: entry.version
5056
+ };
5057
+ if (warnings.length > 0) {
5058
+ result.warnings = warnings;
5059
+ }
5060
+ return result;
5061
+ }
5062
+ function createUninstallCommand() {
5063
+ const cmd = new Command51("uninstall");
5064
+ cmd.description("Uninstall a community skill").argument("<skill>", "Skill name or @harness-skills/scoped package name").option("--force", "Remove even if other skills depend on this one").action(async (skill, opts) => {
3191
5065
  try {
3192
- const output = await runGraphExport(projectPath, opts.format);
3193
- console.log(output);
5066
+ const result = await runUninstall(skill, opts);
5067
+ if (result.warnings) {
5068
+ for (const warning of result.warnings) {
5069
+ logger.warn(warning);
5070
+ }
5071
+ }
5072
+ logger.success(`Uninstalled ${result.name}@${result.version}`);
3194
5073
  } catch (err) {
3195
- console.error("Export failed:", err instanceof Error ? err.message : err);
3196
- process.exit(2);
5074
+ logger.error(err instanceof Error ? err.message : String(err));
5075
+ process.exit(1);
3197
5076
  }
3198
5077
  });
3199
- return graph;
5078
+ return cmd;
3200
5079
  }
3201
5080
 
3202
- // src/commands/mcp.ts
3203
- import { Command as Command40 } from "commander";
3204
- function createMcpCommand() {
3205
- return new Command40("mcp").description("Start the MCP (Model Context Protocol) server on stdio").action(async () => {
3206
- const { startServer: startServer2 } = await import("./mcp-BNLBTCXZ.js");
3207
- await startServer2();
5081
+ // src/commands/orchestrator.ts
5082
+ import { Command as Command52 } from "commander";
5083
+ import * as path43 from "path";
5084
+ import { Orchestrator, WorkflowLoader, launchTUI } from "@harness-engineering/orchestrator";
5085
+ function createOrchestratorCommand() {
5086
+ const orchestrator = new Command52("orchestrator");
5087
+ orchestrator.command("run").description("Run the orchestrator daemon").option("-w, --workflow <path>", "Path to WORKFLOW.md", "WORKFLOW.md").action(async (opts) => {
5088
+ const workflowPath = path43.resolve(process.cwd(), opts.workflow);
5089
+ const loader = new WorkflowLoader();
5090
+ const result = await loader.loadWorkflow(workflowPath);
5091
+ if (!result.ok) {
5092
+ logger.error(`Failed to load workflow: ${result.error.message}`);
5093
+ process.exit(ExitCode.ERROR);
5094
+ }
5095
+ const { config, promptTemplate } = result.value;
5096
+ const daemon = new Orchestrator(config, promptTemplate);
5097
+ const shutdown = () => {
5098
+ daemon.stop();
5099
+ process.exit(ExitCode.SUCCESS);
5100
+ };
5101
+ process.on("SIGINT", shutdown);
5102
+ process.on("SIGTERM", shutdown);
5103
+ daemon.start();
5104
+ const { waitUntilExit } = launchTUI(daemon);
5105
+ await waitUntilExit();
5106
+ process.exit(ExitCode.SUCCESS);
3208
5107
  });
5108
+ return orchestrator;
3209
5109
  }
3210
5110
 
3211
5111
  // src/index.ts
3212
5112
  function createProgram() {
3213
- const program = new Command41();
5113
+ const program = new Command53();
3214
5114
  program.name("harness").description("CLI for Harness Engineering toolkit").version(CLI_VERSION).option("-c, --config <path>", "Path to config file").option("--json", "Output as JSON").option("--verbose", "Verbose output").option("--quiet", "Minimal output");
3215
5115
  program.addCommand(createValidateCommand());
3216
5116
  program.addCommand(createCheckDepsCommand());
@@ -3240,6 +5140,15 @@ function createProgram() {
3240
5140
  program.addCommand(createQueryCommand());
3241
5141
  program.addCommand(createGraphCommand());
3242
5142
  program.addCommand(createMcpCommand());
5143
+ program.addCommand(createImpactPreviewCommand());
5144
+ program.addCommand(createCheckArchCommand());
5145
+ program.addCommand(createBlueprintCommand());
5146
+ program.addCommand(createShareCommand());
5147
+ program.addCommand(createInstallCommand());
5148
+ program.addCommand(createInstallConstraintsCommand());
5149
+ program.addCommand(createUninstallConstraintsCommand());
5150
+ program.addCommand(createUninstallCommand());
5151
+ program.addCommand(createOrchestratorCommand());
3243
5152
  return program;
3244
5153
  }
3245
5154
 
@@ -3250,5 +5159,11 @@ export {
3250
5159
  runQuery,
3251
5160
  runGraphStatus,
3252
5161
  runGraphExport,
5162
+ runImpactPreview,
5163
+ runCheckArch,
5164
+ runInstall,
5165
+ runInstallConstraints,
5166
+ runUninstallConstraints,
5167
+ runUninstall,
3253
5168
  createProgram
3254
5169
  };