@harness-engineering/cli 1.10.0 → 1.11.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/enforce-architecture/SKILL.md +4 -0
- package/dist/agents/skills/claude-code/harness-parallel-agents/SKILL.md +105 -20
- package/dist/agents/skills/claude-code/harness-pre-commit-review/SKILL.md +37 -0
- package/dist/agents/skills/gemini-cli/enforce-architecture/SKILL.md +4 -0
- package/dist/agents/skills/gemini-cli/harness-parallel-agents/SKILL.md +105 -20
- package/dist/agents/skills/gemini-cli/harness-pre-commit-review/SKILL.md +37 -0
- package/dist/{agents-md-EMRFLNBC.js → agents-md-ZFV6RR5J.js} +1 -1
- package/dist/{architecture-5JNN5L3M.js → architecture-EXNUMH5R.js} +2 -2
- package/dist/bin/harness-mcp.js +14 -14
- package/dist/bin/harness.js +20 -20
- package/dist/{check-phase-gate-WOKIYGAM.js → check-phase-gate-VZFOY2PO.js} +4 -4
- package/dist/{chunk-7X7ZAYMY.js → chunk-2NCIKJES.js} +102 -5
- package/dist/{chunk-OPXH4CQN.js → chunk-2YPZKGAG.js} +1 -1
- package/dist/{chunk-NX6DSZSM.js → chunk-2YSQOUHO.js} +4483 -2668
- package/dist/{chunk-B7HFEHWP.js → chunk-3WGJMBKH.js} +10 -0
- package/dist/{chunk-ECUJQS3B.js → chunk-6N4R6FVX.js} +3 -3
- package/dist/chunk-EBJQ6N4M.js +39 -0
- package/dist/{chunk-46YA6FI3.js → chunk-GNGELAXY.js} +2 -2
- package/dist/{chunk-FPIPT36X.js → chunk-GSIVNYVJ.js} +6 -6
- package/dist/{chunk-EOLRW32Q.js → chunk-HD4IBGLA.js} +9 -1
- package/dist/{chunk-LXU5M77O.js → chunk-I6JZYEGT.js} +390 -57
- package/dist/{chunk-F3YDAJFQ.js → chunk-L2KLU56K.js} +2 -2
- package/dist/{chunk-F4PTVZWA.js → chunk-NC6PXVWT.js} +7 -7
- package/dist/{chunk-4PFMY3H7.js → chunk-PA2XHK75.js} +9 -9
- package/dist/{chunk-MO4YQOMB.js → chunk-TI4TGEX6.js} +3 -3
- package/dist/{chunk-MDUK2J2O.js → chunk-VRFZWGMS.js} +2 -1
- package/dist/{chunk-FX7SQHGD.js → chunk-WJZDO6OY.js} +2 -2
- package/dist/{chunk-PMTFPOCT.js → chunk-WUJTCNOU.js} +1 -1
- package/dist/{chunk-PSXF277V.js → chunk-X3MN5UQJ.js} +1 -1
- package/dist/{chunk-CWZ4Y2PO.js → chunk-Z75JC6I2.js} +4 -4
- package/dist/{chunk-PAHHT2IK.js → chunk-ZWC3MN5E.js} +1707 -294
- package/dist/{ci-workflow-ZBBUNTHQ.js → ci-workflow-K5RCRNYR.js} +1 -1
- package/dist/create-skill-WPXHSLX2.js +11 -0
- package/dist/{dist-PBTNVK6K.js → dist-JVZ2MKBC.js} +101 -1
- package/dist/{dist-I7DB5VKB.js → dist-M6BQODWC.js} +1145 -0
- package/dist/{docs-PTJGD6XI.js → docs-PWCUVYWU.js} +2 -2
- package/dist/{engine-SCMZ3G3E.js → engine-6XUP6GAK.js} +1 -1
- package/dist/{entropy-YIUBGKY7.js → entropy-4I6JEYAC.js} +2 -2
- package/dist/{feedback-WEVQSLAA.js → feedback-TNIW534S.js} +1 -1
- package/dist/{generate-agent-definitions-BU5LOJTI.js → generate-agent-definitions-MWKEA5NU.js} +4 -4
- package/dist/{graph-loader-RLO3KRIX.js → graph-loader-KO4GJ5N2.js} +1 -1
- package/dist/index.d.ts +318 -12
- package/dist/index.js +29 -21
- package/dist/{loader-6S6PVGSF.js → loader-4FIPIFII.js} +1 -1
- package/dist/mcp-MOKLYNZL.js +34 -0
- package/dist/{performance-5TVW6SA6.js → performance-BTOJCPXU.js} +2 -2
- package/dist/{review-pipeline-4JTQAWKW.js → review-pipeline-3YTW3463.js} +1 -1
- package/dist/{runtime-PXIM7UV6.js → runtime-GO7K2PJE.js} +1 -1
- package/dist/{security-URYTKLGK.js → security-4P2GGFF6.js} +1 -1
- package/dist/skill-executor-RG45LUO5.js +8 -0
- package/dist/templates/orchestrator/WORKFLOW.md +48 -0
- package/dist/templates/orchestrator/template.json +6 -0
- package/dist/{validate-KSDUUK2M.js → validate-JN44D2Q7.js} +2 -2
- package/dist/{validate-cross-check-WZAX357V.js → validate-cross-check-DB7RIFFF.js} +1 -1
- package/package.json +10 -6
- package/dist/chunk-HIOXKZYF.js +0 -15
- package/dist/create-skill-LUWO46WF.js +0 -11
- package/dist/mcp-BNLBTCXZ.js +0 -34
- package/dist/skill-executor-KVS47DAU.js +0 -8
|
@@ -7,11 +7,11 @@ import {
|
|
|
7
7
|
createCheckPhaseGateCommand,
|
|
8
8
|
findFiles,
|
|
9
9
|
resolveConfig
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-2NCIKJES.js";
|
|
11
11
|
import {
|
|
12
12
|
createGenerateAgentDefinitionsCommand,
|
|
13
13
|
generateAgentDefinitions
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-GNGELAXY.js";
|
|
15
15
|
import {
|
|
16
16
|
listPersonas,
|
|
17
17
|
loadPersona
|
|
@@ -21,16 +21,16 @@ import {
|
|
|
21
21
|
} from "./chunk-TRAPF4IX.js";
|
|
22
22
|
import {
|
|
23
23
|
executeSkill
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-L2KLU56K.js";
|
|
25
25
|
import {
|
|
26
26
|
ALLOWED_PERSONA_COMMANDS
|
|
27
27
|
} from "./chunk-TEFCFC4H.js";
|
|
28
28
|
import {
|
|
29
29
|
createCreateSkillCommand
|
|
30
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-6N4R6FVX.js";
|
|
31
31
|
import {
|
|
32
32
|
logger
|
|
33
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-EBJQ6N4M.js";
|
|
34
34
|
import {
|
|
35
35
|
generate,
|
|
36
36
|
validate
|
|
@@ -47,24 +47,27 @@ import {
|
|
|
47
47
|
import {
|
|
48
48
|
createGenerateSlashCommandsCommand,
|
|
49
49
|
generateSlashCommands,
|
|
50
|
+
handleGetImpact,
|
|
50
51
|
handleOrphanDeletion
|
|
51
|
-
} from "./chunk-
|
|
52
|
+
} from "./chunk-I6JZYEGT.js";
|
|
52
53
|
import {
|
|
53
54
|
VALID_PLATFORMS
|
|
54
55
|
} from "./chunk-ZOAWBDWU.js";
|
|
55
56
|
import {
|
|
57
|
+
resolveGlobalSkillsDir,
|
|
56
58
|
resolvePersonasDir,
|
|
59
|
+
resolveProjectSkillsDir,
|
|
57
60
|
resolveSkillsDir,
|
|
58
61
|
resolveTemplatesDir
|
|
59
|
-
} from "./chunk-
|
|
62
|
+
} from "./chunk-HD4IBGLA.js";
|
|
60
63
|
import {
|
|
61
64
|
CLIError,
|
|
62
65
|
ExitCode,
|
|
63
66
|
handleError
|
|
64
|
-
} from "./chunk-
|
|
67
|
+
} from "./chunk-3WGJMBKH.js";
|
|
65
68
|
import {
|
|
66
69
|
SkillMetadataSchema
|
|
67
|
-
} from "./chunk-
|
|
70
|
+
} from "./chunk-VRFZWGMS.js";
|
|
68
71
|
import {
|
|
69
72
|
CLI_VERSION
|
|
70
73
|
} from "./chunk-BM3PWGXQ.js";
|
|
@@ -72,9 +75,13 @@ import {
|
|
|
72
75
|
TemplateEngine
|
|
73
76
|
} from "./chunk-C2ERUR3L.js";
|
|
74
77
|
import {
|
|
78
|
+
ArchBaselineManager,
|
|
79
|
+
ArchConfigSchema,
|
|
75
80
|
BaselineManager,
|
|
81
|
+
BlueprintGenerator,
|
|
76
82
|
CriticalPathResolver,
|
|
77
83
|
EntropyAnalyzer,
|
|
84
|
+
ProjectScanner,
|
|
78
85
|
SecurityScanner,
|
|
79
86
|
TypeScriptParser,
|
|
80
87
|
appendLearning,
|
|
@@ -88,28 +95,33 @@ import {
|
|
|
88
95
|
detectCircularDepsInFiles,
|
|
89
96
|
detectDeadCode,
|
|
90
97
|
detectDocDrift,
|
|
98
|
+
diff,
|
|
99
|
+
extractBundle,
|
|
91
100
|
generateSuggestions,
|
|
92
101
|
listStreams,
|
|
93
102
|
loadState,
|
|
94
103
|
loadStreamIndex,
|
|
95
104
|
parseDiff,
|
|
105
|
+
parseManifest,
|
|
96
106
|
parseSecurityConfig,
|
|
97
107
|
requestPeerReview,
|
|
98
108
|
resolveStreamPath,
|
|
109
|
+
runAll,
|
|
99
110
|
runCIChecks,
|
|
100
111
|
runReviewPipeline,
|
|
101
112
|
setActiveStream,
|
|
102
113
|
validateAgentsMap,
|
|
103
114
|
validateDependencies,
|
|
104
|
-
validateKnowledgeMap
|
|
105
|
-
|
|
115
|
+
validateKnowledgeMap,
|
|
116
|
+
writeConfig
|
|
117
|
+
} from "./chunk-2YSQOUHO.js";
|
|
106
118
|
import {
|
|
107
119
|
Err,
|
|
108
120
|
Ok
|
|
109
121
|
} from "./chunk-MHBMTPW7.js";
|
|
110
122
|
|
|
111
123
|
// src/index.ts
|
|
112
|
-
import { Command as
|
|
124
|
+
import { Command as Command51 } from "commander";
|
|
113
125
|
|
|
114
126
|
// src/commands/validate.ts
|
|
115
127
|
import { Command } from "commander";
|
|
@@ -188,7 +200,7 @@ function createValidateCommand() {
|
|
|
188
200
|
process.exit(result.error.exitCode);
|
|
189
201
|
}
|
|
190
202
|
if (opts.crossCheck) {
|
|
191
|
-
const { runCrossCheck: runCrossCheck2 } = await import("./validate-cross-check-
|
|
203
|
+
const { runCrossCheck: runCrossCheck2 } = await import("./validate-cross-check-DB7RIFFF.js");
|
|
192
204
|
const cwd = process.cwd();
|
|
193
205
|
const specsDir = path.join(cwd, "docs", "specs");
|
|
194
206
|
const plansDir = path.join(cwd, "docs", "plans");
|
|
@@ -314,13 +326,13 @@ function createCheckDepsCommand() {
|
|
|
314
326
|
import { Command as Command3 } from "commander";
|
|
315
327
|
import * as path3 from "path";
|
|
316
328
|
async function runCheckPerf(cwd, options) {
|
|
317
|
-
const
|
|
329
|
+
const runAll2 = !options.structural && !options.size && !options.coupling;
|
|
318
330
|
const analyzer = new EntropyAnalyzer({
|
|
319
331
|
rootDir: path3.resolve(cwd),
|
|
320
332
|
analyze: {
|
|
321
|
-
complexity:
|
|
322
|
-
coupling:
|
|
323
|
-
sizeBudget:
|
|
333
|
+
complexity: runAll2 || !!options.structural,
|
|
334
|
+
coupling: runAll2 || !!options.coupling,
|
|
335
|
+
sizeBudget: runAll2 || !!options.size
|
|
324
336
|
}
|
|
325
337
|
});
|
|
326
338
|
const analysisResult = await analyzer.analyze();
|
|
@@ -456,10 +468,10 @@ async function runCheckSecurity(cwd, options) {
|
|
|
456
468
|
const projectRoot = path4.resolve(cwd);
|
|
457
469
|
let configData = {};
|
|
458
470
|
try {
|
|
459
|
-
const
|
|
471
|
+
const fs20 = await import("fs");
|
|
460
472
|
const configPath = path4.join(projectRoot, "harness.config.json");
|
|
461
|
-
if (
|
|
462
|
-
const raw =
|
|
473
|
+
if (fs20.existsSync(configPath)) {
|
|
474
|
+
const raw = fs20.readFileSync(configPath, "utf-8");
|
|
463
475
|
const parsed = JSON.parse(raw);
|
|
464
476
|
configData = parsed.security ?? {};
|
|
465
477
|
}
|
|
@@ -546,7 +558,7 @@ function createPerfCommand() {
|
|
|
546
558
|
perf.command("bench [glob]").description("Run benchmarks via vitest bench").action(async (glob, _opts, cmd) => {
|
|
547
559
|
const globalOpts = cmd.optsWithGlobals();
|
|
548
560
|
const cwd = process.cwd();
|
|
549
|
-
const { BenchmarkRunner } = await import("./dist-
|
|
561
|
+
const { BenchmarkRunner } = await import("./dist-JVZ2MKBC.js");
|
|
550
562
|
const runner = new BenchmarkRunner();
|
|
551
563
|
const benchFiles = runner.discover(cwd, glob);
|
|
552
564
|
if (benchFiles.length === 0) {
|
|
@@ -615,7 +627,7 @@ Results (${result.results.length} benchmarks):`);
|
|
|
615
627
|
baselines.command("update").description("Update baselines from latest benchmark run").action(async (_opts, cmd) => {
|
|
616
628
|
const globalOpts = cmd.optsWithGlobals();
|
|
617
629
|
const cwd = process.cwd();
|
|
618
|
-
const { BenchmarkRunner } = await import("./dist-
|
|
630
|
+
const { BenchmarkRunner } = await import("./dist-JVZ2MKBC.js");
|
|
619
631
|
const runner = new BenchmarkRunner();
|
|
620
632
|
const manager = new BaselineManager(cwd);
|
|
621
633
|
logger.info("Running benchmarks to update baselines...");
|
|
@@ -628,8 +640,8 @@ Results (${result.results.length} benchmarks):`);
|
|
|
628
640
|
}
|
|
629
641
|
let commitHash = "unknown";
|
|
630
642
|
try {
|
|
631
|
-
const { execSync:
|
|
632
|
-
commitHash =
|
|
643
|
+
const { execSync: execSync5 } = await import("child_process");
|
|
644
|
+
commitHash = execSync5("git rev-parse --short HEAD", { cwd, encoding: "utf-8" }).trim();
|
|
633
645
|
} catch {
|
|
634
646
|
}
|
|
635
647
|
manager.save(benchResult.results, commitHash);
|
|
@@ -643,7 +655,7 @@ Results (${result.results.length} benchmarks):`);
|
|
|
643
655
|
perf.command("report").description("Full performance report with metrics, trends, and hotspots").action(async (_opts, cmd) => {
|
|
644
656
|
const globalOpts = cmd.optsWithGlobals();
|
|
645
657
|
const cwd = process.cwd();
|
|
646
|
-
const { EntropyAnalyzer: EntropyAnalyzer2 } = await import("./dist-
|
|
658
|
+
const { EntropyAnalyzer: EntropyAnalyzer2 } = await import("./dist-JVZ2MKBC.js");
|
|
647
659
|
const analyzer = new EntropyAnalyzer2({
|
|
648
660
|
rootDir: path5.resolve(cwd),
|
|
649
661
|
analyze: { complexity: true, coupling: true }
|
|
@@ -717,7 +729,14 @@ async function runCheckDocs(options) {
|
|
|
717
729
|
const coverageResult = await checkDocCoverage("project", {
|
|
718
730
|
docsDir,
|
|
719
731
|
sourceDir,
|
|
720
|
-
excludePatterns: [
|
|
732
|
+
excludePatterns: [
|
|
733
|
+
"**/*.test.ts",
|
|
734
|
+
"**/*.spec.ts",
|
|
735
|
+
"**/node_modules/**",
|
|
736
|
+
"**/dist/**",
|
|
737
|
+
"**/coverage/**",
|
|
738
|
+
"**/.turbo/**"
|
|
739
|
+
]
|
|
721
740
|
});
|
|
722
741
|
if (!coverageResult.ok) {
|
|
723
742
|
return Err(
|
|
@@ -1396,22 +1415,22 @@ async function runAgentReview(options) {
|
|
|
1396
1415
|
return configResult;
|
|
1397
1416
|
}
|
|
1398
1417
|
const config = configResult.value;
|
|
1399
|
-
let
|
|
1418
|
+
let diff2;
|
|
1400
1419
|
try {
|
|
1401
|
-
|
|
1402
|
-
if (!
|
|
1403
|
-
|
|
1420
|
+
diff2 = execSync2("git diff --cached", { encoding: "utf-8" });
|
|
1421
|
+
if (!diff2) {
|
|
1422
|
+
diff2 = execSync2("git diff", { encoding: "utf-8" });
|
|
1404
1423
|
}
|
|
1405
1424
|
} catch {
|
|
1406
1425
|
return Err(new CLIError("Failed to get git diff", ExitCode.ERROR));
|
|
1407
1426
|
}
|
|
1408
|
-
if (!
|
|
1427
|
+
if (!diff2) {
|
|
1409
1428
|
return Ok({
|
|
1410
1429
|
passed: true,
|
|
1411
1430
|
checklist: [{ check: "No changes to review", passed: true }]
|
|
1412
1431
|
});
|
|
1413
1432
|
}
|
|
1414
|
-
const parsedDiffResult = parseDiff(
|
|
1433
|
+
const parsedDiffResult = parseDiff(diff2);
|
|
1415
1434
|
if (!parsedDiffResult.ok) {
|
|
1416
1435
|
return Err(new CLIError(parsedDiffResult.error.message, ExitCode.ERROR));
|
|
1417
1436
|
}
|
|
@@ -1425,7 +1444,7 @@ async function runAgentReview(options) {
|
|
|
1425
1444
|
changedFiles: codeChanges.files.map((f) => f.path),
|
|
1426
1445
|
newFiles: codeChanges.files.filter((f) => f.status === "added").map((f) => f.path),
|
|
1427
1446
|
deletedFiles: codeChanges.files.filter((f) => f.status === "deleted").map((f) => f.path),
|
|
1428
|
-
totalDiffLines:
|
|
1447
|
+
totalDiffLines: diff2.split("\n").length,
|
|
1429
1448
|
fileDiffs: new Map(codeChanges.files.map((f) => [f.path, ""]))
|
|
1430
1449
|
};
|
|
1431
1450
|
const pipelineResult = await runReviewPipeline({
|
|
@@ -1592,7 +1611,7 @@ async function runAdd(componentType, name, options) {
|
|
|
1592
1611
|
break;
|
|
1593
1612
|
}
|
|
1594
1613
|
case "skill": {
|
|
1595
|
-
const { generateSkillFiles: generateSkillFiles2 } = await import("./create-skill-
|
|
1614
|
+
const { generateSkillFiles: generateSkillFiles2 } = await import("./create-skill-WPXHSLX2.js");
|
|
1596
1615
|
generateSkillFiles2({
|
|
1597
1616
|
name,
|
|
1598
1617
|
description: `${name} skill`,
|
|
@@ -1857,37 +1876,161 @@ function createPersonaCommand() {
|
|
|
1857
1876
|
}
|
|
1858
1877
|
|
|
1859
1878
|
// src/commands/skill/index.ts
|
|
1860
|
-
import { Command as
|
|
1879
|
+
import { Command as Command28 } from "commander";
|
|
1861
1880
|
|
|
1862
1881
|
// src/commands/skill/list.ts
|
|
1863
1882
|
import { Command as Command21 } from "commander";
|
|
1883
|
+
import * as fs6 from "fs";
|
|
1884
|
+
import * as path15 from "path";
|
|
1885
|
+
import { parse } from "yaml";
|
|
1886
|
+
|
|
1887
|
+
// src/registry/lockfile.ts
|
|
1864
1888
|
import * as fs5 from "fs";
|
|
1865
1889
|
import * as path14 from "path";
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1890
|
+
function createEmptyLockfile() {
|
|
1891
|
+
return { version: 1, skills: {} };
|
|
1892
|
+
}
|
|
1893
|
+
function sortedStringify(obj) {
|
|
1894
|
+
return JSON.stringify(
|
|
1895
|
+
obj,
|
|
1896
|
+
(_key, value) => {
|
|
1897
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
1898
|
+
return Object.keys(value).sort().reduce((sorted, k) => {
|
|
1899
|
+
sorted[k] = value[k];
|
|
1900
|
+
return sorted;
|
|
1901
|
+
}, {});
|
|
1902
|
+
}
|
|
1903
|
+
return value;
|
|
1904
|
+
},
|
|
1905
|
+
2
|
|
1906
|
+
);
|
|
1907
|
+
}
|
|
1908
|
+
function readLockfile(filePath) {
|
|
1909
|
+
if (!fs5.existsSync(filePath)) {
|
|
1910
|
+
return createEmptyLockfile();
|
|
1911
|
+
}
|
|
1912
|
+
const raw = fs5.readFileSync(filePath, "utf-8");
|
|
1913
|
+
let parsed;
|
|
1914
|
+
try {
|
|
1915
|
+
parsed = JSON.parse(raw);
|
|
1916
|
+
} catch {
|
|
1917
|
+
throw new Error(
|
|
1918
|
+
`Failed to parse lockfile at ${filePath}. The file may be corrupted. Delete it and re-run harness install to regenerate.`
|
|
1919
|
+
);
|
|
1920
|
+
}
|
|
1921
|
+
if (!parsed || typeof parsed !== "object" || !("version" in parsed) || parsed.version !== 1 || !("skills" in parsed) || typeof parsed.skills !== "object") {
|
|
1922
|
+
throw new Error(
|
|
1923
|
+
`Invalid lockfile format at ${filePath}. Expected version 1 with a skills object. Delete it and re-run harness install to regenerate.`
|
|
1924
|
+
);
|
|
1925
|
+
}
|
|
1926
|
+
return parsed;
|
|
1927
|
+
}
|
|
1928
|
+
function writeLockfile(filePath, lockfile) {
|
|
1929
|
+
const dir = path14.dirname(filePath);
|
|
1930
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
1931
|
+
fs5.writeFileSync(filePath, sortedStringify(lockfile) + "\n", "utf-8");
|
|
1932
|
+
}
|
|
1933
|
+
function updateLockfileEntry(lockfile, name, entry) {
|
|
1934
|
+
return {
|
|
1935
|
+
...lockfile,
|
|
1936
|
+
skills: {
|
|
1937
|
+
...lockfile.skills,
|
|
1938
|
+
[name]: entry
|
|
1875
1939
|
}
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
function removeLockfileEntry(lockfile, name) {
|
|
1943
|
+
if (!(name in lockfile.skills)) {
|
|
1944
|
+
return lockfile;
|
|
1945
|
+
}
|
|
1946
|
+
const { [name]: _removed, ...rest } = lockfile.skills;
|
|
1947
|
+
return {
|
|
1948
|
+
...lockfile,
|
|
1949
|
+
skills: rest
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
// src/commands/skill/list.ts
|
|
1954
|
+
function scanDirectory(dirPath, source) {
|
|
1955
|
+
if (!fs6.existsSync(dirPath)) return [];
|
|
1956
|
+
const entries = fs6.readdirSync(dirPath, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
1957
|
+
const skills = [];
|
|
1958
|
+
for (const name of entries) {
|
|
1959
|
+
const yamlPath = path15.join(dirPath, name, "skill.yaml");
|
|
1960
|
+
if (!fs6.existsSync(yamlPath)) continue;
|
|
1961
|
+
try {
|
|
1962
|
+
const raw = fs6.readFileSync(yamlPath, "utf-8");
|
|
1963
|
+
const parsed = parse(raw);
|
|
1964
|
+
const result = SkillMetadataSchema.safeParse(parsed);
|
|
1965
|
+
if (result.success) {
|
|
1966
|
+
skills.push({
|
|
1967
|
+
name: result.data.name,
|
|
1968
|
+
description: result.data.description,
|
|
1969
|
+
type: result.data.type,
|
|
1970
|
+
source
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
} catch {
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
return skills;
|
|
1977
|
+
}
|
|
1978
|
+
function collectSkills(opts) {
|
|
1979
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1980
|
+
const allSkills = [];
|
|
1981
|
+
const addUnique = (entries) => {
|
|
1982
|
+
for (const entry of entries) {
|
|
1983
|
+
if (!seen.has(entry.name)) {
|
|
1984
|
+
seen.add(entry.name);
|
|
1985
|
+
allSkills.push(entry);
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
};
|
|
1989
|
+
if (opts.filter === "all" || opts.filter === "local") {
|
|
1990
|
+
const projectDir = resolveProjectSkillsDir();
|
|
1991
|
+
if (projectDir) {
|
|
1992
|
+
addUnique(scanDirectory(projectDir, "local"));
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
if (opts.filter === "all" || opts.filter === "installed") {
|
|
1996
|
+
const globalDir = resolveGlobalSkillsDir();
|
|
1997
|
+
const skillsDir = path15.dirname(globalDir);
|
|
1998
|
+
const communityBase = path15.join(skillsDir, "community");
|
|
1999
|
+
const lockfilePath = path15.join(communityBase, "skills-lock.json");
|
|
2000
|
+
const lockfile = readLockfile(lockfilePath);
|
|
2001
|
+
for (const [pkgName, entry] of Object.entries(lockfile.skills)) {
|
|
2002
|
+
const shortName = pkgName.replace("@harness-skills/", "");
|
|
2003
|
+
if (!seen.has(shortName)) {
|
|
2004
|
+
seen.add(shortName);
|
|
2005
|
+
allSkills.push({
|
|
2006
|
+
name: shortName,
|
|
2007
|
+
description: "",
|
|
2008
|
+
type: "",
|
|
2009
|
+
source: "community",
|
|
2010
|
+
version: entry.version
|
|
2011
|
+
});
|
|
1889
2012
|
}
|
|
1890
2013
|
}
|
|
2014
|
+
}
|
|
2015
|
+
if (opts.filter === "all") {
|
|
2016
|
+
const globalDir = resolveGlobalSkillsDir();
|
|
2017
|
+
addUnique(scanDirectory(globalDir, "bundled"));
|
|
2018
|
+
}
|
|
2019
|
+
if (opts.filter === "installed") {
|
|
2020
|
+
return allSkills.filter((s) => s.source === "community");
|
|
2021
|
+
}
|
|
2022
|
+
if (opts.filter === "local") {
|
|
2023
|
+
return allSkills.filter((s) => s.source === "local");
|
|
2024
|
+
}
|
|
2025
|
+
return allSkills;
|
|
2026
|
+
}
|
|
2027
|
+
function createListCommand2() {
|
|
2028
|
+
return new Command21("list").description("List available skills").option("--installed", "Show only community-installed skills").option("--local", "Show only project-local skills").option("--all", "Show all skills (default)").action(async (opts, cmd) => {
|
|
2029
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
2030
|
+
let filter = "all";
|
|
2031
|
+
if (opts.installed) filter = "installed";
|
|
2032
|
+
else if (opts.local) filter = "local";
|
|
2033
|
+
const skills = collectSkills({ filter });
|
|
1891
2034
|
if (globalOpts.json) {
|
|
1892
2035
|
logger.raw(skills);
|
|
1893
2036
|
} else if (globalOpts.quiet) {
|
|
@@ -1898,9 +2041,12 @@ function createListCommand2() {
|
|
|
1898
2041
|
} else {
|
|
1899
2042
|
console.log("Available skills:\n");
|
|
1900
2043
|
for (const s of skills) {
|
|
1901
|
-
|
|
1902
|
-
console.log(`
|
|
1903
|
-
|
|
2044
|
+
const version = s.version ? `@${s.version}` : "";
|
|
2045
|
+
console.log(` ${s.name}${version} [${s.source}] (${s.type || "unknown"})`);
|
|
2046
|
+
if (s.description) {
|
|
2047
|
+
console.log(` ${s.description}`);
|
|
2048
|
+
}
|
|
2049
|
+
console.log();
|
|
1904
2050
|
}
|
|
1905
2051
|
}
|
|
1906
2052
|
}
|
|
@@ -1910,8 +2056,8 @@ function createListCommand2() {
|
|
|
1910
2056
|
|
|
1911
2057
|
// src/commands/skill/run.ts
|
|
1912
2058
|
import { Command as Command22 } from "commander";
|
|
1913
|
-
import * as
|
|
1914
|
-
import * as
|
|
2059
|
+
import * as fs7 from "fs";
|
|
2060
|
+
import * as path16 from "path";
|
|
1915
2061
|
import { parse as parse2 } from "yaml";
|
|
1916
2062
|
|
|
1917
2063
|
// src/skill/complexity.ts
|
|
@@ -2001,17 +2147,17 @@ ${options.priorState}`);
|
|
|
2001
2147
|
function createRunCommand2() {
|
|
2002
2148
|
return new Command22("run").description("Run a skill (outputs SKILL.md content with context preamble)").argument("<name>", "Skill name (e.g., harness-tdd)").option("--path <path>", "Project root path for context injection").option("--complexity <level>", "Complexity: auto, light, full", "auto").option("--phase <name>", "Start at a specific phase (for re-entry)").option("--party", "Enable multi-perspective evaluation").action(async (name, opts, _cmd) => {
|
|
2003
2149
|
const skillsDir = resolveSkillsDir();
|
|
2004
|
-
const skillDir =
|
|
2005
|
-
if (!
|
|
2150
|
+
const skillDir = path16.join(skillsDir, name);
|
|
2151
|
+
if (!fs7.existsSync(skillDir)) {
|
|
2006
2152
|
logger.error(`Skill not found: ${name}`);
|
|
2007
2153
|
process.exit(ExitCode.ERROR);
|
|
2008
2154
|
return;
|
|
2009
2155
|
}
|
|
2010
|
-
const yamlPath =
|
|
2156
|
+
const yamlPath = path16.join(skillDir, "skill.yaml");
|
|
2011
2157
|
let metadata = null;
|
|
2012
|
-
if (
|
|
2158
|
+
if (fs7.existsSync(yamlPath)) {
|
|
2013
2159
|
try {
|
|
2014
|
-
const raw =
|
|
2160
|
+
const raw = fs7.readFileSync(yamlPath, "utf-8");
|
|
2015
2161
|
const parsed = parse2(raw);
|
|
2016
2162
|
const result = SkillMetadataSchema.safeParse(parsed);
|
|
2017
2163
|
if (result.success) metadata = result.data;
|
|
@@ -2022,17 +2168,17 @@ function createRunCommand2() {
|
|
|
2022
2168
|
if (metadata?.phases && metadata.phases.length > 0) {
|
|
2023
2169
|
const requested = opts.complexity ?? "auto";
|
|
2024
2170
|
if (requested === "auto") {
|
|
2025
|
-
const projectPath2 = opts.path ?
|
|
2171
|
+
const projectPath2 = opts.path ? path16.resolve(opts.path) : process.cwd();
|
|
2026
2172
|
complexity = detectComplexity(projectPath2);
|
|
2027
2173
|
} else {
|
|
2028
2174
|
complexity = requested;
|
|
2029
2175
|
}
|
|
2030
2176
|
}
|
|
2031
2177
|
let principles;
|
|
2032
|
-
const projectPath = opts.path ?
|
|
2033
|
-
const principlesPath =
|
|
2034
|
-
if (
|
|
2035
|
-
principles =
|
|
2178
|
+
const projectPath = opts.path ? path16.resolve(opts.path) : process.cwd();
|
|
2179
|
+
const principlesPath = path16.join(projectPath, "docs", "principles.md");
|
|
2180
|
+
if (fs7.existsSync(principlesPath)) {
|
|
2181
|
+
principles = fs7.readFileSync(principlesPath, "utf-8");
|
|
2036
2182
|
}
|
|
2037
2183
|
let priorState;
|
|
2038
2184
|
let stateWarning;
|
|
@@ -2047,16 +2193,16 @@ function createRunCommand2() {
|
|
|
2047
2193
|
}
|
|
2048
2194
|
if (metadata?.state.persistent && metadata.state.files.length > 0) {
|
|
2049
2195
|
for (const stateFilePath of metadata.state.files) {
|
|
2050
|
-
const fullPath =
|
|
2051
|
-
if (
|
|
2052
|
-
const stat =
|
|
2196
|
+
const fullPath = path16.join(projectPath, stateFilePath);
|
|
2197
|
+
if (fs7.existsSync(fullPath)) {
|
|
2198
|
+
const stat = fs7.statSync(fullPath);
|
|
2053
2199
|
if (stat.isDirectory()) {
|
|
2054
|
-
const files =
|
|
2200
|
+
const files = fs7.readdirSync(fullPath).map((f) => ({ name: f, mtime: fs7.statSync(path16.join(fullPath, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
|
|
2055
2201
|
if (files.length > 0) {
|
|
2056
|
-
priorState =
|
|
2202
|
+
priorState = fs7.readFileSync(path16.join(fullPath, files[0].name), "utf-8");
|
|
2057
2203
|
}
|
|
2058
2204
|
} else {
|
|
2059
|
-
priorState =
|
|
2205
|
+
priorState = fs7.readFileSync(fullPath, "utf-8");
|
|
2060
2206
|
}
|
|
2061
2207
|
break;
|
|
2062
2208
|
}
|
|
@@ -2075,17 +2221,17 @@ function createRunCommand2() {
|
|
|
2075
2221
|
...stateWarning !== void 0 && { stateWarning },
|
|
2076
2222
|
party: opts.party
|
|
2077
2223
|
});
|
|
2078
|
-
const skillMdPath =
|
|
2079
|
-
if (!
|
|
2224
|
+
const skillMdPath = path16.join(skillDir, "SKILL.md");
|
|
2225
|
+
if (!fs7.existsSync(skillMdPath)) {
|
|
2080
2226
|
logger.error(`SKILL.md not found for skill: ${name}`);
|
|
2081
2227
|
process.exit(ExitCode.ERROR);
|
|
2082
2228
|
return;
|
|
2083
2229
|
}
|
|
2084
|
-
let content =
|
|
2230
|
+
let content = fs7.readFileSync(skillMdPath, "utf-8");
|
|
2085
2231
|
if (metadata?.state.persistent && opts.path) {
|
|
2086
|
-
const stateFile =
|
|
2087
|
-
if (
|
|
2088
|
-
const stateContent =
|
|
2232
|
+
const stateFile = path16.join(projectPath, ".harness", "state.json");
|
|
2233
|
+
if (fs7.existsSync(stateFile)) {
|
|
2234
|
+
const stateContent = fs7.readFileSync(stateFile, "utf-8");
|
|
2089
2235
|
content += `
|
|
2090
2236
|
|
|
2091
2237
|
---
|
|
@@ -2103,8 +2249,8 @@ ${stateContent}
|
|
|
2103
2249
|
|
|
2104
2250
|
// src/commands/skill/validate.ts
|
|
2105
2251
|
import { Command as Command23 } from "commander";
|
|
2106
|
-
import * as
|
|
2107
|
-
import * as
|
|
2252
|
+
import * as fs8 from "fs";
|
|
2253
|
+
import * as path17 from "path";
|
|
2108
2254
|
import { parse as parse3 } from "yaml";
|
|
2109
2255
|
var REQUIRED_SECTIONS = [
|
|
2110
2256
|
"## When to Use",
|
|
@@ -2117,32 +2263,32 @@ function createValidateCommand3() {
|
|
|
2117
2263
|
return new Command23("validate").description("Validate all skill.yaml files and SKILL.md structure").action(async (_opts, cmd) => {
|
|
2118
2264
|
const globalOpts = cmd.optsWithGlobals();
|
|
2119
2265
|
const skillsDir = resolveSkillsDir();
|
|
2120
|
-
if (!
|
|
2266
|
+
if (!fs8.existsSync(skillsDir)) {
|
|
2121
2267
|
logger.info("No skills directory found.");
|
|
2122
2268
|
process.exit(ExitCode.SUCCESS);
|
|
2123
2269
|
return;
|
|
2124
2270
|
}
|
|
2125
|
-
const entries =
|
|
2271
|
+
const entries = fs8.readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
2126
2272
|
const errors = [];
|
|
2127
2273
|
let validated = 0;
|
|
2128
2274
|
for (const name of entries) {
|
|
2129
|
-
const skillDir =
|
|
2130
|
-
const yamlPath =
|
|
2131
|
-
const skillMdPath =
|
|
2132
|
-
if (!
|
|
2275
|
+
const skillDir = path17.join(skillsDir, name);
|
|
2276
|
+
const yamlPath = path17.join(skillDir, "skill.yaml");
|
|
2277
|
+
const skillMdPath = path17.join(skillDir, "SKILL.md");
|
|
2278
|
+
if (!fs8.existsSync(yamlPath)) {
|
|
2133
2279
|
errors.push(`${name}: missing skill.yaml`);
|
|
2134
2280
|
continue;
|
|
2135
2281
|
}
|
|
2136
2282
|
try {
|
|
2137
|
-
const raw =
|
|
2283
|
+
const raw = fs8.readFileSync(yamlPath, "utf-8");
|
|
2138
2284
|
const parsed = parse3(raw);
|
|
2139
2285
|
const result = SkillMetadataSchema.safeParse(parsed);
|
|
2140
2286
|
if (!result.success) {
|
|
2141
2287
|
errors.push(`${name}/skill.yaml: ${result.error.message}`);
|
|
2142
2288
|
continue;
|
|
2143
2289
|
}
|
|
2144
|
-
if (
|
|
2145
|
-
const mdContent =
|
|
2290
|
+
if (fs8.existsSync(skillMdPath)) {
|
|
2291
|
+
const mdContent = fs8.readFileSync(skillMdPath, "utf-8");
|
|
2146
2292
|
for (const section of REQUIRED_SECTIONS) {
|
|
2147
2293
|
if (!mdContent.includes(section)) {
|
|
2148
2294
|
errors.push(`${name}/SKILL.md: missing section "${section}"`);
|
|
@@ -2184,27 +2330,27 @@ function createValidateCommand3() {
|
|
|
2184
2330
|
|
|
2185
2331
|
// src/commands/skill/info.ts
|
|
2186
2332
|
import { Command as Command24 } from "commander";
|
|
2187
|
-
import * as
|
|
2188
|
-
import * as
|
|
2333
|
+
import * as fs9 from "fs";
|
|
2334
|
+
import * as path18 from "path";
|
|
2189
2335
|
import { parse as parse4 } from "yaml";
|
|
2190
2336
|
function createInfoCommand() {
|
|
2191
2337
|
return new Command24("info").description("Show metadata for a skill").argument("<name>", "Skill name (e.g., harness-tdd)").action(async (name, _opts, cmd) => {
|
|
2192
2338
|
const globalOpts = cmd.optsWithGlobals();
|
|
2193
2339
|
const skillsDir = resolveSkillsDir();
|
|
2194
|
-
const skillDir =
|
|
2195
|
-
if (!
|
|
2340
|
+
const skillDir = path18.join(skillsDir, name);
|
|
2341
|
+
if (!fs9.existsSync(skillDir)) {
|
|
2196
2342
|
logger.error(`Skill not found: ${name}`);
|
|
2197
2343
|
process.exit(ExitCode.ERROR);
|
|
2198
2344
|
return;
|
|
2199
2345
|
}
|
|
2200
|
-
const yamlPath =
|
|
2201
|
-
if (!
|
|
2346
|
+
const yamlPath = path18.join(skillDir, "skill.yaml");
|
|
2347
|
+
if (!fs9.existsSync(yamlPath)) {
|
|
2202
2348
|
logger.error(`skill.yaml not found for skill: ${name}`);
|
|
2203
2349
|
process.exit(ExitCode.ERROR);
|
|
2204
2350
|
return;
|
|
2205
2351
|
}
|
|
2206
2352
|
try {
|
|
2207
|
-
const raw =
|
|
2353
|
+
const raw = fs9.readFileSync(yamlPath, "utf-8");
|
|
2208
2354
|
const parsed = parse4(raw);
|
|
2209
2355
|
const result = SkillMetadataSchema.safeParse(parsed);
|
|
2210
2356
|
if (!result.success) {
|
|
@@ -2243,138 +2389,623 @@ function createInfoCommand() {
|
|
|
2243
2389
|
});
|
|
2244
2390
|
}
|
|
2245
2391
|
|
|
2246
|
-
// src/commands/skill/
|
|
2247
|
-
|
|
2248
|
-
const command = new Command25("skill").description("Skill management commands");
|
|
2249
|
-
command.addCommand(createListCommand2());
|
|
2250
|
-
command.addCommand(createRunCommand2());
|
|
2251
|
-
command.addCommand(createValidateCommand3());
|
|
2252
|
-
command.addCommand(createInfoCommand());
|
|
2253
|
-
return command;
|
|
2254
|
-
}
|
|
2255
|
-
|
|
2256
|
-
// src/commands/state/index.ts
|
|
2257
|
-
import { Command as Command30 } from "commander";
|
|
2392
|
+
// src/commands/skill/search.ts
|
|
2393
|
+
import { Command as Command25 } from "commander";
|
|
2258
2394
|
|
|
2259
|
-
// src/
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2395
|
+
// src/registry/npm-client.ts
|
|
2396
|
+
var NPM_REGISTRY = "https://registry.npmjs.org";
|
|
2397
|
+
var FETCH_TIMEOUT_MS = 3e4;
|
|
2398
|
+
var HARNESS_SKILLS_SCOPE = "@harness-skills/";
|
|
2399
|
+
function resolvePackageName(name) {
|
|
2400
|
+
if (name.startsWith(HARNESS_SKILLS_SCOPE)) {
|
|
2401
|
+
return name;
|
|
2402
|
+
}
|
|
2403
|
+
if (name.startsWith("@")) {
|
|
2404
|
+
throw new Error(`Only @harness-skills/ scoped packages are supported. Got: ${name}`);
|
|
2405
|
+
}
|
|
2406
|
+
return `${HARNESS_SKILLS_SCOPE}${name}`;
|
|
2407
|
+
}
|
|
2408
|
+
function extractSkillName(packageName) {
|
|
2409
|
+
if (packageName.startsWith(HARNESS_SKILLS_SCOPE)) {
|
|
2410
|
+
return packageName.slice(HARNESS_SKILLS_SCOPE.length);
|
|
2411
|
+
}
|
|
2412
|
+
return packageName;
|
|
2413
|
+
}
|
|
2414
|
+
async function fetchPackageMetadata(packageName) {
|
|
2415
|
+
const encodedName = encodeURIComponent(packageName);
|
|
2416
|
+
const url = `${NPM_REGISTRY}/${encodedName}`;
|
|
2417
|
+
let response;
|
|
2418
|
+
try {
|
|
2419
|
+
response = await fetch(url, {
|
|
2420
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
2421
|
+
});
|
|
2422
|
+
} catch {
|
|
2423
|
+
throw new Error("Cannot reach npm registry. Check your network connection.");
|
|
2424
|
+
}
|
|
2425
|
+
if (!response.ok) {
|
|
2426
|
+
if (response.status === 404) {
|
|
2427
|
+
throw new Error(`Package ${packageName} not found on npm registry.`);
|
|
2271
2428
|
}
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
}
|
|
2285
|
-
if (
|
|
2286
|
-
|
|
2287
|
-
for (const [task, status] of Object.entries(state.progress)) {
|
|
2288
|
-
console.log(` ${task}: ${status}`);
|
|
2289
|
-
}
|
|
2290
|
-
}
|
|
2291
|
-
if (state.decisions.length > 0) {
|
|
2292
|
-
console.log(`
|
|
2293
|
-
Decisions: ${state.decisions.length}`);
|
|
2294
|
-
}
|
|
2295
|
-
if (state.blockers.length > 0) {
|
|
2296
|
-
const open = state.blockers.filter((b) => b.status === "open").length;
|
|
2297
|
-
console.log(`Blockers: ${open} open / ${state.blockers.length} total`);
|
|
2429
|
+
throw new Error(
|
|
2430
|
+
`npm registry returned ${response.status} ${response.statusText} for ${packageName}.`
|
|
2431
|
+
);
|
|
2432
|
+
}
|
|
2433
|
+
return await response.json();
|
|
2434
|
+
}
|
|
2435
|
+
async function downloadTarball(tarballUrl) {
|
|
2436
|
+
let lastError;
|
|
2437
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
2438
|
+
try {
|
|
2439
|
+
const response = await fetch(tarballUrl, {
|
|
2440
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
2441
|
+
});
|
|
2442
|
+
if (!response.ok) {
|
|
2443
|
+
throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
2298
2444
|
}
|
|
2445
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
2446
|
+
return Buffer.from(arrayBuffer);
|
|
2447
|
+
} catch (err) {
|
|
2448
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
2299
2449
|
}
|
|
2300
|
-
|
|
2301
|
-
});
|
|
2450
|
+
}
|
|
2451
|
+
throw new Error(`Download failed for ${tarballUrl}. Try again. (${lastError?.message})`);
|
|
2452
|
+
}
|
|
2453
|
+
async function searchNpmRegistry(query) {
|
|
2454
|
+
const searchText = encodeURIComponent(`scope:harness-skills ${query}`);
|
|
2455
|
+
const url = `${NPM_REGISTRY}/-/v1/search?text=${searchText}&size=20`;
|
|
2456
|
+
let response;
|
|
2457
|
+
try {
|
|
2458
|
+
response = await fetch(url, {
|
|
2459
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
2460
|
+
});
|
|
2461
|
+
} catch {
|
|
2462
|
+
throw new Error("Cannot reach npm registry. Check your network connection.");
|
|
2463
|
+
}
|
|
2464
|
+
if (!response.ok) {
|
|
2465
|
+
throw new Error(`npm registry search returned ${response.status} ${response.statusText}.`);
|
|
2466
|
+
}
|
|
2467
|
+
const data = await response.json();
|
|
2468
|
+
return data.objects.map((obj) => ({
|
|
2469
|
+
name: obj.package.name,
|
|
2470
|
+
version: obj.package.version,
|
|
2471
|
+
description: obj.package.description,
|
|
2472
|
+
keywords: obj.package.keywords || [],
|
|
2473
|
+
date: obj.package.date
|
|
2474
|
+
}));
|
|
2302
2475
|
}
|
|
2303
2476
|
|
|
2304
|
-
// src/commands/
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
return new Command27("reset").description("Reset project state (deletes .harness/state.json)").option("--path <path>", "Project root path", ".").option("--stream <name>", "Target a specific stream").option("--yes", "Skip confirmation prompt").action(async (opts, _cmd) => {
|
|
2311
|
-
const projectPath = path19.resolve(opts.path);
|
|
2312
|
-
let statePath;
|
|
2313
|
-
if (opts.stream) {
|
|
2314
|
-
const streamResult = await resolveStreamPath(projectPath, { stream: opts.stream });
|
|
2315
|
-
if (!streamResult.ok) {
|
|
2316
|
-
logger.error(streamResult.error.message);
|
|
2317
|
-
process.exit(ExitCode.ERROR);
|
|
2318
|
-
return;
|
|
2319
|
-
}
|
|
2320
|
-
statePath = path19.join(streamResult.value, "state.json");
|
|
2321
|
-
} else {
|
|
2322
|
-
statePath = path19.join(projectPath, ".harness", "state.json");
|
|
2477
|
+
// src/commands/skill/search.ts
|
|
2478
|
+
async function runSearch(query, opts) {
|
|
2479
|
+
const results = await searchNpmRegistry(query);
|
|
2480
|
+
return results.filter((r) => {
|
|
2481
|
+
if (opts.platform && !r.keywords.includes(opts.platform)) {
|
|
2482
|
+
return false;
|
|
2323
2483
|
}
|
|
2324
|
-
if (!
|
|
2325
|
-
|
|
2326
|
-
process.exit(ExitCode.SUCCESS);
|
|
2327
|
-
return;
|
|
2484
|
+
if (opts.trigger && !r.keywords.includes(opts.trigger)) {
|
|
2485
|
+
return false;
|
|
2328
2486
|
}
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2487
|
+
return true;
|
|
2488
|
+
});
|
|
2489
|
+
}
|
|
2490
|
+
function createSearchCommand() {
|
|
2491
|
+
return new Command25("search").description("Search for community skills on the @harness-skills registry").argument("<query>", "Search query").option("--platform <platform>", "Filter by platform (e.g., claude-code)").option("--trigger <trigger>", "Filter by trigger type (e.g., manual, automatic)").action(async (query, opts, cmd) => {
|
|
2492
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
2493
|
+
try {
|
|
2494
|
+
const results = await runSearch(query, opts);
|
|
2495
|
+
if (globalOpts.json) {
|
|
2496
|
+
logger.raw(results);
|
|
2337
2497
|
process.exit(ExitCode.SUCCESS);
|
|
2338
2498
|
return;
|
|
2339
2499
|
}
|
|
2500
|
+
if (results.length === 0) {
|
|
2501
|
+
logger.info(`No skills found matching "${query}".`);
|
|
2502
|
+
process.exit(ExitCode.SUCCESS);
|
|
2503
|
+
return;
|
|
2504
|
+
}
|
|
2505
|
+
console.log(`
|
|
2506
|
+
Found ${results.length} skill(s):
|
|
2507
|
+
`);
|
|
2508
|
+
for (const r of results) {
|
|
2509
|
+
const shortName = extractSkillName(r.name);
|
|
2510
|
+
console.log(` ${shortName}@${r.version}`);
|
|
2511
|
+
console.log(` ${r.description}`);
|
|
2512
|
+
if (r.keywords.length > 0) {
|
|
2513
|
+
console.log(` keywords: ${r.keywords.join(", ")}`);
|
|
2514
|
+
}
|
|
2515
|
+
console.log();
|
|
2516
|
+
}
|
|
2517
|
+
logger.info(`Install with: harness install <skill-name>`);
|
|
2518
|
+
process.exit(ExitCode.SUCCESS);
|
|
2519
|
+
} catch (err) {
|
|
2520
|
+
logger.error(err instanceof Error ? err.message : String(err));
|
|
2521
|
+
process.exit(ExitCode.VALIDATION_FAILED);
|
|
2340
2522
|
}
|
|
2341
|
-
try {
|
|
2342
|
-
fs9.unlinkSync(statePath);
|
|
2343
|
-
logger.success("Project state reset.");
|
|
2344
|
-
} catch (e) {
|
|
2345
|
-
logger.error(`Failed to reset state: ${e instanceof Error ? e.message : String(e)}`);
|
|
2346
|
-
process.exit(ExitCode.ERROR);
|
|
2347
|
-
return;
|
|
2348
|
-
}
|
|
2349
|
-
process.exit(ExitCode.SUCCESS);
|
|
2350
2523
|
});
|
|
2351
2524
|
}
|
|
2352
2525
|
|
|
2353
|
-
// src/commands/
|
|
2354
|
-
import { Command as
|
|
2355
|
-
import * as
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2526
|
+
// src/commands/skill/create.ts
|
|
2527
|
+
import { Command as Command26 } from "commander";
|
|
2528
|
+
import * as path19 from "path";
|
|
2529
|
+
import * as fs10 from "fs";
|
|
2530
|
+
import YAML from "yaml";
|
|
2531
|
+
var KEBAB_CASE_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
2532
|
+
function buildReadme(name, description) {
|
|
2533
|
+
return `# @harness-skills/${name}
|
|
2534
|
+
|
|
2535
|
+
${description}
|
|
2536
|
+
|
|
2537
|
+
## Installation
|
|
2538
|
+
|
|
2539
|
+
\`\`\`bash
|
|
2540
|
+
harness install ${name}
|
|
2541
|
+
\`\`\`
|
|
2542
|
+
|
|
2543
|
+
## Usage
|
|
2544
|
+
|
|
2545
|
+
This skill is automatically available after installation. Invoke it via:
|
|
2546
|
+
|
|
2547
|
+
\`\`\`bash
|
|
2548
|
+
harness skill run ${name}
|
|
2549
|
+
\`\`\`
|
|
2550
|
+
|
|
2551
|
+
Or use the slash command \`/${name}\` in your AI coding assistant.
|
|
2552
|
+
|
|
2553
|
+
## Development
|
|
2554
|
+
|
|
2555
|
+
Edit \`skill.yaml\` to configure the skill metadata and \`SKILL.md\` to define the skill's behavior.
|
|
2556
|
+
|
|
2557
|
+
### Validate
|
|
2558
|
+
|
|
2559
|
+
\`\`\`bash
|
|
2560
|
+
harness skill validate ${name}
|
|
2561
|
+
\`\`\`
|
|
2562
|
+
|
|
2563
|
+
### Publish
|
|
2564
|
+
|
|
2565
|
+
\`\`\`bash
|
|
2566
|
+
harness skills publish
|
|
2567
|
+
\`\`\`
|
|
2568
|
+
`;
|
|
2569
|
+
}
|
|
2570
|
+
function buildSkillYaml(name, opts) {
|
|
2571
|
+
const platforms = opts.platforms ? opts.platforms.split(",").map((p) => p.trim()) : ["claude-code"];
|
|
2572
|
+
const triggers = opts.triggers ? opts.triggers.split(",").map((t) => t.trim()) : ["manual"];
|
|
2573
|
+
return {
|
|
2574
|
+
name,
|
|
2575
|
+
version: "0.1.0",
|
|
2576
|
+
description: opts.description || `A community skill: ${name}`,
|
|
2577
|
+
triggers,
|
|
2578
|
+
platforms,
|
|
2579
|
+
tools: ["Read", "Grep", "Glob", "Edit", "Write", "Bash"],
|
|
2580
|
+
type: opts.type || "flexible",
|
|
2581
|
+
state: {
|
|
2582
|
+
persistent: false,
|
|
2583
|
+
files: []
|
|
2584
|
+
},
|
|
2585
|
+
depends_on: []
|
|
2586
|
+
};
|
|
2368
2587
|
}
|
|
2588
|
+
function buildSkillMd(name, description) {
|
|
2589
|
+
return `# ${name}
|
|
2369
2590
|
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2591
|
+
${description}
|
|
2592
|
+
|
|
2593
|
+
## When to Use
|
|
2594
|
+
|
|
2595
|
+
- [Describe when this skill should be invoked]
|
|
2596
|
+
- [Describe the trigger conditions]
|
|
2597
|
+
|
|
2598
|
+
## Process
|
|
2599
|
+
|
|
2600
|
+
1. [Describe the step-by-step process]
|
|
2601
|
+
2. [Add additional steps as needed]
|
|
2602
|
+
|
|
2603
|
+
## Success Criteria
|
|
2604
|
+
|
|
2605
|
+
- [Define what a successful execution looks like]
|
|
2606
|
+
- [Add measurable criteria]
|
|
2607
|
+
`;
|
|
2608
|
+
}
|
|
2609
|
+
function runCreate(name, opts) {
|
|
2610
|
+
if (!KEBAB_CASE_RE.test(name)) {
|
|
2611
|
+
throw new Error(`Invalid skill name "${name}". Must be kebab-case (e.g., my-skill).`);
|
|
2612
|
+
}
|
|
2613
|
+
const baseDir = opts.outputDir ?? path19.join(process.cwd(), "agents", "skills", "claude-code");
|
|
2614
|
+
const skillDir = path19.join(baseDir, name);
|
|
2615
|
+
if (fs10.existsSync(skillDir)) {
|
|
2616
|
+
throw new Error(`Skill directory already exists: ${skillDir}`);
|
|
2617
|
+
}
|
|
2618
|
+
fs10.mkdirSync(skillDir, { recursive: true });
|
|
2619
|
+
const description = opts.description || `A community skill: ${name}`;
|
|
2620
|
+
const skillYaml = buildSkillYaml(name, opts);
|
|
2621
|
+
const skillYamlPath = path19.join(skillDir, "skill.yaml");
|
|
2622
|
+
fs10.writeFileSync(skillYamlPath, YAML.stringify(skillYaml));
|
|
2623
|
+
const skillMd = buildSkillMd(name, description);
|
|
2624
|
+
const skillMdPath = path19.join(skillDir, "SKILL.md");
|
|
2625
|
+
fs10.writeFileSync(skillMdPath, skillMd);
|
|
2626
|
+
const readme = buildReadme(name, description);
|
|
2627
|
+
const readmePath = path19.join(skillDir, "README.md");
|
|
2628
|
+
fs10.writeFileSync(readmePath, readme);
|
|
2629
|
+
return {
|
|
2630
|
+
name,
|
|
2631
|
+
directory: skillDir,
|
|
2632
|
+
files: [skillYamlPath, skillMdPath, readmePath]
|
|
2633
|
+
};
|
|
2634
|
+
}
|
|
2635
|
+
function createCreateCommand() {
|
|
2636
|
+
return new Command26("create").description("Scaffold a new community skill").argument("<name>", "Skill name (kebab-case)").option("--description <desc>", "Skill description").option("--type <type>", "Skill type: rigid or flexible", "flexible").option("--platforms <platforms>", "Comma-separated platforms (default: claude-code)").option("--triggers <triggers>", "Comma-separated triggers (default: manual)").option("--output-dir <dir>", "Output directory (default: agents/skills/claude-code/)").action(async (name, opts, cmd) => {
|
|
2376
2637
|
const globalOpts = cmd.optsWithGlobals();
|
|
2377
|
-
|
|
2638
|
+
try {
|
|
2639
|
+
const result = runCreate(name, {
|
|
2640
|
+
description: opts.description,
|
|
2641
|
+
type: opts.type,
|
|
2642
|
+
platforms: opts.platforms,
|
|
2643
|
+
triggers: opts.triggers,
|
|
2644
|
+
outputDir: opts.outputDir
|
|
2645
|
+
});
|
|
2646
|
+
if (globalOpts.json) {
|
|
2647
|
+
logger.raw(result);
|
|
2648
|
+
} else {
|
|
2649
|
+
logger.success(`Created skill "${name}"`);
|
|
2650
|
+
for (const f of result.files) {
|
|
2651
|
+
logger.info(` ${f}`);
|
|
2652
|
+
}
|
|
2653
|
+
logger.info(`
|
|
2654
|
+
Next steps:`);
|
|
2655
|
+
logger.info(
|
|
2656
|
+
` 1. Edit ${path19.join(result.directory, "SKILL.md")} with your skill content`
|
|
2657
|
+
);
|
|
2658
|
+
logger.info(` 2. Run: harness skill validate ${name}`);
|
|
2659
|
+
logger.info(` 3. Run: harness skills publish`);
|
|
2660
|
+
}
|
|
2661
|
+
} catch (err) {
|
|
2662
|
+
logger.error(err instanceof Error ? err.message : String(err));
|
|
2663
|
+
process.exit(ExitCode.VALIDATION_FAILED);
|
|
2664
|
+
}
|
|
2665
|
+
});
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
// src/commands/skill/publish.ts
|
|
2669
|
+
import { Command as Command27 } from "commander";
|
|
2670
|
+
import * as fs13 from "fs";
|
|
2671
|
+
import * as path21 from "path";
|
|
2672
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
2673
|
+
|
|
2674
|
+
// src/registry/validator.ts
|
|
2675
|
+
import * as fs12 from "fs";
|
|
2676
|
+
import * as path20 from "path";
|
|
2677
|
+
import { parse as parse5 } from "yaml";
|
|
2678
|
+
import semver from "semver";
|
|
2679
|
+
|
|
2680
|
+
// src/registry/bundled-skills.ts
|
|
2681
|
+
import * as fs11 from "fs";
|
|
2682
|
+
function getBundledSkillNames(bundledSkillsDir) {
|
|
2683
|
+
if (!fs11.existsSync(bundledSkillsDir)) {
|
|
2684
|
+
return /* @__PURE__ */ new Set();
|
|
2685
|
+
}
|
|
2686
|
+
const entries = fs11.readdirSync(bundledSkillsDir);
|
|
2687
|
+
const names = /* @__PURE__ */ new Set();
|
|
2688
|
+
for (const entry of entries) {
|
|
2689
|
+
try {
|
|
2690
|
+
const stat = fs11.statSync(`${bundledSkillsDir}/${entry}`);
|
|
2691
|
+
if (stat.isDirectory()) {
|
|
2692
|
+
names.add(String(entry));
|
|
2693
|
+
}
|
|
2694
|
+
} catch {
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
return names;
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
// src/registry/validator.ts
|
|
2701
|
+
async function validateForPublish(skillDir) {
|
|
2702
|
+
const errors = [];
|
|
2703
|
+
const skillYamlPath = path20.join(skillDir, "skill.yaml");
|
|
2704
|
+
if (!fs12.existsSync(skillYamlPath)) {
|
|
2705
|
+
errors.push("skill.yaml not found. Create one with: harness skill create <name>");
|
|
2706
|
+
return { valid: false, errors };
|
|
2707
|
+
}
|
|
2708
|
+
let skillMeta;
|
|
2709
|
+
try {
|
|
2710
|
+
const raw = fs12.readFileSync(skillYamlPath, "utf-8");
|
|
2711
|
+
const parsed = parse5(raw);
|
|
2712
|
+
const result = SkillMetadataSchema.safeParse(parsed);
|
|
2713
|
+
if (!result.success) {
|
|
2714
|
+
const issues = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
2715
|
+
errors.push(`skill.yaml validation failed: ${issues}`);
|
|
2716
|
+
return { valid: false, errors };
|
|
2717
|
+
}
|
|
2718
|
+
skillMeta = result.data;
|
|
2719
|
+
} catch (err) {
|
|
2720
|
+
errors.push(`Failed to parse skill.yaml: ${err instanceof Error ? err.message : String(err)}`);
|
|
2721
|
+
return { valid: false, errors };
|
|
2722
|
+
}
|
|
2723
|
+
if (!skillMeta.description || skillMeta.description.trim().length === 0) {
|
|
2724
|
+
errors.push("description must not be empty. Add a meaningful description to skill.yaml.");
|
|
2725
|
+
}
|
|
2726
|
+
if (!skillMeta.platforms || skillMeta.platforms.length === 0) {
|
|
2727
|
+
errors.push("At least one platform is required. Add platforms to skill.yaml.");
|
|
2728
|
+
}
|
|
2729
|
+
if (!skillMeta.triggers || skillMeta.triggers.length === 0) {
|
|
2730
|
+
errors.push("At least one trigger is required. Add triggers to skill.yaml.");
|
|
2731
|
+
}
|
|
2732
|
+
const skillMdPath = path20.join(skillDir, "SKILL.md");
|
|
2733
|
+
if (!fs12.existsSync(skillMdPath)) {
|
|
2734
|
+
errors.push("SKILL.md not found. Create it with content describing your skill.");
|
|
2735
|
+
} else {
|
|
2736
|
+
const content = fs12.readFileSync(skillMdPath, "utf-8");
|
|
2737
|
+
if (!content.includes("## When to Use")) {
|
|
2738
|
+
errors.push('SKILL.md must contain a "## When to Use" section.');
|
|
2739
|
+
}
|
|
2740
|
+
if (!content.includes("## Process")) {
|
|
2741
|
+
errors.push('SKILL.md must contain a "## Process" section.');
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
const globalSkillsDir = resolveGlobalSkillsDir();
|
|
2745
|
+
const bundledNames = getBundledSkillNames(globalSkillsDir);
|
|
2746
|
+
if (bundledNames.has(skillMeta.name)) {
|
|
2747
|
+
errors.push(
|
|
2748
|
+
`Skill name "${skillMeta.name}" conflicts with a bundled skill. Choose a different name.`
|
|
2749
|
+
);
|
|
2750
|
+
}
|
|
2751
|
+
try {
|
|
2752
|
+
const packageName = resolvePackageName(skillMeta.name);
|
|
2753
|
+
const metadata = await fetchPackageMetadata(packageName);
|
|
2754
|
+
const publishedVersion = metadata["dist-tags"]?.latest;
|
|
2755
|
+
if (publishedVersion && !semver.gt(skillMeta.version, publishedVersion)) {
|
|
2756
|
+
errors.push(
|
|
2757
|
+
`Version ${skillMeta.version} must be greater than published version ${publishedVersion}. Bump the version in skill.yaml.`
|
|
2758
|
+
);
|
|
2759
|
+
}
|
|
2760
|
+
} catch {
|
|
2761
|
+
}
|
|
2762
|
+
if (skillMeta.depends_on && skillMeta.depends_on.length > 0) {
|
|
2763
|
+
for (const dep of skillMeta.depends_on) {
|
|
2764
|
+
if (bundledNames.has(dep)) continue;
|
|
2765
|
+
try {
|
|
2766
|
+
const depPkg = resolvePackageName(dep);
|
|
2767
|
+
await fetchPackageMetadata(depPkg);
|
|
2768
|
+
} catch {
|
|
2769
|
+
errors.push(
|
|
2770
|
+
`Dependency "${dep}" not found on npm or as a bundled skill. Publish it first or remove from depends_on.`
|
|
2771
|
+
);
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
return {
|
|
2776
|
+
valid: errors.length === 0,
|
|
2777
|
+
errors,
|
|
2778
|
+
...skillMeta ? { skillMeta } : {}
|
|
2779
|
+
};
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
// src/skill/package-json.ts
|
|
2783
|
+
function derivePackageJson(skill) {
|
|
2784
|
+
const keywords = ["harness-skill", ...skill.platforms, ...skill.triggers];
|
|
2785
|
+
const pkg = {
|
|
2786
|
+
name: `@harness-skills/${skill.name}`,
|
|
2787
|
+
version: skill.version,
|
|
2788
|
+
description: skill.description,
|
|
2789
|
+
keywords,
|
|
2790
|
+
files: ["skill.yaml", "SKILL.md", "README.md"],
|
|
2791
|
+
license: "MIT"
|
|
2792
|
+
};
|
|
2793
|
+
if (skill.repository) {
|
|
2794
|
+
pkg.repository = {
|
|
2795
|
+
type: "git",
|
|
2796
|
+
url: skill.repository
|
|
2797
|
+
};
|
|
2798
|
+
}
|
|
2799
|
+
return pkg;
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
// src/commands/skill/publish.ts
|
|
2803
|
+
async function runPublish(skillDir, opts) {
|
|
2804
|
+
const validation = await validateForPublish(skillDir);
|
|
2805
|
+
if (!validation.valid) {
|
|
2806
|
+
const errorList = validation.errors.map((e) => ` - ${e}`).join("\n");
|
|
2807
|
+
throw new Error(`Pre-publish validation failed:
|
|
2808
|
+
${errorList}`);
|
|
2809
|
+
}
|
|
2810
|
+
const meta = validation.skillMeta;
|
|
2811
|
+
const pkg = derivePackageJson(meta);
|
|
2812
|
+
const pkgPath = path21.join(skillDir, "package.json");
|
|
2813
|
+
fs13.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
2814
|
+
const readmePath = path21.join(skillDir, "README.md");
|
|
2815
|
+
if (!fs13.existsSync(readmePath)) {
|
|
2816
|
+
const skillMdContent = fs13.readFileSync(path21.join(skillDir, "SKILL.md"), "utf-8");
|
|
2817
|
+
const readme = `# ${pkg.name}
|
|
2818
|
+
|
|
2819
|
+
${meta.description}
|
|
2820
|
+
|
|
2821
|
+
## Installation
|
|
2822
|
+
|
|
2823
|
+
\`\`\`bash
|
|
2824
|
+
harness install ${meta.name}
|
|
2825
|
+
\`\`\`
|
|
2826
|
+
|
|
2827
|
+
---
|
|
2828
|
+
|
|
2829
|
+
${skillMdContent}`;
|
|
2830
|
+
fs13.writeFileSync(readmePath, readme);
|
|
2831
|
+
}
|
|
2832
|
+
if (opts.dryRun) {
|
|
2833
|
+
return {
|
|
2834
|
+
name: pkg.name,
|
|
2835
|
+
version: pkg.version,
|
|
2836
|
+
published: false,
|
|
2837
|
+
dryRun: true
|
|
2838
|
+
};
|
|
2839
|
+
}
|
|
2840
|
+
execFileSync3("npm", ["publish", "--access", "public"], {
|
|
2841
|
+
cwd: skillDir,
|
|
2842
|
+
stdio: "pipe",
|
|
2843
|
+
timeout: 6e4
|
|
2844
|
+
});
|
|
2845
|
+
return {
|
|
2846
|
+
name: pkg.name,
|
|
2847
|
+
version: pkg.version,
|
|
2848
|
+
published: true
|
|
2849
|
+
};
|
|
2850
|
+
}
|
|
2851
|
+
function createPublishCommand() {
|
|
2852
|
+
return new Command27("publish").description("Validate and publish a skill to @harness-skills on npm").option("--dry-run", "Run validation and generate package.json without publishing").option("--dir <dir>", "Skill directory (default: current directory)").action(async (opts, cmd) => {
|
|
2853
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
2854
|
+
const skillDir = opts.dir || process.cwd();
|
|
2855
|
+
try {
|
|
2856
|
+
const result = await runPublish(skillDir, {
|
|
2857
|
+
dryRun: opts.dryRun
|
|
2858
|
+
});
|
|
2859
|
+
if (globalOpts.json) {
|
|
2860
|
+
logger.raw(result);
|
|
2861
|
+
} else if (result.dryRun) {
|
|
2862
|
+
logger.success(`Validation passed. Would publish: ${result.name}@${result.version}`);
|
|
2863
|
+
logger.info("Run without --dry-run to publish.");
|
|
2864
|
+
} else {
|
|
2865
|
+
logger.success(`Published ${result.name}@${result.version}`);
|
|
2866
|
+
}
|
|
2867
|
+
} catch (err) {
|
|
2868
|
+
logger.error(err instanceof Error ? err.message : String(err));
|
|
2869
|
+
process.exit(ExitCode.VALIDATION_FAILED);
|
|
2870
|
+
}
|
|
2871
|
+
});
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
// src/commands/skill/index.ts
|
|
2875
|
+
function createSkillCommand() {
|
|
2876
|
+
const command = new Command28("skill").description("Skill management commands");
|
|
2877
|
+
command.addCommand(createListCommand2());
|
|
2878
|
+
command.addCommand(createRunCommand2());
|
|
2879
|
+
command.addCommand(createValidateCommand3());
|
|
2880
|
+
command.addCommand(createInfoCommand());
|
|
2881
|
+
command.addCommand(createSearchCommand());
|
|
2882
|
+
command.addCommand(createCreateCommand());
|
|
2883
|
+
command.addCommand(createPublishCommand());
|
|
2884
|
+
return command;
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
// src/commands/state/index.ts
|
|
2888
|
+
import { Command as Command33 } from "commander";
|
|
2889
|
+
|
|
2890
|
+
// src/commands/state/show.ts
|
|
2891
|
+
import { Command as Command29 } from "commander";
|
|
2892
|
+
import * as path22 from "path";
|
|
2893
|
+
function createShowCommand() {
|
|
2894
|
+
return new Command29("show").description("Show current project state").option("--path <path>", "Project root path", ".").option("--stream <name>", "Target a specific stream").action(async (opts, cmd) => {
|
|
2895
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
2896
|
+
const projectPath = path22.resolve(opts.path);
|
|
2897
|
+
const result = await loadState(projectPath, opts.stream);
|
|
2898
|
+
if (!result.ok) {
|
|
2899
|
+
logger.error(result.error.message);
|
|
2900
|
+
process.exit(ExitCode.ERROR);
|
|
2901
|
+
return;
|
|
2902
|
+
}
|
|
2903
|
+
const state = result.value;
|
|
2904
|
+
if (globalOpts.json) {
|
|
2905
|
+
logger.raw(state);
|
|
2906
|
+
} else if (globalOpts.quiet) {
|
|
2907
|
+
console.log(JSON.stringify(state));
|
|
2908
|
+
} else {
|
|
2909
|
+
if (opts.stream) console.log(`Stream: ${opts.stream}`);
|
|
2910
|
+
console.log(`Schema Version: ${state.schemaVersion}`);
|
|
2911
|
+
if (state.position.phase) console.log(`Phase: ${state.position.phase}`);
|
|
2912
|
+
if (state.position.task) console.log(`Task: ${state.position.task}`);
|
|
2913
|
+
if (state.lastSession) {
|
|
2914
|
+
console.log(`Last Session: ${state.lastSession.date} \u2014 ${state.lastSession.summary}`);
|
|
2915
|
+
}
|
|
2916
|
+
if (Object.keys(state.progress).length > 0) {
|
|
2917
|
+
console.log("\nProgress:");
|
|
2918
|
+
for (const [task, status] of Object.entries(state.progress)) {
|
|
2919
|
+
console.log(` ${task}: ${status}`);
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
if (state.decisions.length > 0) {
|
|
2923
|
+
console.log(`
|
|
2924
|
+
Decisions: ${state.decisions.length}`);
|
|
2925
|
+
}
|
|
2926
|
+
if (state.blockers.length > 0) {
|
|
2927
|
+
const open = state.blockers.filter((b) => b.status === "open").length;
|
|
2928
|
+
console.log(`Blockers: ${open} open / ${state.blockers.length} total`);
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
process.exit(ExitCode.SUCCESS);
|
|
2932
|
+
});
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
// src/commands/state/reset.ts
|
|
2936
|
+
import { Command as Command30 } from "commander";
|
|
2937
|
+
import * as fs14 from "fs";
|
|
2938
|
+
import * as path23 from "path";
|
|
2939
|
+
import * as readline from "readline";
|
|
2940
|
+
function createResetCommand() {
|
|
2941
|
+
return new Command30("reset").description("Reset project state (deletes .harness/state.json)").option("--path <path>", "Project root path", ".").option("--stream <name>", "Target a specific stream").option("--yes", "Skip confirmation prompt").action(async (opts, _cmd) => {
|
|
2942
|
+
const projectPath = path23.resolve(opts.path);
|
|
2943
|
+
let statePath;
|
|
2944
|
+
if (opts.stream) {
|
|
2945
|
+
const streamResult = await resolveStreamPath(projectPath, { stream: opts.stream });
|
|
2946
|
+
if (!streamResult.ok) {
|
|
2947
|
+
logger.error(streamResult.error.message);
|
|
2948
|
+
process.exit(ExitCode.ERROR);
|
|
2949
|
+
return;
|
|
2950
|
+
}
|
|
2951
|
+
statePath = path23.join(streamResult.value, "state.json");
|
|
2952
|
+
} else {
|
|
2953
|
+
statePath = path23.join(projectPath, ".harness", "state.json");
|
|
2954
|
+
}
|
|
2955
|
+
if (!fs14.existsSync(statePath)) {
|
|
2956
|
+
logger.info("No state file found. Nothing to reset.");
|
|
2957
|
+
process.exit(ExitCode.SUCCESS);
|
|
2958
|
+
return;
|
|
2959
|
+
}
|
|
2960
|
+
if (!opts.yes) {
|
|
2961
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
2962
|
+
const answer = await new Promise((resolve24) => {
|
|
2963
|
+
rl.question("Reset project state? This cannot be undone. [y/N] ", resolve24);
|
|
2964
|
+
});
|
|
2965
|
+
rl.close();
|
|
2966
|
+
if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
|
|
2967
|
+
logger.info("Reset cancelled.");
|
|
2968
|
+
process.exit(ExitCode.SUCCESS);
|
|
2969
|
+
return;
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
try {
|
|
2973
|
+
fs14.unlinkSync(statePath);
|
|
2974
|
+
logger.success("Project state reset.");
|
|
2975
|
+
} catch (e) {
|
|
2976
|
+
logger.error(`Failed to reset state: ${e instanceof Error ? e.message : String(e)}`);
|
|
2977
|
+
process.exit(ExitCode.ERROR);
|
|
2978
|
+
return;
|
|
2979
|
+
}
|
|
2980
|
+
process.exit(ExitCode.SUCCESS);
|
|
2981
|
+
});
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
// src/commands/state/learn.ts
|
|
2985
|
+
import { Command as Command31 } from "commander";
|
|
2986
|
+
import * as path24 from "path";
|
|
2987
|
+
function createLearnCommand() {
|
|
2988
|
+
return new Command31("learn").description("Append a learning to .harness/learnings.md").argument("<message>", "The learning to record").option("--path <path>", "Project root path", ".").option("--stream <name>", "Target a specific stream").action(async (message, opts, _cmd) => {
|
|
2989
|
+
const projectPath = path24.resolve(opts.path);
|
|
2990
|
+
const result = await appendLearning(projectPath, message, void 0, void 0, opts.stream);
|
|
2991
|
+
if (!result.ok) {
|
|
2992
|
+
logger.error(result.error.message);
|
|
2993
|
+
process.exit(ExitCode.ERROR);
|
|
2994
|
+
return;
|
|
2995
|
+
}
|
|
2996
|
+
logger.success(`Learning recorded.`);
|
|
2997
|
+
process.exit(ExitCode.SUCCESS);
|
|
2998
|
+
});
|
|
2999
|
+
}
|
|
3000
|
+
|
|
3001
|
+
// src/commands/state/streams.ts
|
|
3002
|
+
import { Command as Command32 } from "commander";
|
|
3003
|
+
import * as path25 from "path";
|
|
3004
|
+
function createStreamsCommand() {
|
|
3005
|
+
const command = new Command32("streams").description("Manage state streams");
|
|
3006
|
+
command.command("list").description("List all known streams").option("--path <path>", "Project root path", ".").action(async (opts, cmd) => {
|
|
3007
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
3008
|
+
const projectPath = path25.resolve(opts.path);
|
|
2378
3009
|
const indexResult = await loadStreamIndex(projectPath);
|
|
2379
3010
|
const result = await listStreams(projectPath);
|
|
2380
3011
|
if (!result.ok) {
|
|
@@ -2398,7 +3029,7 @@ function createStreamsCommand() {
|
|
|
2398
3029
|
process.exit(ExitCode.SUCCESS);
|
|
2399
3030
|
});
|
|
2400
3031
|
command.command("create <name>").description("Create a new stream").option("--path <path>", "Project root path", ".").option("--branch <branch>", "Associate with a git branch").action(async (name, opts) => {
|
|
2401
|
-
const projectPath =
|
|
3032
|
+
const projectPath = path25.resolve(opts.path);
|
|
2402
3033
|
const result = await createStream(projectPath, name, opts.branch);
|
|
2403
3034
|
if (!result.ok) {
|
|
2404
3035
|
logger.error(result.error.message);
|
|
@@ -2409,7 +3040,7 @@ function createStreamsCommand() {
|
|
|
2409
3040
|
process.exit(ExitCode.SUCCESS);
|
|
2410
3041
|
});
|
|
2411
3042
|
command.command("archive <name>").description("Archive a stream").option("--path <path>", "Project root path", ".").action(async (name, opts) => {
|
|
2412
|
-
const projectPath =
|
|
3043
|
+
const projectPath = path25.resolve(opts.path);
|
|
2413
3044
|
const result = await archiveStream(projectPath, name);
|
|
2414
3045
|
if (!result.ok) {
|
|
2415
3046
|
logger.error(result.error.message);
|
|
@@ -2420,7 +3051,7 @@ function createStreamsCommand() {
|
|
|
2420
3051
|
process.exit(ExitCode.SUCCESS);
|
|
2421
3052
|
});
|
|
2422
3053
|
command.command("activate <name>").description("Set the active stream").option("--path <path>", "Project root path", ".").action(async (name, opts) => {
|
|
2423
|
-
const projectPath =
|
|
3054
|
+
const projectPath = path25.resolve(opts.path);
|
|
2424
3055
|
const result = await setActiveStream(projectPath, name);
|
|
2425
3056
|
if (!result.ok) {
|
|
2426
3057
|
logger.error(result.error.message);
|
|
@@ -2435,7 +3066,7 @@ function createStreamsCommand() {
|
|
|
2435
3066
|
|
|
2436
3067
|
// src/commands/state/index.ts
|
|
2437
3068
|
function createStateCommand() {
|
|
2438
|
-
const command = new
|
|
3069
|
+
const command = new Command33("state").description("Project state management commands");
|
|
2439
3070
|
command.addCommand(createShowCommand());
|
|
2440
3071
|
command.addCommand(createResetCommand());
|
|
2441
3072
|
command.addCommand(createLearnCommand());
|
|
@@ -2444,11 +3075,20 @@ function createStateCommand() {
|
|
|
2444
3075
|
}
|
|
2445
3076
|
|
|
2446
3077
|
// src/commands/ci/index.ts
|
|
2447
|
-
import { Command as
|
|
3078
|
+
import { Command as Command36 } from "commander";
|
|
2448
3079
|
|
|
2449
3080
|
// src/commands/ci/check.ts
|
|
2450
|
-
import { Command as
|
|
2451
|
-
var VALID_CHECKS = [
|
|
3081
|
+
import { Command as Command34 } from "commander";
|
|
3082
|
+
var VALID_CHECKS = [
|
|
3083
|
+
"validate",
|
|
3084
|
+
"deps",
|
|
3085
|
+
"docs",
|
|
3086
|
+
"entropy",
|
|
3087
|
+
"security",
|
|
3088
|
+
"perf",
|
|
3089
|
+
"phase-gate",
|
|
3090
|
+
"arch"
|
|
3091
|
+
];
|
|
2452
3092
|
async function runCICheck(options) {
|
|
2453
3093
|
const configResult = resolveConfig(options.configPath);
|
|
2454
3094
|
if (!configResult.ok) {
|
|
@@ -2478,7 +3118,7 @@ function parseFailOn(failOn) {
|
|
|
2478
3118
|
return "error";
|
|
2479
3119
|
}
|
|
2480
3120
|
function createCheckCommand() {
|
|
2481
|
-
return new
|
|
3121
|
+
return new Command34("check").description("Run all harness checks for CI (validate, deps, docs, entropy, phase-gate, arch)").option("--skip <checks>", "Comma-separated checks to skip (e.g., entropy,docs)").option("--fail-on <severity>", "Fail on severity level: error (default) or warning", "error").action(async (opts, cmd) => {
|
|
2482
3122
|
const globalOpts = cmd.optsWithGlobals();
|
|
2483
3123
|
const mode = globalOpts.json ? OutputMode.JSON : globalOpts.quiet ? OutputMode.QUIET : globalOpts.verbose ? OutputMode.VERBOSE : OutputMode.TEXT;
|
|
2484
3124
|
const skip = parseSkip(opts.skip);
|
|
@@ -2522,10 +3162,19 @@ function createCheckCommand() {
|
|
|
2522
3162
|
}
|
|
2523
3163
|
|
|
2524
3164
|
// src/commands/ci/init.ts
|
|
2525
|
-
import { Command as
|
|
2526
|
-
import * as
|
|
2527
|
-
import * as
|
|
2528
|
-
var ALL_CHECKS = [
|
|
3165
|
+
import { Command as Command35 } from "commander";
|
|
3166
|
+
import * as fs15 from "fs";
|
|
3167
|
+
import * as path26 from "path";
|
|
3168
|
+
var ALL_CHECKS = [
|
|
3169
|
+
"validate",
|
|
3170
|
+
"deps",
|
|
3171
|
+
"docs",
|
|
3172
|
+
"entropy",
|
|
3173
|
+
"security",
|
|
3174
|
+
"perf",
|
|
3175
|
+
"phase-gate",
|
|
3176
|
+
"arch"
|
|
3177
|
+
];
|
|
2529
3178
|
function buildSkipFlag(checks) {
|
|
2530
3179
|
if (!checks) return "";
|
|
2531
3180
|
const skipChecks = ALL_CHECKS.filter((c) => !checks.includes(c));
|
|
@@ -2616,12 +3265,12 @@ function generateCIConfig(options) {
|
|
|
2616
3265
|
});
|
|
2617
3266
|
}
|
|
2618
3267
|
function detectPlatform() {
|
|
2619
|
-
if (
|
|
2620
|
-
if (
|
|
3268
|
+
if (fs15.existsSync(".github")) return "github";
|
|
3269
|
+
if (fs15.existsSync(".gitlab-ci.yml")) return "gitlab";
|
|
2621
3270
|
return null;
|
|
2622
3271
|
}
|
|
2623
3272
|
function createInitCommand2() {
|
|
2624
|
-
return new
|
|
3273
|
+
return new Command35("init").description("Generate CI configuration for harness checks").option("--platform <platform>", "CI platform: github, gitlab, or generic").option("--checks <list>", "Comma-separated list of checks to include").action(async (opts, cmd) => {
|
|
2625
3274
|
const globalOpts = cmd.optsWithGlobals();
|
|
2626
3275
|
const platform = opts.platform ?? detectPlatform() ?? "generic";
|
|
2627
3276
|
const checks = opts.checks ? opts.checks.split(",").map((s) => s.trim()) : void 0;
|
|
@@ -2633,12 +3282,12 @@ function createInitCommand2() {
|
|
|
2633
3282
|
process.exit(result.error.exitCode);
|
|
2634
3283
|
}
|
|
2635
3284
|
const { filename, content } = result.value;
|
|
2636
|
-
const targetPath =
|
|
2637
|
-
const dir =
|
|
2638
|
-
|
|
2639
|
-
|
|
3285
|
+
const targetPath = path26.resolve(filename);
|
|
3286
|
+
const dir = path26.dirname(targetPath);
|
|
3287
|
+
fs15.mkdirSync(dir, { recursive: true });
|
|
3288
|
+
fs15.writeFileSync(targetPath, content);
|
|
2640
3289
|
if (platform === "generic" && process.platform !== "win32") {
|
|
2641
|
-
|
|
3290
|
+
fs15.chmodSync(targetPath, "755");
|
|
2642
3291
|
}
|
|
2643
3292
|
if (globalOpts.json) {
|
|
2644
3293
|
console.log(JSON.stringify({ file: filename, platform }));
|
|
@@ -2651,15 +3300,15 @@ function createInitCommand2() {
|
|
|
2651
3300
|
|
|
2652
3301
|
// src/commands/ci/index.ts
|
|
2653
3302
|
function createCICommand() {
|
|
2654
|
-
const command = new
|
|
3303
|
+
const command = new Command36("ci").description("CI/CD integration commands");
|
|
2655
3304
|
command.addCommand(createCheckCommand());
|
|
2656
3305
|
command.addCommand(createInitCommand2());
|
|
2657
3306
|
return command;
|
|
2658
3307
|
}
|
|
2659
3308
|
|
|
2660
3309
|
// src/commands/update.ts
|
|
2661
|
-
import { Command as
|
|
2662
|
-
import { execFileSync as
|
|
3310
|
+
import { Command as Command37 } from "commander";
|
|
3311
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
2663
3312
|
import { realpathSync } from "fs";
|
|
2664
3313
|
import readline2 from "readline";
|
|
2665
3314
|
import chalk3 from "chalk";
|
|
@@ -2681,7 +3330,7 @@ function detectPackageManager() {
|
|
|
2681
3330
|
return "npm";
|
|
2682
3331
|
}
|
|
2683
3332
|
function getLatestVersion(pkg = "@harness-engineering/cli") {
|
|
2684
|
-
const output =
|
|
3333
|
+
const output = execFileSync4("npm", ["view", pkg, "dist-tags.latest"], {
|
|
2685
3334
|
encoding: "utf-8",
|
|
2686
3335
|
timeout: 15e3
|
|
2687
3336
|
});
|
|
@@ -2689,7 +3338,7 @@ function getLatestVersion(pkg = "@harness-engineering/cli") {
|
|
|
2689
3338
|
}
|
|
2690
3339
|
function getInstalledVersion(pm) {
|
|
2691
3340
|
try {
|
|
2692
|
-
const output =
|
|
3341
|
+
const output = execFileSync4(pm, ["list", "-g", "@harness-engineering/cli", "--json"], {
|
|
2693
3342
|
encoding: "utf-8",
|
|
2694
3343
|
timeout: 15e3
|
|
2695
3344
|
});
|
|
@@ -2702,7 +3351,7 @@ function getInstalledVersion(pm) {
|
|
|
2702
3351
|
}
|
|
2703
3352
|
function getInstalledPackages(pm) {
|
|
2704
3353
|
try {
|
|
2705
|
-
const output =
|
|
3354
|
+
const output = execFileSync4(pm, ["list", "-g", "--json"], {
|
|
2706
3355
|
encoding: "utf-8",
|
|
2707
3356
|
timeout: 15e3
|
|
2708
3357
|
});
|
|
@@ -2718,15 +3367,15 @@ function prompt(question) {
|
|
|
2718
3367
|
input: process.stdin,
|
|
2719
3368
|
output: process.stdout
|
|
2720
3369
|
});
|
|
2721
|
-
return new Promise((
|
|
3370
|
+
return new Promise((resolve24) => {
|
|
2722
3371
|
rl.question(question, (answer) => {
|
|
2723
3372
|
rl.close();
|
|
2724
|
-
|
|
3373
|
+
resolve24(answer.trim().toLowerCase());
|
|
2725
3374
|
});
|
|
2726
3375
|
});
|
|
2727
3376
|
}
|
|
2728
3377
|
function createUpdateCommand() {
|
|
2729
|
-
return new
|
|
3378
|
+
return new Command37("update").description("Update all @harness-engineering packages to the latest version").option("--version <semver>", "Pin @harness-engineering/cli to a specific version").action(async (opts, cmd) => {
|
|
2730
3379
|
const globalOpts = cmd.optsWithGlobals();
|
|
2731
3380
|
const pm = detectPackageManager();
|
|
2732
3381
|
if (globalOpts.verbose) {
|
|
@@ -2769,7 +3418,7 @@ function createUpdateCommand() {
|
|
|
2769
3418
|
}
|
|
2770
3419
|
try {
|
|
2771
3420
|
logger.info("Updating packages...");
|
|
2772
|
-
|
|
3421
|
+
execFileSync4(pm, ["install", "-g", ...installPkgs], { stdio: "inherit", timeout: 12e4 });
|
|
2773
3422
|
console.log("");
|
|
2774
3423
|
logger.success("Update complete");
|
|
2775
3424
|
} catch {
|
|
@@ -2779,12 +3428,12 @@ function createUpdateCommand() {
|
|
|
2779
3428
|
process.exit(ExitCode.ERROR);
|
|
2780
3429
|
}
|
|
2781
3430
|
console.log("");
|
|
2782
|
-
const regenAnswer = await prompt("Regenerate slash commands and agent definitions? (
|
|
2783
|
-
if (regenAnswer
|
|
2784
|
-
const scopeAnswer = await prompt("Generate for (
|
|
2785
|
-
const isGlobal = scopeAnswer
|
|
3431
|
+
const regenAnswer = await prompt("Regenerate slash commands and agent definitions? (Y/n) ");
|
|
3432
|
+
if (regenAnswer !== "n" && regenAnswer !== "no") {
|
|
3433
|
+
const scopeAnswer = await prompt("Generate for (G)lobal or (l)ocal project? (G/l) ");
|
|
3434
|
+
const isGlobal = scopeAnswer !== "l" && scopeAnswer !== "local";
|
|
2786
3435
|
try {
|
|
2787
|
-
|
|
3436
|
+
execFileSync4("harness", ["generate", ...isGlobal ? ["--global"] : []], {
|
|
2788
3437
|
stdio: "inherit"
|
|
2789
3438
|
});
|
|
2790
3439
|
} catch {
|
|
@@ -2797,9 +3446,9 @@ function createUpdateCommand() {
|
|
|
2797
3446
|
}
|
|
2798
3447
|
|
|
2799
3448
|
// src/commands/generate.ts
|
|
2800
|
-
import { Command as
|
|
3449
|
+
import { Command as Command38 } from "commander";
|
|
2801
3450
|
function createGenerateCommand3() {
|
|
2802
|
-
return new
|
|
3451
|
+
return new Command38("generate").description("Generate all platform integrations (slash commands + agent definitions)").option("--platforms <list>", "Target platforms (comma-separated)", "claude-code,gemini-cli").option("--global", "Write to global directories", false).option("--include-global", "Include built-in global skills", false).option("--output <dir>", "Custom output directory").option("--dry-run", "Show what would change without writing", false).option("--yes", "Skip deletion confirmation prompts", false).action(async (opts, cmd) => {
|
|
2803
3452
|
const globalOpts = cmd.optsWithGlobals();
|
|
2804
3453
|
const platforms = opts.platforms.split(",").map((p) => p.trim());
|
|
2805
3454
|
for (const p of platforms) {
|
|
@@ -2858,10 +3507,10 @@ function createGenerateCommand3() {
|
|
|
2858
3507
|
}
|
|
2859
3508
|
|
|
2860
3509
|
// src/commands/graph/scan.ts
|
|
2861
|
-
import { Command as
|
|
2862
|
-
import * as
|
|
3510
|
+
import { Command as Command39 } from "commander";
|
|
3511
|
+
import * as path27 from "path";
|
|
2863
3512
|
async function runScan(projectPath) {
|
|
2864
|
-
const { GraphStore, CodeIngestor, TopologicalLinker, KnowledgeIngestor, GitIngestor } = await import("./dist-
|
|
3513
|
+
const { GraphStore, CodeIngestor, TopologicalLinker, KnowledgeIngestor, GitIngestor } = await import("./dist-M6BQODWC.js");
|
|
2865
3514
|
const store = new GraphStore();
|
|
2866
3515
|
const start = Date.now();
|
|
2867
3516
|
await new CodeIngestor(store).ingest(projectPath);
|
|
@@ -2872,13 +3521,13 @@ async function runScan(projectPath) {
|
|
|
2872
3521
|
await new GitIngestor(store).ingest(projectPath);
|
|
2873
3522
|
} catch {
|
|
2874
3523
|
}
|
|
2875
|
-
const graphDir =
|
|
3524
|
+
const graphDir = path27.join(projectPath, ".harness", "graph");
|
|
2876
3525
|
await store.save(graphDir);
|
|
2877
3526
|
return { nodeCount: store.nodeCount, edgeCount: store.edgeCount, durationMs: Date.now() - start };
|
|
2878
3527
|
}
|
|
2879
3528
|
function createScanCommand() {
|
|
2880
|
-
return new
|
|
2881
|
-
const projectPath =
|
|
3529
|
+
return new Command39("scan").description("Scan project and build knowledge graph").argument("[path]", "Project root path", ".").action(async (inputPath, _opts, cmd) => {
|
|
3530
|
+
const projectPath = path27.resolve(inputPath);
|
|
2882
3531
|
const globalOpts = cmd.optsWithGlobals();
|
|
2883
3532
|
try {
|
|
2884
3533
|
const result = await runScan(projectPath);
|
|
@@ -2897,13 +3546,13 @@ function createScanCommand() {
|
|
|
2897
3546
|
}
|
|
2898
3547
|
|
|
2899
3548
|
// src/commands/graph/ingest.ts
|
|
2900
|
-
import { Command as
|
|
2901
|
-
import * as
|
|
3549
|
+
import { Command as Command40 } from "commander";
|
|
3550
|
+
import * as path28 from "path";
|
|
2902
3551
|
async function loadConnectorConfig(projectPath, source) {
|
|
2903
3552
|
try {
|
|
2904
|
-
const
|
|
2905
|
-
const configPath =
|
|
2906
|
-
const config = JSON.parse(await
|
|
3553
|
+
const fs20 = await import("fs/promises");
|
|
3554
|
+
const configPath = path28.join(projectPath, "harness.config.json");
|
|
3555
|
+
const config = JSON.parse(await fs20.readFile(configPath, "utf-8"));
|
|
2907
3556
|
const connector = config.graph?.connectors?.find(
|
|
2908
3557
|
(c) => c.source === source
|
|
2909
3558
|
);
|
|
@@ -2942,8 +3591,8 @@ async function runIngest(projectPath, source, opts) {
|
|
|
2942
3591
|
SyncManager,
|
|
2943
3592
|
JiraConnector,
|
|
2944
3593
|
SlackConnector
|
|
2945
|
-
} = await import("./dist-
|
|
2946
|
-
const graphDir =
|
|
3594
|
+
} = await import("./dist-M6BQODWC.js");
|
|
3595
|
+
const graphDir = path28.join(projectPath, ".harness", "graph");
|
|
2947
3596
|
const store = new GraphStore();
|
|
2948
3597
|
await store.load(graphDir);
|
|
2949
3598
|
if (opts?.all) {
|
|
@@ -3004,13 +3653,13 @@ async function runIngest(projectPath, source, opts) {
|
|
|
3004
3653
|
return result;
|
|
3005
3654
|
}
|
|
3006
3655
|
function createIngestCommand() {
|
|
3007
|
-
return new
|
|
3656
|
+
return new Command40("ingest").description("Ingest data into the knowledge graph").option("--source <name>", "Source to ingest (code, knowledge, git, jira, slack)").option("--all", "Run all sources (code, knowledge, git, and configured connectors)").option("--full", "Force full re-ingestion").action(async (opts, cmd) => {
|
|
3008
3657
|
if (!opts.source && !opts.all) {
|
|
3009
3658
|
console.error("Error: --source or --all is required");
|
|
3010
3659
|
process.exit(1);
|
|
3011
3660
|
}
|
|
3012
3661
|
const globalOpts = cmd.optsWithGlobals();
|
|
3013
|
-
const projectPath =
|
|
3662
|
+
const projectPath = path28.resolve(globalOpts.config ? path28.dirname(globalOpts.config) : ".");
|
|
3014
3663
|
try {
|
|
3015
3664
|
const result = await runIngest(projectPath, opts.source ?? "", {
|
|
3016
3665
|
full: opts.full,
|
|
@@ -3032,12 +3681,12 @@ function createIngestCommand() {
|
|
|
3032
3681
|
}
|
|
3033
3682
|
|
|
3034
3683
|
// src/commands/graph/query.ts
|
|
3035
|
-
import { Command as
|
|
3036
|
-
import * as
|
|
3684
|
+
import { Command as Command41 } from "commander";
|
|
3685
|
+
import * as path29 from "path";
|
|
3037
3686
|
async function runQuery(projectPath, rootNodeId, opts) {
|
|
3038
|
-
const { GraphStore, ContextQL } = await import("./dist-
|
|
3687
|
+
const { GraphStore, ContextQL } = await import("./dist-M6BQODWC.js");
|
|
3039
3688
|
const store = new GraphStore();
|
|
3040
|
-
const graphDir =
|
|
3689
|
+
const graphDir = path29.join(projectPath, ".harness", "graph");
|
|
3041
3690
|
const loaded = await store.load(graphDir);
|
|
3042
3691
|
if (!loaded) throw new Error("No graph found. Run `harness scan` first.");
|
|
3043
3692
|
const params = {
|
|
@@ -3051,9 +3700,9 @@ async function runQuery(projectPath, rootNodeId, opts) {
|
|
|
3051
3700
|
return cql.execute(params);
|
|
3052
3701
|
}
|
|
3053
3702
|
function createQueryCommand() {
|
|
3054
|
-
return new
|
|
3703
|
+
return new Command41("query").description("Query the knowledge graph").argument("<rootNodeId>", "Starting node ID").option("--depth <n>", "Max traversal depth", "3").option("--types <types>", "Comma-separated node types to include").option("--edges <edges>", "Comma-separated edge types to include").option("--bidirectional", "Traverse both directions").action(async (rootNodeId, opts, cmd) => {
|
|
3055
3704
|
const globalOpts = cmd.optsWithGlobals();
|
|
3056
|
-
const projectPath =
|
|
3705
|
+
const projectPath = path29.resolve(globalOpts.config ? path29.dirname(globalOpts.config) : ".");
|
|
3057
3706
|
try {
|
|
3058
3707
|
const result = await runQuery(projectPath, rootNodeId, {
|
|
3059
3708
|
depth: parseInt(opts.depth),
|
|
@@ -3079,21 +3728,21 @@ function createQueryCommand() {
|
|
|
3079
3728
|
}
|
|
3080
3729
|
|
|
3081
3730
|
// src/commands/graph/index.ts
|
|
3082
|
-
import { Command as
|
|
3731
|
+
import { Command as Command42 } from "commander";
|
|
3083
3732
|
|
|
3084
3733
|
// src/commands/graph/status.ts
|
|
3085
|
-
import * as
|
|
3734
|
+
import * as path30 from "path";
|
|
3086
3735
|
async function runGraphStatus(projectPath) {
|
|
3087
|
-
const { GraphStore } = await import("./dist-
|
|
3088
|
-
const graphDir =
|
|
3736
|
+
const { GraphStore } = await import("./dist-M6BQODWC.js");
|
|
3737
|
+
const graphDir = path30.join(projectPath, ".harness", "graph");
|
|
3089
3738
|
const store = new GraphStore();
|
|
3090
3739
|
const loaded = await store.load(graphDir);
|
|
3091
3740
|
if (!loaded) return { status: "no_graph", message: "No graph found. Run `harness scan` first." };
|
|
3092
|
-
const
|
|
3093
|
-
const metaPath =
|
|
3741
|
+
const fs20 = await import("fs/promises");
|
|
3742
|
+
const metaPath = path30.join(graphDir, "metadata.json");
|
|
3094
3743
|
let lastScan = "unknown";
|
|
3095
3744
|
try {
|
|
3096
|
-
const meta = JSON.parse(await
|
|
3745
|
+
const meta = JSON.parse(await fs20.readFile(metaPath, "utf-8"));
|
|
3097
3746
|
lastScan = meta.lastScanTimestamp;
|
|
3098
3747
|
} catch {
|
|
3099
3748
|
}
|
|
@@ -3104,8 +3753,8 @@ async function runGraphStatus(projectPath) {
|
|
|
3104
3753
|
}
|
|
3105
3754
|
let connectorSyncStatus = {};
|
|
3106
3755
|
try {
|
|
3107
|
-
const syncMetaPath =
|
|
3108
|
-
const syncMeta = JSON.parse(await
|
|
3756
|
+
const syncMetaPath = path30.join(graphDir, "sync-metadata.json");
|
|
3757
|
+
const syncMeta = JSON.parse(await fs20.readFile(syncMetaPath, "utf-8"));
|
|
3109
3758
|
for (const [name, data] of Object.entries(syncMeta.connectors ?? {})) {
|
|
3110
3759
|
connectorSyncStatus[name] = data.lastSyncTimestamp;
|
|
3111
3760
|
}
|
|
@@ -3122,10 +3771,10 @@ async function runGraphStatus(projectPath) {
|
|
|
3122
3771
|
}
|
|
3123
3772
|
|
|
3124
3773
|
// src/commands/graph/export.ts
|
|
3125
|
-
import * as
|
|
3774
|
+
import * as path31 from "path";
|
|
3126
3775
|
async function runGraphExport(projectPath, format) {
|
|
3127
|
-
const { GraphStore } = await import("./dist-
|
|
3128
|
-
const graphDir =
|
|
3776
|
+
const { GraphStore } = await import("./dist-M6BQODWC.js");
|
|
3777
|
+
const graphDir = path31.join(projectPath, ".harness", "graph");
|
|
3129
3778
|
const store = new GraphStore();
|
|
3130
3779
|
const loaded = await store.load(graphDir);
|
|
3131
3780
|
if (!loaded) throw new Error("No graph found. Run `harness scan` first.");
|
|
@@ -3154,13 +3803,13 @@ async function runGraphExport(projectPath, format) {
|
|
|
3154
3803
|
}
|
|
3155
3804
|
|
|
3156
3805
|
// src/commands/graph/index.ts
|
|
3157
|
-
import * as
|
|
3806
|
+
import * as path32 from "path";
|
|
3158
3807
|
function createGraphCommand() {
|
|
3159
|
-
const graph = new
|
|
3808
|
+
const graph = new Command42("graph").description("Knowledge graph management");
|
|
3160
3809
|
graph.command("status").description("Show graph statistics").action(async (_opts, cmd) => {
|
|
3161
3810
|
try {
|
|
3162
3811
|
const globalOpts = cmd.optsWithGlobals();
|
|
3163
|
-
const projectPath =
|
|
3812
|
+
const projectPath = path32.resolve(globalOpts.config ? path32.dirname(globalOpts.config) : ".");
|
|
3164
3813
|
const result = await runGraphStatus(projectPath);
|
|
3165
3814
|
if (globalOpts.json) {
|
|
3166
3815
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -3187,7 +3836,7 @@ function createGraphCommand() {
|
|
|
3187
3836
|
});
|
|
3188
3837
|
graph.command("export").description("Export graph").requiredOption("--format <format>", "Output format (json, mermaid)").action(async (opts, cmd) => {
|
|
3189
3838
|
const globalOpts = cmd.optsWithGlobals();
|
|
3190
|
-
const projectPath =
|
|
3839
|
+
const projectPath = path32.resolve(globalOpts.config ? path32.dirname(globalOpts.config) : ".");
|
|
3191
3840
|
try {
|
|
3192
3841
|
const output = await runGraphExport(projectPath, opts.format);
|
|
3193
3842
|
console.log(output);
|
|
@@ -3200,17 +3849,770 @@ function createGraphCommand() {
|
|
|
3200
3849
|
}
|
|
3201
3850
|
|
|
3202
3851
|
// src/commands/mcp.ts
|
|
3203
|
-
import { Command as
|
|
3852
|
+
import { Command as Command43 } from "commander";
|
|
3204
3853
|
function createMcpCommand() {
|
|
3205
|
-
return new
|
|
3206
|
-
const { startServer: startServer2 } = await import("./mcp-
|
|
3854
|
+
return new Command43("mcp").description("Start the MCP (Model Context Protocol) server on stdio").action(async () => {
|
|
3855
|
+
const { startServer: startServer2 } = await import("./mcp-MOKLYNZL.js");
|
|
3207
3856
|
await startServer2();
|
|
3208
3857
|
});
|
|
3209
3858
|
}
|
|
3210
3859
|
|
|
3860
|
+
// src/commands/impact-preview.ts
|
|
3861
|
+
import { Command as Command44 } from "commander";
|
|
3862
|
+
import { execSync as execSync3 } from "child_process";
|
|
3863
|
+
import * as path33 from "path";
|
|
3864
|
+
import * as fs16 from "fs";
|
|
3865
|
+
function getStagedFiles(cwd) {
|
|
3866
|
+
try {
|
|
3867
|
+
const output = execSync3("git diff --cached --name-only", {
|
|
3868
|
+
cwd,
|
|
3869
|
+
encoding: "utf-8"
|
|
3870
|
+
});
|
|
3871
|
+
return output.trim().split("\n").filter((f) => f.length > 0);
|
|
3872
|
+
} catch {
|
|
3873
|
+
return [];
|
|
3874
|
+
}
|
|
3875
|
+
}
|
|
3876
|
+
function graphExists(projectPath) {
|
|
3877
|
+
try {
|
|
3878
|
+
return fs16.existsSync(path33.join(projectPath, ".harness", "graph", "graph.json"));
|
|
3879
|
+
} catch {
|
|
3880
|
+
return false;
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
function extractNodeName(id) {
|
|
3884
|
+
const parts = id.split(":");
|
|
3885
|
+
if (parts.length > 1) {
|
|
3886
|
+
const fullPath = parts.slice(1).join(":");
|
|
3887
|
+
return path33.basename(fullPath);
|
|
3888
|
+
}
|
|
3889
|
+
return id;
|
|
3890
|
+
}
|
|
3891
|
+
var TEST_NODE_TYPES = /* @__PURE__ */ new Set(["test_result"]);
|
|
3892
|
+
var DOC_NODE_TYPES = /* @__PURE__ */ new Set(["adr", "decision", "document", "learning"]);
|
|
3893
|
+
function parseImpactResponse(response) {
|
|
3894
|
+
if (response.isError) return null;
|
|
3895
|
+
const text = response.content[0]?.text;
|
|
3896
|
+
if (!text) return null;
|
|
3897
|
+
try {
|
|
3898
|
+
const data = JSON.parse(text);
|
|
3899
|
+
if (data.mode === "summary") {
|
|
3900
|
+
const items = { code: [], tests: [], docs: [], other: [] };
|
|
3901
|
+
for (const item of data.highestRiskItems ?? []) {
|
|
3902
|
+
if (TEST_NODE_TYPES.has(item.type)) items.tests.push(item);
|
|
3903
|
+
else if (DOC_NODE_TYPES.has(item.type)) items.docs.push(item);
|
|
3904
|
+
else items.code.push(item);
|
|
3905
|
+
}
|
|
3906
|
+
return { counts: data.impactCounts, items };
|
|
3907
|
+
} else {
|
|
3908
|
+
const impact = data.impact ?? {};
|
|
3909
|
+
const items = {
|
|
3910
|
+
code: (impact.code ?? []).map((n) => ({
|
|
3911
|
+
id: n.id,
|
|
3912
|
+
type: n.type
|
|
3913
|
+
})),
|
|
3914
|
+
tests: (impact.tests ?? []).map((n) => ({
|
|
3915
|
+
id: n.id,
|
|
3916
|
+
type: n.type
|
|
3917
|
+
})),
|
|
3918
|
+
docs: (impact.docs ?? []).map((n) => ({
|
|
3919
|
+
id: n.id,
|
|
3920
|
+
type: n.type
|
|
3921
|
+
})),
|
|
3922
|
+
other: (impact.other ?? []).map((n) => ({
|
|
3923
|
+
id: n.id,
|
|
3924
|
+
type: n.type
|
|
3925
|
+
}))
|
|
3926
|
+
};
|
|
3927
|
+
return {
|
|
3928
|
+
counts: {
|
|
3929
|
+
code: items.code.length,
|
|
3930
|
+
tests: items.tests.length,
|
|
3931
|
+
docs: items.docs.length,
|
|
3932
|
+
other: items.other.length
|
|
3933
|
+
},
|
|
3934
|
+
items
|
|
3935
|
+
};
|
|
3936
|
+
}
|
|
3937
|
+
} catch {
|
|
3938
|
+
return null;
|
|
3939
|
+
}
|
|
3940
|
+
}
|
|
3941
|
+
function mergeImpactGroups(groups) {
|
|
3942
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3943
|
+
const merged = { code: [], tests: [], docs: [], other: [] };
|
|
3944
|
+
for (const group of groups) {
|
|
3945
|
+
for (const category of ["code", "tests", "docs", "other"]) {
|
|
3946
|
+
for (const item of group[category]) {
|
|
3947
|
+
if (!seen.has(item.id)) {
|
|
3948
|
+
seen.add(item.id);
|
|
3949
|
+
merged[category].push(item);
|
|
3950
|
+
}
|
|
3951
|
+
}
|
|
3952
|
+
}
|
|
3953
|
+
}
|
|
3954
|
+
return merged;
|
|
3955
|
+
}
|
|
3956
|
+
function formatCompactLine(label, count, unit, items, maxItems) {
|
|
3957
|
+
if (count === 0) return "";
|
|
3958
|
+
const labelPad = label.padEnd(6);
|
|
3959
|
+
const countStr = String(count).padStart(3);
|
|
3960
|
+
const topNames = items.slice(0, maxItems).map((i) => extractNodeName(i.id));
|
|
3961
|
+
const remaining = count - topNames.length;
|
|
3962
|
+
const namePart = remaining > 0 ? `(${topNames.join(", ")}, +${remaining})` : topNames.length > 0 ? `(${topNames.join(", ")})` : "";
|
|
3963
|
+
return ` ${labelPad}${countStr} ${unit.padEnd(7)} ${namePart}`;
|
|
3964
|
+
}
|
|
3965
|
+
function formatCompact(stagedCount, merged, counts) {
|
|
3966
|
+
const lines = [];
|
|
3967
|
+
lines.push(`Impact Preview (${stagedCount} staged file${stagedCount === 1 ? "" : "s"})`);
|
|
3968
|
+
const codeLine = formatCompactLine("Code:", counts.code, "files", merged.code, 2);
|
|
3969
|
+
const testsLine = formatCompactLine("Tests:", counts.tests, "tests", merged.tests, 2);
|
|
3970
|
+
const docsLine = formatCompactLine("Docs:", counts.docs, "docs", merged.docs, 2);
|
|
3971
|
+
if (codeLine) lines.push(codeLine);
|
|
3972
|
+
if (testsLine) lines.push(testsLine);
|
|
3973
|
+
if (docsLine) lines.push(docsLine);
|
|
3974
|
+
const total = counts.code + counts.tests + counts.docs + counts.other;
|
|
3975
|
+
lines.push(` Total: ${total} affected`);
|
|
3976
|
+
return lines.join("\n");
|
|
3977
|
+
}
|
|
3978
|
+
function formatDetailed(stagedCount, merged, counts) {
|
|
3979
|
+
const lines = [];
|
|
3980
|
+
lines.push(`Impact Preview (${stagedCount} staged file${stagedCount === 1 ? "" : "s"})`);
|
|
3981
|
+
const sections = [
|
|
3982
|
+
{ label: "Code", count: counts.code, unit: "files", items: merged.code },
|
|
3983
|
+
{ label: "Tests", count: counts.tests, unit: "tests", items: merged.tests },
|
|
3984
|
+
{ label: "Docs", count: counts.docs, unit: "docs", items: merged.docs }
|
|
3985
|
+
];
|
|
3986
|
+
for (const section of sections) {
|
|
3987
|
+
if (section.count === 0 && section.items.length === 0) continue;
|
|
3988
|
+
lines.push(` ${section.label}: ${section.count} ${section.unit}`);
|
|
3989
|
+
for (const item of section.items) {
|
|
3990
|
+
lines.push(` ${extractNodeName(item.id)}`);
|
|
3991
|
+
}
|
|
3992
|
+
}
|
|
3993
|
+
const total = counts.code + counts.tests + counts.docs + counts.other;
|
|
3994
|
+
lines.push(` Total: ${total} affected`);
|
|
3995
|
+
return lines.join("\n");
|
|
3996
|
+
}
|
|
3997
|
+
function formatPerFile(perFileResults) {
|
|
3998
|
+
const lines = [];
|
|
3999
|
+
lines.push(
|
|
4000
|
+
`Impact Preview (${perFileResults.length} staged file${perFileResults.length === 1 ? "" : "s"})`
|
|
4001
|
+
);
|
|
4002
|
+
const maxLen = Math.max(...perFileResults.map((r) => r.file.length));
|
|
4003
|
+
for (const result of perFileResults) {
|
|
4004
|
+
const padded = result.file.padEnd(maxLen);
|
|
4005
|
+
lines.push(` ${padded} -> ${result.code} files, ${result.tests} tests, ${result.docs} docs`);
|
|
4006
|
+
}
|
|
4007
|
+
return lines.join("\n");
|
|
4008
|
+
}
|
|
4009
|
+
async function runImpactPreview(options) {
|
|
4010
|
+
const projectPath = path33.resolve(options.path ?? process.cwd());
|
|
4011
|
+
const stagedFiles = getStagedFiles(projectPath);
|
|
4012
|
+
if (stagedFiles.length === 0) {
|
|
4013
|
+
return "Impact Preview: no staged changes";
|
|
4014
|
+
}
|
|
4015
|
+
if (!graphExists(projectPath)) {
|
|
4016
|
+
return "Impact Preview: skipped (no graph \u2014 run `harness scan` to enable)";
|
|
4017
|
+
}
|
|
4018
|
+
const mode = options.detailed ? "detailed" : "summary";
|
|
4019
|
+
const perFileResults = [];
|
|
4020
|
+
const allGroups = [];
|
|
4021
|
+
const aggregateCounts = { code: 0, tests: 0, docs: 0, other: 0 };
|
|
4022
|
+
for (const file of stagedFiles) {
|
|
4023
|
+
const response = await handleGetImpact({
|
|
4024
|
+
path: projectPath,
|
|
4025
|
+
filePath: file,
|
|
4026
|
+
mode: options.perFile ? "summary" : mode
|
|
4027
|
+
});
|
|
4028
|
+
const parsed = parseImpactResponse(response);
|
|
4029
|
+
if (!parsed) continue;
|
|
4030
|
+
aggregateCounts.code += parsed.counts.code;
|
|
4031
|
+
aggregateCounts.tests += parsed.counts.tests;
|
|
4032
|
+
aggregateCounts.docs += parsed.counts.docs;
|
|
4033
|
+
aggregateCounts.other += parsed.counts.other;
|
|
4034
|
+
if (options.perFile) {
|
|
4035
|
+
perFileResults.push({
|
|
4036
|
+
file,
|
|
4037
|
+
code: parsed.counts.code,
|
|
4038
|
+
tests: parsed.counts.tests,
|
|
4039
|
+
docs: parsed.counts.docs
|
|
4040
|
+
});
|
|
4041
|
+
}
|
|
4042
|
+
allGroups.push(parsed.items);
|
|
4043
|
+
}
|
|
4044
|
+
if (options.perFile) {
|
|
4045
|
+
if (perFileResults.length === 0) {
|
|
4046
|
+
return `Impact Preview (${stagedFiles.length} staged file${stagedFiles.length === 1 ? "" : "s"}): no impact data`;
|
|
4047
|
+
}
|
|
4048
|
+
return formatPerFile(perFileResults);
|
|
4049
|
+
}
|
|
4050
|
+
const merged = mergeImpactGroups(allGroups);
|
|
4051
|
+
if (options.detailed) {
|
|
4052
|
+
return formatDetailed(stagedFiles.length, merged, aggregateCounts);
|
|
4053
|
+
}
|
|
4054
|
+
return formatCompact(stagedFiles.length, merged, aggregateCounts);
|
|
4055
|
+
}
|
|
4056
|
+
function createImpactPreviewCommand() {
|
|
4057
|
+
const command = new Command44("impact-preview").description("Show blast radius of staged changes using the knowledge graph").option("--detailed", "Show all affected files instead of top items").option("--per-file", "Show impact per staged file instead of aggregate").option("--path <dir>", "Project root (default: cwd)").action(async (opts) => {
|
|
4058
|
+
const output = await runImpactPreview({
|
|
4059
|
+
detailed: opts.detailed,
|
|
4060
|
+
perFile: opts.perFile,
|
|
4061
|
+
path: opts.path
|
|
4062
|
+
});
|
|
4063
|
+
console.log(output);
|
|
4064
|
+
process.exit(0);
|
|
4065
|
+
});
|
|
4066
|
+
return command;
|
|
4067
|
+
}
|
|
4068
|
+
|
|
4069
|
+
// src/commands/check-arch.ts
|
|
4070
|
+
import { Command as Command45 } from "commander";
|
|
4071
|
+
import { execSync as execSync4 } from "child_process";
|
|
4072
|
+
function getCommitHash(cwd) {
|
|
4073
|
+
try {
|
|
4074
|
+
return execSync4("git rev-parse --short HEAD", { cwd, encoding: "utf-8" }).toString().trim();
|
|
4075
|
+
} catch {
|
|
4076
|
+
return "unknown";
|
|
4077
|
+
}
|
|
4078
|
+
}
|
|
4079
|
+
function filterByModule(results, modulePath) {
|
|
4080
|
+
const normalized = modulePath.replace(/\/+$/, "");
|
|
4081
|
+
return results.filter((r) => r.scope === normalized || r.scope.startsWith(normalized + "/"));
|
|
4082
|
+
}
|
|
4083
|
+
function findThresholdViolations(results) {
|
|
4084
|
+
const violations = [];
|
|
4085
|
+
for (const result of results) {
|
|
4086
|
+
for (const v of result.violations) {
|
|
4087
|
+
if (v.severity === "error") {
|
|
4088
|
+
violations.push(v);
|
|
4089
|
+
}
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
4092
|
+
return violations;
|
|
4093
|
+
}
|
|
4094
|
+
async function runCheckArch(options) {
|
|
4095
|
+
const cwd = options.cwd ?? process.cwd();
|
|
4096
|
+
const configResult = resolveConfig(options.configPath);
|
|
4097
|
+
if (!configResult.ok) {
|
|
4098
|
+
return configResult;
|
|
4099
|
+
}
|
|
4100
|
+
const config = configResult.value;
|
|
4101
|
+
const archConfig = config.architecture ?? ArchConfigSchema.parse({});
|
|
4102
|
+
if (!archConfig.enabled) {
|
|
4103
|
+
return Ok({
|
|
4104
|
+
passed: true,
|
|
4105
|
+
mode: "threshold-only",
|
|
4106
|
+
totalViolations: 0,
|
|
4107
|
+
newViolations: [],
|
|
4108
|
+
resolvedViolations: [],
|
|
4109
|
+
preExisting: [],
|
|
4110
|
+
regressions: [],
|
|
4111
|
+
thresholdViolations: []
|
|
4112
|
+
});
|
|
4113
|
+
}
|
|
4114
|
+
let results = await runAll(archConfig, cwd);
|
|
4115
|
+
if (options.module) {
|
|
4116
|
+
results = filterByModule(results, options.module);
|
|
4117
|
+
}
|
|
4118
|
+
const manager = new ArchBaselineManager(cwd, archConfig.baselinePath);
|
|
4119
|
+
if (options.updateBaseline) {
|
|
4120
|
+
const commitHash = getCommitHash(cwd);
|
|
4121
|
+
const baseline2 = manager.capture(results, commitHash);
|
|
4122
|
+
manager.save(baseline2);
|
|
4123
|
+
return Ok({
|
|
4124
|
+
passed: true,
|
|
4125
|
+
mode: "baseline",
|
|
4126
|
+
totalViolations: 0,
|
|
4127
|
+
newViolations: [],
|
|
4128
|
+
resolvedViolations: [],
|
|
4129
|
+
preExisting: [],
|
|
4130
|
+
regressions: [],
|
|
4131
|
+
thresholdViolations: [],
|
|
4132
|
+
baselineUpdated: true
|
|
4133
|
+
});
|
|
4134
|
+
}
|
|
4135
|
+
const thresholdViolations = findThresholdViolations(results);
|
|
4136
|
+
const baseline = manager.load();
|
|
4137
|
+
if (!baseline) {
|
|
4138
|
+
const passed2 = thresholdViolations.length === 0;
|
|
4139
|
+
return Ok({
|
|
4140
|
+
passed: passed2,
|
|
4141
|
+
mode: "threshold-only",
|
|
4142
|
+
totalViolations: thresholdViolations.length,
|
|
4143
|
+
newViolations: [],
|
|
4144
|
+
resolvedViolations: [],
|
|
4145
|
+
preExisting: [],
|
|
4146
|
+
regressions: [],
|
|
4147
|
+
thresholdViolations,
|
|
4148
|
+
warning: "No baseline found. Running in threshold-only mode. Run with --update-baseline to capture current state."
|
|
4149
|
+
});
|
|
4150
|
+
}
|
|
4151
|
+
const diffResult = diff(results, baseline);
|
|
4152
|
+
const passed = diffResult.passed && thresholdViolations.length === 0;
|
|
4153
|
+
return Ok({
|
|
4154
|
+
passed,
|
|
4155
|
+
mode: "baseline",
|
|
4156
|
+
totalViolations: diffResult.newViolations.length + thresholdViolations.length,
|
|
4157
|
+
newViolations: diffResult.newViolations,
|
|
4158
|
+
resolvedViolations: diffResult.resolvedViolations,
|
|
4159
|
+
preExisting: diffResult.preExisting,
|
|
4160
|
+
regressions: diffResult.regressions,
|
|
4161
|
+
thresholdViolations
|
|
4162
|
+
});
|
|
4163
|
+
}
|
|
4164
|
+
function createCheckArchCommand() {
|
|
4165
|
+
const command = new Command45("check-arch").description("Check architecture assertions against baseline and thresholds").option("--update-baseline", "Capture current state as new baseline").option("--module <path>", "Check a single module").action(async (opts, cmd) => {
|
|
4166
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
4167
|
+
const mode = globalOpts.json ? OutputMode.JSON : globalOpts.quiet ? OutputMode.QUIET : globalOpts.verbose ? OutputMode.VERBOSE : OutputMode.TEXT;
|
|
4168
|
+
const formatter = new OutputFormatter(mode);
|
|
4169
|
+
const result = await runCheckArch({
|
|
4170
|
+
configPath: globalOpts.config,
|
|
4171
|
+
updateBaseline: opts.updateBaseline,
|
|
4172
|
+
json: globalOpts.json,
|
|
4173
|
+
module: opts.module
|
|
4174
|
+
});
|
|
4175
|
+
if (!result.ok) {
|
|
4176
|
+
if (mode === OutputMode.JSON) {
|
|
4177
|
+
console.log(JSON.stringify({ error: result.error.message }));
|
|
4178
|
+
} else {
|
|
4179
|
+
logger.error(result.error.message);
|
|
4180
|
+
}
|
|
4181
|
+
process.exit(result.error.exitCode);
|
|
4182
|
+
}
|
|
4183
|
+
const value = result.value;
|
|
4184
|
+
if (value.warning && mode !== OutputMode.JSON) {
|
|
4185
|
+
logger.warn(value.warning);
|
|
4186
|
+
}
|
|
4187
|
+
if (value.baselineUpdated) {
|
|
4188
|
+
if (mode === OutputMode.JSON) {
|
|
4189
|
+
console.log(JSON.stringify({ baselineUpdated: true }));
|
|
4190
|
+
} else {
|
|
4191
|
+
logger.success("Baseline updated successfully.");
|
|
4192
|
+
}
|
|
4193
|
+
process.exit(ExitCode.SUCCESS);
|
|
4194
|
+
return;
|
|
4195
|
+
}
|
|
4196
|
+
const issues = [
|
|
4197
|
+
...value.newViolations.map((v) => ({
|
|
4198
|
+
file: v.file,
|
|
4199
|
+
message: `New violation [${v.severity}]: ${v.detail}`
|
|
4200
|
+
})),
|
|
4201
|
+
...value.thresholdViolations.map((v) => ({
|
|
4202
|
+
file: v.file,
|
|
4203
|
+
message: `Threshold exceeded: ${v.detail}`
|
|
4204
|
+
})),
|
|
4205
|
+
...value.regressions.map((r) => ({
|
|
4206
|
+
message: `Regression in ${r.category}: ${r.baselineValue} -> ${r.currentValue} (+${r.delta})`
|
|
4207
|
+
}))
|
|
4208
|
+
];
|
|
4209
|
+
if (mode === OutputMode.JSON) {
|
|
4210
|
+
console.log(JSON.stringify(value, null, 2));
|
|
4211
|
+
} else {
|
|
4212
|
+
if (value.resolvedViolations.length > 0 && mode !== OutputMode.QUIET) {
|
|
4213
|
+
logger.success(
|
|
4214
|
+
`${value.resolvedViolations.length} violation(s) resolved since baseline.`
|
|
4215
|
+
);
|
|
4216
|
+
}
|
|
4217
|
+
const output = formatter.formatValidation({
|
|
4218
|
+
valid: value.passed,
|
|
4219
|
+
issues
|
|
4220
|
+
});
|
|
4221
|
+
if (output) {
|
|
4222
|
+
console.log(output);
|
|
4223
|
+
}
|
|
4224
|
+
}
|
|
4225
|
+
process.exit(value.passed ? ExitCode.SUCCESS : ExitCode.VALIDATION_FAILED);
|
|
4226
|
+
});
|
|
4227
|
+
return command;
|
|
4228
|
+
}
|
|
4229
|
+
|
|
4230
|
+
// src/commands/blueprint.ts
|
|
4231
|
+
import { Command as Command46 } from "commander";
|
|
4232
|
+
import * as path34 from "path";
|
|
4233
|
+
function createBlueprintCommand() {
|
|
4234
|
+
return new Command46("blueprint").description("Generate a self-contained, interactive blueprint of the codebase").argument("[path]", "Path to the project root", ".").option("-o, --output <dir>", "Output directory", "docs/blueprint").action(async (projectPath, options) => {
|
|
4235
|
+
try {
|
|
4236
|
+
const rootDir = path34.resolve(projectPath);
|
|
4237
|
+
const outputDir = path34.resolve(options.output);
|
|
4238
|
+
logger.info(`Scanning project at ${rootDir}...`);
|
|
4239
|
+
const scanner = new ProjectScanner(rootDir);
|
|
4240
|
+
const data = await scanner.scan();
|
|
4241
|
+
logger.info(`Generating blueprint to ${outputDir}...`);
|
|
4242
|
+
const generator = new BlueprintGenerator();
|
|
4243
|
+
await generator.generate(data, { outputDir });
|
|
4244
|
+
logger.success(`Blueprint generated successfully at ${path34.join(outputDir, "index.html")}`);
|
|
4245
|
+
} catch (error) {
|
|
4246
|
+
logger.error(
|
|
4247
|
+
`Failed to generate blueprint: ${error instanceof Error ? error.message : String(error)}`
|
|
4248
|
+
);
|
|
4249
|
+
process.exit(1);
|
|
4250
|
+
}
|
|
4251
|
+
});
|
|
4252
|
+
}
|
|
4253
|
+
|
|
4254
|
+
// src/commands/share.ts
|
|
4255
|
+
import { Command as Command47 } from "commander";
|
|
4256
|
+
import * as fs17 from "fs";
|
|
4257
|
+
import * as path35 from "path";
|
|
4258
|
+
import { parse as parseYaml } from "yaml";
|
|
4259
|
+
var MANIFEST_FILENAME = "constraints.yaml";
|
|
4260
|
+
function createShareCommand() {
|
|
4261
|
+
return new Command47("share").description("Extract and publish a constraints bundle from constraints.yaml").argument("[path]", "Path to the project root", ".").option("-o, --output <dir>", "Output directory for the bundle", ".").action(async (projectPath, options) => {
|
|
4262
|
+
const rootDir = path35.resolve(projectPath);
|
|
4263
|
+
const manifestPath = path35.join(rootDir, MANIFEST_FILENAME);
|
|
4264
|
+
if (!fs17.existsSync(manifestPath)) {
|
|
4265
|
+
logger.error(
|
|
4266
|
+
`No ${MANIFEST_FILENAME} found at ${manifestPath}.
|
|
4267
|
+
Create a constraints.yaml in your project root to define what to share.`
|
|
4268
|
+
);
|
|
4269
|
+
process.exit(1);
|
|
4270
|
+
}
|
|
4271
|
+
let parsed;
|
|
4272
|
+
try {
|
|
4273
|
+
const raw = fs17.readFileSync(manifestPath, "utf-8");
|
|
4274
|
+
parsed = parseYaml(raw);
|
|
4275
|
+
} catch (err) {
|
|
4276
|
+
logger.error(
|
|
4277
|
+
`Failed to read ${MANIFEST_FILENAME}: ${err instanceof Error ? err.message : String(err)}`
|
|
4278
|
+
);
|
|
4279
|
+
process.exit(1);
|
|
4280
|
+
}
|
|
4281
|
+
const manifestResult = parseManifest(parsed);
|
|
4282
|
+
if (!manifestResult.ok) {
|
|
4283
|
+
logger.error(`Invalid ${MANIFEST_FILENAME}: ${manifestResult.error}`);
|
|
4284
|
+
process.exit(1);
|
|
4285
|
+
}
|
|
4286
|
+
const manifest = manifestResult.value;
|
|
4287
|
+
const configResult = resolveConfig(path35.join(rootDir, "harness.config.json"));
|
|
4288
|
+
if (!configResult.ok) {
|
|
4289
|
+
logger.error(configResult.error.message);
|
|
4290
|
+
process.exit(1);
|
|
4291
|
+
}
|
|
4292
|
+
const config = configResult.value;
|
|
4293
|
+
const bundleResult = extractBundle(manifest, config);
|
|
4294
|
+
if (!bundleResult.ok) {
|
|
4295
|
+
logger.error(`Failed to extract bundle: ${bundleResult.error}`);
|
|
4296
|
+
process.exit(1);
|
|
4297
|
+
}
|
|
4298
|
+
const bundle = bundleResult.value;
|
|
4299
|
+
if (Object.keys(bundle.constraints).length === 0) {
|
|
4300
|
+
logger.error(
|
|
4301
|
+
"No constraints found for the include paths in constraints.yaml.\nCheck that your harness config contains the declared sections."
|
|
4302
|
+
);
|
|
4303
|
+
process.exit(1);
|
|
4304
|
+
}
|
|
4305
|
+
const outputDir = path35.resolve(options.output);
|
|
4306
|
+
const outputPath = path35.join(outputDir, `${manifest.name}.harness-constraints.json`);
|
|
4307
|
+
try {
|
|
4308
|
+
await writeConfig(outputPath, bundle);
|
|
4309
|
+
} catch (err) {
|
|
4310
|
+
logger.error(`Failed to write bundle: ${err instanceof Error ? err.message : String(err)}`);
|
|
4311
|
+
process.exit(1);
|
|
4312
|
+
}
|
|
4313
|
+
logger.success(`Bundle written to ${outputPath}`);
|
|
4314
|
+
});
|
|
4315
|
+
}
|
|
4316
|
+
|
|
4317
|
+
// src/commands/install.ts
|
|
4318
|
+
import * as fs19 from "fs";
|
|
4319
|
+
import * as path37 from "path";
|
|
4320
|
+
import { Command as Command48 } from "commander";
|
|
4321
|
+
import { parse as yamlParse } from "yaml";
|
|
4322
|
+
|
|
4323
|
+
// src/registry/tarball.ts
|
|
4324
|
+
import * as fs18 from "fs";
|
|
4325
|
+
import * as path36 from "path";
|
|
4326
|
+
import * as os2 from "os";
|
|
4327
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
4328
|
+
function extractTarball(tarballBuffer) {
|
|
4329
|
+
const tmpDir = fs18.mkdtempSync(path36.join(os2.tmpdir(), "harness-skill-install-"));
|
|
4330
|
+
const tarballPath = path36.join(tmpDir, "package.tgz");
|
|
4331
|
+
try {
|
|
4332
|
+
fs18.writeFileSync(tarballPath, tarballBuffer);
|
|
4333
|
+
execFileSync5("tar", ["-xzf", tarballPath, "-C", tmpDir], {
|
|
4334
|
+
timeout: 3e4
|
|
4335
|
+
});
|
|
4336
|
+
fs18.unlinkSync(tarballPath);
|
|
4337
|
+
} catch (err) {
|
|
4338
|
+
cleanupTempDir(tmpDir);
|
|
4339
|
+
throw new Error(
|
|
4340
|
+
`Failed to extract tarball: ${err instanceof Error ? err.message : String(err)}`,
|
|
4341
|
+
{ cause: err }
|
|
4342
|
+
);
|
|
4343
|
+
}
|
|
4344
|
+
return tmpDir;
|
|
4345
|
+
}
|
|
4346
|
+
function placeSkillContent(extractedPkgDir, communityBaseDir, skillName, platforms) {
|
|
4347
|
+
const files = fs18.readdirSync(extractedPkgDir);
|
|
4348
|
+
for (const platform of platforms) {
|
|
4349
|
+
const targetDir = path36.join(communityBaseDir, platform, skillName);
|
|
4350
|
+
if (fs18.existsSync(targetDir)) {
|
|
4351
|
+
fs18.rmSync(targetDir, { recursive: true, force: true });
|
|
4352
|
+
}
|
|
4353
|
+
fs18.mkdirSync(targetDir, { recursive: true });
|
|
4354
|
+
for (const file of files) {
|
|
4355
|
+
if (file === "package.json" || file === "node_modules") continue;
|
|
4356
|
+
const srcPath = path36.join(extractedPkgDir, file);
|
|
4357
|
+
const destPath = path36.join(targetDir, file);
|
|
4358
|
+
const stat = fs18.statSync(srcPath);
|
|
4359
|
+
if (stat.isDirectory()) {
|
|
4360
|
+
fs18.cpSync(srcPath, destPath, { recursive: true });
|
|
4361
|
+
} else {
|
|
4362
|
+
fs18.copyFileSync(srcPath, destPath);
|
|
4363
|
+
}
|
|
4364
|
+
}
|
|
4365
|
+
}
|
|
4366
|
+
}
|
|
4367
|
+
function removeSkillContent(communityBaseDir, skillName, platforms) {
|
|
4368
|
+
for (const platform of platforms) {
|
|
4369
|
+
const targetDir = path36.join(communityBaseDir, platform, skillName);
|
|
4370
|
+
if (fs18.existsSync(targetDir)) {
|
|
4371
|
+
fs18.rmSync(targetDir, { recursive: true, force: true });
|
|
4372
|
+
}
|
|
4373
|
+
}
|
|
4374
|
+
}
|
|
4375
|
+
function cleanupTempDir(dirPath) {
|
|
4376
|
+
try {
|
|
4377
|
+
fs18.rmSync(dirPath, { recursive: true, force: true });
|
|
4378
|
+
} catch {
|
|
4379
|
+
}
|
|
4380
|
+
}
|
|
4381
|
+
|
|
4382
|
+
// src/registry/resolver.ts
|
|
4383
|
+
import semver2 from "semver";
|
|
4384
|
+
function resolveVersion(metadata, versionRange) {
|
|
4385
|
+
const versions = Object.keys(metadata.versions);
|
|
4386
|
+
if (versions.length === 0) {
|
|
4387
|
+
throw new Error(`No versions available for ${metadata.name}.`);
|
|
4388
|
+
}
|
|
4389
|
+
if (!versionRange) {
|
|
4390
|
+
const latestTag = metadata["dist-tags"].latest;
|
|
4391
|
+
if (latestTag) {
|
|
4392
|
+
const latestInfo = metadata.versions[latestTag];
|
|
4393
|
+
if (latestInfo) return latestInfo;
|
|
4394
|
+
}
|
|
4395
|
+
const highest = semver2.maxSatisfying(versions, "*");
|
|
4396
|
+
if (!highest || !metadata.versions[highest]) {
|
|
4397
|
+
throw new Error(`No versions available for ${metadata.name}.`);
|
|
4398
|
+
}
|
|
4399
|
+
return metadata.versions[highest];
|
|
4400
|
+
}
|
|
4401
|
+
const matched = semver2.maxSatisfying(versions, versionRange);
|
|
4402
|
+
if (!matched || !metadata.versions[matched]) {
|
|
4403
|
+
throw new Error(
|
|
4404
|
+
`No version of ${metadata.name} matches range ${versionRange}. Available: ${versions.join(", ")}`
|
|
4405
|
+
);
|
|
4406
|
+
}
|
|
4407
|
+
return metadata.versions[matched];
|
|
4408
|
+
}
|
|
4409
|
+
function findDependentsOf(lockfile, targetPackageName) {
|
|
4410
|
+
const entry = lockfile.skills[targetPackageName];
|
|
4411
|
+
if (!entry?.dependencyOf) return [];
|
|
4412
|
+
return [entry.dependencyOf];
|
|
4413
|
+
}
|
|
4414
|
+
|
|
4415
|
+
// src/commands/install.ts
|
|
4416
|
+
function validateSkillYaml(parsed) {
|
|
4417
|
+
if (!parsed || typeof parsed !== "object" || !("name" in parsed) || !("version" in parsed) || !("platforms" in parsed)) {
|
|
4418
|
+
throw new Error("contains invalid skill.yaml");
|
|
4419
|
+
}
|
|
4420
|
+
const obj = parsed;
|
|
4421
|
+
if (typeof obj["name"] !== "string" || typeof obj["version"] !== "string" || !Array.isArray(obj["platforms"])) {
|
|
4422
|
+
throw new Error("contains invalid skill.yaml");
|
|
4423
|
+
}
|
|
4424
|
+
return {
|
|
4425
|
+
name: obj["name"],
|
|
4426
|
+
version: obj["version"],
|
|
4427
|
+
platforms: obj["platforms"],
|
|
4428
|
+
depends_on: Array.isArray(obj["depends_on"]) ? obj["depends_on"] : []
|
|
4429
|
+
};
|
|
4430
|
+
}
|
|
4431
|
+
async function runInstall(skillName, options) {
|
|
4432
|
+
const packageName = resolvePackageName(skillName);
|
|
4433
|
+
const shortName = extractSkillName(packageName);
|
|
4434
|
+
const globalDir = resolveGlobalSkillsDir();
|
|
4435
|
+
const skillsDir = path37.dirname(globalDir);
|
|
4436
|
+
const communityBase = path37.join(skillsDir, "community");
|
|
4437
|
+
const lockfilePath = path37.join(communityBase, "skills-lock.json");
|
|
4438
|
+
const bundledNames = getBundledSkillNames(globalDir);
|
|
4439
|
+
if (bundledNames.has(shortName)) {
|
|
4440
|
+
throw new Error(
|
|
4441
|
+
`'${shortName}' is a bundled skill and cannot be overridden by community installs.`
|
|
4442
|
+
);
|
|
4443
|
+
}
|
|
4444
|
+
const metadata = await fetchPackageMetadata(packageName);
|
|
4445
|
+
const versionInfo = resolveVersion(metadata, options.version);
|
|
4446
|
+
const resolvedVersion = versionInfo.version;
|
|
4447
|
+
const lockfile = readLockfile(lockfilePath);
|
|
4448
|
+
const existingEntry = lockfile.skills[packageName];
|
|
4449
|
+
const previousVersion = existingEntry?.version;
|
|
4450
|
+
if (existingEntry && existingEntry.version === resolvedVersion && !options.force) {
|
|
4451
|
+
return {
|
|
4452
|
+
installed: false,
|
|
4453
|
+
skipped: true,
|
|
4454
|
+
name: packageName,
|
|
4455
|
+
version: resolvedVersion
|
|
4456
|
+
};
|
|
4457
|
+
}
|
|
4458
|
+
const tarballBuffer = await downloadTarball(versionInfo.dist.tarball);
|
|
4459
|
+
const extractDir = extractTarball(tarballBuffer);
|
|
4460
|
+
let skillYaml;
|
|
4461
|
+
try {
|
|
4462
|
+
const extractedPkgDir = path37.join(extractDir, "package");
|
|
4463
|
+
const skillYamlPath = path37.join(extractedPkgDir, "skill.yaml");
|
|
4464
|
+
if (!fs19.existsSync(skillYamlPath)) {
|
|
4465
|
+
throw new Error(`contains invalid skill.yaml: file not found in package`);
|
|
4466
|
+
}
|
|
4467
|
+
const rawYaml = fs19.readFileSync(skillYamlPath, "utf-8");
|
|
4468
|
+
const parsed = yamlParse(rawYaml);
|
|
4469
|
+
skillYaml = validateSkillYaml(parsed);
|
|
4470
|
+
placeSkillContent(extractedPkgDir, communityBase, shortName, skillYaml.platforms);
|
|
4471
|
+
} catch (err) {
|
|
4472
|
+
cleanupTempDir(extractDir);
|
|
4473
|
+
throw err;
|
|
4474
|
+
}
|
|
4475
|
+
cleanupTempDir(extractDir);
|
|
4476
|
+
const entry = {
|
|
4477
|
+
version: resolvedVersion,
|
|
4478
|
+
resolved: versionInfo.dist.tarball,
|
|
4479
|
+
integrity: versionInfo.dist.integrity,
|
|
4480
|
+
platforms: skillYaml.platforms,
|
|
4481
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4482
|
+
dependencyOf: null
|
|
4483
|
+
};
|
|
4484
|
+
let updatedLockfile = updateLockfileEntry(lockfile, packageName, entry);
|
|
4485
|
+
writeLockfile(lockfilePath, updatedLockfile);
|
|
4486
|
+
const result = {
|
|
4487
|
+
installed: true,
|
|
4488
|
+
name: packageName,
|
|
4489
|
+
version: resolvedVersion
|
|
4490
|
+
};
|
|
4491
|
+
if (previousVersion && previousVersion !== resolvedVersion) {
|
|
4492
|
+
result.upgraded = true;
|
|
4493
|
+
result.previousVersion = previousVersion;
|
|
4494
|
+
}
|
|
4495
|
+
const deps = skillYaml.depends_on ?? [];
|
|
4496
|
+
for (const dep of deps) {
|
|
4497
|
+
logger.info(`Installing dependency: ${dep}`);
|
|
4498
|
+
await runInstall(dep, {});
|
|
4499
|
+
}
|
|
4500
|
+
return result;
|
|
4501
|
+
}
|
|
4502
|
+
function createInstallCommand() {
|
|
4503
|
+
const cmd = new Command48("install");
|
|
4504
|
+
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").action(async (skill, opts) => {
|
|
4505
|
+
try {
|
|
4506
|
+
const result = await runInstall(skill, opts);
|
|
4507
|
+
if (result.skipped) {
|
|
4508
|
+
logger.info(
|
|
4509
|
+
`${result.name}@${result.version} is already installed. Use --force to reinstall.`
|
|
4510
|
+
);
|
|
4511
|
+
} else if (result.upgraded) {
|
|
4512
|
+
logger.success(
|
|
4513
|
+
`Upgraded ${result.name} from ${result.previousVersion} to ${result.version}`
|
|
4514
|
+
);
|
|
4515
|
+
} else {
|
|
4516
|
+
logger.success(`Installed ${result.name}@${result.version}`);
|
|
4517
|
+
}
|
|
4518
|
+
} catch (err) {
|
|
4519
|
+
logger.error(err instanceof Error ? err.message : String(err));
|
|
4520
|
+
process.exit(1);
|
|
4521
|
+
}
|
|
4522
|
+
});
|
|
4523
|
+
return cmd;
|
|
4524
|
+
}
|
|
4525
|
+
|
|
4526
|
+
// src/commands/uninstall.ts
|
|
4527
|
+
import * as path38 from "path";
|
|
4528
|
+
import { Command as Command49 } from "commander";
|
|
4529
|
+
async function runUninstall(skillName, options) {
|
|
4530
|
+
const packageName = resolvePackageName(skillName);
|
|
4531
|
+
const shortName = extractSkillName(packageName);
|
|
4532
|
+
const globalDir = resolveGlobalSkillsDir();
|
|
4533
|
+
const skillsDir = path38.dirname(globalDir);
|
|
4534
|
+
const communityBase = path38.join(skillsDir, "community");
|
|
4535
|
+
const lockfilePath = path38.join(communityBase, "skills-lock.json");
|
|
4536
|
+
const lockfile = readLockfile(lockfilePath);
|
|
4537
|
+
const entry = lockfile.skills[packageName];
|
|
4538
|
+
if (!entry) {
|
|
4539
|
+
throw new Error(`Skill '${shortName}' is not installed.`);
|
|
4540
|
+
}
|
|
4541
|
+
const dependents = findDependentsOf(lockfile, packageName);
|
|
4542
|
+
const warnings = [];
|
|
4543
|
+
if (dependents.length > 0) {
|
|
4544
|
+
if (!options.force) {
|
|
4545
|
+
throw new Error(
|
|
4546
|
+
`Cannot uninstall '${shortName}' because it is required by: ${dependents.join(", ")}. Use --force to remove anyway.`
|
|
4547
|
+
);
|
|
4548
|
+
}
|
|
4549
|
+
warnings.push(`Forced removal despite dependents: ${dependents.join(", ")}`);
|
|
4550
|
+
}
|
|
4551
|
+
removeSkillContent(communityBase, shortName, entry.platforms);
|
|
4552
|
+
const updatedLockfile = removeLockfileEntry(lockfile, packageName);
|
|
4553
|
+
writeLockfile(lockfilePath, updatedLockfile);
|
|
4554
|
+
const result = {
|
|
4555
|
+
removed: true,
|
|
4556
|
+
name: packageName,
|
|
4557
|
+
version: entry.version
|
|
4558
|
+
};
|
|
4559
|
+
if (warnings.length > 0) {
|
|
4560
|
+
result.warnings = warnings;
|
|
4561
|
+
}
|
|
4562
|
+
return result;
|
|
4563
|
+
}
|
|
4564
|
+
function createUninstallCommand() {
|
|
4565
|
+
const cmd = new Command49("uninstall");
|
|
4566
|
+
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) => {
|
|
4567
|
+
try {
|
|
4568
|
+
const result = await runUninstall(skill, opts);
|
|
4569
|
+
if (result.warnings) {
|
|
4570
|
+
for (const warning of result.warnings) {
|
|
4571
|
+
logger.warn(warning);
|
|
4572
|
+
}
|
|
4573
|
+
}
|
|
4574
|
+
logger.success(`Uninstalled ${result.name}@${result.version}`);
|
|
4575
|
+
} catch (err) {
|
|
4576
|
+
logger.error(err instanceof Error ? err.message : String(err));
|
|
4577
|
+
process.exit(1);
|
|
4578
|
+
}
|
|
4579
|
+
});
|
|
4580
|
+
return cmd;
|
|
4581
|
+
}
|
|
4582
|
+
|
|
4583
|
+
// src/commands/orchestrator.ts
|
|
4584
|
+
import { Command as Command50 } from "commander";
|
|
4585
|
+
import * as path39 from "path";
|
|
4586
|
+
import { Orchestrator, WorkflowLoader, launchTUI } from "@harness-engineering/orchestrator";
|
|
4587
|
+
function createOrchestratorCommand() {
|
|
4588
|
+
const orchestrator = new Command50("orchestrator");
|
|
4589
|
+
orchestrator.command("run").description("Run the orchestrator daemon").option("-w, --workflow <path>", "Path to WORKFLOW.md", "WORKFLOW.md").action(async (opts) => {
|
|
4590
|
+
const workflowPath = path39.resolve(process.cwd(), opts.workflow);
|
|
4591
|
+
const loader = new WorkflowLoader();
|
|
4592
|
+
const result = await loader.loadWorkflow(workflowPath);
|
|
4593
|
+
if (!result.ok) {
|
|
4594
|
+
logger.error(`Failed to load workflow: ${result.error.message}`);
|
|
4595
|
+
process.exit(ExitCode.ERROR);
|
|
4596
|
+
}
|
|
4597
|
+
const { config, promptTemplate } = result.value;
|
|
4598
|
+
const daemon = new Orchestrator(config, promptTemplate);
|
|
4599
|
+
const shutdown = () => {
|
|
4600
|
+
daemon.stop();
|
|
4601
|
+
process.exit(ExitCode.SUCCESS);
|
|
4602
|
+
};
|
|
4603
|
+
process.on("SIGINT", shutdown);
|
|
4604
|
+
process.on("SIGTERM", shutdown);
|
|
4605
|
+
daemon.start();
|
|
4606
|
+
const { waitUntilExit } = launchTUI(daemon);
|
|
4607
|
+
await waitUntilExit();
|
|
4608
|
+
process.exit(ExitCode.SUCCESS);
|
|
4609
|
+
});
|
|
4610
|
+
return orchestrator;
|
|
4611
|
+
}
|
|
4612
|
+
|
|
3211
4613
|
// src/index.ts
|
|
3212
4614
|
function createProgram() {
|
|
3213
|
-
const program = new
|
|
4615
|
+
const program = new Command51();
|
|
3214
4616
|
program.name("harness").description("CLI for Harness Engineering toolkit").version(CLI_VERSION).option("-c, --config <path>", "Path to config file").option("--json", "Output as JSON").option("--verbose", "Verbose output").option("--quiet", "Minimal output");
|
|
3215
4617
|
program.addCommand(createValidateCommand());
|
|
3216
4618
|
program.addCommand(createCheckDepsCommand());
|
|
@@ -3240,6 +4642,13 @@ function createProgram() {
|
|
|
3240
4642
|
program.addCommand(createQueryCommand());
|
|
3241
4643
|
program.addCommand(createGraphCommand());
|
|
3242
4644
|
program.addCommand(createMcpCommand());
|
|
4645
|
+
program.addCommand(createImpactPreviewCommand());
|
|
4646
|
+
program.addCommand(createCheckArchCommand());
|
|
4647
|
+
program.addCommand(createBlueprintCommand());
|
|
4648
|
+
program.addCommand(createShareCommand());
|
|
4649
|
+
program.addCommand(createInstallCommand());
|
|
4650
|
+
program.addCommand(createUninstallCommand());
|
|
4651
|
+
program.addCommand(createOrchestratorCommand());
|
|
3243
4652
|
return program;
|
|
3244
4653
|
}
|
|
3245
4654
|
|
|
@@ -3250,5 +4659,9 @@ export {
|
|
|
3250
4659
|
runQuery,
|
|
3251
4660
|
runGraphStatus,
|
|
3252
4661
|
runGraphExport,
|
|
4662
|
+
runImpactPreview,
|
|
4663
|
+
runCheckArch,
|
|
4664
|
+
runInstall,
|
|
4665
|
+
runUninstall,
|
|
3253
4666
|
createProgram
|
|
3254
4667
|
};
|