@harness-engineering/cli 1.13.1 → 1.15.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.
- package/dist/agents/skills/claude-code/harness-autopilot/SKILL.md +240 -39
- package/dist/agents/skills/claude-code/harness-autopilot/skill.yaml +6 -0
- package/dist/agents/skills/claude-code/harness-brainstorming/SKILL.md +39 -0
- package/dist/agents/skills/claude-code/harness-code-review/SKILL.md +44 -0
- package/dist/agents/skills/claude-code/harness-execution/SKILL.md +44 -0
- package/dist/agents/skills/claude-code/harness-planning/SKILL.md +39 -0
- package/dist/agents/skills/claude-code/harness-product-spec/SKILL.md +5 -5
- package/dist/agents/skills/claude-code/harness-release-readiness/SKILL.md +3 -3
- package/dist/agents/skills/claude-code/harness-verification/SKILL.md +35 -0
- package/dist/agents/skills/claude-code/initialize-harness-project/SKILL.md +11 -3
- package/dist/agents/skills/gemini-cli/harness-autopilot/SKILL.md +240 -39
- package/dist/agents/skills/gemini-cli/harness-autopilot/skill.yaml +6 -0
- package/dist/agents/skills/gemini-cli/harness-brainstorming/SKILL.md +39 -0
- package/dist/agents/skills/gemini-cli/harness-code-review/SKILL.md +44 -0
- package/dist/agents/skills/gemini-cli/harness-execution/SKILL.md +44 -0
- package/dist/agents/skills/gemini-cli/harness-planning/SKILL.md +39 -0
- package/dist/agents/skills/gemini-cli/harness-product-spec/SKILL.md +5 -5
- package/dist/agents/skills/gemini-cli/harness-release-readiness/SKILL.md +3 -3
- package/dist/agents/skills/gemini-cli/harness-verification/SKILL.md +35 -0
- package/dist/agents/skills/gemini-cli/initialize-harness-project/SKILL.md +11 -3
- package/dist/agents/skills/package.json +1 -0
- package/dist/agents/skills/vitest.config.mts +5 -0
- package/dist/agents-md-ZGNIDWAF.js +8 -0
- package/dist/{architecture-2R5Z4ZAF.js → architecture-ZLIH5533.js} +4 -4
- package/dist/bin/harness-mcp.js +14 -14
- package/dist/bin/harness.js +27 -25
- package/dist/{check-phase-gate-2OFZ7OWW.js → check-phase-gate-ZOXVBDCN.js} +4 -4
- package/dist/{chunk-ND6PNADU.js → chunk-2BKLWLY6.js} +9 -9
- package/dist/{chunk-65FRIL4D.js → chunk-3ZZKVN62.js} +1 -1
- package/dist/{chunk-C2ERUR3L.js → chunk-7MJAPE3Z.js} +165 -49
- package/dist/{chunk-Z77YQRQT.js → chunk-B2HKP423.js} +16 -5
- package/dist/{chunk-QPEH2QPG.js → chunk-DBSOCI3G.js} +53 -54
- package/dist/{chunk-TKJZKICB.js → chunk-EDXIVMAP.js} +7 -7
- package/dist/{chunk-MHBMTPW7.js → chunk-ERS5EVUZ.js} +9 -0
- package/dist/{chunk-JSTQ3AWB.js → chunk-FIAPHX37.js} +1 -1
- package/dist/{chunk-IMFVFNJE.js → chunk-FTMXDOR6.js} +1 -1
- package/dist/{chunk-72GHBOL2.js → chunk-GZKSBLQL.js} +1 -1
- package/dist/{chunk-K6XAPGML.js → chunk-H7Y5CKTM.js} +1 -1
- package/dist/{chunk-SSKDAOX5.js → chunk-J4RAX7YB.js} +1164 -516
- package/dist/{chunk-UAX4I5ZE.js → chunk-LGYBN7Y6.js} +2 -2
- package/dist/{chunk-QY4T6YAZ.js → chunk-N25INEIX.js} +4 -4
- package/dist/{chunk-4ZMOCPYO.js → chunk-ND2ENWDM.js} +1 -1
- package/dist/{chunk-NERR4TAO.js → chunk-NNHDDXYT.js} +1250 -765
- package/dist/{chunk-NKDM3FMH.js → chunk-OD3S2NHN.js} +1 -1
- package/dist/{chunk-NOPU4RZ4.js → chunk-OFXQSFOW.js} +3 -3
- package/dist/{chunk-TS3XWPW5.js → chunk-RCWZBSK5.js} +1 -1
- package/dist/{chunk-VUCPTQ6G.js → chunk-SD3SQOZ2.js} +1 -1
- package/dist/{chunk-DZS7CJKL.js → chunk-VEPAJXBW.js} +45 -47
- package/dist/{chunk-IM32EEDM.js → chunk-YLXFKVJE.js} +9 -9
- package/dist/{chunk-Q6AB7W5Z.js → chunk-YQ6KC6TE.js} +1 -1
- package/dist/{chunk-PQ5YK4AY.js → chunk-Z2OOPXJO.js} +2740 -1221
- package/dist/ci-workflow-765LSHRD.js +8 -0
- package/dist/{dist-2B363XUH.js → dist-ALQDD67R.js} +64 -2
- package/dist/{dist-HXHWB7SV.js → dist-B26DFXMP.js} +571 -478
- package/dist/{dist-L7LAAQAS.js → dist-DZ63LLUD.js} +1 -1
- package/dist/{dist-D4RYGUZE.js → dist-USY2C5JL.js} +3 -1
- package/dist/{docs-FZOPM4GK.js → docs-NRMQCOJ6.js} +4 -4
- package/dist/engine-3RB7MXPP.js +8 -0
- package/dist/{entropy-LVHJMFGH.js → entropy-6AGX2ZUN.js} +3 -3
- package/dist/{feedback-IHLVLMRD.js → feedback-MY4QZIFD.js} +1 -1
- package/dist/{generate-agent-definitions-64S3CLEZ.js → generate-agent-definitions-ZAE726AU.js} +4 -4
- package/dist/{graph-loader-GJZ4FN4Y.js → graph-loader-2M2HXDQI.js} +1 -1
- package/dist/index.d.ts +156 -17
- package/dist/index.js +24 -24
- package/dist/loader-UUTVMQCC.js +10 -0
- package/dist/{mcp-JQUI7BVZ.js → mcp-VU5FMO52.js} +14 -14
- package/dist/{performance-ZTVSUANN.js → performance-2D7G6NMJ.js} +3 -3
- package/dist/{review-pipeline-76JHKGSV.js → review-pipeline-RAQ55ISU.js} +1 -1
- package/dist/runtime-BCK5RRZQ.js +9 -0
- package/dist/{security-FWQZF2IZ.js → security-2RPQEN62.js} +1 -1
- package/dist/templates/axum/Cargo.toml.hbs +8 -0
- package/dist/templates/axum/src/main.rs +12 -0
- package/dist/templates/axum/template.json +16 -0
- package/dist/templates/django/manage.py.hbs +19 -0
- package/dist/templates/django/requirements.txt.hbs +1 -0
- package/dist/templates/django/src/settings.py.hbs +44 -0
- package/dist/templates/django/src/urls.py +6 -0
- package/dist/templates/django/src/wsgi.py.hbs +9 -0
- package/dist/templates/django/template.json +21 -0
- package/dist/templates/express/package.json.hbs +15 -0
- package/dist/templates/express/src/app.ts +12 -0
- package/dist/templates/express/src/lib/.gitkeep +0 -0
- package/dist/templates/express/template.json +16 -0
- package/dist/templates/fastapi/requirements.txt.hbs +2 -0
- package/dist/templates/fastapi/src/main.py +8 -0
- package/dist/templates/fastapi/template.json +20 -0
- package/dist/templates/gin/go.mod.hbs +5 -0
- package/dist/templates/gin/main.go +15 -0
- package/dist/templates/gin/template.json +19 -0
- package/dist/templates/go-base/.golangci.yml +16 -0
- package/dist/templates/go-base/AGENTS.md.hbs +35 -0
- package/dist/templates/go-base/go.mod.hbs +3 -0
- package/dist/templates/go-base/harness.config.json.hbs +17 -0
- package/dist/templates/go-base/main.go +7 -0
- package/dist/templates/go-base/template.json +14 -0
- package/dist/templates/java-base/AGENTS.md.hbs +35 -0
- package/dist/templates/java-base/checkstyle.xml +20 -0
- package/dist/templates/java-base/harness.config.json.hbs +16 -0
- package/dist/templates/java-base/pom.xml.hbs +39 -0
- package/dist/templates/java-base/src/main/java/App.java.hbs +5 -0
- package/dist/templates/java-base/template.json +13 -0
- package/dist/templates/nestjs/nest-cli.json +5 -0
- package/dist/templates/nestjs/package.json.hbs +18 -0
- package/dist/templates/nestjs/src/app.module.ts +8 -0
- package/dist/templates/nestjs/src/lib/.gitkeep +0 -0
- package/dist/templates/nestjs/src/main.ts +11 -0
- package/dist/templates/nestjs/template.json +16 -0
- package/dist/templates/nextjs/template.json +15 -1
- package/dist/templates/python-base/.python-version +1 -0
- package/dist/templates/python-base/AGENTS.md.hbs +32 -0
- package/dist/templates/python-base/harness.config.json.hbs +16 -0
- package/dist/templates/python-base/pyproject.toml.hbs +18 -0
- package/dist/templates/python-base/ruff.toml +5 -0
- package/dist/templates/python-base/src/__init__.py +0 -0
- package/dist/templates/python-base/template.json +13 -0
- package/dist/templates/react-vite/index.html +12 -0
- package/dist/templates/react-vite/package.json.hbs +18 -0
- package/dist/templates/react-vite/src/App.tsx +7 -0
- package/dist/templates/react-vite/src/lib/.gitkeep +0 -0
- package/dist/templates/react-vite/src/main.tsx +9 -0
- package/dist/templates/react-vite/template.json +19 -0
- package/dist/templates/react-vite/vite.config.ts +6 -0
- package/dist/templates/rust-base/AGENTS.md.hbs +35 -0
- package/dist/templates/rust-base/Cargo.toml.hbs +6 -0
- package/dist/templates/rust-base/clippy.toml +2 -0
- package/dist/templates/rust-base/harness.config.json.hbs +17 -0
- package/dist/templates/rust-base/src/main.rs +3 -0
- package/dist/templates/rust-base/template.json +14 -0
- package/dist/templates/spring-boot/pom.xml.hbs +50 -0
- package/dist/templates/spring-boot/src/main/java/Application.java.hbs +19 -0
- package/dist/templates/spring-boot/template.json +15 -0
- package/dist/templates/vue/index.html +12 -0
- package/dist/templates/vue/package.json.hbs +16 -0
- package/dist/templates/vue/src/App.vue +7 -0
- package/dist/templates/vue/src/lib/.gitkeep +0 -0
- package/dist/templates/vue/src/main.ts +4 -0
- package/dist/templates/vue/template.json +19 -0
- package/dist/templates/vue/vite.config.ts +6 -0
- package/dist/{validate-GCHZJIL7.js → validate-KBYQAEWE.js} +4 -4
- package/dist/validate-cross-check-OABMREW4.js +8 -0
- package/package.json +7 -5
- package/dist/agents-md-XU3BHE22.js +0 -8
- package/dist/ci-workflow-EHV65NQB.js +0 -8
- package/dist/engine-OL4T6NZS.js +0 -8
- package/dist/loader-DPYFB6R6.js +0 -10
- package/dist/runtime-X7U6SC7K.js +0 -9
- package/dist/validate-cross-check-STFHYMAZ.js +0 -8
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
2
|
generateCIWorkflow
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-SD3SQOZ2.js";
|
|
4
4
|
import {
|
|
5
5
|
OutputFormatter,
|
|
6
6
|
OutputMode,
|
|
7
7
|
createCheckPhaseGateCommand,
|
|
8
8
|
findFiles
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-LGYBN7Y6.js";
|
|
10
10
|
import {
|
|
11
11
|
createGenerateAgentDefinitionsCommand,
|
|
12
12
|
generateAgentDefinitions
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-RCWZBSK5.js";
|
|
14
14
|
import {
|
|
15
15
|
listPersonas,
|
|
16
16
|
loadPersona
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-YQ6KC6TE.js";
|
|
18
18
|
import {
|
|
19
19
|
runPersona
|
|
20
20
|
} from "./chunk-TRAPF4IX.js";
|
|
@@ -33,29 +33,31 @@ import {
|
|
|
33
33
|
import {
|
|
34
34
|
generate,
|
|
35
35
|
validate
|
|
36
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-DBSOCI3G.js";
|
|
37
37
|
import {
|
|
38
38
|
generateRuntime
|
|
39
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-FIAPHX37.js";
|
|
40
40
|
import {
|
|
41
41
|
toKebabCase
|
|
42
42
|
} from "./chunk-KET4QQZB.js";
|
|
43
43
|
import {
|
|
44
44
|
generateAgentsMd
|
|
45
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-OD3S2NHN.js";
|
|
46
46
|
import {
|
|
47
|
+
appendFrameworkAgents,
|
|
47
48
|
createGenerateSlashCommandsCommand,
|
|
48
49
|
generateSlashCommands,
|
|
49
50
|
handleGetImpact,
|
|
50
|
-
handleOrphanDeletion
|
|
51
|
-
|
|
51
|
+
handleOrphanDeletion,
|
|
52
|
+
persistToolingConfig
|
|
53
|
+
} from "./chunk-NNHDDXYT.js";
|
|
52
54
|
import {
|
|
53
55
|
VALID_PLATFORMS
|
|
54
56
|
} from "./chunk-ZOAWBDWU.js";
|
|
55
57
|
import {
|
|
56
58
|
findConfigFile,
|
|
57
59
|
resolveConfig
|
|
58
|
-
} from "./chunk-
|
|
60
|
+
} from "./chunk-B2HKP423.js";
|
|
59
61
|
import {
|
|
60
62
|
resolveGlobalSkillsDir,
|
|
61
63
|
resolvePersonasDir,
|
|
@@ -76,7 +78,7 @@ import {
|
|
|
76
78
|
} from "./chunk-BM3PWGXQ.js";
|
|
77
79
|
import {
|
|
78
80
|
TemplateEngine
|
|
79
|
-
} from "./chunk-
|
|
81
|
+
} from "./chunk-7MJAPE3Z.js";
|
|
80
82
|
import {
|
|
81
83
|
ArchBaselineManager,
|
|
82
84
|
ArchConfigSchema,
|
|
@@ -125,14 +127,14 @@ import {
|
|
|
125
127
|
validateKnowledgeMap,
|
|
126
128
|
writeConfig,
|
|
127
129
|
writeLockfile
|
|
128
|
-
} from "./chunk-
|
|
130
|
+
} from "./chunk-Z2OOPXJO.js";
|
|
129
131
|
import {
|
|
130
132
|
Err,
|
|
131
133
|
Ok
|
|
132
|
-
} from "./chunk-
|
|
134
|
+
} from "./chunk-ERS5EVUZ.js";
|
|
133
135
|
|
|
134
136
|
// src/index.ts
|
|
135
|
-
import { Command as
|
|
137
|
+
import { Command as Command61 } from "commander";
|
|
136
138
|
|
|
137
139
|
// src/commands/validate.ts
|
|
138
140
|
import { Command } from "commander";
|
|
@@ -211,7 +213,7 @@ function createValidateCommand() {
|
|
|
211
213
|
process.exit(result.error.exitCode);
|
|
212
214
|
}
|
|
213
215
|
if (opts.crossCheck) {
|
|
214
|
-
const { runCrossCheck: runCrossCheck2 } = await import("./validate-cross-check-
|
|
216
|
+
const { runCrossCheck: runCrossCheck2 } = await import("./validate-cross-check-OABMREW4.js");
|
|
215
217
|
const cwd = process.cwd();
|
|
216
218
|
const specsDir = path.join(cwd, "docs", "specs");
|
|
217
219
|
const plansDir = path.join(cwd, "docs", "plans");
|
|
@@ -479,10 +481,10 @@ async function runCheckSecurity(cwd, options) {
|
|
|
479
481
|
const projectRoot = path4.resolve(cwd);
|
|
480
482
|
let configData = {};
|
|
481
483
|
try {
|
|
482
|
-
const
|
|
484
|
+
const fs29 = await import("fs");
|
|
483
485
|
const configPath = path4.join(projectRoot, "harness.config.json");
|
|
484
|
-
if (
|
|
485
|
-
const raw =
|
|
486
|
+
if (fs29.existsSync(configPath)) {
|
|
487
|
+
const raw = fs29.readFileSync(configPath, "utf-8");
|
|
486
488
|
const parsed = JSON.parse(raw);
|
|
487
489
|
configData = parsed.security ?? {};
|
|
488
490
|
}
|
|
@@ -564,12 +566,11 @@ function createCheckSecurityCommand() {
|
|
|
564
566
|
// src/commands/perf.ts
|
|
565
567
|
import { Command as Command5 } from "commander";
|
|
566
568
|
import * as path5 from "path";
|
|
567
|
-
function
|
|
568
|
-
const perf = new Command5("perf").description("Performance benchmark and baseline management");
|
|
569
|
+
function registerBenchCommand(perf) {
|
|
569
570
|
perf.command("bench [glob]").description("Run benchmarks via vitest bench").action(async (glob, _opts, cmd) => {
|
|
570
571
|
const globalOpts = cmd.optsWithGlobals();
|
|
571
572
|
const cwd = process.cwd();
|
|
572
|
-
const { BenchmarkRunner } = await import("./dist-
|
|
573
|
+
const { BenchmarkRunner } = await import("./dist-ALQDD67R.js");
|
|
573
574
|
const runner = new BenchmarkRunner();
|
|
574
575
|
const benchFiles = runner.discover(cwd, glob);
|
|
575
576
|
if (benchFiles.length === 0) {
|
|
@@ -580,48 +581,47 @@ function createPerfCommand() {
|
|
|
580
581
|
}
|
|
581
582
|
return;
|
|
582
583
|
}
|
|
583
|
-
|
|
584
|
-
logger.info(`Found ${benchFiles.length} benchmark file(s). Running...`);
|
|
585
|
-
} else {
|
|
586
|
-
logger.info(`Found ${benchFiles.length} benchmark file(s):`);
|
|
587
|
-
for (const f of benchFiles) {
|
|
588
|
-
logger.info(` ${f}`);
|
|
589
|
-
}
|
|
590
|
-
logger.info("Running benchmarks...");
|
|
591
|
-
}
|
|
584
|
+
logBenchDiscovery(globalOpts.json, benchFiles);
|
|
592
585
|
const result = await runner.run(glob ? { cwd, glob } : { cwd });
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
586
|
+
outputBenchResults(globalOpts.json, result);
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
function logBenchDiscovery(json, benchFiles) {
|
|
590
|
+
if (json) {
|
|
591
|
+
logger.info(`Found ${benchFiles.length} benchmark file(s). Running...`);
|
|
592
|
+
} else {
|
|
593
|
+
logger.info(`Found ${benchFiles.length} benchmark file(s):`);
|
|
594
|
+
for (const f of benchFiles) logger.info(` ${f}`);
|
|
595
|
+
logger.info("Running benchmarks...");
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
function outputBenchResults(json, result) {
|
|
599
|
+
if (json) {
|
|
600
|
+
console.log(JSON.stringify({ results: result.results, success: result.success }));
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
if (result.success && result.results.length > 0) {
|
|
604
|
+
logger.info(`
|
|
598
605
|
Results (${result.results.length} benchmarks):`);
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
` ${r.file}::${r.name}: ${r.opsPerSec} ops/s (mean: ${r.meanMs.toFixed(2)}ms)`
|
|
602
|
-
);
|
|
603
|
-
}
|
|
604
|
-
logger.info("\nTo save as baselines: harness perf baselines update");
|
|
605
|
-
} else {
|
|
606
|
-
logger.info("Benchmark run completed. Check output above for details.");
|
|
607
|
-
if (result.rawOutput) {
|
|
608
|
-
console.log(result.rawOutput);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
606
|
+
for (const r of result.results) {
|
|
607
|
+
logger.info(` ${r.file}::${r.name}: ${r.opsPerSec} ops/s (mean: ${r.meanMs.toFixed(2)}ms)`);
|
|
611
608
|
}
|
|
612
|
-
|
|
609
|
+
logger.info("\nTo save as baselines: harness perf baselines update");
|
|
610
|
+
} else {
|
|
611
|
+
logger.info("Benchmark run completed. Check output above for details.");
|
|
612
|
+
if (result.rawOutput) console.log(result.rawOutput);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
function registerBaselinesCommands(perf) {
|
|
613
616
|
const baselines = perf.command("baselines").description("Manage performance baselines");
|
|
614
617
|
baselines.command("show").description("Display current baselines").action(async (_opts, cmd) => {
|
|
615
618
|
const globalOpts = cmd.optsWithGlobals();
|
|
616
|
-
const
|
|
617
|
-
const manager = new BaselineManager(cwd);
|
|
619
|
+
const manager = new BaselineManager(process.cwd());
|
|
618
620
|
const data = manager.load();
|
|
619
621
|
if (!data) {
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
logger.info("No baselines file found at .harness/perf/baselines.json");
|
|
624
|
-
}
|
|
622
|
+
console.log(
|
|
623
|
+
globalOpts.json ? JSON.stringify({ baselines: null, message: "No baselines file found" }) : "No baselines file found at .harness/perf/baselines.json"
|
|
624
|
+
);
|
|
625
625
|
return;
|
|
626
626
|
}
|
|
627
627
|
if (globalOpts.json) {
|
|
@@ -638,7 +638,7 @@ Results (${result.results.length} benchmarks):`);
|
|
|
638
638
|
baselines.command("update").description("Update baselines from latest benchmark run").action(async (_opts, cmd) => {
|
|
639
639
|
const globalOpts = cmd.optsWithGlobals();
|
|
640
640
|
const cwd = process.cwd();
|
|
641
|
-
const { BenchmarkRunner } = await import("./dist-
|
|
641
|
+
const { BenchmarkRunner } = await import("./dist-ALQDD67R.js");
|
|
642
642
|
const runner = new BenchmarkRunner();
|
|
643
643
|
const manager = new BaselineManager(cwd);
|
|
644
644
|
logger.info("Running benchmarks to update baselines...");
|
|
@@ -649,12 +649,7 @@ Results (${result.results.length} benchmarks):`);
|
|
|
649
649
|
);
|
|
650
650
|
return;
|
|
651
651
|
}
|
|
652
|
-
|
|
653
|
-
try {
|
|
654
|
-
const { execSync: execSync5 } = await import("child_process");
|
|
655
|
-
commitHash = execSync5("git rev-parse --short HEAD", { cwd, encoding: "utf-8" }).trim();
|
|
656
|
-
} catch {
|
|
657
|
-
}
|
|
652
|
+
const commitHash = await getCommitHash(cwd);
|
|
658
653
|
manager.save(benchResult.results, commitHash);
|
|
659
654
|
if (globalOpts.json) {
|
|
660
655
|
console.log(JSON.stringify({ updated: benchResult.results.length, commitHash }));
|
|
@@ -663,10 +658,20 @@ Results (${result.results.length} benchmarks):`);
|
|
|
663
658
|
logger.info("Baselines saved to .harness/perf/baselines.json");
|
|
664
659
|
}
|
|
665
660
|
});
|
|
661
|
+
}
|
|
662
|
+
async function getCommitHash(cwd) {
|
|
663
|
+
try {
|
|
664
|
+
const { execSync: execSync5 } = await import("child_process");
|
|
665
|
+
return execSync5("git rev-parse --short HEAD", { cwd, encoding: "utf-8" }).trim();
|
|
666
|
+
} catch {
|
|
667
|
+
return "unknown";
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
function registerReportCommand(perf) {
|
|
666
671
|
perf.command("report").description("Full performance report with metrics, trends, and hotspots").action(async (_opts, cmd) => {
|
|
667
672
|
const globalOpts = cmd.optsWithGlobals();
|
|
668
673
|
const cwd = process.cwd();
|
|
669
|
-
const { EntropyAnalyzer: EntropyAnalyzer2 } = await import("./dist-
|
|
674
|
+
const { EntropyAnalyzer: EntropyAnalyzer2 } = await import("./dist-ALQDD67R.js");
|
|
670
675
|
const analyzer = new EntropyAnalyzer2({
|
|
671
676
|
rootDir: path5.resolve(cwd),
|
|
672
677
|
analyze: { complexity: true, coupling: true }
|
|
@@ -703,10 +708,11 @@ Results (${result.results.length} benchmarks):`);
|
|
|
703
708
|
}
|
|
704
709
|
}
|
|
705
710
|
});
|
|
711
|
+
}
|
|
712
|
+
function registerCriticalPathsCommand(perf) {
|
|
706
713
|
perf.command("critical-paths").description("Show resolved critical path set (annotations + graph inference)").action(async (_opts, cmd) => {
|
|
707
714
|
const globalOpts = cmd.optsWithGlobals();
|
|
708
|
-
const
|
|
709
|
-
const resolver = new CriticalPathResolver(cwd);
|
|
715
|
+
const resolver = new CriticalPathResolver(process.cwd());
|
|
710
716
|
const result = await resolver.resolve();
|
|
711
717
|
if (globalOpts.json) {
|
|
712
718
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -721,12 +727,29 @@ Results (${result.results.length} benchmarks):`);
|
|
|
721
727
|
}
|
|
722
728
|
}
|
|
723
729
|
});
|
|
730
|
+
}
|
|
731
|
+
function createPerfCommand() {
|
|
732
|
+
const perf = new Command5("perf").description("Performance benchmark and baseline management");
|
|
733
|
+
registerBenchCommand(perf);
|
|
734
|
+
registerBaselinesCommands(perf);
|
|
735
|
+
registerReportCommand(perf);
|
|
736
|
+
registerCriticalPathsCommand(perf);
|
|
724
737
|
return perf;
|
|
725
738
|
}
|
|
726
739
|
|
|
727
740
|
// src/commands/check-docs.ts
|
|
728
741
|
import { Command as Command6 } from "commander";
|
|
729
742
|
import * as path6 from "path";
|
|
743
|
+
|
|
744
|
+
// src/utils/output.ts
|
|
745
|
+
function resolveOutputMode(globalOpts) {
|
|
746
|
+
if (globalOpts.json) return OutputMode.JSON;
|
|
747
|
+
if (globalOpts.quiet) return OutputMode.QUIET;
|
|
748
|
+
if (globalOpts.verbose) return OutputMode.VERBOSE;
|
|
749
|
+
return OutputMode.TEXT;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// src/commands/check-docs.ts
|
|
730
753
|
async function runCheckDocs(options) {
|
|
731
754
|
const cwd = options.cwd ?? process.cwd();
|
|
732
755
|
const minCoverage = options.minCoverage ?? 80;
|
|
@@ -777,7 +800,7 @@ async function runCheckDocs(options) {
|
|
|
777
800
|
function createCheckDocsCommand() {
|
|
778
801
|
const command = new Command6("check-docs").description("Check documentation coverage").option("--min-coverage <percent>", "Minimum coverage percentage", "80").action(async (opts, cmd) => {
|
|
779
802
|
const globalOpts = cmd.optsWithGlobals();
|
|
780
|
-
const mode = globalOpts
|
|
803
|
+
const mode = resolveOutputMode(globalOpts);
|
|
781
804
|
const formatter = new OutputFormatter(mode);
|
|
782
805
|
const result = await runCheckDocs({
|
|
783
806
|
configPath: globalOpts.config,
|
|
@@ -947,7 +970,6 @@ function createSetupMcpCommand() {
|
|
|
947
970
|
async function runInit(options) {
|
|
948
971
|
const cwd = options.cwd ?? process.cwd();
|
|
949
972
|
const name = options.name ?? path8.basename(cwd);
|
|
950
|
-
const level = options.level ?? "basic";
|
|
951
973
|
const force = options.force ?? false;
|
|
952
974
|
const configPath = path8.join(cwd, "harness.config.json");
|
|
953
975
|
if (!force && fs2.existsSync(configPath)) {
|
|
@@ -955,33 +977,82 @@ async function runInit(options) {
|
|
|
955
977
|
new CLIError("Project already initialized. Use --force to overwrite.", ExitCode.ERROR)
|
|
956
978
|
);
|
|
957
979
|
}
|
|
958
|
-
const
|
|
959
|
-
const
|
|
960
|
-
const
|
|
961
|
-
|
|
962
|
-
|
|
980
|
+
const engine = new TemplateEngine(resolveTemplatesDir());
|
|
981
|
+
const templates = engine.listTemplates();
|
|
982
|
+
const templateList = templates.ok ? templates.value : [];
|
|
983
|
+
const validationError = validateFrameworkLanguage(options, templateList);
|
|
984
|
+
if (validationError) return Err(validationError);
|
|
985
|
+
const detected = tryAutoDetect(engine, cwd, options);
|
|
986
|
+
if (detected) return Ok(detected);
|
|
987
|
+
const language = resolveLanguage(options, templateList);
|
|
988
|
+
return scaffoldProject(engine, { cwd, name, force, language, options });
|
|
989
|
+
}
|
|
990
|
+
function validateFrameworkLanguage(options, templateList) {
|
|
991
|
+
if (!options.framework || !options.language) return null;
|
|
992
|
+
const fwTemplate = templateList.find((t) => t.framework === options.framework);
|
|
993
|
+
if (fwTemplate?.language && fwTemplate.language !== options.language) {
|
|
994
|
+
return new CLIError(
|
|
995
|
+
`Framework "${options.framework}" is a ${fwTemplate.language} framework, but --language ${options.language} was specified. Remove --language or use --language ${fwTemplate.language}.`,
|
|
996
|
+
ExitCode.ERROR
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
return null;
|
|
1000
|
+
}
|
|
1001
|
+
function tryAutoDetect(engine, cwd, options) {
|
|
1002
|
+
if (options.framework || options.language) return null;
|
|
1003
|
+
const detectResult = engine.detectFramework(cwd);
|
|
1004
|
+
if (detectResult.ok && detectResult.value.length > 0) {
|
|
1005
|
+
return { filesCreated: [], skippedConfigs: [], detectedFrameworks: detectResult.value };
|
|
1006
|
+
}
|
|
1007
|
+
return null;
|
|
1008
|
+
}
|
|
1009
|
+
function resolveLanguage(options, templateList) {
|
|
1010
|
+
if (options.language) return options.language;
|
|
1011
|
+
if (options.framework) {
|
|
1012
|
+
const fwTemplate = templateList.find((t) => t.framework === options.framework);
|
|
1013
|
+
if (fwTemplate?.language) return fwTemplate.language;
|
|
963
1014
|
}
|
|
1015
|
+
return void 0;
|
|
1016
|
+
}
|
|
1017
|
+
function scaffoldProject(engine, ctx) {
|
|
1018
|
+
const { cwd, name, force, language, options } = ctx;
|
|
1019
|
+
const isNonJs = language && language !== "typescript";
|
|
1020
|
+
const level = isNonJs ? void 0 : options.level ?? "basic";
|
|
1021
|
+
const resolveResult = engine.resolveTemplate(level, options.framework, language);
|
|
1022
|
+
if (!resolveResult.ok) return Err(new CLIError(resolveResult.error.message, ExitCode.ERROR));
|
|
964
1023
|
const renderResult = engine.render(resolveResult.value, {
|
|
965
1024
|
projectName: name,
|
|
966
|
-
level,
|
|
967
|
-
...options.framework !== void 0 && { framework: options.framework }
|
|
1025
|
+
level: level ?? "",
|
|
1026
|
+
...options.framework !== void 0 && { framework: options.framework },
|
|
1027
|
+
...language !== void 0 && { language }
|
|
968
1028
|
});
|
|
969
|
-
if (!renderResult.ok)
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1029
|
+
if (!renderResult.ok) return Err(new CLIError(renderResult.error.message, ExitCode.ERROR));
|
|
1030
|
+
const writeResult = engine.write(renderResult.value, cwd, {
|
|
1031
|
+
overwrite: force,
|
|
1032
|
+
...language !== void 0 && { language }
|
|
1033
|
+
});
|
|
1034
|
+
if (!writeResult.ok) return Err(new CLIError(writeResult.error.message, ExitCode.ERROR));
|
|
1035
|
+
if (writeResult.value.skippedConfigs.length > 0) {
|
|
1036
|
+
logger.warn("Skipped existing package config files:");
|
|
1037
|
+
for (const file of writeResult.value.skippedConfigs) {
|
|
1038
|
+
logger.info(` - ${file} (add harness dependencies manually)`);
|
|
1039
|
+
}
|
|
975
1040
|
}
|
|
976
|
-
|
|
1041
|
+
persistToolingConfig(cwd, resolveResult.value, options.framework);
|
|
1042
|
+
appendFrameworkAgents(cwd, options.framework, language);
|
|
1043
|
+
return Ok({
|
|
1044
|
+
filesCreated: writeResult.value.written,
|
|
1045
|
+
skippedConfigs: writeResult.value.skippedConfigs
|
|
1046
|
+
});
|
|
977
1047
|
}
|
|
978
1048
|
function createInitCommand() {
|
|
979
|
-
const command = new Command8("init").description("Initialize a new harness-engineering project").option("-n, --name <name>", "Project name").option("-l, --level <level>", "Adoption level (basic, intermediate, advanced)", "basic").option("--framework <framework>", "Framework overlay (nextjs)").option("-f, --force", "Overwrite existing files").option("-y, --yes", "Use defaults without prompting").action(async (opts, cmd) => {
|
|
1049
|
+
const command = new Command8("init").description("Initialize a new harness-engineering project").option("-n, --name <name>", "Project name").option("-l, --level <level>", "Adoption level (basic, intermediate, advanced)", "basic").option("--framework <framework>", "Framework overlay (nextjs)").option("--language <language>", "Target language (typescript, python, go, rust, java)").option("-f, --force", "Overwrite existing files").option("-y, --yes", "Use defaults without prompting").action(async (opts, cmd) => {
|
|
980
1050
|
const globalOpts = cmd.optsWithGlobals();
|
|
981
1051
|
const result = await runInit({
|
|
982
1052
|
name: opts.name,
|
|
983
1053
|
level: opts.level,
|
|
984
1054
|
framework: opts.framework,
|
|
1055
|
+
language: opts.language,
|
|
985
1056
|
force: opts.force
|
|
986
1057
|
});
|
|
987
1058
|
if (!result.ok) {
|
|
@@ -1077,10 +1148,39 @@ async function runCleanup(options) {
|
|
|
1077
1148
|
result.totalIssues = result.driftIssues.length + result.deadCode.length + result.patternViolations.length;
|
|
1078
1149
|
return Ok(result);
|
|
1079
1150
|
}
|
|
1151
|
+
function printCleanupResult(value, formatter) {
|
|
1152
|
+
console.log(
|
|
1153
|
+
formatter.formatSummary("Entropy issues", value.totalIssues.toString(), value.totalIssues === 0)
|
|
1154
|
+
);
|
|
1155
|
+
if (value.driftIssues.length > 0) {
|
|
1156
|
+
console.log("\nDocumentation drift:");
|
|
1157
|
+
for (const issue of value.driftIssues) {
|
|
1158
|
+
console.log(` - ${issue.file}: ${issue.issue}`);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
if (value.deadCode.length > 0) {
|
|
1162
|
+
console.log("\nDead code:");
|
|
1163
|
+
for (const item of value.deadCode.slice(0, 10)) {
|
|
1164
|
+
console.log(` - ${item.file}${item.symbol ? `: ${item.symbol}` : ""}`);
|
|
1165
|
+
}
|
|
1166
|
+
if (value.deadCode.length > 10) {
|
|
1167
|
+
console.log(` ... and ${value.deadCode.length - 10} more`);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
if (value.patternViolations.length > 0) {
|
|
1171
|
+
console.log("\nPattern violations:");
|
|
1172
|
+
for (const violation of value.patternViolations.slice(0, 10)) {
|
|
1173
|
+
console.log(` - ${violation.file} [${violation.pattern}]: ${violation.message}`);
|
|
1174
|
+
}
|
|
1175
|
+
if (value.patternViolations.length > 10) {
|
|
1176
|
+
console.log(` ... and ${value.patternViolations.length - 10} more`);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1080
1180
|
function createCleanupCommand() {
|
|
1081
1181
|
const command = new Command9("cleanup").description("Detect entropy issues (doc drift, dead code, patterns)").option("-t, --type <type>", "Issue type: drift, dead-code, patterns, all", "all").action(async (opts, cmd) => {
|
|
1082
1182
|
const globalOpts = cmd.optsWithGlobals();
|
|
1083
|
-
const mode = globalOpts
|
|
1183
|
+
const mode = resolveOutputMode(globalOpts);
|
|
1084
1184
|
const formatter = new OutputFormatter(mode);
|
|
1085
1185
|
const result = await runCleanup({
|
|
1086
1186
|
configPath: globalOpts.config,
|
|
@@ -1100,37 +1200,7 @@ function createCleanupCommand() {
|
|
|
1100
1200
|
if (mode === OutputMode.JSON) {
|
|
1101
1201
|
console.log(JSON.stringify(result.value, null, 2));
|
|
1102
1202
|
} else if (mode !== OutputMode.QUIET || result.value.totalIssues > 0) {
|
|
1103
|
-
|
|
1104
|
-
formatter.formatSummary(
|
|
1105
|
-
"Entropy issues",
|
|
1106
|
-
result.value.totalIssues.toString(),
|
|
1107
|
-
result.value.totalIssues === 0
|
|
1108
|
-
)
|
|
1109
|
-
);
|
|
1110
|
-
if (result.value.driftIssues.length > 0) {
|
|
1111
|
-
console.log("\nDocumentation drift:");
|
|
1112
|
-
for (const issue of result.value.driftIssues) {
|
|
1113
|
-
console.log(` - ${issue.file}: ${issue.issue}`);
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
if (result.value.deadCode.length > 0) {
|
|
1117
|
-
console.log("\nDead code:");
|
|
1118
|
-
for (const item of result.value.deadCode.slice(0, 10)) {
|
|
1119
|
-
console.log(` - ${item.file}${item.symbol ? `: ${item.symbol}` : ""}`);
|
|
1120
|
-
}
|
|
1121
|
-
if (result.value.deadCode.length > 10) {
|
|
1122
|
-
console.log(` ... and ${result.value.deadCode.length - 10} more`);
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
if (result.value.patternViolations.length > 0) {
|
|
1126
|
-
console.log("\nPattern violations:");
|
|
1127
|
-
for (const violation of result.value.patternViolations.slice(0, 10)) {
|
|
1128
|
-
console.log(` - ${violation.file} [${violation.pattern}]: ${violation.message}`);
|
|
1129
|
-
}
|
|
1130
|
-
if (result.value.patternViolations.length > 10) {
|
|
1131
|
-
console.log(` ... and ${result.value.patternViolations.length - 10} more`);
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1203
|
+
printCleanupResult(result.value, formatter);
|
|
1134
1204
|
}
|
|
1135
1205
|
process.exit(result.value.totalIssues === 0 ? ExitCode.SUCCESS : ExitCode.VALIDATION_FAILED);
|
|
1136
1206
|
});
|
|
@@ -1238,10 +1308,42 @@ async function runFixDrift(options) {
|
|
|
1238
1308
|
};
|
|
1239
1309
|
return Ok(result);
|
|
1240
1310
|
}
|
|
1311
|
+
function printFixDriftResult(value, mode, formatter) {
|
|
1312
|
+
const statusMessage = value.dryRun ? "(dry-run)" : "";
|
|
1313
|
+
console.log(
|
|
1314
|
+
formatter.formatSummary(
|
|
1315
|
+
`Fix drift ${statusMessage}`,
|
|
1316
|
+
`${value.fixes.length} fixes, ${value.suggestions.length} suggestions`,
|
|
1317
|
+
value.fixes.length === 0 && value.suggestions.length === 0
|
|
1318
|
+
)
|
|
1319
|
+
);
|
|
1320
|
+
if (value.fixes.length > 0) {
|
|
1321
|
+
console.log("\nFixes:");
|
|
1322
|
+
for (const fix of value.fixes.slice(0, 10)) {
|
|
1323
|
+
const status = fix.applied ? "[applied]" : "[pending]";
|
|
1324
|
+
console.log(` ${status} ${fix.action}: ${fix.file}`);
|
|
1325
|
+
}
|
|
1326
|
+
if (value.fixes.length > 10) {
|
|
1327
|
+
console.log(` ... and ${value.fixes.length - 10} more`);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
if (value.suggestions.length > 0 && (mode === OutputMode.VERBOSE || value.fixes.length === 0)) {
|
|
1331
|
+
console.log("\nSuggestions:");
|
|
1332
|
+
for (const suggestion of value.suggestions.slice(0, 10)) {
|
|
1333
|
+
console.log(` - ${suggestion.file}: ${suggestion.suggestion}`);
|
|
1334
|
+
}
|
|
1335
|
+
if (value.suggestions.length > 10) {
|
|
1336
|
+
console.log(` ... and ${value.suggestions.length - 10} more`);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
if (value.dryRun && value.fixes.length > 0) {
|
|
1340
|
+
console.log("\nRun with --no-dry-run to apply fixes.");
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1241
1343
|
function createFixDriftCommand() {
|
|
1242
1344
|
const command = new Command10("fix-drift").description("Auto-fix entropy issues (doc drift, dead code)").option("--no-dry-run", "Actually apply fixes (default is dry-run mode)").action(async (opts, cmd) => {
|
|
1243
1345
|
const globalOpts = cmd.optsWithGlobals();
|
|
1244
|
-
const mode = globalOpts
|
|
1346
|
+
const mode = resolveOutputMode(globalOpts);
|
|
1245
1347
|
const formatter = new OutputFormatter(mode);
|
|
1246
1348
|
const result = await runFixDrift({
|
|
1247
1349
|
configPath: globalOpts.config,
|
|
@@ -1261,37 +1363,7 @@ function createFixDriftCommand() {
|
|
|
1261
1363
|
if (mode === OutputMode.JSON) {
|
|
1262
1364
|
console.log(JSON.stringify(result.value, null, 2));
|
|
1263
1365
|
} else if (mode !== OutputMode.QUIET || result.value.fixes.length > 0 || result.value.suggestions.length > 0) {
|
|
1264
|
-
|
|
1265
|
-
const statusMessage = value.dryRun ? "(dry-run)" : "";
|
|
1266
|
-
console.log(
|
|
1267
|
-
formatter.formatSummary(
|
|
1268
|
-
`Fix drift ${statusMessage}`,
|
|
1269
|
-
`${value.fixes.length} fixes, ${value.suggestions.length} suggestions`,
|
|
1270
|
-
value.fixes.length === 0 && value.suggestions.length === 0
|
|
1271
|
-
)
|
|
1272
|
-
);
|
|
1273
|
-
if (value.fixes.length > 0) {
|
|
1274
|
-
console.log("\nFixes:");
|
|
1275
|
-
for (const fix of value.fixes.slice(0, 10)) {
|
|
1276
|
-
const status = fix.applied ? "[applied]" : "[pending]";
|
|
1277
|
-
console.log(` ${status} ${fix.action}: ${fix.file}`);
|
|
1278
|
-
}
|
|
1279
|
-
if (value.fixes.length > 10) {
|
|
1280
|
-
console.log(` ... and ${value.fixes.length - 10} more`);
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
if (value.suggestions.length > 0 && (mode === OutputMode.VERBOSE || value.fixes.length === 0)) {
|
|
1284
|
-
console.log("\nSuggestions:");
|
|
1285
|
-
for (const suggestion of value.suggestions.slice(0, 10)) {
|
|
1286
|
-
console.log(` - ${suggestion.file}: ${suggestion.suggestion}`);
|
|
1287
|
-
}
|
|
1288
|
-
if (value.suggestions.length > 10) {
|
|
1289
|
-
console.log(` ... and ${value.suggestions.length - 10} more`);
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
if (value.dryRun && value.fixes.length > 0) {
|
|
1293
|
-
console.log("\nRun with --no-dry-run to apply fixes.");
|
|
1294
|
-
}
|
|
1366
|
+
printFixDriftResult(result.value, mode, formatter);
|
|
1295
1367
|
}
|
|
1296
1368
|
process.exit(ExitCode.SUCCESS);
|
|
1297
1369
|
});
|
|
@@ -1356,49 +1428,54 @@ var VALID_TRIGGERS = /* @__PURE__ */ new Set([
|
|
|
1356
1428
|
"on_plan_approved",
|
|
1357
1429
|
"auto"
|
|
1358
1430
|
]);
|
|
1431
|
+
function resolveTrigger(triggerOpt) {
|
|
1432
|
+
if (triggerOpt === "auto") return "auto";
|
|
1433
|
+
return VALID_TRIGGERS.has(triggerOpt) ? triggerOpt : "manual";
|
|
1434
|
+
}
|
|
1435
|
+
function createCommandExecutor() {
|
|
1436
|
+
return async (command) => {
|
|
1437
|
+
if (!ALLOWED_PERSONA_COMMANDS.has(command)) {
|
|
1438
|
+
return Err(new Error(`Unknown harness command: ${command}`));
|
|
1439
|
+
}
|
|
1440
|
+
try {
|
|
1441
|
+
childProcess.execFileSync("npx", ["harness", command], { stdio: "inherit" });
|
|
1442
|
+
return Ok(null);
|
|
1443
|
+
} catch (error) {
|
|
1444
|
+
return Err(new Error(error instanceof Error ? error.message : String(error)));
|
|
1445
|
+
}
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
async function runPersonaMode(opts, quiet) {
|
|
1449
|
+
const personasDir = resolvePersonasDir();
|
|
1450
|
+
const filePath = path11.join(personasDir, `${opts.persona}.yaml`);
|
|
1451
|
+
const personaResult = loadPersona(filePath);
|
|
1452
|
+
if (!personaResult.ok) {
|
|
1453
|
+
logger.error(personaResult.error.message);
|
|
1454
|
+
process.exit(ExitCode.ERROR);
|
|
1455
|
+
}
|
|
1456
|
+
const report = await runPersona(personaResult.value, {
|
|
1457
|
+
trigger: resolveTrigger(opts.trigger),
|
|
1458
|
+
commandExecutor: createCommandExecutor(),
|
|
1459
|
+
skillExecutor: executeSkill,
|
|
1460
|
+
projectPath: process.cwd()
|
|
1461
|
+
});
|
|
1462
|
+
if (!quiet) {
|
|
1463
|
+
logger.info(`Persona '${report.persona}' status: ${report.status}`);
|
|
1464
|
+
for (const s of report.steps) {
|
|
1465
|
+
const icon = s.status === "pass" ? "v" : s.status === "fail" ? "x" : "-";
|
|
1466
|
+
const typeTag = s.type === "skill" ? " [skill]" : "";
|
|
1467
|
+
console.log(` [${icon}] ${s.name}${typeTag} (${s.durationMs}ms)`);
|
|
1468
|
+
if (s.artifactPath) console.log(` artifact: ${s.artifactPath}`);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
process.exit(report.status === "fail" ? ExitCode.ERROR : ExitCode.SUCCESS);
|
|
1472
|
+
}
|
|
1359
1473
|
function createRunCommand() {
|
|
1360
1474
|
return new Command11("run").description("Run an agent task").argument("[task]", "Task to run (review, doc-review, test-review)").option("--timeout <ms>", "Timeout in milliseconds", "300000").option("--persona <name>", "Run a persona by name").option("--trigger <context>", "Trigger context (auto, on_pr, on_commit, manual)", "auto").action(async (task, opts, cmd) => {
|
|
1361
1475
|
const globalOpts = cmd.optsWithGlobals();
|
|
1362
1476
|
if (opts.persona) {
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
const personaResult = loadPersona(filePath);
|
|
1366
|
-
if (!personaResult.ok) {
|
|
1367
|
-
logger.error(personaResult.error.message);
|
|
1368
|
-
process.exit(ExitCode.ERROR);
|
|
1369
|
-
}
|
|
1370
|
-
const persona = personaResult.value;
|
|
1371
|
-
const projectPath = process.cwd();
|
|
1372
|
-
const trigger = opts.trigger === "auto" ? "auto" : VALID_TRIGGERS.has(opts.trigger) ? opts.trigger : "manual";
|
|
1373
|
-
const commandExecutor = async (command) => {
|
|
1374
|
-
if (!ALLOWED_PERSONA_COMMANDS.has(command)) {
|
|
1375
|
-
return Err(new Error(`Unknown harness command: ${command}`));
|
|
1376
|
-
}
|
|
1377
|
-
try {
|
|
1378
|
-
childProcess.execFileSync("npx", ["harness", command], { stdio: "inherit" });
|
|
1379
|
-
return Ok(null);
|
|
1380
|
-
} catch (error) {
|
|
1381
|
-
return Err(new Error(error instanceof Error ? error.message : String(error)));
|
|
1382
|
-
}
|
|
1383
|
-
};
|
|
1384
|
-
const report = await runPersona(persona, {
|
|
1385
|
-
trigger,
|
|
1386
|
-
commandExecutor,
|
|
1387
|
-
skillExecutor: executeSkill,
|
|
1388
|
-
projectPath
|
|
1389
|
-
});
|
|
1390
|
-
if (!globalOpts.quiet) {
|
|
1391
|
-
logger.info(`Persona '${report.persona}' status: ${report.status}`);
|
|
1392
|
-
for (const s of report.steps) {
|
|
1393
|
-
const icon = s.status === "pass" ? "v" : s.status === "fail" ? "x" : "-";
|
|
1394
|
-
const typeTag = s.type === "skill" ? " [skill]" : "";
|
|
1395
|
-
console.log(` [${icon}] ${s.name}${typeTag} (${s.durationMs}ms)`);
|
|
1396
|
-
if (s.artifactPath) {
|
|
1397
|
-
console.log(` artifact: ${s.artifactPath}`);
|
|
1398
|
-
}
|
|
1399
|
-
}
|
|
1400
|
-
}
|
|
1401
|
-
process.exit(report.status === "fail" ? ExitCode.ERROR : ExitCode.SUCCESS);
|
|
1477
|
+
await runPersonaMode(opts, globalOpts.quiet);
|
|
1478
|
+
return;
|
|
1402
1479
|
}
|
|
1403
1480
|
if (!task) {
|
|
1404
1481
|
logger.error("Either a task argument or --persona flag is required.");
|
|
@@ -1987,6 +2064,33 @@ function scanDirectory(dirPath, source) {
|
|
|
1987
2064
|
}
|
|
1988
2065
|
return skills;
|
|
1989
2066
|
}
|
|
2067
|
+
function collectCommunitySkills(seen, allSkills) {
|
|
2068
|
+
const globalDir = resolveGlobalSkillsDir();
|
|
2069
|
+
const skillsDir = path15.dirname(globalDir);
|
|
2070
|
+
const communityBase = path15.join(skillsDir, "community");
|
|
2071
|
+
const communityPlatformDir = path15.join(communityBase, "claude-code");
|
|
2072
|
+
const lockfilePath = path15.join(communityBase, "skills-lock.json");
|
|
2073
|
+
const lockfile = readLockfile2(lockfilePath);
|
|
2074
|
+
const communitySkills = scanDirectory(communityPlatformDir, "community");
|
|
2075
|
+
for (const skill of communitySkills) {
|
|
2076
|
+
const lockEntry = lockfile.skills[`@harness-skills/${skill.name}`];
|
|
2077
|
+
if (lockEntry) skill.version = lockEntry.version;
|
|
2078
|
+
}
|
|
2079
|
+
for (const [pkgName, entry] of Object.entries(lockfile.skills)) {
|
|
2080
|
+
const shortName = pkgName.replace("@harness-skills/", "");
|
|
2081
|
+
if (!seen.has(shortName)) {
|
|
2082
|
+
seen.add(shortName);
|
|
2083
|
+
allSkills.push({
|
|
2084
|
+
name: shortName,
|
|
2085
|
+
description: "",
|
|
2086
|
+
type: "",
|
|
2087
|
+
source: "community",
|
|
2088
|
+
version: entry.version
|
|
2089
|
+
});
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
return communitySkills;
|
|
2093
|
+
}
|
|
1990
2094
|
function collectSkills(opts) {
|
|
1991
2095
|
const seen = /* @__PURE__ */ new Set();
|
|
1992
2096
|
const allSkills = [];
|
|
@@ -2005,34 +2109,7 @@ function collectSkills(opts) {
|
|
|
2005
2109
|
}
|
|
2006
2110
|
}
|
|
2007
2111
|
if (opts.filter === "all" || opts.filter === "installed") {
|
|
2008
|
-
|
|
2009
|
-
const skillsDir = path15.dirname(globalDir);
|
|
2010
|
-
const communityBase = path15.join(skillsDir, "community");
|
|
2011
|
-
const communityPlatformDir = path15.join(communityBase, "claude-code");
|
|
2012
|
-
const lockfilePath = path15.join(communityBase, "skills-lock.json");
|
|
2013
|
-
const lockfile = readLockfile2(lockfilePath);
|
|
2014
|
-
const communitySkills = scanDirectory(communityPlatformDir, "community");
|
|
2015
|
-
for (const skill of communitySkills) {
|
|
2016
|
-
const pkgName = `@harness-skills/${skill.name}`;
|
|
2017
|
-
const lockEntry = lockfile.skills[pkgName];
|
|
2018
|
-
if (lockEntry) {
|
|
2019
|
-
skill.version = lockEntry.version;
|
|
2020
|
-
}
|
|
2021
|
-
}
|
|
2022
|
-
addUnique(communitySkills);
|
|
2023
|
-
for (const [pkgName, entry] of Object.entries(lockfile.skills)) {
|
|
2024
|
-
const shortName = pkgName.replace("@harness-skills/", "");
|
|
2025
|
-
if (!seen.has(shortName)) {
|
|
2026
|
-
seen.add(shortName);
|
|
2027
|
-
allSkills.push({
|
|
2028
|
-
name: shortName,
|
|
2029
|
-
description: "",
|
|
2030
|
-
type: "",
|
|
2031
|
-
source: "community",
|
|
2032
|
-
version: entry.version
|
|
2033
|
-
});
|
|
2034
|
-
}
|
|
2035
|
-
}
|
|
2112
|
+
addUnique(collectCommunitySkills(seen, allSkills));
|
|
2036
2113
|
}
|
|
2037
2114
|
if (opts.filter === "all") {
|
|
2038
2115
|
const globalDir = resolveGlobalSkillsDir();
|
|
@@ -2166,6 +2243,69 @@ ${options.priorState}`);
|
|
|
2166
2243
|
}
|
|
2167
2244
|
|
|
2168
2245
|
// src/commands/skill/run.ts
|
|
2246
|
+
function loadSkillMetadata(skillDir) {
|
|
2247
|
+
const yamlPath = path16.join(skillDir, "skill.yaml");
|
|
2248
|
+
if (!fs7.existsSync(yamlPath)) return null;
|
|
2249
|
+
try {
|
|
2250
|
+
const result = SkillMetadataSchema.safeParse(parse2(fs7.readFileSync(yamlPath, "utf-8")));
|
|
2251
|
+
return result.success ? result.data : null;
|
|
2252
|
+
} catch {
|
|
2253
|
+
return null;
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
function resolveComplexity(metadata, requested, projectPath) {
|
|
2257
|
+
if (!metadata?.phases || metadata.phases.length === 0) return void 0;
|
|
2258
|
+
if (requested === "auto") return detectComplexity(projectPath);
|
|
2259
|
+
return requested;
|
|
2260
|
+
}
|
|
2261
|
+
function loadPrinciples(projectPath) {
|
|
2262
|
+
const principlesPath = path16.join(projectPath, "docs", "principles.md");
|
|
2263
|
+
return fs7.existsSync(principlesPath) ? fs7.readFileSync(principlesPath, "utf-8") : void 0;
|
|
2264
|
+
}
|
|
2265
|
+
function readMostRecentFileInDir(dirPath) {
|
|
2266
|
+
const files = fs7.readdirSync(dirPath).map((f) => ({ name: f, mtime: fs7.statSync(path16.join(dirPath, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
|
|
2267
|
+
if (files.length > 0) return fs7.readFileSync(path16.join(dirPath, files[0].name), "utf-8");
|
|
2268
|
+
return void 0;
|
|
2269
|
+
}
|
|
2270
|
+
function loadPriorState(metadata, projectPath) {
|
|
2271
|
+
if (!metadata?.state.persistent || metadata.state.files.length === 0) return void 0;
|
|
2272
|
+
for (const stateFilePath of metadata.state.files) {
|
|
2273
|
+
const fullPath = path16.join(projectPath, stateFilePath);
|
|
2274
|
+
if (!fs7.existsSync(fullPath)) continue;
|
|
2275
|
+
const stat = fs7.statSync(fullPath);
|
|
2276
|
+
if (stat.isDirectory()) return readMostRecentFileInDir(fullPath);
|
|
2277
|
+
return fs7.readFileSync(fullPath, "utf-8");
|
|
2278
|
+
}
|
|
2279
|
+
return void 0;
|
|
2280
|
+
}
|
|
2281
|
+
function validatePhaseName(metadata, phase) {
|
|
2282
|
+
if (!metadata?.phases) return true;
|
|
2283
|
+
return metadata.phases.map((p) => p.name).includes(phase);
|
|
2284
|
+
}
|
|
2285
|
+
function resolvePhaseState(metadata, projectPath, phase) {
|
|
2286
|
+
if (!validatePhaseName(metadata, phase)) {
|
|
2287
|
+
const validPhases = metadata.phases.map((p) => p.name);
|
|
2288
|
+
logger.error(`Unknown phase: ${phase}. Valid phases: ${validPhases.join(", ")}`);
|
|
2289
|
+
return null;
|
|
2290
|
+
}
|
|
2291
|
+
const priorState = loadPriorState(metadata, projectPath);
|
|
2292
|
+
const stateWarning = !priorState && metadata?.state.persistent ? "No prior phase data found. Earlier phases have not been completed. Proceed with caution." : void 0;
|
|
2293
|
+
return { priorState, stateWarning };
|
|
2294
|
+
}
|
|
2295
|
+
function appendProjectState(content, metadata, projectPath, hasPathOpt) {
|
|
2296
|
+
if (!metadata?.state.persistent || !hasPathOpt) return content;
|
|
2297
|
+
const stateFile = path16.join(projectPath, ".harness", "state.json");
|
|
2298
|
+
if (!fs7.existsSync(stateFile)) return content;
|
|
2299
|
+
const stateContent = fs7.readFileSync(stateFile, "utf-8");
|
|
2300
|
+
return content + `
|
|
2301
|
+
|
|
2302
|
+
---
|
|
2303
|
+
## Project State
|
|
2304
|
+
\`\`\`json
|
|
2305
|
+
${stateContent}
|
|
2306
|
+
\`\`\`
|
|
2307
|
+
`;
|
|
2308
|
+
}
|
|
2169
2309
|
function createRunCommand2() {
|
|
2170
2310
|
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) => {
|
|
2171
2311
|
const skillsDir = resolveSkillsDir();
|
|
@@ -2175,64 +2315,24 @@ function createRunCommand2() {
|
|
|
2175
2315
|
process.exit(ExitCode.ERROR);
|
|
2176
2316
|
return;
|
|
2177
2317
|
}
|
|
2178
|
-
const
|
|
2179
|
-
let metadata = null;
|
|
2180
|
-
if (fs7.existsSync(yamlPath)) {
|
|
2181
|
-
try {
|
|
2182
|
-
const raw = fs7.readFileSync(yamlPath, "utf-8");
|
|
2183
|
-
const parsed = parse2(raw);
|
|
2184
|
-
const result = SkillMetadataSchema.safeParse(parsed);
|
|
2185
|
-
if (result.success) metadata = result.data;
|
|
2186
|
-
} catch {
|
|
2187
|
-
}
|
|
2188
|
-
}
|
|
2189
|
-
let complexity;
|
|
2190
|
-
if (metadata?.phases && metadata.phases.length > 0) {
|
|
2191
|
-
const requested = opts.complexity ?? "auto";
|
|
2192
|
-
if (requested === "auto") {
|
|
2193
|
-
const projectPath2 = opts.path ? path16.resolve(opts.path) : process.cwd();
|
|
2194
|
-
complexity = detectComplexity(projectPath2);
|
|
2195
|
-
} else {
|
|
2196
|
-
complexity = requested;
|
|
2197
|
-
}
|
|
2198
|
-
}
|
|
2199
|
-
let principles;
|
|
2318
|
+
const metadata = loadSkillMetadata(skillDir);
|
|
2200
2319
|
const projectPath = opts.path ? path16.resolve(opts.path) : process.cwd();
|
|
2201
|
-
const
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2320
|
+
const complexity = resolveComplexity(
|
|
2321
|
+
metadata,
|
|
2322
|
+
opts.complexity ?? "auto",
|
|
2323
|
+
projectPath
|
|
2324
|
+
);
|
|
2325
|
+
const principles = loadPrinciples(projectPath);
|
|
2205
2326
|
let priorState;
|
|
2206
2327
|
let stateWarning;
|
|
2207
2328
|
if (opts.phase) {
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
process.exit(ExitCode.ERROR);
|
|
2213
|
-
return;
|
|
2214
|
-
}
|
|
2215
|
-
}
|
|
2216
|
-
if (metadata?.state.persistent && metadata.state.files.length > 0) {
|
|
2217
|
-
for (const stateFilePath of metadata.state.files) {
|
|
2218
|
-
const fullPath = path16.join(projectPath, stateFilePath);
|
|
2219
|
-
if (fs7.existsSync(fullPath)) {
|
|
2220
|
-
const stat = fs7.statSync(fullPath);
|
|
2221
|
-
if (stat.isDirectory()) {
|
|
2222
|
-
const files = fs7.readdirSync(fullPath).map((f) => ({ name: f, mtime: fs7.statSync(path16.join(fullPath, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
|
|
2223
|
-
if (files.length > 0) {
|
|
2224
|
-
priorState = fs7.readFileSync(path16.join(fullPath, files[0].name), "utf-8");
|
|
2225
|
-
}
|
|
2226
|
-
} else {
|
|
2227
|
-
priorState = fs7.readFileSync(fullPath, "utf-8");
|
|
2228
|
-
}
|
|
2229
|
-
break;
|
|
2230
|
-
}
|
|
2231
|
-
}
|
|
2232
|
-
if (!priorState) {
|
|
2233
|
-
stateWarning = "No prior phase data found. Earlier phases have not been completed. Proceed with caution.";
|
|
2234
|
-
}
|
|
2329
|
+
const phaseResult = resolvePhaseState(metadata, projectPath, opts.phase);
|
|
2330
|
+
if (!phaseResult) {
|
|
2331
|
+
process.exit(ExitCode.ERROR);
|
|
2332
|
+
return;
|
|
2235
2333
|
}
|
|
2334
|
+
priorState = phaseResult.priorState;
|
|
2335
|
+
stateWarning = phaseResult.stateWarning;
|
|
2236
2336
|
}
|
|
2237
2337
|
const preamble = buildPreamble({
|
|
2238
2338
|
...complexity !== void 0 && { complexity },
|
|
@@ -2249,21 +2349,12 @@ function createRunCommand2() {
|
|
|
2249
2349
|
process.exit(ExitCode.ERROR);
|
|
2250
2350
|
return;
|
|
2251
2351
|
}
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
---
|
|
2260
|
-
## Project State
|
|
2261
|
-
\`\`\`json
|
|
2262
|
-
${stateContent}
|
|
2263
|
-
\`\`\`
|
|
2264
|
-
`;
|
|
2265
|
-
}
|
|
2266
|
-
}
|
|
2352
|
+
const content = appendProjectState(
|
|
2353
|
+
fs7.readFileSync(skillMdPath, "utf-8"),
|
|
2354
|
+
metadata,
|
|
2355
|
+
projectPath,
|
|
2356
|
+
!!opts.path
|
|
2357
|
+
);
|
|
2267
2358
|
process.stdout.write(preamble + content);
|
|
2268
2359
|
process.exit(ExitCode.SUCCESS);
|
|
2269
2360
|
});
|
|
@@ -2281,6 +2372,48 @@ var REQUIRED_SECTIONS = [
|
|
|
2281
2372
|
"## Success Criteria",
|
|
2282
2373
|
"## Examples"
|
|
2283
2374
|
];
|
|
2375
|
+
function validateSkillMd(name, skillMdPath, skillType, errors) {
|
|
2376
|
+
if (!fs8.existsSync(skillMdPath)) {
|
|
2377
|
+
errors.push(`${name}: missing SKILL.md`);
|
|
2378
|
+
return;
|
|
2379
|
+
}
|
|
2380
|
+
const mdContent = fs8.readFileSync(skillMdPath, "utf-8");
|
|
2381
|
+
for (const section of REQUIRED_SECTIONS) {
|
|
2382
|
+
if (!mdContent.includes(section)) {
|
|
2383
|
+
errors.push(`${name}/SKILL.md: missing section "${section}"`);
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
if (!mdContent.trim().startsWith("# ")) {
|
|
2387
|
+
errors.push(`${name}/SKILL.md: must start with an h1 heading`);
|
|
2388
|
+
}
|
|
2389
|
+
if (skillType === "rigid") {
|
|
2390
|
+
if (!mdContent.includes("## Gates"))
|
|
2391
|
+
errors.push(`${name}/SKILL.md: rigid skill missing "## Gates" section`);
|
|
2392
|
+
if (!mdContent.includes("## Escalation"))
|
|
2393
|
+
errors.push(`${name}/SKILL.md: rigid skill missing "## Escalation" section`);
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
function validateSkillEntry(name, skillsDir, errors) {
|
|
2397
|
+
const skillDir = path17.join(skillsDir, name);
|
|
2398
|
+
const yamlPath = path17.join(skillDir, "skill.yaml");
|
|
2399
|
+
if (!fs8.existsSync(yamlPath)) {
|
|
2400
|
+
errors.push(`${name}: missing skill.yaml`);
|
|
2401
|
+
return false;
|
|
2402
|
+
}
|
|
2403
|
+
try {
|
|
2404
|
+
const raw = fs8.readFileSync(yamlPath, "utf-8");
|
|
2405
|
+
const result = SkillMetadataSchema.safeParse(parse3(raw));
|
|
2406
|
+
if (!result.success) {
|
|
2407
|
+
errors.push(`${name}/skill.yaml: ${result.error.message}`);
|
|
2408
|
+
return false;
|
|
2409
|
+
}
|
|
2410
|
+
validateSkillMd(name, path17.join(skillDir, "SKILL.md"), result.data.type, errors);
|
|
2411
|
+
return true;
|
|
2412
|
+
} catch (e) {
|
|
2413
|
+
errors.push(`${name}: parse error \u2014 ${e instanceof Error ? e.message : String(e)}`);
|
|
2414
|
+
return false;
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2284
2417
|
function createValidateCommand3() {
|
|
2285
2418
|
return new Command23("validate").description("Validate all skill.yaml files and SKILL.md structure").action(async (_opts, cmd) => {
|
|
2286
2419
|
const globalOpts = cmd.optsWithGlobals();
|
|
@@ -2294,46 +2427,7 @@ function createValidateCommand3() {
|
|
|
2294
2427
|
const errors = [];
|
|
2295
2428
|
let validated = 0;
|
|
2296
2429
|
for (const name of entries) {
|
|
2297
|
-
|
|
2298
|
-
const yamlPath = path17.join(skillDir, "skill.yaml");
|
|
2299
|
-
const skillMdPath = path17.join(skillDir, "SKILL.md");
|
|
2300
|
-
if (!fs8.existsSync(yamlPath)) {
|
|
2301
|
-
errors.push(`${name}: missing skill.yaml`);
|
|
2302
|
-
continue;
|
|
2303
|
-
}
|
|
2304
|
-
try {
|
|
2305
|
-
const raw = fs8.readFileSync(yamlPath, "utf-8");
|
|
2306
|
-
const parsed = parse3(raw);
|
|
2307
|
-
const result = SkillMetadataSchema.safeParse(parsed);
|
|
2308
|
-
if (!result.success) {
|
|
2309
|
-
errors.push(`${name}/skill.yaml: ${result.error.message}`);
|
|
2310
|
-
continue;
|
|
2311
|
-
}
|
|
2312
|
-
if (fs8.existsSync(skillMdPath)) {
|
|
2313
|
-
const mdContent = fs8.readFileSync(skillMdPath, "utf-8");
|
|
2314
|
-
for (const section of REQUIRED_SECTIONS) {
|
|
2315
|
-
if (!mdContent.includes(section)) {
|
|
2316
|
-
errors.push(`${name}/SKILL.md: missing section "${section}"`);
|
|
2317
|
-
}
|
|
2318
|
-
}
|
|
2319
|
-
if (!mdContent.trim().startsWith("# ")) {
|
|
2320
|
-
errors.push(`${name}/SKILL.md: must start with an h1 heading`);
|
|
2321
|
-
}
|
|
2322
|
-
if (result.data.type === "rigid") {
|
|
2323
|
-
if (!mdContent.includes("## Gates")) {
|
|
2324
|
-
errors.push(`${name}/SKILL.md: rigid skill missing "## Gates" section`);
|
|
2325
|
-
}
|
|
2326
|
-
if (!mdContent.includes("## Escalation")) {
|
|
2327
|
-
errors.push(`${name}/SKILL.md: rigid skill missing "## Escalation" section`);
|
|
2328
|
-
}
|
|
2329
|
-
}
|
|
2330
|
-
} else {
|
|
2331
|
-
errors.push(`${name}: missing SKILL.md`);
|
|
2332
|
-
}
|
|
2333
|
-
validated++;
|
|
2334
|
-
} catch (e) {
|
|
2335
|
-
errors.push(`${name}: parse error \u2014 ${e instanceof Error ? e.message : String(e)}`);
|
|
2336
|
-
}
|
|
2430
|
+
if (validateSkillEntry(name, skillsDir, errors)) validated++;
|
|
2337
2431
|
}
|
|
2338
2432
|
if (globalOpts.json) {
|
|
2339
2433
|
logger.raw({ validated, errors });
|
|
@@ -3031,8 +3125,8 @@ function createResetCommand() {
|
|
|
3031
3125
|
}
|
|
3032
3126
|
if (!opts.yes) {
|
|
3033
3127
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
3034
|
-
const answer = await new Promise((
|
|
3035
|
-
rl.question("Reset project state? This cannot be undone. [y/N] ",
|
|
3128
|
+
const answer = await new Promise((resolve30) => {
|
|
3129
|
+
rl.question("Reset project state? This cannot be undone. [y/N] ", resolve30);
|
|
3036
3130
|
});
|
|
3037
3131
|
rl.close();
|
|
3038
3132
|
if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
|
|
@@ -3146,11 +3240,302 @@ function createStateCommand() {
|
|
|
3146
3240
|
return command;
|
|
3147
3241
|
}
|
|
3148
3242
|
|
|
3243
|
+
// src/commands/setup.ts
|
|
3244
|
+
import { Command as Command34 } from "commander";
|
|
3245
|
+
import * as fs17 from "fs";
|
|
3246
|
+
import * as os4 from "os";
|
|
3247
|
+
import * as path29 from "path";
|
|
3248
|
+
import chalk3 from "chalk";
|
|
3249
|
+
|
|
3250
|
+
// src/utils/first-run.ts
|
|
3251
|
+
import * as fs16 from "fs";
|
|
3252
|
+
import * as os3 from "os";
|
|
3253
|
+
import * as path28 from "path";
|
|
3254
|
+
var HARNESS_DIR = path28.join(os3.homedir(), ".harness");
|
|
3255
|
+
var MARKER_FILE = path28.join(HARNESS_DIR, ".setup-complete");
|
|
3256
|
+
function isFirstRun() {
|
|
3257
|
+
return !fs16.existsSync(MARKER_FILE);
|
|
3258
|
+
}
|
|
3259
|
+
function markSetupComplete() {
|
|
3260
|
+
fs16.mkdirSync(HARNESS_DIR, { recursive: true });
|
|
3261
|
+
fs16.writeFileSync(MARKER_FILE, "", "utf-8");
|
|
3262
|
+
}
|
|
3263
|
+
function printFirstRunWelcome() {
|
|
3264
|
+
try {
|
|
3265
|
+
if (!isFirstRun()) return;
|
|
3266
|
+
if (process.env.CI) return;
|
|
3267
|
+
if (process.argv.includes("--quiet")) return;
|
|
3268
|
+
process.stderr.write("Welcome to harness! Run `harness setup` to get started.\n");
|
|
3269
|
+
} catch {
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
|
|
3273
|
+
// src/utils/node-version.ts
|
|
3274
|
+
import semver2 from "semver";
|
|
3275
|
+
var REQUIRED_NODE_VERSION = ">=22.0.0";
|
|
3276
|
+
function checkNodeVersion() {
|
|
3277
|
+
return {
|
|
3278
|
+
satisfies: semver2.satisfies(process.version, REQUIRED_NODE_VERSION),
|
|
3279
|
+
current: process.version,
|
|
3280
|
+
required: ">=22"
|
|
3281
|
+
};
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
// src/commands/setup.ts
|
|
3285
|
+
function checkNodeVersion2() {
|
|
3286
|
+
const result = checkNodeVersion();
|
|
3287
|
+
if (result.satisfies) {
|
|
3288
|
+
return { status: "pass", message: `Node.js ${result.current} (requires ${result.required})` };
|
|
3289
|
+
}
|
|
3290
|
+
return { status: "fail", message: `Node.js ${result.current} \u2014 requires ${result.required}` };
|
|
3291
|
+
}
|
|
3292
|
+
function runSlashCommandGeneration() {
|
|
3293
|
+
try {
|
|
3294
|
+
const results = generateSlashCommands({
|
|
3295
|
+
global: true,
|
|
3296
|
+
platforms: ["claude-code", "gemini-cli"],
|
|
3297
|
+
yes: true,
|
|
3298
|
+
includeGlobal: false,
|
|
3299
|
+
skillsDir: "",
|
|
3300
|
+
dryRun: false
|
|
3301
|
+
});
|
|
3302
|
+
const outputDirs = results.map((r) => r.outputDir).join(", ");
|
|
3303
|
+
return { status: "pass", message: `Generated global slash commands -> ${outputDirs}` };
|
|
3304
|
+
} catch (error) {
|
|
3305
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
3306
|
+
return { status: "fail", message: `Slash command generation failed \u2014 ${msg}` };
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
function detectClient(dirName) {
|
|
3310
|
+
return fs17.existsSync(path29.join(os4.homedir(), dirName));
|
|
3311
|
+
}
|
|
3312
|
+
function runMcpSetup(cwd) {
|
|
3313
|
+
const results = [];
|
|
3314
|
+
const clients = [
|
|
3315
|
+
{ name: "Claude Code", dir: ".claude", client: "claude", configTarget: ".mcp.json" },
|
|
3316
|
+
{
|
|
3317
|
+
name: "Gemini CLI",
|
|
3318
|
+
dir: ".gemini",
|
|
3319
|
+
client: "gemini",
|
|
3320
|
+
configTarget: ".gemini/settings.json"
|
|
3321
|
+
}
|
|
3322
|
+
];
|
|
3323
|
+
for (const { name, dir, client, configTarget } of clients) {
|
|
3324
|
+
if (!detectClient(dir)) {
|
|
3325
|
+
results.push({
|
|
3326
|
+
status: "warn",
|
|
3327
|
+
message: `${name} not detected \u2014 skipped MCP configuration`
|
|
3328
|
+
});
|
|
3329
|
+
continue;
|
|
3330
|
+
}
|
|
3331
|
+
try {
|
|
3332
|
+
setupMcp(cwd, client);
|
|
3333
|
+
results.push({ status: "pass", message: `Configured MCP for ${name} -> ${configTarget}` });
|
|
3334
|
+
} catch (error) {
|
|
3335
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
3336
|
+
results.push({
|
|
3337
|
+
status: "fail",
|
|
3338
|
+
message: `MCP configuration failed for ${name} \u2014 ${msg}`
|
|
3339
|
+
});
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
return results;
|
|
3343
|
+
}
|
|
3344
|
+
function formatStep(result) {
|
|
3345
|
+
const icon = result.status === "pass" ? chalk3.green("\u2713") : result.status === "warn" ? chalk3.yellow("\u26A0") : chalk3.red("\u2717");
|
|
3346
|
+
return ` ${icon} ${result.message}`;
|
|
3347
|
+
}
|
|
3348
|
+
function runSetup(cwd) {
|
|
3349
|
+
const steps = [];
|
|
3350
|
+
const nodeResult = checkNodeVersion2();
|
|
3351
|
+
steps.push(nodeResult);
|
|
3352
|
+
if (nodeResult.status === "fail") {
|
|
3353
|
+
return { steps, success: false };
|
|
3354
|
+
}
|
|
3355
|
+
const slashResult = runSlashCommandGeneration();
|
|
3356
|
+
steps.push(slashResult);
|
|
3357
|
+
const mcpResults = runMcpSetup(cwd);
|
|
3358
|
+
steps.push(...mcpResults);
|
|
3359
|
+
const success = steps.every((s) => s.status !== "fail");
|
|
3360
|
+
if (success) {
|
|
3361
|
+
markSetupComplete();
|
|
3362
|
+
}
|
|
3363
|
+
return { steps, success };
|
|
3364
|
+
}
|
|
3365
|
+
function createSetupCommand() {
|
|
3366
|
+
return new Command34("setup").description("Configure harness environment: slash commands, MCP, and more").action(() => {
|
|
3367
|
+
const cwd = process.cwd();
|
|
3368
|
+
console.log("");
|
|
3369
|
+
console.log(` ${chalk3.bold("harness setup")}`);
|
|
3370
|
+
console.log("");
|
|
3371
|
+
const { steps, success } = runSetup(cwd);
|
|
3372
|
+
for (const step of steps) {
|
|
3373
|
+
console.log(formatStep(step));
|
|
3374
|
+
}
|
|
3375
|
+
console.log("");
|
|
3376
|
+
if (success) {
|
|
3377
|
+
console.log(" Setup complete. Next steps:");
|
|
3378
|
+
console.log(" - Open a project directory and run /harness:initialize-project");
|
|
3379
|
+
console.log(" - Or run harness init --name my-project to scaffold a new one");
|
|
3380
|
+
console.log(" - Run harness doctor anytime to check your environment");
|
|
3381
|
+
console.log("");
|
|
3382
|
+
}
|
|
3383
|
+
process.exit(success ? ExitCode.SUCCESS : ExitCode.VALIDATION_FAILED);
|
|
3384
|
+
});
|
|
3385
|
+
}
|
|
3386
|
+
|
|
3387
|
+
// src/commands/doctor.ts
|
|
3388
|
+
import { Command as Command35 } from "commander";
|
|
3389
|
+
import * as fs18 from "fs";
|
|
3390
|
+
import * as os5 from "os";
|
|
3391
|
+
import * as path30 from "path";
|
|
3392
|
+
import chalk4 from "chalk";
|
|
3393
|
+
function checkNodeVersion3() {
|
|
3394
|
+
const result = checkNodeVersion();
|
|
3395
|
+
if (result.satisfies) {
|
|
3396
|
+
return {
|
|
3397
|
+
name: "node",
|
|
3398
|
+
status: "pass",
|
|
3399
|
+
message: `Node.js ${result.current} (requires ${result.required})`
|
|
3400
|
+
};
|
|
3401
|
+
}
|
|
3402
|
+
return {
|
|
3403
|
+
name: "node",
|
|
3404
|
+
status: "fail",
|
|
3405
|
+
message: `Node.js ${result.current} (requires ${result.required})`,
|
|
3406
|
+
fix: "Install Node.js >= 22: https://nodejs.org/"
|
|
3407
|
+
};
|
|
3408
|
+
}
|
|
3409
|
+
function countCommandFiles(dir, ext) {
|
|
3410
|
+
try {
|
|
3411
|
+
return fs18.readdirSync(dir).filter((f) => f.endsWith(ext)).length;
|
|
3412
|
+
} catch {
|
|
3413
|
+
return 0;
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
function checkSlashCommands() {
|
|
3417
|
+
const platforms = [
|
|
3418
|
+
{
|
|
3419
|
+
name: "Claude Code",
|
|
3420
|
+
dir: path30.join(os5.homedir(), ".claude", "commands", "harness"),
|
|
3421
|
+
ext: ".md",
|
|
3422
|
+
client: "claude-code"
|
|
3423
|
+
},
|
|
3424
|
+
{
|
|
3425
|
+
name: "Gemini CLI",
|
|
3426
|
+
dir: path30.join(os5.homedir(), ".gemini", "commands", "harness"),
|
|
3427
|
+
ext: ".toml",
|
|
3428
|
+
client: "gemini-cli"
|
|
3429
|
+
}
|
|
3430
|
+
];
|
|
3431
|
+
return platforms.map(({ name, dir, ext, client }) => {
|
|
3432
|
+
const count = countCommandFiles(dir, ext);
|
|
3433
|
+
if (count > 0) {
|
|
3434
|
+
return {
|
|
3435
|
+
name: `slash-commands-${client}`,
|
|
3436
|
+
status: "pass",
|
|
3437
|
+
message: `Slash commands installed -> ${dir} (${count} commands)`
|
|
3438
|
+
};
|
|
3439
|
+
}
|
|
3440
|
+
return {
|
|
3441
|
+
name: `slash-commands-${client}`,
|
|
3442
|
+
status: "fail",
|
|
3443
|
+
message: `No slash commands found for ${name}`,
|
|
3444
|
+
fix: "Run: harness setup"
|
|
3445
|
+
};
|
|
3446
|
+
});
|
|
3447
|
+
}
|
|
3448
|
+
function readJsonSafe(filePath) {
|
|
3449
|
+
try {
|
|
3450
|
+
if (!fs18.existsSync(filePath)) return null;
|
|
3451
|
+
return JSON.parse(fs18.readFileSync(filePath, "utf-8"));
|
|
3452
|
+
} catch {
|
|
3453
|
+
return null;
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
function checkMcpConfig(cwd) {
|
|
3457
|
+
const results = [];
|
|
3458
|
+
const claudeConfigPath = path30.join(cwd, ".mcp.json");
|
|
3459
|
+
const claudeConfig = readJsonSafe(claudeConfigPath);
|
|
3460
|
+
if (claudeConfig?.mcpServers?.["harness"]) {
|
|
3461
|
+
results.push({
|
|
3462
|
+
name: "mcp-claude",
|
|
3463
|
+
status: "pass",
|
|
3464
|
+
message: "MCP configured for Claude Code"
|
|
3465
|
+
});
|
|
3466
|
+
} else {
|
|
3467
|
+
results.push({
|
|
3468
|
+
name: "mcp-claude",
|
|
3469
|
+
status: "fail",
|
|
3470
|
+
message: "MCP not configured for Claude Code",
|
|
3471
|
+
fix: "Run: harness setup-mcp --client claude"
|
|
3472
|
+
});
|
|
3473
|
+
}
|
|
3474
|
+
const geminiConfigPath = path30.join(os5.homedir(), ".gemini", "settings.json");
|
|
3475
|
+
const geminiConfig = readJsonSafe(geminiConfigPath);
|
|
3476
|
+
if (geminiConfig?.mcpServers?.["harness"]) {
|
|
3477
|
+
results.push({
|
|
3478
|
+
name: "mcp-gemini",
|
|
3479
|
+
status: "pass",
|
|
3480
|
+
message: "MCP configured for Gemini CLI"
|
|
3481
|
+
});
|
|
3482
|
+
} else {
|
|
3483
|
+
results.push({
|
|
3484
|
+
name: "mcp-gemini",
|
|
3485
|
+
status: "fail",
|
|
3486
|
+
message: "MCP not configured for Gemini CLI",
|
|
3487
|
+
fix: "Run: harness setup-mcp --client gemini"
|
|
3488
|
+
});
|
|
3489
|
+
}
|
|
3490
|
+
return results;
|
|
3491
|
+
}
|
|
3492
|
+
function runDoctor(cwd) {
|
|
3493
|
+
const checks = [];
|
|
3494
|
+
checks.push(checkNodeVersion3());
|
|
3495
|
+
checks.push(...checkSlashCommands());
|
|
3496
|
+
checks.push(...checkMcpConfig(cwd));
|
|
3497
|
+
const allPassed = checks.every((c) => c.status === "pass");
|
|
3498
|
+
return { checks, allPassed };
|
|
3499
|
+
}
|
|
3500
|
+
function formatCheck(check) {
|
|
3501
|
+
const icon = check.status === "pass" ? chalk4.green("\u2713") : chalk4.red("\u2717");
|
|
3502
|
+
let line = ` ${icon} ${check.message}`;
|
|
3503
|
+
if (check.status === "fail" && check.fix) {
|
|
3504
|
+
line += `
|
|
3505
|
+
-> ${check.fix}`;
|
|
3506
|
+
}
|
|
3507
|
+
return line;
|
|
3508
|
+
}
|
|
3509
|
+
function createDoctorCommand() {
|
|
3510
|
+
return new Command35("doctor").description("Check environment health: Node version, slash commands, MCP configuration").action((_opts, cmd) => {
|
|
3511
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
3512
|
+
const cwd = process.cwd();
|
|
3513
|
+
const useJson = globalOpts.json;
|
|
3514
|
+
const result = runDoctor(cwd);
|
|
3515
|
+
if (useJson) {
|
|
3516
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3517
|
+
} else {
|
|
3518
|
+
console.log("");
|
|
3519
|
+
console.log(` ${chalk4.bold("harness doctor")}`);
|
|
3520
|
+
console.log("");
|
|
3521
|
+
for (const check of result.checks) {
|
|
3522
|
+
console.log(formatCheck(check));
|
|
3523
|
+
}
|
|
3524
|
+
console.log("");
|
|
3525
|
+
const passed = result.checks.filter((c) => c.status === "pass").length;
|
|
3526
|
+
const total = result.checks.length;
|
|
3527
|
+
console.log(` ${passed}/${total} checks passed`);
|
|
3528
|
+
console.log("");
|
|
3529
|
+
}
|
|
3530
|
+
process.exit(result.allPassed ? ExitCode.SUCCESS : ExitCode.VALIDATION_FAILED);
|
|
3531
|
+
});
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3149
3534
|
// src/commands/ci/index.ts
|
|
3150
|
-
import { Command as
|
|
3535
|
+
import { Command as Command38 } from "commander";
|
|
3151
3536
|
|
|
3152
3537
|
// src/commands/ci/check.ts
|
|
3153
|
-
import { Command as
|
|
3538
|
+
import { Command as Command36 } from "commander";
|
|
3154
3539
|
var VALID_CHECKS = [
|
|
3155
3540
|
"validate",
|
|
3156
3541
|
"deps",
|
|
@@ -3190,9 +3575,9 @@ function parseFailOn(failOn) {
|
|
|
3190
3575
|
return "error";
|
|
3191
3576
|
}
|
|
3192
3577
|
function createCheckCommand() {
|
|
3193
|
-
return new
|
|
3578
|
+
return new Command36("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) => {
|
|
3194
3579
|
const globalOpts = cmd.optsWithGlobals();
|
|
3195
|
-
const mode = globalOpts
|
|
3580
|
+
const mode = resolveOutputMode(globalOpts);
|
|
3196
3581
|
const skip = parseSkip(opts.skip);
|
|
3197
3582
|
const failOn = parseFailOn(opts.failOn);
|
|
3198
3583
|
const result = await runCICheck({
|
|
@@ -3234,9 +3619,9 @@ function createCheckCommand() {
|
|
|
3234
3619
|
}
|
|
3235
3620
|
|
|
3236
3621
|
// src/commands/ci/init.ts
|
|
3237
|
-
import { Command as
|
|
3238
|
-
import * as
|
|
3239
|
-
import * as
|
|
3622
|
+
import { Command as Command37 } from "commander";
|
|
3623
|
+
import * as fs19 from "fs";
|
|
3624
|
+
import * as path31 from "path";
|
|
3240
3625
|
var ALL_CHECKS = [
|
|
3241
3626
|
"validate",
|
|
3242
3627
|
"deps",
|
|
@@ -3337,12 +3722,12 @@ function generateCIConfig(options) {
|
|
|
3337
3722
|
});
|
|
3338
3723
|
}
|
|
3339
3724
|
function detectPlatform() {
|
|
3340
|
-
if (
|
|
3341
|
-
if (
|
|
3725
|
+
if (fs19.existsSync(".github")) return "github";
|
|
3726
|
+
if (fs19.existsSync(".gitlab-ci.yml")) return "gitlab";
|
|
3342
3727
|
return null;
|
|
3343
3728
|
}
|
|
3344
3729
|
function createInitCommand2() {
|
|
3345
|
-
return new
|
|
3730
|
+
return new Command37("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) => {
|
|
3346
3731
|
const globalOpts = cmd.optsWithGlobals();
|
|
3347
3732
|
const platform = opts.platform ?? detectPlatform() ?? "generic";
|
|
3348
3733
|
const checks = opts.checks ? opts.checks.split(",").map((s) => s.trim()) : void 0;
|
|
@@ -3354,12 +3739,12 @@ function createInitCommand2() {
|
|
|
3354
3739
|
process.exit(result.error.exitCode);
|
|
3355
3740
|
}
|
|
3356
3741
|
const { filename, content } = result.value;
|
|
3357
|
-
const targetPath =
|
|
3358
|
-
const dir =
|
|
3359
|
-
|
|
3360
|
-
|
|
3742
|
+
const targetPath = path31.resolve(filename);
|
|
3743
|
+
const dir = path31.dirname(targetPath);
|
|
3744
|
+
fs19.mkdirSync(dir, { recursive: true });
|
|
3745
|
+
fs19.writeFileSync(targetPath, content);
|
|
3361
3746
|
if (platform === "generic" && process.platform !== "win32") {
|
|
3362
|
-
|
|
3747
|
+
fs19.chmodSync(targetPath, "755");
|
|
3363
3748
|
}
|
|
3364
3749
|
if (globalOpts.json) {
|
|
3365
3750
|
console.log(JSON.stringify({ file: filename, platform }));
|
|
@@ -3372,18 +3757,275 @@ function createInitCommand2() {
|
|
|
3372
3757
|
|
|
3373
3758
|
// src/commands/ci/index.ts
|
|
3374
3759
|
function createCICommand() {
|
|
3375
|
-
const command = new
|
|
3760
|
+
const command = new Command38("ci").description("CI/CD integration commands");
|
|
3376
3761
|
command.addCommand(createCheckCommand());
|
|
3377
3762
|
command.addCommand(createInitCommand2());
|
|
3378
3763
|
return command;
|
|
3379
3764
|
}
|
|
3380
3765
|
|
|
3766
|
+
// src/commands/hooks/index.ts
|
|
3767
|
+
import { Command as Command42 } from "commander";
|
|
3768
|
+
|
|
3769
|
+
// src/commands/hooks/init.ts
|
|
3770
|
+
import { Command as Command39 } from "commander";
|
|
3771
|
+
import * as fs20 from "fs";
|
|
3772
|
+
import * as path32 from "path";
|
|
3773
|
+
import { fileURLToPath } from "url";
|
|
3774
|
+
|
|
3775
|
+
// src/hooks/profiles.ts
|
|
3776
|
+
var HOOK_SCRIPTS = [
|
|
3777
|
+
{ name: "block-no-verify", event: "PreToolUse", matcher: "Bash", minProfile: "minimal" },
|
|
3778
|
+
{ name: "protect-config", event: "PreToolUse", matcher: "Write|Edit", minProfile: "standard" },
|
|
3779
|
+
{ name: "quality-gate", event: "PostToolUse", matcher: "Edit|Write", minProfile: "standard" },
|
|
3780
|
+
{ name: "pre-compact-state", event: "PreCompact", matcher: "*", minProfile: "standard" },
|
|
3781
|
+
{ name: "cost-tracker", event: "Stop", matcher: "*", minProfile: "strict" }
|
|
3782
|
+
];
|
|
3783
|
+
var PROFILE_ORDER = ["minimal", "standard", "strict"];
|
|
3784
|
+
function hooksForProfile(profile) {
|
|
3785
|
+
const profileIndex = PROFILE_ORDER.indexOf(profile);
|
|
3786
|
+
return HOOK_SCRIPTS.filter((h) => PROFILE_ORDER.indexOf(h.minProfile) <= profileIndex).map(
|
|
3787
|
+
(h) => h.name
|
|
3788
|
+
);
|
|
3789
|
+
}
|
|
3790
|
+
var PROFILES = {
|
|
3791
|
+
minimal: hooksForProfile("minimal"),
|
|
3792
|
+
standard: hooksForProfile("standard"),
|
|
3793
|
+
strict: hooksForProfile("strict")
|
|
3794
|
+
};
|
|
3795
|
+
|
|
3796
|
+
// src/commands/hooks/init.ts
|
|
3797
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
3798
|
+
var __dirname = path32.dirname(__filename);
|
|
3799
|
+
var VALID_PROFILES = ["minimal", "standard", "strict"];
|
|
3800
|
+
function resolveHookSourceDir() {
|
|
3801
|
+
const candidate = path32.resolve(__dirname, "..", "..", "hooks");
|
|
3802
|
+
if (fs20.existsSync(candidate)) {
|
|
3803
|
+
return candidate;
|
|
3804
|
+
}
|
|
3805
|
+
throw new Error(`Cannot locate hook scripts directory. Expected at: ${candidate}`);
|
|
3806
|
+
}
|
|
3807
|
+
function buildSettingsHooks(profile) {
|
|
3808
|
+
const activeHookNames = PROFILES[profile];
|
|
3809
|
+
const activeScripts = HOOK_SCRIPTS.filter((h) => activeHookNames.includes(h.name));
|
|
3810
|
+
const hooks = {};
|
|
3811
|
+
for (const script of activeScripts) {
|
|
3812
|
+
if (!hooks[script.event]) {
|
|
3813
|
+
hooks[script.event] = [];
|
|
3814
|
+
}
|
|
3815
|
+
hooks[script.event].push({
|
|
3816
|
+
matcher: script.matcher,
|
|
3817
|
+
hooks: [{ type: "command", command: `node .harness/hooks/${script.name}.js` }]
|
|
3818
|
+
});
|
|
3819
|
+
}
|
|
3820
|
+
return hooks;
|
|
3821
|
+
}
|
|
3822
|
+
function mergeSettings(existing, hooksConfig) {
|
|
3823
|
+
return {
|
|
3824
|
+
...existing,
|
|
3825
|
+
hooks: hooksConfig
|
|
3826
|
+
};
|
|
3827
|
+
}
|
|
3828
|
+
function initHooks(options) {
|
|
3829
|
+
const { profile, projectDir } = options;
|
|
3830
|
+
const hooksDestDir = path32.join(projectDir, ".harness", "hooks");
|
|
3831
|
+
fs20.mkdirSync(hooksDestDir, { recursive: true });
|
|
3832
|
+
if (fs20.existsSync(hooksDestDir)) {
|
|
3833
|
+
for (const entry of fs20.readdirSync(hooksDestDir)) {
|
|
3834
|
+
if (entry.endsWith(".js")) {
|
|
3835
|
+
fs20.unlinkSync(path32.join(hooksDestDir, entry));
|
|
3836
|
+
}
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
const sourceDir = resolveHookSourceDir();
|
|
3840
|
+
const copiedScripts = [];
|
|
3841
|
+
const activeNames = PROFILES[profile];
|
|
3842
|
+
const activeScripts = HOOK_SCRIPTS.filter((h) => activeNames.includes(h.name));
|
|
3843
|
+
for (const script of activeScripts) {
|
|
3844
|
+
const srcFile = path32.join(sourceDir, `${script.name}.js`);
|
|
3845
|
+
const destFile = path32.join(hooksDestDir, `${script.name}.js`);
|
|
3846
|
+
if (fs20.existsSync(srcFile)) {
|
|
3847
|
+
fs20.copyFileSync(srcFile, destFile);
|
|
3848
|
+
copiedScripts.push(script.name);
|
|
3849
|
+
}
|
|
3850
|
+
}
|
|
3851
|
+
const profilePath = path32.join(hooksDestDir, "profile.json");
|
|
3852
|
+
fs20.writeFileSync(profilePath, JSON.stringify({ profile }, null, 2) + "\n");
|
|
3853
|
+
const claudeDir = path32.join(projectDir, ".claude");
|
|
3854
|
+
fs20.mkdirSync(claudeDir, { recursive: true });
|
|
3855
|
+
const settingsPath = path32.join(claudeDir, "settings.json");
|
|
3856
|
+
let existing = {};
|
|
3857
|
+
if (fs20.existsSync(settingsPath)) {
|
|
3858
|
+
try {
|
|
3859
|
+
existing = JSON.parse(fs20.readFileSync(settingsPath, "utf-8"));
|
|
3860
|
+
} catch (e) {
|
|
3861
|
+
throw new Error(
|
|
3862
|
+
`Malformed .claude/settings.json \u2014 fix the JSON syntax before running hooks init. Parse error: ${e instanceof Error ? e.message : String(e)}`,
|
|
3863
|
+
{ cause: e }
|
|
3864
|
+
);
|
|
3865
|
+
}
|
|
3866
|
+
}
|
|
3867
|
+
const hooksConfig = buildSettingsHooks(profile);
|
|
3868
|
+
const merged = mergeSettings(existing, hooksConfig);
|
|
3869
|
+
fs20.writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + "\n");
|
|
3870
|
+
return { copiedScripts, settingsPath, profilePath };
|
|
3871
|
+
}
|
|
3872
|
+
function createInitCommand3() {
|
|
3873
|
+
return new Command39("init").description("Install Claude Code hook configurations into the current project").option("--profile <profile>", "Hook profile: minimal, standard, or strict", "standard").action(async (opts, cmd) => {
|
|
3874
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
3875
|
+
const profile = opts.profile;
|
|
3876
|
+
if (!VALID_PROFILES.includes(profile)) {
|
|
3877
|
+
logger.error(`Invalid profile: ${profile}. Must be one of: ${VALID_PROFILES.join(", ")}`);
|
|
3878
|
+
process.exit(2);
|
|
3879
|
+
}
|
|
3880
|
+
const projectDir = process.cwd();
|
|
3881
|
+
try {
|
|
3882
|
+
const result = initHooks({ profile, projectDir });
|
|
3883
|
+
if (globalOpts.json) {
|
|
3884
|
+
console.log(
|
|
3885
|
+
JSON.stringify({
|
|
3886
|
+
profile,
|
|
3887
|
+
copiedScripts: result.copiedScripts,
|
|
3888
|
+
settingsPath: result.settingsPath,
|
|
3889
|
+
profilePath: result.profilePath
|
|
3890
|
+
})
|
|
3891
|
+
);
|
|
3892
|
+
} else {
|
|
3893
|
+
logger.success(
|
|
3894
|
+
`Installed ${result.copiedScripts.length} hook scripts to .harness/hooks/`
|
|
3895
|
+
);
|
|
3896
|
+
logger.info(`Profile: ${profile}`);
|
|
3897
|
+
logger.info(`Settings: ${path32.relative(projectDir, result.settingsPath)}`);
|
|
3898
|
+
logger.dim("Run 'harness hooks list' to see installed hooks");
|
|
3899
|
+
}
|
|
3900
|
+
} catch (err) {
|
|
3901
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3902
|
+
logger.error(`Failed to initialize hooks: ${message}`);
|
|
3903
|
+
process.exit(2);
|
|
3904
|
+
}
|
|
3905
|
+
});
|
|
3906
|
+
}
|
|
3907
|
+
|
|
3908
|
+
// src/commands/hooks/list.ts
|
|
3909
|
+
import { Command as Command40 } from "commander";
|
|
3910
|
+
import * as fs21 from "fs";
|
|
3911
|
+
import * as path33 from "path";
|
|
3912
|
+
function listHooks(projectDir) {
|
|
3913
|
+
const hooksDir = path33.join(projectDir, ".harness", "hooks");
|
|
3914
|
+
const profilePath = path33.join(hooksDir, "profile.json");
|
|
3915
|
+
if (!fs21.existsSync(profilePath)) {
|
|
3916
|
+
return { installed: false, profile: null, hooks: [] };
|
|
3917
|
+
}
|
|
3918
|
+
let profile = "standard";
|
|
3919
|
+
let warning;
|
|
3920
|
+
try {
|
|
3921
|
+
const data = JSON.parse(fs21.readFileSync(profilePath, "utf-8"));
|
|
3922
|
+
if (data.profile && ["minimal", "standard", "strict"].includes(data.profile)) {
|
|
3923
|
+
profile = data.profile;
|
|
3924
|
+
}
|
|
3925
|
+
} catch {
|
|
3926
|
+
warning = "Malformed profile.json \u2014 defaulting to standard profile";
|
|
3927
|
+
}
|
|
3928
|
+
const activeNames = PROFILES[profile];
|
|
3929
|
+
const hooks = HOOK_SCRIPTS.filter((h) => activeNames.includes(h.name)).map((h) => ({
|
|
3930
|
+
name: h.name,
|
|
3931
|
+
event: h.event,
|
|
3932
|
+
matcher: h.matcher,
|
|
3933
|
+
scriptPath: path33.join(".harness", "hooks", `${h.name}.js`)
|
|
3934
|
+
}));
|
|
3935
|
+
const result = { installed: true, profile, hooks };
|
|
3936
|
+
if (warning) {
|
|
3937
|
+
result.warning = warning;
|
|
3938
|
+
}
|
|
3939
|
+
return result;
|
|
3940
|
+
}
|
|
3941
|
+
function createListCommand3() {
|
|
3942
|
+
return new Command40("list").description("Show installed hooks and active profile").action(async (_opts, cmd) => {
|
|
3943
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
3944
|
+
const projectDir = process.cwd();
|
|
3945
|
+
const result = listHooks(projectDir);
|
|
3946
|
+
if (globalOpts.json) {
|
|
3947
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3948
|
+
return;
|
|
3949
|
+
}
|
|
3950
|
+
if (!result.installed) {
|
|
3951
|
+
logger.info("No harness hooks installed. Run 'harness hooks init' to set up hooks.");
|
|
3952
|
+
return;
|
|
3953
|
+
}
|
|
3954
|
+
logger.info(`Profile: ${result.profile}`);
|
|
3955
|
+
logger.info(`Hooks (${result.hooks.length}):`);
|
|
3956
|
+
for (const hook of result.hooks) {
|
|
3957
|
+
console.log(` ${hook.name} ${hook.event}:${hook.matcher} ${hook.scriptPath}`);
|
|
3958
|
+
}
|
|
3959
|
+
});
|
|
3960
|
+
}
|
|
3961
|
+
|
|
3962
|
+
// src/commands/hooks/remove.ts
|
|
3963
|
+
import { Command as Command41 } from "commander";
|
|
3964
|
+
import * as fs22 from "fs";
|
|
3965
|
+
import * as path34 from "path";
|
|
3966
|
+
function removeHooks(projectDir) {
|
|
3967
|
+
const hooksDir = path34.join(projectDir, ".harness", "hooks");
|
|
3968
|
+
const settingsPath = path34.join(projectDir, ".claude", "settings.json");
|
|
3969
|
+
let removed = false;
|
|
3970
|
+
let settingsCleaned = false;
|
|
3971
|
+
if (fs22.existsSync(hooksDir)) {
|
|
3972
|
+
fs22.rmSync(hooksDir, { recursive: true, force: true });
|
|
3973
|
+
removed = true;
|
|
3974
|
+
}
|
|
3975
|
+
if (fs22.existsSync(settingsPath)) {
|
|
3976
|
+
try {
|
|
3977
|
+
const settings = JSON.parse(fs22.readFileSync(settingsPath, "utf-8"));
|
|
3978
|
+
if (settings.hooks !== void 0) {
|
|
3979
|
+
delete settings.hooks;
|
|
3980
|
+
settingsCleaned = true;
|
|
3981
|
+
if (Object.keys(settings).length === 0) {
|
|
3982
|
+
fs22.unlinkSync(settingsPath);
|
|
3983
|
+
} else {
|
|
3984
|
+
fs22.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
3985
|
+
}
|
|
3986
|
+
}
|
|
3987
|
+
} catch {
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3990
|
+
return { removed, hooksDir, settingsCleaned };
|
|
3991
|
+
}
|
|
3992
|
+
function createRemoveCommand() {
|
|
3993
|
+
return new Command41("remove").description("Remove harness-managed hooks from the current project").action(async (_opts, cmd) => {
|
|
3994
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
3995
|
+
const projectDir = process.cwd();
|
|
3996
|
+
const result = removeHooks(projectDir);
|
|
3997
|
+
if (globalOpts.json) {
|
|
3998
|
+
console.log(JSON.stringify(result));
|
|
3999
|
+
return;
|
|
4000
|
+
}
|
|
4001
|
+
if (!result.removed && !result.settingsCleaned) {
|
|
4002
|
+
logger.info("No harness hooks found to remove.");
|
|
4003
|
+
return;
|
|
4004
|
+
}
|
|
4005
|
+
if (result.removed) {
|
|
4006
|
+
logger.success("Removed .harness/hooks/ directory");
|
|
4007
|
+
}
|
|
4008
|
+
if (result.settingsCleaned) {
|
|
4009
|
+
logger.success("Cleaned hook entries from .claude/settings.json");
|
|
4010
|
+
}
|
|
4011
|
+
});
|
|
4012
|
+
}
|
|
4013
|
+
|
|
4014
|
+
// src/commands/hooks/index.ts
|
|
4015
|
+
function createHooksCommand() {
|
|
4016
|
+
const command = new Command42("hooks").description("Manage Claude Code hook configurations");
|
|
4017
|
+
command.addCommand(createInitCommand3());
|
|
4018
|
+
command.addCommand(createListCommand3());
|
|
4019
|
+
command.addCommand(createRemoveCommand());
|
|
4020
|
+
return command;
|
|
4021
|
+
}
|
|
4022
|
+
|
|
3381
4023
|
// src/commands/update.ts
|
|
3382
|
-
import { Command as
|
|
4024
|
+
import { Command as Command43 } from "commander";
|
|
3383
4025
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
3384
4026
|
import { realpathSync } from "fs";
|
|
3385
4027
|
import readline2 from "readline";
|
|
3386
|
-
import
|
|
4028
|
+
import chalk5 from "chalk";
|
|
3387
4029
|
function detectPackageManager() {
|
|
3388
4030
|
try {
|
|
3389
4031
|
const argv1 = process.argv[1];
|
|
@@ -3439,15 +4081,30 @@ function prompt(question) {
|
|
|
3439
4081
|
input: process.stdin,
|
|
3440
4082
|
output: process.stdout
|
|
3441
4083
|
});
|
|
3442
|
-
return new Promise((
|
|
4084
|
+
return new Promise((resolve30) => {
|
|
3443
4085
|
rl.question(question, (answer) => {
|
|
3444
4086
|
rl.close();
|
|
3445
|
-
|
|
4087
|
+
resolve30(answer.trim().toLowerCase());
|
|
3446
4088
|
});
|
|
3447
4089
|
});
|
|
3448
4090
|
}
|
|
4091
|
+
async function offerRegeneration() {
|
|
4092
|
+
console.log("");
|
|
4093
|
+
const regenAnswer = await prompt("Regenerate slash commands and agent definitions? (Y/n) ");
|
|
4094
|
+
if (regenAnswer === "n" || regenAnswer === "no") return;
|
|
4095
|
+
const scopeAnswer = await prompt("Generate for (G)lobal or (l)ocal project? (G/l) ");
|
|
4096
|
+
const isGlobal = scopeAnswer !== "l" && scopeAnswer !== "local";
|
|
4097
|
+
try {
|
|
4098
|
+
execFileSync4("harness", ["generate", ...isGlobal ? ["--global"] : []], {
|
|
4099
|
+
stdio: "inherit"
|
|
4100
|
+
});
|
|
4101
|
+
} catch {
|
|
4102
|
+
logger.warn("Generation failed. Run manually:");
|
|
4103
|
+
console.log(` ${chalk5.cyan(`harness generate${isGlobal ? " --global" : ""}`)}`);
|
|
4104
|
+
}
|
|
4105
|
+
}
|
|
3449
4106
|
function createUpdateCommand() {
|
|
3450
|
-
return new
|
|
4107
|
+
return new Command43("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) => {
|
|
3451
4108
|
const globalOpts = cmd.optsWithGlobals();
|
|
3452
4109
|
const pm = detectPackageManager();
|
|
3453
4110
|
if (globalOpts.verbose) {
|
|
@@ -3469,8 +4126,8 @@ function createUpdateCommand() {
|
|
|
3469
4126
|
}
|
|
3470
4127
|
if (currentVersion) {
|
|
3471
4128
|
console.log("");
|
|
3472
|
-
logger.info(`Current CLI version: ${
|
|
3473
|
-
logger.info(`Latest CLI version: ${
|
|
4129
|
+
logger.info(`Current CLI version: ${chalk5.dim(`v${currentVersion}`)}`);
|
|
4130
|
+
logger.info(`Latest CLI version: ${chalk5.green(`v${latestCliVersion}`)}`);
|
|
3474
4131
|
console.log("");
|
|
3475
4132
|
}
|
|
3476
4133
|
}
|
|
@@ -3496,31 +4153,18 @@ function createUpdateCommand() {
|
|
|
3496
4153
|
} catch {
|
|
3497
4154
|
console.log("");
|
|
3498
4155
|
logger.error("Update failed. You can try manually:");
|
|
3499
|
-
console.log(` ${
|
|
4156
|
+
console.log(` ${chalk5.cyan(installCmd)}`);
|
|
3500
4157
|
process.exit(ExitCode.ERROR);
|
|
3501
4158
|
}
|
|
3502
|
-
|
|
3503
|
-
const regenAnswer = await prompt("Regenerate slash commands and agent definitions? (Y/n) ");
|
|
3504
|
-
if (regenAnswer !== "n" && regenAnswer !== "no") {
|
|
3505
|
-
const scopeAnswer = await prompt("Generate for (G)lobal or (l)ocal project? (G/l) ");
|
|
3506
|
-
const isGlobal = scopeAnswer !== "l" && scopeAnswer !== "local";
|
|
3507
|
-
try {
|
|
3508
|
-
execFileSync4("harness", ["generate", ...isGlobal ? ["--global"] : []], {
|
|
3509
|
-
stdio: "inherit"
|
|
3510
|
-
});
|
|
3511
|
-
} catch {
|
|
3512
|
-
logger.warn("Generation failed. Run manually:");
|
|
3513
|
-
console.log(` ${chalk3.cyan(`harness generate${isGlobal ? " --global" : ""}`)}`);
|
|
3514
|
-
}
|
|
3515
|
-
}
|
|
4159
|
+
await offerRegeneration();
|
|
3516
4160
|
process.exit(ExitCode.SUCCESS);
|
|
3517
4161
|
});
|
|
3518
4162
|
}
|
|
3519
4163
|
|
|
3520
4164
|
// src/commands/generate.ts
|
|
3521
|
-
import { Command as
|
|
4165
|
+
import { Command as Command44 } from "commander";
|
|
3522
4166
|
function createGenerateCommand3() {
|
|
3523
|
-
return new
|
|
4167
|
+
return new Command44("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) => {
|
|
3524
4168
|
const globalOpts = cmd.optsWithGlobals();
|
|
3525
4169
|
const platforms = opts.platforms.split(",").map((p) => p.trim());
|
|
3526
4170
|
for (const p of platforms) {
|
|
@@ -3579,10 +4223,10 @@ function createGenerateCommand3() {
|
|
|
3579
4223
|
}
|
|
3580
4224
|
|
|
3581
4225
|
// src/commands/graph/scan.ts
|
|
3582
|
-
import { Command as
|
|
3583
|
-
import * as
|
|
4226
|
+
import { Command as Command45 } from "commander";
|
|
4227
|
+
import * as path35 from "path";
|
|
3584
4228
|
async function runScan(projectPath) {
|
|
3585
|
-
const { GraphStore, CodeIngestor, TopologicalLinker, KnowledgeIngestor, GitIngestor } = await import("./dist-
|
|
4229
|
+
const { GraphStore, CodeIngestor, TopologicalLinker, KnowledgeIngestor, GitIngestor } = await import("./dist-B26DFXMP.js");
|
|
3586
4230
|
const store = new GraphStore();
|
|
3587
4231
|
const start = Date.now();
|
|
3588
4232
|
await new CodeIngestor(store).ingest(projectPath);
|
|
@@ -3593,13 +4237,13 @@ async function runScan(projectPath) {
|
|
|
3593
4237
|
await new GitIngestor(store).ingest(projectPath);
|
|
3594
4238
|
} catch {
|
|
3595
4239
|
}
|
|
3596
|
-
const graphDir =
|
|
4240
|
+
const graphDir = path35.join(projectPath, ".harness", "graph");
|
|
3597
4241
|
await store.save(graphDir);
|
|
3598
4242
|
return { nodeCount: store.nodeCount, edgeCount: store.edgeCount, durationMs: Date.now() - start };
|
|
3599
4243
|
}
|
|
3600
4244
|
function createScanCommand() {
|
|
3601
|
-
return new
|
|
3602
|
-
const projectPath =
|
|
4245
|
+
return new Command45("scan").description("Scan project and build knowledge graph").argument("[path]", "Project root path", ".").action(async (inputPath, _opts, cmd) => {
|
|
4246
|
+
const projectPath = path35.resolve(inputPath);
|
|
3603
4247
|
const globalOpts = cmd.optsWithGlobals();
|
|
3604
4248
|
try {
|
|
3605
4249
|
const result = await runScan(projectPath);
|
|
@@ -3618,13 +4262,13 @@ function createScanCommand() {
|
|
|
3618
4262
|
}
|
|
3619
4263
|
|
|
3620
4264
|
// src/commands/graph/ingest.ts
|
|
3621
|
-
import { Command as
|
|
3622
|
-
import * as
|
|
4265
|
+
import { Command as Command46 } from "commander";
|
|
4266
|
+
import * as path36 from "path";
|
|
3623
4267
|
async function loadConnectorConfig(projectPath, source) {
|
|
3624
4268
|
try {
|
|
3625
|
-
const
|
|
3626
|
-
const configPath =
|
|
3627
|
-
const config = JSON.parse(await
|
|
4269
|
+
const fs29 = await import("fs/promises");
|
|
4270
|
+
const configPath = path36.join(projectPath, "harness.config.json");
|
|
4271
|
+
const config = JSON.parse(await fs29.readFile(configPath, "utf-8"));
|
|
3628
4272
|
const connector = config.graph?.connectors?.find(
|
|
3629
4273
|
(c) => c.source === source
|
|
3630
4274
|
);
|
|
@@ -3663,8 +4307,8 @@ async function runIngest(projectPath, source, opts) {
|
|
|
3663
4307
|
SyncManager,
|
|
3664
4308
|
JiraConnector,
|
|
3665
4309
|
SlackConnector
|
|
3666
|
-
} = await import("./dist-
|
|
3667
|
-
const graphDir =
|
|
4310
|
+
} = await import("./dist-B26DFXMP.js");
|
|
4311
|
+
const graphDir = path36.join(projectPath, ".harness", "graph");
|
|
3668
4312
|
const store = new GraphStore();
|
|
3669
4313
|
await store.load(graphDir);
|
|
3670
4314
|
if (opts?.all) {
|
|
@@ -3725,13 +4369,13 @@ async function runIngest(projectPath, source, opts) {
|
|
|
3725
4369
|
return result;
|
|
3726
4370
|
}
|
|
3727
4371
|
function createIngestCommand() {
|
|
3728
|
-
return new
|
|
4372
|
+
return new Command46("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) => {
|
|
3729
4373
|
if (!opts.source && !opts.all) {
|
|
3730
4374
|
console.error("Error: --source or --all is required");
|
|
3731
4375
|
process.exit(1);
|
|
3732
4376
|
}
|
|
3733
4377
|
const globalOpts = cmd.optsWithGlobals();
|
|
3734
|
-
const projectPath =
|
|
4378
|
+
const projectPath = path36.resolve(globalOpts.config ? path36.dirname(globalOpts.config) : ".");
|
|
3735
4379
|
try {
|
|
3736
4380
|
const result = await runIngest(projectPath, opts.source ?? "", {
|
|
3737
4381
|
full: opts.full,
|
|
@@ -3753,12 +4397,12 @@ function createIngestCommand() {
|
|
|
3753
4397
|
}
|
|
3754
4398
|
|
|
3755
4399
|
// src/commands/graph/query.ts
|
|
3756
|
-
import { Command as
|
|
3757
|
-
import * as
|
|
4400
|
+
import { Command as Command47 } from "commander";
|
|
4401
|
+
import * as path37 from "path";
|
|
3758
4402
|
async function runQuery(projectPath, rootNodeId, opts) {
|
|
3759
|
-
const { GraphStore, ContextQL } = await import("./dist-
|
|
4403
|
+
const { GraphStore, ContextQL } = await import("./dist-B26DFXMP.js");
|
|
3760
4404
|
const store = new GraphStore();
|
|
3761
|
-
const graphDir =
|
|
4405
|
+
const graphDir = path37.join(projectPath, ".harness", "graph");
|
|
3762
4406
|
const loaded = await store.load(graphDir);
|
|
3763
4407
|
if (!loaded) throw new Error("No graph found. Run `harness scan` first.");
|
|
3764
4408
|
const params = {
|
|
@@ -3772,9 +4416,9 @@ async function runQuery(projectPath, rootNodeId, opts) {
|
|
|
3772
4416
|
return cql.execute(params);
|
|
3773
4417
|
}
|
|
3774
4418
|
function createQueryCommand() {
|
|
3775
|
-
return new
|
|
4419
|
+
return new Command47("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) => {
|
|
3776
4420
|
const globalOpts = cmd.optsWithGlobals();
|
|
3777
|
-
const projectPath =
|
|
4421
|
+
const projectPath = path37.resolve(globalOpts.config ? path37.dirname(globalOpts.config) : ".");
|
|
3778
4422
|
try {
|
|
3779
4423
|
const result = await runQuery(projectPath, rootNodeId, {
|
|
3780
4424
|
depth: parseInt(opts.depth),
|
|
@@ -3800,21 +4444,21 @@ function createQueryCommand() {
|
|
|
3800
4444
|
}
|
|
3801
4445
|
|
|
3802
4446
|
// src/commands/graph/index.ts
|
|
3803
|
-
import { Command as
|
|
4447
|
+
import { Command as Command48 } from "commander";
|
|
3804
4448
|
|
|
3805
4449
|
// src/commands/graph/status.ts
|
|
3806
|
-
import * as
|
|
4450
|
+
import * as path38 from "path";
|
|
3807
4451
|
async function runGraphStatus(projectPath) {
|
|
3808
|
-
const { GraphStore } = await import("./dist-
|
|
3809
|
-
const graphDir =
|
|
4452
|
+
const { GraphStore } = await import("./dist-B26DFXMP.js");
|
|
4453
|
+
const graphDir = path38.join(projectPath, ".harness", "graph");
|
|
3810
4454
|
const store = new GraphStore();
|
|
3811
4455
|
const loaded = await store.load(graphDir);
|
|
3812
4456
|
if (!loaded) return { status: "no_graph", message: "No graph found. Run `harness scan` first." };
|
|
3813
|
-
const
|
|
3814
|
-
const metaPath =
|
|
4457
|
+
const fs29 = await import("fs/promises");
|
|
4458
|
+
const metaPath = path38.join(graphDir, "metadata.json");
|
|
3815
4459
|
let lastScan = "unknown";
|
|
3816
4460
|
try {
|
|
3817
|
-
const meta = JSON.parse(await
|
|
4461
|
+
const meta = JSON.parse(await fs29.readFile(metaPath, "utf-8"));
|
|
3818
4462
|
lastScan = meta.lastScanTimestamp;
|
|
3819
4463
|
} catch {
|
|
3820
4464
|
}
|
|
@@ -3825,8 +4469,8 @@ async function runGraphStatus(projectPath) {
|
|
|
3825
4469
|
}
|
|
3826
4470
|
let connectorSyncStatus = {};
|
|
3827
4471
|
try {
|
|
3828
|
-
const syncMetaPath =
|
|
3829
|
-
const syncMeta = JSON.parse(await
|
|
4472
|
+
const syncMetaPath = path38.join(graphDir, "sync-metadata.json");
|
|
4473
|
+
const syncMeta = JSON.parse(await fs29.readFile(syncMetaPath, "utf-8"));
|
|
3830
4474
|
for (const [name, data] of Object.entries(syncMeta.connectors ?? {})) {
|
|
3831
4475
|
connectorSyncStatus[name] = data.lastSyncTimestamp;
|
|
3832
4476
|
}
|
|
@@ -3843,10 +4487,10 @@ async function runGraphStatus(projectPath) {
|
|
|
3843
4487
|
}
|
|
3844
4488
|
|
|
3845
4489
|
// src/commands/graph/export.ts
|
|
3846
|
-
import * as
|
|
4490
|
+
import * as path39 from "path";
|
|
3847
4491
|
async function runGraphExport(projectPath, format) {
|
|
3848
|
-
const { GraphStore } = await import("./dist-
|
|
3849
|
-
const graphDir =
|
|
4492
|
+
const { GraphStore } = await import("./dist-B26DFXMP.js");
|
|
4493
|
+
const graphDir = path39.join(projectPath, ".harness", "graph");
|
|
3850
4494
|
const store = new GraphStore();
|
|
3851
4495
|
const loaded = await store.load(graphDir);
|
|
3852
4496
|
if (!loaded) throw new Error("No graph found. Run `harness scan` first.");
|
|
@@ -3875,13 +4519,13 @@ async function runGraphExport(projectPath, format) {
|
|
|
3875
4519
|
}
|
|
3876
4520
|
|
|
3877
4521
|
// src/commands/graph/index.ts
|
|
3878
|
-
import * as
|
|
4522
|
+
import * as path40 from "path";
|
|
3879
4523
|
function createGraphCommand() {
|
|
3880
|
-
const graph = new
|
|
4524
|
+
const graph = new Command48("graph").description("Knowledge graph management");
|
|
3881
4525
|
graph.command("status").description("Show graph statistics").action(async (_opts, cmd) => {
|
|
3882
4526
|
try {
|
|
3883
4527
|
const globalOpts = cmd.optsWithGlobals();
|
|
3884
|
-
const projectPath =
|
|
4528
|
+
const projectPath = path40.resolve(globalOpts.config ? path40.dirname(globalOpts.config) : ".");
|
|
3885
4529
|
const result = await runGraphStatus(projectPath);
|
|
3886
4530
|
if (globalOpts.json) {
|
|
3887
4531
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -3908,7 +4552,7 @@ function createGraphCommand() {
|
|
|
3908
4552
|
});
|
|
3909
4553
|
graph.command("export").description("Export graph").requiredOption("--format <format>", "Output format (json, mermaid)").action(async (opts, cmd) => {
|
|
3910
4554
|
const globalOpts = cmd.optsWithGlobals();
|
|
3911
|
-
const projectPath =
|
|
4555
|
+
const projectPath = path40.resolve(globalOpts.config ? path40.dirname(globalOpts.config) : ".");
|
|
3912
4556
|
try {
|
|
3913
4557
|
const output = await runGraphExport(projectPath, opts.format);
|
|
3914
4558
|
console.log(output);
|
|
@@ -3921,19 +4565,19 @@ function createGraphCommand() {
|
|
|
3921
4565
|
}
|
|
3922
4566
|
|
|
3923
4567
|
// src/commands/mcp.ts
|
|
3924
|
-
import { Command as
|
|
4568
|
+
import { Command as Command49 } from "commander";
|
|
3925
4569
|
function createMcpCommand() {
|
|
3926
|
-
return new
|
|
3927
|
-
const { startServer: startServer2 } = await import("./mcp-
|
|
4570
|
+
return new Command49("mcp").description("Start the MCP (Model Context Protocol) server on stdio").action(async () => {
|
|
4571
|
+
const { startServer: startServer2 } = await import("./mcp-VU5FMO52.js");
|
|
3928
4572
|
await startServer2();
|
|
3929
4573
|
});
|
|
3930
4574
|
}
|
|
3931
4575
|
|
|
3932
4576
|
// src/commands/impact-preview.ts
|
|
3933
|
-
import { Command as
|
|
4577
|
+
import { Command as Command50 } from "commander";
|
|
3934
4578
|
import { execSync as execSync3 } from "child_process";
|
|
3935
|
-
import * as
|
|
3936
|
-
import * as
|
|
4579
|
+
import * as path41 from "path";
|
|
4580
|
+
import * as fs23 from "fs";
|
|
3937
4581
|
function getStagedFiles(cwd) {
|
|
3938
4582
|
try {
|
|
3939
4583
|
const output = execSync3("git diff --cached --name-only", {
|
|
@@ -3947,7 +4591,7 @@ function getStagedFiles(cwd) {
|
|
|
3947
4591
|
}
|
|
3948
4592
|
function graphExists(projectPath) {
|
|
3949
4593
|
try {
|
|
3950
|
-
return
|
|
4594
|
+
return fs23.existsSync(path41.join(projectPath, ".harness", "graph", "graph.json"));
|
|
3951
4595
|
} catch {
|
|
3952
4596
|
return false;
|
|
3953
4597
|
}
|
|
@@ -3956,7 +4600,7 @@ function extractNodeName(id) {
|
|
|
3956
4600
|
const parts = id.split(":");
|
|
3957
4601
|
if (parts.length > 1) {
|
|
3958
4602
|
const fullPath = parts.slice(1).join(":");
|
|
3959
|
-
return
|
|
4603
|
+
return path41.basename(fullPath);
|
|
3960
4604
|
}
|
|
3961
4605
|
return id;
|
|
3962
4606
|
}
|
|
@@ -4079,7 +4723,7 @@ function formatPerFile(perFileResults) {
|
|
|
4079
4723
|
return lines.join("\n");
|
|
4080
4724
|
}
|
|
4081
4725
|
async function runImpactPreview(options) {
|
|
4082
|
-
const projectPath =
|
|
4726
|
+
const projectPath = path41.resolve(options.path ?? process.cwd());
|
|
4083
4727
|
const stagedFiles = getStagedFiles(projectPath);
|
|
4084
4728
|
if (stagedFiles.length === 0) {
|
|
4085
4729
|
return "Impact Preview: no staged changes";
|
|
@@ -4126,7 +4770,7 @@ async function runImpactPreview(options) {
|
|
|
4126
4770
|
return formatCompact(stagedFiles.length, merged, aggregateCounts);
|
|
4127
4771
|
}
|
|
4128
4772
|
function createImpactPreviewCommand() {
|
|
4129
|
-
const command = new
|
|
4773
|
+
const command = new Command50("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) => {
|
|
4130
4774
|
const output = await runImpactPreview({
|
|
4131
4775
|
detailed: opts.detailed,
|
|
4132
4776
|
perFile: opts.perFile,
|
|
@@ -4139,9 +4783,9 @@ function createImpactPreviewCommand() {
|
|
|
4139
4783
|
}
|
|
4140
4784
|
|
|
4141
4785
|
// src/commands/check-arch.ts
|
|
4142
|
-
import { Command as
|
|
4786
|
+
import { Command as Command51 } from "commander";
|
|
4143
4787
|
import { execSync as execSync4 } from "child_process";
|
|
4144
|
-
function
|
|
4788
|
+
function getCommitHash2(cwd) {
|
|
4145
4789
|
try {
|
|
4146
4790
|
return execSync4("git rev-parse --short HEAD", { cwd, encoding: "utf-8" }).toString().trim();
|
|
4147
4791
|
} catch {
|
|
@@ -4189,7 +4833,7 @@ async function runCheckArch(options) {
|
|
|
4189
4833
|
}
|
|
4190
4834
|
const manager = new ArchBaselineManager(cwd, archConfig.baselinePath);
|
|
4191
4835
|
if (options.updateBaseline) {
|
|
4192
|
-
const commitHash =
|
|
4836
|
+
const commitHash = getCommitHash2(cwd);
|
|
4193
4837
|
const baseline2 = manager.capture(results, commitHash);
|
|
4194
4838
|
manager.save(baseline2);
|
|
4195
4839
|
return Ok({
|
|
@@ -4234,7 +4878,7 @@ async function runCheckArch(options) {
|
|
|
4234
4878
|
});
|
|
4235
4879
|
}
|
|
4236
4880
|
function createCheckArchCommand() {
|
|
4237
|
-
const command = new
|
|
4881
|
+
const command = new Command51("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) => {
|
|
4238
4882
|
const globalOpts = cmd.optsWithGlobals();
|
|
4239
4883
|
const mode = globalOpts.json ? OutputMode.JSON : globalOpts.quiet ? OutputMode.QUIET : globalOpts.verbose ? OutputMode.VERBOSE : OutputMode.TEXT;
|
|
4240
4884
|
const formatter = new OutputFormatter(mode);
|
|
@@ -4300,20 +4944,20 @@ function createCheckArchCommand() {
|
|
|
4300
4944
|
}
|
|
4301
4945
|
|
|
4302
4946
|
// src/commands/blueprint.ts
|
|
4303
|
-
import { Command as
|
|
4304
|
-
import * as
|
|
4947
|
+
import { Command as Command52 } from "commander";
|
|
4948
|
+
import * as path42 from "path";
|
|
4305
4949
|
function createBlueprintCommand() {
|
|
4306
|
-
return new
|
|
4950
|
+
return new Command52("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) => {
|
|
4307
4951
|
try {
|
|
4308
|
-
const rootDir =
|
|
4309
|
-
const outputDir =
|
|
4952
|
+
const rootDir = path42.resolve(projectPath);
|
|
4953
|
+
const outputDir = path42.resolve(options.output);
|
|
4310
4954
|
logger.info(`Scanning project at ${rootDir}...`);
|
|
4311
4955
|
const scanner = new ProjectScanner(rootDir);
|
|
4312
4956
|
const data = await scanner.scan();
|
|
4313
4957
|
logger.info(`Generating blueprint to ${outputDir}...`);
|
|
4314
4958
|
const generator = new BlueprintGenerator();
|
|
4315
4959
|
await generator.generate(data, { outputDir });
|
|
4316
|
-
logger.success(`Blueprint generated successfully at ${
|
|
4960
|
+
logger.success(`Blueprint generated successfully at ${path42.join(outputDir, "index.html")}`);
|
|
4317
4961
|
} catch (error) {
|
|
4318
4962
|
logger.error(
|
|
4319
4963
|
`Failed to generate blueprint: ${error instanceof Error ? error.message : String(error)}`
|
|
@@ -4324,16 +4968,16 @@ function createBlueprintCommand() {
|
|
|
4324
4968
|
}
|
|
4325
4969
|
|
|
4326
4970
|
// src/commands/share.ts
|
|
4327
|
-
import { Command as
|
|
4328
|
-
import * as
|
|
4329
|
-
import * as
|
|
4971
|
+
import { Command as Command53 } from "commander";
|
|
4972
|
+
import * as fs24 from "fs";
|
|
4973
|
+
import * as path43 from "path";
|
|
4330
4974
|
import { parse as parseYaml } from "yaml";
|
|
4331
4975
|
var MANIFEST_FILENAME = "constraints.yaml";
|
|
4332
4976
|
function createShareCommand() {
|
|
4333
|
-
return new
|
|
4334
|
-
const rootDir =
|
|
4335
|
-
const manifestPath =
|
|
4336
|
-
if (!
|
|
4977
|
+
return new Command53("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) => {
|
|
4978
|
+
const rootDir = path43.resolve(projectPath);
|
|
4979
|
+
const manifestPath = path43.join(rootDir, MANIFEST_FILENAME);
|
|
4980
|
+
if (!fs24.existsSync(manifestPath)) {
|
|
4337
4981
|
logger.error(
|
|
4338
4982
|
`No ${MANIFEST_FILENAME} found at ${manifestPath}.
|
|
4339
4983
|
Create a constraints.yaml in your project root to define what to share.`
|
|
@@ -4342,7 +4986,7 @@ Create a constraints.yaml in your project root to define what to share.`
|
|
|
4342
4986
|
}
|
|
4343
4987
|
let parsed;
|
|
4344
4988
|
try {
|
|
4345
|
-
const raw =
|
|
4989
|
+
const raw = fs24.readFileSync(manifestPath, "utf-8");
|
|
4346
4990
|
parsed = parseYaml(raw);
|
|
4347
4991
|
} catch (err) {
|
|
4348
4992
|
logger.error(
|
|
@@ -4356,7 +5000,7 @@ Create a constraints.yaml in your project root to define what to share.`
|
|
|
4356
5000
|
process.exit(1);
|
|
4357
5001
|
}
|
|
4358
5002
|
const manifest = manifestResult.value;
|
|
4359
|
-
const configResult = resolveConfig(
|
|
5003
|
+
const configResult = resolveConfig(path43.join(rootDir, "harness.config.json"));
|
|
4360
5004
|
if (!configResult.ok) {
|
|
4361
5005
|
logger.error(configResult.error.message);
|
|
4362
5006
|
process.exit(1);
|
|
@@ -4374,8 +5018,8 @@ Create a constraints.yaml in your project root to define what to share.`
|
|
|
4374
5018
|
);
|
|
4375
5019
|
process.exit(1);
|
|
4376
5020
|
}
|
|
4377
|
-
const outputDir =
|
|
4378
|
-
const outputPath =
|
|
5021
|
+
const outputDir = path43.resolve(options.output);
|
|
5022
|
+
const outputPath = path43.join(outputDir, `${manifest.name}.harness-constraints.json`);
|
|
4379
5023
|
const writeResult = await writeConfig(outputPath, bundle);
|
|
4380
5024
|
if (!writeResult.ok) {
|
|
4381
5025
|
logger.error(`Failed to write bundle: ${writeResult.error.message}`);
|
|
@@ -4386,25 +5030,25 @@ Create a constraints.yaml in your project root to define what to share.`
|
|
|
4386
5030
|
}
|
|
4387
5031
|
|
|
4388
5032
|
// src/commands/install.ts
|
|
4389
|
-
import * as
|
|
4390
|
-
import * as
|
|
4391
|
-
import { Command as
|
|
5033
|
+
import * as fs26 from "fs";
|
|
5034
|
+
import * as path45 from "path";
|
|
5035
|
+
import { Command as Command54 } from "commander";
|
|
4392
5036
|
import { parse as yamlParse } from "yaml";
|
|
4393
5037
|
|
|
4394
5038
|
// src/registry/tarball.ts
|
|
4395
|
-
import * as
|
|
4396
|
-
import * as
|
|
4397
|
-
import * as
|
|
5039
|
+
import * as fs25 from "fs";
|
|
5040
|
+
import * as path44 from "path";
|
|
5041
|
+
import * as os6 from "os";
|
|
4398
5042
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
4399
5043
|
function extractTarball(tarballBuffer) {
|
|
4400
|
-
const tmpDir =
|
|
4401
|
-
const tarballPath =
|
|
5044
|
+
const tmpDir = fs25.mkdtempSync(path44.join(os6.tmpdir(), "harness-skill-install-"));
|
|
5045
|
+
const tarballPath = path44.join(tmpDir, "package.tgz");
|
|
4402
5046
|
try {
|
|
4403
|
-
|
|
5047
|
+
fs25.writeFileSync(tarballPath, tarballBuffer);
|
|
4404
5048
|
execFileSync5("tar", ["-xzf", tarballPath, "-C", tmpDir], {
|
|
4405
5049
|
timeout: 3e4
|
|
4406
5050
|
});
|
|
4407
|
-
|
|
5051
|
+
fs25.unlinkSync(tarballPath);
|
|
4408
5052
|
} catch (err) {
|
|
4409
5053
|
cleanupTempDir(tmpDir);
|
|
4410
5054
|
throw new Error(
|
|
@@ -4415,43 +5059,43 @@ function extractTarball(tarballBuffer) {
|
|
|
4415
5059
|
return tmpDir;
|
|
4416
5060
|
}
|
|
4417
5061
|
function placeSkillContent(extractedPkgDir, communityBaseDir, skillName, platforms) {
|
|
4418
|
-
const files =
|
|
5062
|
+
const files = fs25.readdirSync(extractedPkgDir);
|
|
4419
5063
|
for (const platform of platforms) {
|
|
4420
|
-
const targetDir =
|
|
4421
|
-
if (
|
|
4422
|
-
|
|
5064
|
+
const targetDir = path44.join(communityBaseDir, platform, skillName);
|
|
5065
|
+
if (fs25.existsSync(targetDir)) {
|
|
5066
|
+
fs25.rmSync(targetDir, { recursive: true, force: true });
|
|
4423
5067
|
}
|
|
4424
|
-
|
|
5068
|
+
fs25.mkdirSync(targetDir, { recursive: true });
|
|
4425
5069
|
for (const file of files) {
|
|
4426
5070
|
if (file === "package.json" || file === "node_modules") continue;
|
|
4427
|
-
const srcPath =
|
|
4428
|
-
const destPath =
|
|
4429
|
-
const stat =
|
|
5071
|
+
const srcPath = path44.join(extractedPkgDir, file);
|
|
5072
|
+
const destPath = path44.join(targetDir, file);
|
|
5073
|
+
const stat = fs25.statSync(srcPath);
|
|
4430
5074
|
if (stat.isDirectory()) {
|
|
4431
|
-
|
|
5075
|
+
fs25.cpSync(srcPath, destPath, { recursive: true });
|
|
4432
5076
|
} else {
|
|
4433
|
-
|
|
5077
|
+
fs25.copyFileSync(srcPath, destPath);
|
|
4434
5078
|
}
|
|
4435
5079
|
}
|
|
4436
5080
|
}
|
|
4437
5081
|
}
|
|
4438
5082
|
function removeSkillContent(communityBaseDir, skillName, platforms) {
|
|
4439
5083
|
for (const platform of platforms) {
|
|
4440
|
-
const targetDir =
|
|
4441
|
-
if (
|
|
4442
|
-
|
|
5084
|
+
const targetDir = path44.join(communityBaseDir, platform, skillName);
|
|
5085
|
+
if (fs25.existsSync(targetDir)) {
|
|
5086
|
+
fs25.rmSync(targetDir, { recursive: true, force: true });
|
|
4443
5087
|
}
|
|
4444
5088
|
}
|
|
4445
5089
|
}
|
|
4446
5090
|
function cleanupTempDir(dirPath) {
|
|
4447
5091
|
try {
|
|
4448
|
-
|
|
5092
|
+
fs25.rmSync(dirPath, { recursive: true, force: true });
|
|
4449
5093
|
} catch {
|
|
4450
5094
|
}
|
|
4451
5095
|
}
|
|
4452
5096
|
|
|
4453
5097
|
// src/registry/resolver.ts
|
|
4454
|
-
import
|
|
5098
|
+
import semver3 from "semver";
|
|
4455
5099
|
function resolveVersion(metadata, versionRange) {
|
|
4456
5100
|
const versions = Object.keys(metadata.versions);
|
|
4457
5101
|
if (versions.length === 0) {
|
|
@@ -4463,13 +5107,13 @@ function resolveVersion(metadata, versionRange) {
|
|
|
4463
5107
|
const latestInfo = metadata.versions[latestTag];
|
|
4464
5108
|
if (latestInfo) return latestInfo;
|
|
4465
5109
|
}
|
|
4466
|
-
const highest =
|
|
5110
|
+
const highest = semver3.maxSatisfying(versions, "*");
|
|
4467
5111
|
if (!highest || !metadata.versions[highest]) {
|
|
4468
5112
|
throw new Error(`No versions available for ${metadata.name}.`);
|
|
4469
5113
|
}
|
|
4470
5114
|
return metadata.versions[highest];
|
|
4471
5115
|
}
|
|
4472
|
-
const matched =
|
|
5116
|
+
const matched = semver3.maxSatisfying(versions, versionRange);
|
|
4473
5117
|
if (!matched || !metadata.versions[matched]) {
|
|
4474
5118
|
throw new Error(
|
|
4475
5119
|
`No version of ${metadata.name} matches range ${versionRange}. Available: ${versions.join(", ")}`
|
|
@@ -4501,35 +5145,35 @@ function validateSkillYaml(parsed) {
|
|
|
4501
5145
|
};
|
|
4502
5146
|
}
|
|
4503
5147
|
async function runLocalInstall(fromPath, options) {
|
|
4504
|
-
const resolvedPath =
|
|
4505
|
-
if (!
|
|
5148
|
+
const resolvedPath = path45.resolve(fromPath);
|
|
5149
|
+
if (!fs26.existsSync(resolvedPath)) {
|
|
4506
5150
|
throw new Error(`--from path does not exist: ${resolvedPath}`);
|
|
4507
5151
|
}
|
|
4508
|
-
const stat =
|
|
5152
|
+
const stat = fs26.statSync(resolvedPath);
|
|
4509
5153
|
let extractDir = null;
|
|
4510
5154
|
let pkgDir;
|
|
4511
5155
|
if (stat.isDirectory()) {
|
|
4512
5156
|
pkgDir = resolvedPath;
|
|
4513
5157
|
} else if (resolvedPath.endsWith(".tgz") || resolvedPath.endsWith(".tar.gz")) {
|
|
4514
|
-
const tarballBuffer =
|
|
5158
|
+
const tarballBuffer = fs26.readFileSync(resolvedPath);
|
|
4515
5159
|
extractDir = extractTarball(tarballBuffer);
|
|
4516
|
-
pkgDir =
|
|
5160
|
+
pkgDir = path45.join(extractDir, "package");
|
|
4517
5161
|
} else {
|
|
4518
5162
|
throw new Error(`--from path must be a directory or .tgz file. Got: ${resolvedPath}`);
|
|
4519
5163
|
}
|
|
4520
5164
|
try {
|
|
4521
|
-
const skillYamlPath =
|
|
4522
|
-
if (!
|
|
5165
|
+
const skillYamlPath = path45.join(pkgDir, "skill.yaml");
|
|
5166
|
+
if (!fs26.existsSync(skillYamlPath)) {
|
|
4523
5167
|
throw new Error(`No skill.yaml found at ${skillYamlPath}`);
|
|
4524
5168
|
}
|
|
4525
|
-
const rawYaml =
|
|
5169
|
+
const rawYaml = fs26.readFileSync(skillYamlPath, "utf-8");
|
|
4526
5170
|
const parsed = yamlParse(rawYaml);
|
|
4527
5171
|
const skillYaml = validateSkillYaml(parsed);
|
|
4528
5172
|
const shortName = skillYaml.name;
|
|
4529
5173
|
const globalDir = resolveGlobalSkillsDir();
|
|
4530
|
-
const skillsDir =
|
|
4531
|
-
const communityBase =
|
|
4532
|
-
const lockfilePath =
|
|
5174
|
+
const skillsDir = path45.dirname(globalDir);
|
|
5175
|
+
const communityBase = path45.join(skillsDir, "community");
|
|
5176
|
+
const lockfilePath = path45.join(communityBase, "skills-lock.json");
|
|
4533
5177
|
const bundledNames = getBundledSkillNames(globalDir);
|
|
4534
5178
|
if (bundledNames.has(shortName)) {
|
|
4535
5179
|
throw new Error(
|
|
@@ -4570,9 +5214,9 @@ async function runInstall(skillName, options) {
|
|
|
4570
5214
|
const packageName = resolvePackageName(skillName);
|
|
4571
5215
|
const shortName = extractSkillName(packageName);
|
|
4572
5216
|
const globalDir = resolveGlobalSkillsDir();
|
|
4573
|
-
const skillsDir =
|
|
4574
|
-
const communityBase =
|
|
4575
|
-
const lockfilePath =
|
|
5217
|
+
const skillsDir = path45.dirname(globalDir);
|
|
5218
|
+
const communityBase = path45.join(skillsDir, "community");
|
|
5219
|
+
const lockfilePath = path45.join(communityBase, "skills-lock.json");
|
|
4576
5220
|
const bundledNames = getBundledSkillNames(globalDir);
|
|
4577
5221
|
if (bundledNames.has(shortName)) {
|
|
4578
5222
|
throw new Error(
|
|
@@ -4598,12 +5242,12 @@ async function runInstall(skillName, options) {
|
|
|
4598
5242
|
const extractDir = extractTarball(tarballBuffer);
|
|
4599
5243
|
let skillYaml;
|
|
4600
5244
|
try {
|
|
4601
|
-
const extractedPkgDir =
|
|
4602
|
-
const skillYamlPath =
|
|
4603
|
-
if (!
|
|
5245
|
+
const extractedPkgDir = path45.join(extractDir, "package");
|
|
5246
|
+
const skillYamlPath = path45.join(extractedPkgDir, "skill.yaml");
|
|
5247
|
+
if (!fs26.existsSync(skillYamlPath)) {
|
|
4604
5248
|
throw new Error(`contains invalid skill.yaml: file not found in package`);
|
|
4605
5249
|
}
|
|
4606
|
-
const rawYaml =
|
|
5250
|
+
const rawYaml = fs26.readFileSync(skillYamlPath, "utf-8");
|
|
4607
5251
|
const parsed = yamlParse(rawYaml);
|
|
4608
5252
|
skillYaml = validateSkillYaml(parsed);
|
|
4609
5253
|
placeSkillContent(extractedPkgDir, communityBase, shortName, skillYaml.platforms);
|
|
@@ -4642,7 +5286,7 @@ async function runInstall(skillName, options) {
|
|
|
4642
5286
|
return result;
|
|
4643
5287
|
}
|
|
4644
5288
|
function createInstallCommand() {
|
|
4645
|
-
const cmd = new
|
|
5289
|
+
const cmd = new Command54("install");
|
|
4646
5290
|
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) => {
|
|
4647
5291
|
try {
|
|
4648
5292
|
const result = await runInstall(skill, opts);
|
|
@@ -4666,15 +5310,15 @@ function createInstallCommand() {
|
|
|
4666
5310
|
}
|
|
4667
5311
|
|
|
4668
5312
|
// src/commands/install-constraints.ts
|
|
4669
|
-
import * as
|
|
4670
|
-
import * as
|
|
4671
|
-
import { Command as
|
|
4672
|
-
import
|
|
5313
|
+
import * as fs27 from "fs/promises";
|
|
5314
|
+
import * as path46 from "path";
|
|
5315
|
+
import { Command as Command55 } from "commander";
|
|
5316
|
+
import semver4 from "semver";
|
|
4673
5317
|
async function runInstallConstraints(options) {
|
|
4674
5318
|
const { source, configPath, lockfilePath } = options;
|
|
4675
5319
|
let rawBundle;
|
|
4676
5320
|
try {
|
|
4677
|
-
rawBundle = await
|
|
5321
|
+
rawBundle = await fs27.readFile(source, "utf-8");
|
|
4678
5322
|
} catch (err) {
|
|
4679
5323
|
if (isNodeError(err) && err.code === "ENOENT") {
|
|
4680
5324
|
return { ok: false, error: `Bundle file not found: ${source}` };
|
|
@@ -4697,9 +5341,9 @@ async function runInstallConstraints(options) {
|
|
|
4697
5341
|
}
|
|
4698
5342
|
const bundle = bundleResult.data;
|
|
4699
5343
|
if (bundle.minHarnessVersion) {
|
|
4700
|
-
const installed =
|
|
4701
|
-
const required =
|
|
4702
|
-
if (installed && required &&
|
|
5344
|
+
const installed = semver4.valid(semver4.coerce(CLI_VERSION));
|
|
5345
|
+
const required = semver4.valid(semver4.coerce(bundle.minHarnessVersion));
|
|
5346
|
+
if (installed && required && semver4.lt(installed, required)) {
|
|
4703
5347
|
return {
|
|
4704
5348
|
ok: false,
|
|
4705
5349
|
error: `Bundle requires harness version >= ${bundle.minHarnessVersion}, but installed version is ${CLI_VERSION}. Please upgrade.`
|
|
@@ -4717,7 +5361,7 @@ async function runInstallConstraints(options) {
|
|
|
4717
5361
|
}
|
|
4718
5362
|
let localConfig;
|
|
4719
5363
|
try {
|
|
4720
|
-
const raw = await
|
|
5364
|
+
const raw = await fs27.readFile(configPath, "utf-8");
|
|
4721
5365
|
localConfig = JSON.parse(raw);
|
|
4722
5366
|
} catch (err) {
|
|
4723
5367
|
return {
|
|
@@ -4865,7 +5509,7 @@ function isNodeError(err) {
|
|
|
4865
5509
|
return err instanceof Error && "code" in err;
|
|
4866
5510
|
}
|
|
4867
5511
|
function resolveConfigPath(opts) {
|
|
4868
|
-
if (opts.config) return
|
|
5512
|
+
if (opts.config) return path46.resolve(opts.config);
|
|
4869
5513
|
const found = findConfigFile();
|
|
4870
5514
|
if (!found.ok) {
|
|
4871
5515
|
logger.error(found.error.message);
|
|
@@ -4900,9 +5544,9 @@ function logInstallResult(val, opts) {
|
|
|
4900
5544
|
}
|
|
4901
5545
|
async function handleInstallConstraints(source, opts) {
|
|
4902
5546
|
const configPath = resolveConfigPath(opts);
|
|
4903
|
-
const projectRoot =
|
|
4904
|
-
const lockfilePath =
|
|
4905
|
-
const resolvedSource =
|
|
5547
|
+
const projectRoot = path46.dirname(configPath);
|
|
5548
|
+
const lockfilePath = path46.join(projectRoot, ".harness", "constraints.lock.json");
|
|
5549
|
+
const resolvedSource = path46.resolve(source);
|
|
4906
5550
|
if (opts.forceLocal && opts.forcePackage) {
|
|
4907
5551
|
logger.error("Cannot use both --force-local and --force-package.");
|
|
4908
5552
|
process.exit(1);
|
|
@@ -4922,15 +5566,15 @@ async function handleInstallConstraints(source, opts) {
|
|
|
4922
5566
|
logInstallResult(result.value, opts);
|
|
4923
5567
|
}
|
|
4924
5568
|
function createInstallConstraintsCommand() {
|
|
4925
|
-
const cmd = new
|
|
5569
|
+
const cmd = new Command55("install-constraints");
|
|
4926
5570
|
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);
|
|
4927
5571
|
return cmd;
|
|
4928
5572
|
}
|
|
4929
5573
|
|
|
4930
5574
|
// src/commands/uninstall-constraints.ts
|
|
4931
|
-
import * as
|
|
4932
|
-
import * as
|
|
4933
|
-
import { Command as
|
|
5575
|
+
import * as fs28 from "fs/promises";
|
|
5576
|
+
import * as path47 from "path";
|
|
5577
|
+
import { Command as Command56 } from "commander";
|
|
4934
5578
|
async function runUninstallConstraints(options) {
|
|
4935
5579
|
const { packageName, configPath, lockfilePath } = options;
|
|
4936
5580
|
const lockfileResult = await readLockfile(lockfilePath);
|
|
@@ -4950,7 +5594,7 @@ async function runUninstallConstraints(options) {
|
|
|
4950
5594
|
}
|
|
4951
5595
|
let localConfig;
|
|
4952
5596
|
try {
|
|
4953
|
-
const raw = await
|
|
5597
|
+
const raw = await fs28.readFile(configPath, "utf-8");
|
|
4954
5598
|
localConfig = JSON.parse(raw);
|
|
4955
5599
|
} catch (err) {
|
|
4956
5600
|
return {
|
|
@@ -4987,11 +5631,11 @@ async function runUninstallConstraints(options) {
|
|
|
4987
5631
|
};
|
|
4988
5632
|
}
|
|
4989
5633
|
function createUninstallConstraintsCommand() {
|
|
4990
|
-
const cmd = new
|
|
5634
|
+
const cmd = new Command56("uninstall-constraints");
|
|
4991
5635
|
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) => {
|
|
4992
5636
|
let configPath;
|
|
4993
5637
|
if (opts.config) {
|
|
4994
|
-
configPath =
|
|
5638
|
+
configPath = path47.resolve(opts.config);
|
|
4995
5639
|
} else {
|
|
4996
5640
|
const found = findConfigFile();
|
|
4997
5641
|
if (!found.ok) {
|
|
@@ -5000,8 +5644,8 @@ function createUninstallConstraintsCommand() {
|
|
|
5000
5644
|
}
|
|
5001
5645
|
configPath = found.value;
|
|
5002
5646
|
}
|
|
5003
|
-
const projectRoot =
|
|
5004
|
-
const lockfilePath =
|
|
5647
|
+
const projectRoot = path47.dirname(configPath);
|
|
5648
|
+
const lockfilePath = path47.join(projectRoot, ".harness", "constraints.lock.json");
|
|
5005
5649
|
const result = await runUninstallConstraints({
|
|
5006
5650
|
packageName: name,
|
|
5007
5651
|
configPath,
|
|
@@ -5026,15 +5670,15 @@ function createUninstallConstraintsCommand() {
|
|
|
5026
5670
|
}
|
|
5027
5671
|
|
|
5028
5672
|
// src/commands/uninstall.ts
|
|
5029
|
-
import * as
|
|
5030
|
-
import { Command as
|
|
5673
|
+
import * as path48 from "path";
|
|
5674
|
+
import { Command as Command57 } from "commander";
|
|
5031
5675
|
async function runUninstall(skillName, options) {
|
|
5032
5676
|
const packageName = resolvePackageName(skillName);
|
|
5033
5677
|
const shortName = extractSkillName(packageName);
|
|
5034
5678
|
const globalDir = resolveGlobalSkillsDir();
|
|
5035
|
-
const skillsDir =
|
|
5036
|
-
const communityBase =
|
|
5037
|
-
const lockfilePath =
|
|
5679
|
+
const skillsDir = path48.dirname(globalDir);
|
|
5680
|
+
const communityBase = path48.join(skillsDir, "community");
|
|
5681
|
+
const lockfilePath = path48.join(communityBase, "skills-lock.json");
|
|
5038
5682
|
const lockfile = readLockfile2(lockfilePath);
|
|
5039
5683
|
const entry = lockfile.skills[packageName];
|
|
5040
5684
|
if (!entry) {
|
|
@@ -5064,7 +5708,7 @@ async function runUninstall(skillName, options) {
|
|
|
5064
5708
|
return result;
|
|
5065
5709
|
}
|
|
5066
5710
|
function createUninstallCommand() {
|
|
5067
|
-
const cmd = new
|
|
5711
|
+
const cmd = new Command57("uninstall");
|
|
5068
5712
|
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) => {
|
|
5069
5713
|
try {
|
|
5070
5714
|
const result = await runUninstall(skill, opts);
|
|
@@ -5083,13 +5727,13 @@ function createUninstallCommand() {
|
|
|
5083
5727
|
}
|
|
5084
5728
|
|
|
5085
5729
|
// src/commands/orchestrator.ts
|
|
5086
|
-
import { Command as
|
|
5087
|
-
import * as
|
|
5730
|
+
import { Command as Command58 } from "commander";
|
|
5731
|
+
import * as path49 from "path";
|
|
5088
5732
|
import { Orchestrator, WorkflowLoader, launchTUI } from "@harness-engineering/orchestrator";
|
|
5089
5733
|
function createOrchestratorCommand() {
|
|
5090
|
-
const orchestrator = new
|
|
5734
|
+
const orchestrator = new Command58("orchestrator");
|
|
5091
5735
|
orchestrator.command("run").description("Run the orchestrator daemon").option("-w, --workflow <path>", "Path to WORKFLOW.md", "WORKFLOW.md").action(async (opts) => {
|
|
5092
|
-
const workflowPath =
|
|
5736
|
+
const workflowPath = path49.resolve(process.cwd(), opts.workflow);
|
|
5093
5737
|
const loader = new WorkflowLoader();
|
|
5094
5738
|
const result = await loader.loadWorkflow(workflowPath);
|
|
5095
5739
|
if (!result.ok) {
|
|
@@ -5113,13 +5757,13 @@ function createOrchestratorCommand() {
|
|
|
5113
5757
|
}
|
|
5114
5758
|
|
|
5115
5759
|
// src/commands/learnings/index.ts
|
|
5116
|
-
import { Command as
|
|
5760
|
+
import { Command as Command60 } from "commander";
|
|
5117
5761
|
|
|
5118
5762
|
// src/commands/learnings/prune.ts
|
|
5119
|
-
import { Command as
|
|
5120
|
-
import * as
|
|
5763
|
+
import { Command as Command59 } from "commander";
|
|
5764
|
+
import * as path50 from "path";
|
|
5121
5765
|
async function handlePrune(opts) {
|
|
5122
|
-
const projectPath =
|
|
5766
|
+
const projectPath = path50.resolve(opts.path);
|
|
5123
5767
|
const result = await pruneLearnings(projectPath, opts.stream);
|
|
5124
5768
|
if (!result.ok) {
|
|
5125
5769
|
logger.error(result.error.message);
|
|
@@ -5158,21 +5802,21 @@ function printPatternProposals(patterns) {
|
|
|
5158
5802
|
);
|
|
5159
5803
|
}
|
|
5160
5804
|
function createPruneCommand() {
|
|
5161
|
-
return new
|
|
5805
|
+
return new Command59("prune").description(
|
|
5162
5806
|
"Analyze global learnings for patterns, present improvement proposals, and archive old entries"
|
|
5163
5807
|
).option("--path <path>", "Project root path", ".").option("--stream <name>", "Target a specific stream").action(handlePrune);
|
|
5164
5808
|
}
|
|
5165
5809
|
|
|
5166
5810
|
// src/commands/learnings/index.ts
|
|
5167
5811
|
function createLearningsCommand() {
|
|
5168
|
-
const command = new
|
|
5812
|
+
const command = new Command60("learnings").description("Learnings management commands");
|
|
5169
5813
|
command.addCommand(createPruneCommand());
|
|
5170
5814
|
return command;
|
|
5171
5815
|
}
|
|
5172
5816
|
|
|
5173
5817
|
// src/index.ts
|
|
5174
5818
|
function createProgram() {
|
|
5175
|
-
const program = new
|
|
5819
|
+
const program = new Command61();
|
|
5176
5820
|
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");
|
|
5177
5821
|
program.addCommand(createValidateCommand());
|
|
5178
5822
|
program.addCommand(createCheckDepsCommand());
|
|
@@ -5193,10 +5837,13 @@ function createProgram() {
|
|
|
5193
5837
|
program.addCommand(createCheckPhaseGateCommand());
|
|
5194
5838
|
program.addCommand(createCreateSkillCommand());
|
|
5195
5839
|
program.addCommand(createSetupMcpCommand());
|
|
5840
|
+
program.addCommand(createSetupCommand());
|
|
5841
|
+
program.addCommand(createDoctorCommand());
|
|
5196
5842
|
program.addCommand(createGenerateSlashCommandsCommand());
|
|
5197
5843
|
program.addCommand(createGenerateAgentDefinitionsCommand());
|
|
5198
5844
|
program.addCommand(createGenerateCommand3());
|
|
5199
5845
|
program.addCommand(createCICommand());
|
|
5846
|
+
program.addCommand(createHooksCommand());
|
|
5200
5847
|
program.addCommand(createUpdateCommand());
|
|
5201
5848
|
program.addCommand(createScanCommand());
|
|
5202
5849
|
program.addCommand(createIngestCommand());
|
|
@@ -5217,6 +5864,7 @@ function createProgram() {
|
|
|
5217
5864
|
|
|
5218
5865
|
export {
|
|
5219
5866
|
buildPreamble,
|
|
5867
|
+
printFirstRunWelcome,
|
|
5220
5868
|
runScan,
|
|
5221
5869
|
runIngest,
|
|
5222
5870
|
runQuery,
|