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