@harness-engineering/cli 1.6.1 → 1.7.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/personas/planner.yaml +27 -0
- package/dist/agents/personas/verifier.yaml +30 -0
- package/dist/agents/skills/claude-code/enforce-architecture/SKILL.md +19 -0
- package/dist/agents/skills/claude-code/harness-accessibility/SKILL.md +274 -0
- package/dist/agents/skills/claude-code/harness-accessibility/skill.yaml +51 -0
- package/dist/agents/skills/claude-code/harness-autopilot/SKILL.md +111 -72
- package/dist/agents/skills/claude-code/harness-autopilot/skill.yaml +4 -2
- package/dist/agents/skills/claude-code/harness-dependency-health/skill.yaml +1 -1
- package/dist/agents/skills/claude-code/harness-design/SKILL.md +265 -0
- package/dist/agents/skills/claude-code/harness-design/skill.yaml +53 -0
- package/dist/agents/skills/claude-code/harness-design-mobile/SKILL.md +336 -0
- package/dist/agents/skills/claude-code/harness-design-mobile/skill.yaml +49 -0
- package/dist/agents/skills/claude-code/harness-design-system/SKILL.md +282 -0
- package/dist/agents/skills/claude-code/harness-design-system/skill.yaml +50 -0
- package/dist/agents/skills/claude-code/harness-design-web/SKILL.md +360 -0
- package/dist/agents/skills/claude-code/harness-design-web/skill.yaml +52 -0
- package/dist/agents/skills/claude-code/harness-hotspot-detector/skill.yaml +1 -1
- package/dist/agents/skills/claude-code/harness-impact-analysis/SKILL.md +16 -0
- package/dist/agents/skills/claude-code/harness-integrity/SKILL.md +19 -1
- package/dist/agents/skills/claude-code/harness-knowledge-mapper/skill.yaml +1 -1
- package/dist/agents/skills/claude-code/harness-onboarding/SKILL.md +19 -1
- package/dist/agents/skills/claude-code/harness-release-readiness/SKILL.md +13 -9
- package/dist/agents/skills/claude-code/harness-security-scan/skill.yaml +1 -1
- package/dist/agents/skills/claude-code/harness-verify/SKILL.md +26 -0
- package/dist/agents/skills/gemini-cli/harness-accessibility/SKILL.md +274 -0
- package/dist/agents/skills/gemini-cli/harness-accessibility/skill.yaml +51 -0
- package/dist/agents/skills/gemini-cli/harness-autopilot/SKILL.md +111 -72
- package/dist/agents/skills/gemini-cli/harness-autopilot/skill.yaml +4 -2
- package/dist/agents/skills/gemini-cli/harness-dependency-health/skill.yaml +1 -1
- package/dist/agents/skills/gemini-cli/harness-design/SKILL.md +265 -0
- package/dist/agents/skills/gemini-cli/harness-design/skill.yaml +53 -0
- package/dist/agents/skills/gemini-cli/harness-design-mobile/SKILL.md +336 -0
- package/dist/agents/skills/gemini-cli/harness-design-mobile/skill.yaml +49 -0
- package/dist/agents/skills/gemini-cli/harness-design-system/SKILL.md +282 -0
- package/dist/agents/skills/gemini-cli/harness-design-system/skill.yaml +50 -0
- package/dist/agents/skills/gemini-cli/harness-design-web/SKILL.md +360 -0
- package/dist/agents/skills/gemini-cli/harness-design-web/skill.yaml +52 -0
- package/dist/agents/skills/gemini-cli/harness-hotspot-detector/skill.yaml +1 -1
- package/dist/agents/skills/gemini-cli/harness-impact-analysis/SKILL.md +16 -0
- package/dist/agents/skills/gemini-cli/harness-knowledge-mapper/skill.yaml +1 -1
- package/dist/agents/skills/gemini-cli/harness-release-readiness/SKILL.md +13 -9
- package/dist/agents/skills/gemini-cli/harness-security-scan/skill.yaml +1 -1
- package/dist/agents/skills/node_modules/.bin/vitest +2 -2
- package/dist/agents/skills/shared/design-knowledge/anti-patterns/color.yaml +106 -0
- package/dist/agents/skills/shared/design-knowledge/anti-patterns/layout.yaml +109 -0
- package/dist/agents/skills/shared/design-knowledge/anti-patterns/motion.yaml +109 -0
- package/dist/agents/skills/shared/design-knowledge/anti-patterns/typography.yaml +112 -0
- package/dist/agents/skills/shared/design-knowledge/industries/creative.yaml +80 -0
- package/dist/agents/skills/shared/design-knowledge/industries/ecommerce.yaml +80 -0
- package/dist/agents/skills/shared/design-knowledge/industries/emerging-tech.yaml +83 -0
- package/dist/agents/skills/shared/design-knowledge/industries/fintech.yaml +80 -0
- package/dist/agents/skills/shared/design-knowledge/industries/healthcare.yaml +80 -0
- package/dist/agents/skills/shared/design-knowledge/industries/lifestyle.yaml +80 -0
- package/dist/agents/skills/shared/design-knowledge/industries/saas.yaml +80 -0
- package/dist/agents/skills/shared/design-knowledge/industries/services.yaml +80 -0
- package/dist/agents/skills/shared/design-knowledge/palettes/curated.yaml +234 -0
- package/dist/agents/skills/shared/design-knowledge/platform-rules/android.yaml +125 -0
- package/dist/agents/skills/shared/design-knowledge/platform-rules/flutter.yaml +144 -0
- package/dist/agents/skills/shared/design-knowledge/platform-rules/ios.yaml +106 -0
- package/dist/agents/skills/shared/design-knowledge/platform-rules/web.yaml +102 -0
- package/dist/agents/skills/shared/design-knowledge/typography/pairings.yaml +274 -0
- package/dist/bin/harness.js +3 -2
- package/dist/{chunk-3U5VZYR7.js → chunk-4WUGOJQ7.js} +6 -3
- package/dist/{chunk-O6NEKDYP.js → chunk-FFIX3QVG.js} +697 -349
- package/dist/chunk-GA6GN5J2.js +6150 -0
- package/dist/dist-C4J67MPP.js +242 -0
- package/dist/dist-N4D4QWFV.js +2809 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.js +3 -2
- package/dist/validate-cross-check-WGXQ7K62.js +7 -0
- package/package.json +12 -8
- package/dist/validate-cross-check-LNIZ7KGZ.js +0 -6
|
@@ -1,3 +1,38 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaselineManager,
|
|
3
|
+
CriticalPathResolver,
|
|
4
|
+
EntropyAnalyzer,
|
|
5
|
+
Err,
|
|
6
|
+
Ok,
|
|
7
|
+
SecurityScanner,
|
|
8
|
+
TypeScriptParser,
|
|
9
|
+
VERSION,
|
|
10
|
+
appendLearning,
|
|
11
|
+
applyFixes,
|
|
12
|
+
archiveStream,
|
|
13
|
+
buildSnapshot,
|
|
14
|
+
checkDocCoverage,
|
|
15
|
+
createFixes,
|
|
16
|
+
createSelfReview,
|
|
17
|
+
createStream,
|
|
18
|
+
defineLayer,
|
|
19
|
+
detectCircularDepsInFiles,
|
|
20
|
+
detectDeadCode,
|
|
21
|
+
detectDocDrift,
|
|
22
|
+
generateSuggestions,
|
|
23
|
+
listStreams,
|
|
24
|
+
loadState,
|
|
25
|
+
loadStreamIndex,
|
|
26
|
+
parseDiff,
|
|
27
|
+
parseSecurityConfig,
|
|
28
|
+
requestPeerReview,
|
|
29
|
+
resolveStreamPath,
|
|
30
|
+
runCIChecks,
|
|
31
|
+
setActiveStream,
|
|
32
|
+
validateAgentsMap,
|
|
33
|
+
validateDependencies,
|
|
34
|
+
validateKnowledgeMap
|
|
35
|
+
} from "./chunk-GA6GN5J2.js";
|
|
1
36
|
import {
|
|
2
37
|
CLIError,
|
|
3
38
|
ExitCode,
|
|
@@ -9,18 +44,14 @@ import {
|
|
|
9
44
|
|
|
10
45
|
// src/index.ts
|
|
11
46
|
import { Command as Command43 } from "commander";
|
|
12
|
-
import { VERSION } from "@harness-engineering/core";
|
|
13
47
|
|
|
14
48
|
// src/commands/validate.ts
|
|
15
49
|
import { Command } from "commander";
|
|
16
50
|
import * as path2 from "path";
|
|
17
|
-
import { Ok as Ok2 } from "@harness-engineering/core";
|
|
18
|
-
import { validateAgentsMap, validateKnowledgeMap } from "@harness-engineering/core";
|
|
19
51
|
|
|
20
52
|
// src/config/loader.ts
|
|
21
53
|
import * as fs from "fs";
|
|
22
54
|
import * as path from "path";
|
|
23
|
-
import { Ok, Err } from "@harness-engineering/core";
|
|
24
55
|
|
|
25
56
|
// src/config/schema.ts
|
|
26
57
|
import { z } from "zod";
|
|
@@ -55,6 +86,23 @@ var PhaseGatesConfigSchema = z.object({
|
|
|
55
86
|
severity: z.enum(["error", "warning"]).default("error"),
|
|
56
87
|
mappings: z.array(PhaseGateMappingSchema).default([{ implPattern: "src/**/*.ts", specPattern: "docs/specs/{feature}.md" }])
|
|
57
88
|
});
|
|
89
|
+
var SecurityConfigSchema = z.object({
|
|
90
|
+
enabled: z.boolean().default(true),
|
|
91
|
+
strict: z.boolean().default(false),
|
|
92
|
+
rules: z.record(z.string(), z.enum(["off", "error", "warning", "info"])).optional(),
|
|
93
|
+
exclude: z.array(z.string()).optional()
|
|
94
|
+
}).passthrough();
|
|
95
|
+
var PerformanceConfigSchema = z.object({
|
|
96
|
+
complexity: z.record(z.unknown()).optional(),
|
|
97
|
+
coupling: z.record(z.unknown()).optional(),
|
|
98
|
+
sizeBudget: z.record(z.unknown()).optional()
|
|
99
|
+
}).passthrough();
|
|
100
|
+
var DesignConfigSchema = z.object({
|
|
101
|
+
strictness: z.enum(["strict", "standard", "permissive"]).default("standard"),
|
|
102
|
+
platforms: z.array(z.enum(["web", "mobile"])).default([]),
|
|
103
|
+
tokenPath: z.string().optional(),
|
|
104
|
+
aestheticIntent: z.string().optional()
|
|
105
|
+
});
|
|
58
106
|
var HarnessConfigSchema = z.object({
|
|
59
107
|
version: z.literal(1),
|
|
60
108
|
name: z.string().optional(),
|
|
@@ -66,12 +114,15 @@ var HarnessConfigSchema = z.object({
|
|
|
66
114
|
docsDir: z.string().default("./docs"),
|
|
67
115
|
agent: AgentConfigSchema.optional(),
|
|
68
116
|
entropy: EntropyConfigSchema.optional(),
|
|
117
|
+
security: SecurityConfigSchema.optional(),
|
|
118
|
+
performance: PerformanceConfigSchema.optional(),
|
|
69
119
|
template: z.object({
|
|
70
120
|
level: z.enum(["basic", "intermediate", "advanced"]),
|
|
71
121
|
framework: z.string().optional(),
|
|
72
122
|
version: z.number()
|
|
73
123
|
}).optional(),
|
|
74
|
-
phaseGates: PhaseGatesConfigSchema.optional()
|
|
124
|
+
phaseGates: PhaseGatesConfigSchema.optional(),
|
|
125
|
+
design: DesignConfigSchema.optional()
|
|
75
126
|
});
|
|
76
127
|
|
|
77
128
|
// src/config/loader.ts
|
|
@@ -241,7 +292,7 @@ async function runValidate(options) {
|
|
|
241
292
|
});
|
|
242
293
|
}
|
|
243
294
|
result.checks.fileStructure = true;
|
|
244
|
-
return
|
|
295
|
+
return Ok(result);
|
|
245
296
|
}
|
|
246
297
|
function createValidateCommand() {
|
|
247
298
|
const command = new Command("validate").description("Run all validation checks").option("--cross-check", "Run cross-artifact consistency validation").action(async (opts, cmd) => {
|
|
@@ -263,7 +314,7 @@ function createValidateCommand() {
|
|
|
263
314
|
process.exit(result.error.exitCode);
|
|
264
315
|
}
|
|
265
316
|
if (opts.crossCheck) {
|
|
266
|
-
const { runCrossCheck: runCrossCheck2 } = await import("./validate-cross-check-
|
|
317
|
+
const { runCrossCheck: runCrossCheck2 } = await import("./validate-cross-check-WGXQ7K62.js");
|
|
267
318
|
const cwd = process.cwd();
|
|
268
319
|
const specsDir = path2.join(cwd, "docs", "specs");
|
|
269
320
|
const plansDir = path2.join(cwd, "docs", "plans");
|
|
@@ -291,13 +342,6 @@ function createValidateCommand() {
|
|
|
291
342
|
// src/commands/check-deps.ts
|
|
292
343
|
import { Command as Command2 } from "commander";
|
|
293
344
|
import * as path3 from "path";
|
|
294
|
-
import { Ok as Ok3 } from "@harness-engineering/core";
|
|
295
|
-
import {
|
|
296
|
-
validateDependencies,
|
|
297
|
-
detectCircularDepsInFiles,
|
|
298
|
-
defineLayer,
|
|
299
|
-
TypeScriptParser
|
|
300
|
-
} from "@harness-engineering/core";
|
|
301
345
|
|
|
302
346
|
// src/utils/files.ts
|
|
303
347
|
import { glob } from "glob";
|
|
@@ -319,7 +363,7 @@ async function runCheckDeps(options) {
|
|
|
319
363
|
circularDeps: []
|
|
320
364
|
};
|
|
321
365
|
if (!config.layers || config.layers.length === 0) {
|
|
322
|
-
return
|
|
366
|
+
return Ok(result);
|
|
323
367
|
}
|
|
324
368
|
const rootDir = path3.resolve(cwd, config.rootDir);
|
|
325
369
|
const parser = new TypeScriptParser();
|
|
@@ -358,7 +402,7 @@ async function runCheckDeps(options) {
|
|
|
358
402
|
}
|
|
359
403
|
}
|
|
360
404
|
}
|
|
361
|
-
return
|
|
405
|
+
return Ok(result);
|
|
362
406
|
}
|
|
363
407
|
function createCheckDepsCommand() {
|
|
364
408
|
const command = new Command2("check-deps").description("Validate dependency layers and detect circular dependencies").action(async (_opts, cmd) => {
|
|
@@ -403,7 +447,6 @@ function createCheckDepsCommand() {
|
|
|
403
447
|
// src/commands/check-perf.ts
|
|
404
448
|
import { Command as Command3 } from "commander";
|
|
405
449
|
import * as path4 from "path";
|
|
406
|
-
import { Ok as Ok4, EntropyAnalyzer } from "@harness-engineering/core";
|
|
407
450
|
async function runCheckPerf(cwd, options) {
|
|
408
451
|
const runAll = !options.structural && !options.size && !options.coupling;
|
|
409
452
|
const analyzer = new EntropyAnalyzer({
|
|
@@ -416,7 +459,7 @@ async function runCheckPerf(cwd, options) {
|
|
|
416
459
|
});
|
|
417
460
|
const analysisResult = await analyzer.analyze();
|
|
418
461
|
if (!analysisResult.ok) {
|
|
419
|
-
return
|
|
462
|
+
return Ok({
|
|
420
463
|
valid: false,
|
|
421
464
|
violations: [
|
|
422
465
|
{
|
|
@@ -477,7 +520,7 @@ async function runCheckPerf(cwd, options) {
|
|
|
477
520
|
const errorCount = violations.filter((v) => v.severity === "error").length;
|
|
478
521
|
const warningCount = violations.filter((v) => v.severity === "warning").length;
|
|
479
522
|
const infoCount = violations.filter((v) => v.severity === "info").length;
|
|
480
|
-
return
|
|
523
|
+
return Ok({
|
|
481
524
|
valid: !hasErrors,
|
|
482
525
|
violations,
|
|
483
526
|
stats: {
|
|
@@ -527,7 +570,6 @@ function createCheckPerfCommand() {
|
|
|
527
570
|
import { Command as Command4 } from "commander";
|
|
528
571
|
import * as path5 from "path";
|
|
529
572
|
import { execSync } from "child_process";
|
|
530
|
-
import { Ok as Ok5, SecurityScanner, parseSecurityConfig } from "@harness-engineering/core";
|
|
531
573
|
var SEVERITY_RANK = {
|
|
532
574
|
error: 3,
|
|
533
575
|
warning: 2,
|
|
@@ -548,10 +590,10 @@ async function runCheckSecurity(cwd, options) {
|
|
|
548
590
|
const projectRoot = path5.resolve(cwd);
|
|
549
591
|
let configData = {};
|
|
550
592
|
try {
|
|
551
|
-
const
|
|
593
|
+
const fs25 = await import("fs");
|
|
552
594
|
const configPath = path5.join(projectRoot, "harness.config.json");
|
|
553
|
-
if (
|
|
554
|
-
const raw =
|
|
595
|
+
if (fs25.existsSync(configPath)) {
|
|
596
|
+
const raw = fs25.readFileSync(configPath, "utf-8");
|
|
555
597
|
const parsed = JSON.parse(raw);
|
|
556
598
|
configData = parsed.security ?? {};
|
|
557
599
|
}
|
|
@@ -579,7 +621,7 @@ async function runCheckSecurity(cwd, options) {
|
|
|
579
621
|
const thresholdRank = SEVERITY_RANK[threshold];
|
|
580
622
|
const filtered = result.findings.filter((f) => SEVERITY_RANK[f.severity] >= thresholdRank);
|
|
581
623
|
const hasErrors = filtered.some((f) => f.severity === "error");
|
|
582
|
-
return
|
|
624
|
+
return Ok({
|
|
583
625
|
valid: !hasErrors,
|
|
584
626
|
findings: filtered,
|
|
585
627
|
stats: {
|
|
@@ -633,13 +675,12 @@ function createCheckSecurityCommand() {
|
|
|
633
675
|
// src/commands/perf.ts
|
|
634
676
|
import { Command as Command5 } from "commander";
|
|
635
677
|
import * as path6 from "path";
|
|
636
|
-
import { BaselineManager, CriticalPathResolver } from "@harness-engineering/core";
|
|
637
678
|
function createPerfCommand() {
|
|
638
679
|
const perf = new Command5("perf").description("Performance benchmark and baseline management");
|
|
639
680
|
perf.command("bench [glob]").description("Run benchmarks via vitest bench").action(async (glob2, _opts, cmd) => {
|
|
640
681
|
const globalOpts = cmd.optsWithGlobals();
|
|
641
682
|
const cwd = process.cwd();
|
|
642
|
-
const { BenchmarkRunner } = await import("
|
|
683
|
+
const { BenchmarkRunner } = await import("./dist-C4J67MPP.js");
|
|
643
684
|
const runner = new BenchmarkRunner();
|
|
644
685
|
const benchFiles = runner.discover(cwd, glob2);
|
|
645
686
|
if (benchFiles.length === 0) {
|
|
@@ -708,7 +749,7 @@ Results (${result.results.length} benchmarks):`);
|
|
|
708
749
|
baselines.command("update").description("Update baselines from latest benchmark run").action(async (_opts, cmd) => {
|
|
709
750
|
const globalOpts = cmd.optsWithGlobals();
|
|
710
751
|
const cwd = process.cwd();
|
|
711
|
-
const { BenchmarkRunner } = await import("
|
|
752
|
+
const { BenchmarkRunner } = await import("./dist-C4J67MPP.js");
|
|
712
753
|
const runner = new BenchmarkRunner();
|
|
713
754
|
const manager = new BaselineManager(cwd);
|
|
714
755
|
logger.info("Running benchmarks to update baselines...");
|
|
@@ -721,8 +762,8 @@ Results (${result.results.length} benchmarks):`);
|
|
|
721
762
|
}
|
|
722
763
|
let commitHash = "unknown";
|
|
723
764
|
try {
|
|
724
|
-
const { execSync:
|
|
725
|
-
commitHash =
|
|
765
|
+
const { execSync: execSync3 } = await import("child_process");
|
|
766
|
+
commitHash = execSync3("git rev-parse --short HEAD", { cwd, encoding: "utf-8" }).trim();
|
|
726
767
|
} catch {
|
|
727
768
|
}
|
|
728
769
|
manager.save(benchResult.results, commitHash);
|
|
@@ -736,8 +777,8 @@ Results (${result.results.length} benchmarks):`);
|
|
|
736
777
|
perf.command("report").description("Full performance report with metrics, trends, and hotspots").action(async (_opts, cmd) => {
|
|
737
778
|
const globalOpts = cmd.optsWithGlobals();
|
|
738
779
|
const cwd = process.cwd();
|
|
739
|
-
const { EntropyAnalyzer:
|
|
740
|
-
const analyzer = new
|
|
780
|
+
const { EntropyAnalyzer: EntropyAnalyzer2 } = await import("./dist-C4J67MPP.js");
|
|
781
|
+
const analyzer = new EntropyAnalyzer2({
|
|
741
782
|
rootDir: path6.resolve(cwd),
|
|
742
783
|
analyze: { complexity: true, coupling: true }
|
|
743
784
|
});
|
|
@@ -797,8 +838,6 @@ Results (${result.results.length} benchmarks):`);
|
|
|
797
838
|
// src/commands/check-docs.ts
|
|
798
839
|
import { Command as Command6 } from "commander";
|
|
799
840
|
import * as path7 from "path";
|
|
800
|
-
import { Ok as Ok6, Err as Err2 } from "@harness-engineering/core";
|
|
801
|
-
import { checkDocCoverage, validateKnowledgeMap as validateKnowledgeMap2 } from "@harness-engineering/core";
|
|
802
841
|
async function runCheckDocs(options) {
|
|
803
842
|
const cwd = options.cwd ?? process.cwd();
|
|
804
843
|
const minCoverage = options.minCoverage ?? 80;
|
|
@@ -815,14 +854,14 @@ async function runCheckDocs(options) {
|
|
|
815
854
|
excludePatterns: ["**/*.test.ts", "**/*.spec.ts", "**/node_modules/**"]
|
|
816
855
|
});
|
|
817
856
|
if (!coverageResult.ok) {
|
|
818
|
-
return
|
|
857
|
+
return Err(
|
|
819
858
|
new CLIError(
|
|
820
859
|
`Documentation coverage check failed: ${coverageResult.error.message}`,
|
|
821
860
|
ExitCode.ERROR
|
|
822
861
|
)
|
|
823
862
|
);
|
|
824
863
|
}
|
|
825
|
-
const knowledgeResult = await
|
|
864
|
+
const knowledgeResult = await validateKnowledgeMap(cwd);
|
|
826
865
|
let brokenLinks = [];
|
|
827
866
|
if (knowledgeResult.ok) {
|
|
828
867
|
brokenLinks = knowledgeResult.value.brokenLinks.map((b) => b.path);
|
|
@@ -837,7 +876,7 @@ async function runCheckDocs(options) {
|
|
|
837
876
|
undocumented: coverageResult.value.undocumented,
|
|
838
877
|
brokenLinks
|
|
839
878
|
};
|
|
840
|
-
return
|
|
879
|
+
return Ok(result);
|
|
841
880
|
}
|
|
842
881
|
function createCheckDocsCommand() {
|
|
843
882
|
const command = new Command6("check-docs").description("Check documentation coverage").option("--min-coverage <percent>", "Minimum coverage percentage", "80").action(async (opts, cmd) => {
|
|
@@ -896,13 +935,11 @@ import { Command as Command8 } from "commander";
|
|
|
896
935
|
import chalk3 from "chalk";
|
|
897
936
|
import * as fs5 from "fs";
|
|
898
937
|
import * as path11 from "path";
|
|
899
|
-
import { Ok as Ok8, Err as Err4 } from "@harness-engineering/core";
|
|
900
938
|
|
|
901
939
|
// src/templates/engine.ts
|
|
902
940
|
import * as fs2 from "fs";
|
|
903
941
|
import * as path8 from "path";
|
|
904
942
|
import Handlebars from "handlebars";
|
|
905
|
-
import { Ok as Ok7, Err as Err3 } from "@harness-engineering/core";
|
|
906
943
|
|
|
907
944
|
// src/templates/schema.ts
|
|
908
945
|
import { z as z2 } from "zod";
|
|
@@ -976,9 +1013,9 @@ var TemplateEngine = class {
|
|
|
976
1013
|
const parsed = TemplateMetadataSchema.safeParse(raw);
|
|
977
1014
|
if (parsed.success) templates.push(parsed.data);
|
|
978
1015
|
}
|
|
979
|
-
return
|
|
1016
|
+
return Ok(templates);
|
|
980
1017
|
} catch (error) {
|
|
981
|
-
return
|
|
1018
|
+
return Err(
|
|
982
1019
|
new Error(
|
|
983
1020
|
`Failed to list templates: ${error instanceof Error ? error.message : String(error)}`
|
|
984
1021
|
)
|
|
@@ -987,12 +1024,12 @@ var TemplateEngine = class {
|
|
|
987
1024
|
}
|
|
988
1025
|
resolveTemplate(level, framework) {
|
|
989
1026
|
const levelDir = this.findTemplateDir(level, "level");
|
|
990
|
-
if (!levelDir) return
|
|
1027
|
+
if (!levelDir) return Err(new Error(`Template not found for level: ${level}`));
|
|
991
1028
|
const metaPath = path8.join(levelDir, "template.json");
|
|
992
1029
|
const metaRaw = JSON.parse(fs2.readFileSync(metaPath, "utf-8"));
|
|
993
1030
|
const metaResult = TemplateMetadataSchema.safeParse(metaRaw);
|
|
994
1031
|
if (!metaResult.success)
|
|
995
|
-
return
|
|
1032
|
+
return Err(new Error(`Invalid template.json in ${level}: ${metaResult.error.message}`));
|
|
996
1033
|
const metadata = metaResult.data;
|
|
997
1034
|
let files = [];
|
|
998
1035
|
if (metadata.extends) {
|
|
@@ -1004,7 +1041,7 @@ var TemplateEngine = class {
|
|
|
1004
1041
|
let overlayMetadata;
|
|
1005
1042
|
if (framework) {
|
|
1006
1043
|
const frameworkDir = this.findTemplateDir(framework, "framework");
|
|
1007
|
-
if (!frameworkDir) return
|
|
1044
|
+
if (!frameworkDir) return Err(new Error(`Framework template not found: ${framework}`));
|
|
1008
1045
|
const fMetaPath = path8.join(frameworkDir, "template.json");
|
|
1009
1046
|
const fMetaRaw = JSON.parse(fs2.readFileSync(fMetaPath, "utf-8"));
|
|
1010
1047
|
const fMetaResult = TemplateMetadataSchema.safeParse(fMetaRaw);
|
|
@@ -1015,7 +1052,7 @@ var TemplateEngine = class {
|
|
|
1015
1052
|
files = files.filter((f) => f.relativePath !== "template.json");
|
|
1016
1053
|
const resolved = { metadata, files };
|
|
1017
1054
|
if (overlayMetadata !== void 0) resolved.overlayMetadata = overlayMetadata;
|
|
1018
|
-
return
|
|
1055
|
+
return Ok(resolved);
|
|
1019
1056
|
}
|
|
1020
1057
|
render(template, context) {
|
|
1021
1058
|
const rendered = [];
|
|
@@ -1035,7 +1072,7 @@ var TemplateEngine = class {
|
|
|
1035
1072
|
}
|
|
1036
1073
|
} catch (error) {
|
|
1037
1074
|
const msg = error instanceof Error ? error.message : String(error);
|
|
1038
|
-
return
|
|
1075
|
+
return Err(
|
|
1039
1076
|
new Error(
|
|
1040
1077
|
`Template render failed in ${file.sourceTemplate}/${file.relativePath}: ${msg}`
|
|
1041
1078
|
)
|
|
@@ -1047,7 +1084,7 @@ var TemplateEngine = class {
|
|
|
1047
1084
|
rendered.push({ relativePath: file.relativePath, content });
|
|
1048
1085
|
} catch (error) {
|
|
1049
1086
|
const msg = error instanceof Error ? error.message : String(error);
|
|
1050
|
-
return
|
|
1087
|
+
return Err(
|
|
1051
1088
|
new Error(
|
|
1052
1089
|
`Template render failed in ${file.sourceTemplate}/${file.relativePath}: ${msg}`
|
|
1053
1090
|
)
|
|
@@ -1065,9 +1102,9 @@ var TemplateEngine = class {
|
|
|
1065
1102
|
}
|
|
1066
1103
|
} catch (error) {
|
|
1067
1104
|
const msg = error instanceof Error ? error.message : String(error);
|
|
1068
|
-
return
|
|
1105
|
+
return Err(new Error(`JSON merge failed: ${msg}`));
|
|
1069
1106
|
}
|
|
1070
|
-
return
|
|
1107
|
+
return Ok({ files: rendered });
|
|
1071
1108
|
}
|
|
1072
1109
|
write(files, targetDir, options) {
|
|
1073
1110
|
try {
|
|
@@ -1080,9 +1117,9 @@ var TemplateEngine = class {
|
|
|
1080
1117
|
fs2.writeFileSync(targetPath, file.content);
|
|
1081
1118
|
written.push(file.relativePath);
|
|
1082
1119
|
}
|
|
1083
|
-
return
|
|
1120
|
+
return Ok(written);
|
|
1084
1121
|
} catch (error) {
|
|
1085
|
-
return
|
|
1122
|
+
return Err(
|
|
1086
1123
|
new Error(
|
|
1087
1124
|
`Failed to write files: ${error instanceof Error ? error.message : String(error)}`
|
|
1088
1125
|
)
|
|
@@ -1213,8 +1250,7 @@ import * as path10 from "path";
|
|
|
1213
1250
|
import * as os from "os";
|
|
1214
1251
|
import chalk2 from "chalk";
|
|
1215
1252
|
var HARNESS_MCP_ENTRY = {
|
|
1216
|
-
command: "
|
|
1217
|
-
args: ["@harness-engineering/mcp-server"]
|
|
1253
|
+
command: "harness-mcp"
|
|
1218
1254
|
};
|
|
1219
1255
|
function readJsonFile(filePath) {
|
|
1220
1256
|
if (!fs4.existsSync(filePath)) return null;
|
|
@@ -1326,7 +1362,7 @@ async function runInit(options) {
|
|
|
1326
1362
|
const force = options.force ?? false;
|
|
1327
1363
|
const configPath = path11.join(cwd, "harness.config.json");
|
|
1328
1364
|
if (!force && fs5.existsSync(configPath)) {
|
|
1329
|
-
return
|
|
1365
|
+
return Err(
|
|
1330
1366
|
new CLIError("Project already initialized. Use --force to overwrite.", ExitCode.ERROR)
|
|
1331
1367
|
);
|
|
1332
1368
|
}
|
|
@@ -1334,7 +1370,7 @@ async function runInit(options) {
|
|
|
1334
1370
|
const engine = new TemplateEngine(templatesDir);
|
|
1335
1371
|
const resolveResult = engine.resolveTemplate(level, options.framework);
|
|
1336
1372
|
if (!resolveResult.ok) {
|
|
1337
|
-
return
|
|
1373
|
+
return Err(new CLIError(resolveResult.error.message, ExitCode.ERROR));
|
|
1338
1374
|
}
|
|
1339
1375
|
const renderResult = engine.render(resolveResult.value, {
|
|
1340
1376
|
projectName: name,
|
|
@@ -1342,13 +1378,13 @@ async function runInit(options) {
|
|
|
1342
1378
|
...options.framework !== void 0 && { framework: options.framework }
|
|
1343
1379
|
});
|
|
1344
1380
|
if (!renderResult.ok) {
|
|
1345
|
-
return
|
|
1381
|
+
return Err(new CLIError(renderResult.error.message, ExitCode.ERROR));
|
|
1346
1382
|
}
|
|
1347
1383
|
const writeResult = engine.write(renderResult.value, cwd, { overwrite: force });
|
|
1348
1384
|
if (!writeResult.ok) {
|
|
1349
|
-
return
|
|
1385
|
+
return Err(new CLIError(writeResult.error.message, ExitCode.ERROR));
|
|
1350
1386
|
}
|
|
1351
|
-
return
|
|
1387
|
+
return Ok({ filesCreated: writeResult.value });
|
|
1352
1388
|
}
|
|
1353
1389
|
function createInitCommand() {
|
|
1354
1390
|
const command = new Command8("init").description("Initialize a new harness-engineering project").option("-n, --name <name>", "Project name").option("-l, --level <level>", "Adoption level (basic, intermediate, advanced)", "basic").option("--framework <framework>", "Framework overlay (nextjs)").option("-f, --force", "Overwrite existing files").option("-y, --yes", "Use defaults without prompting").action(async (opts, cmd) => {
|
|
@@ -1395,13 +1431,12 @@ function createInitCommand() {
|
|
|
1395
1431
|
// src/commands/cleanup.ts
|
|
1396
1432
|
import { Command as Command9 } from "commander";
|
|
1397
1433
|
import * as path12 from "path";
|
|
1398
|
-
import { Ok as Ok9, Err as Err5, EntropyAnalyzer as EntropyAnalyzer2 } from "@harness-engineering/core";
|
|
1399
1434
|
async function runCleanup(options) {
|
|
1400
1435
|
const cwd = options.cwd ?? process.cwd();
|
|
1401
1436
|
const type = options.type ?? "all";
|
|
1402
1437
|
const configResult = resolveConfig(options.configPath);
|
|
1403
1438
|
if (!configResult.ok) {
|
|
1404
|
-
return
|
|
1439
|
+
return Err(configResult.error);
|
|
1405
1440
|
}
|
|
1406
1441
|
const config = configResult.value;
|
|
1407
1442
|
const result = {
|
|
@@ -1423,10 +1458,10 @@ async function runCleanup(options) {
|
|
|
1423
1458
|
},
|
|
1424
1459
|
exclude: config.entropy?.excludePatterns ?? ["**/node_modules/**", "**/*.test.ts"]
|
|
1425
1460
|
};
|
|
1426
|
-
const analyzer = new
|
|
1461
|
+
const analyzer = new EntropyAnalyzer(entropyConfig);
|
|
1427
1462
|
const analysisResult = await analyzer.analyze();
|
|
1428
1463
|
if (!analysisResult.ok) {
|
|
1429
|
-
return
|
|
1464
|
+
return Err(
|
|
1430
1465
|
new CLIError(`Entropy analysis failed: ${analysisResult.error.message}`, ExitCode.ERROR)
|
|
1431
1466
|
);
|
|
1432
1467
|
}
|
|
@@ -1451,7 +1486,7 @@ async function runCleanup(options) {
|
|
|
1451
1486
|
}));
|
|
1452
1487
|
}
|
|
1453
1488
|
result.totalIssues = result.driftIssues.length + result.deadCode.length + result.patternViolations.length;
|
|
1454
|
-
return
|
|
1489
|
+
return Ok(result);
|
|
1455
1490
|
}
|
|
1456
1491
|
function createCleanupCommand() {
|
|
1457
1492
|
const command = new Command9("cleanup").description("Detect entropy issues (doc drift, dead code, patterns)").option("-t, --type <type>", "Issue type: drift, dead-code, patterns, all", "all").action(async (opts, cmd) => {
|
|
@@ -1516,22 +1551,12 @@ function createCleanupCommand() {
|
|
|
1516
1551
|
// src/commands/fix-drift.ts
|
|
1517
1552
|
import { Command as Command10 } from "commander";
|
|
1518
1553
|
import * as path13 from "path";
|
|
1519
|
-
import {
|
|
1520
|
-
Ok as Ok10,
|
|
1521
|
-
Err as Err6,
|
|
1522
|
-
buildSnapshot,
|
|
1523
|
-
detectDocDrift,
|
|
1524
|
-
detectDeadCode,
|
|
1525
|
-
createFixes,
|
|
1526
|
-
applyFixes,
|
|
1527
|
-
generateSuggestions
|
|
1528
|
-
} from "@harness-engineering/core";
|
|
1529
1554
|
async function runFixDrift(options) {
|
|
1530
1555
|
const cwd = options.cwd ?? process.cwd();
|
|
1531
1556
|
const dryRun = options.dryRun !== false;
|
|
1532
1557
|
const configResult = resolveConfig(options.configPath);
|
|
1533
1558
|
if (!configResult.ok) {
|
|
1534
|
-
return
|
|
1559
|
+
return Err(configResult.error);
|
|
1535
1560
|
}
|
|
1536
1561
|
const config = configResult.value;
|
|
1537
1562
|
const rootDir = path13.resolve(cwd, config.rootDir);
|
|
@@ -1549,21 +1574,21 @@ async function runFixDrift(options) {
|
|
|
1549
1574
|
};
|
|
1550
1575
|
const snapshotResult = await buildSnapshot(entropyConfig);
|
|
1551
1576
|
if (!snapshotResult.ok) {
|
|
1552
|
-
return
|
|
1577
|
+
return Err(
|
|
1553
1578
|
new CLIError(`Failed to build snapshot: ${snapshotResult.error.message}`, ExitCode.ERROR)
|
|
1554
1579
|
);
|
|
1555
1580
|
}
|
|
1556
1581
|
const snapshot = snapshotResult.value;
|
|
1557
1582
|
const driftResult = await detectDocDrift(snapshot);
|
|
1558
1583
|
if (!driftResult.ok) {
|
|
1559
|
-
return
|
|
1584
|
+
return Err(
|
|
1560
1585
|
new CLIError(`Failed to detect drift: ${driftResult.error.message}`, ExitCode.ERROR)
|
|
1561
1586
|
);
|
|
1562
1587
|
}
|
|
1563
1588
|
const driftReport = driftResult.value;
|
|
1564
1589
|
const deadCodeResult = await detectDeadCode(snapshot);
|
|
1565
1590
|
if (!deadCodeResult.ok) {
|
|
1566
|
-
return
|
|
1591
|
+
return Err(
|
|
1567
1592
|
new CLIError(`Failed to detect dead code: ${deadCodeResult.error.message}`, ExitCode.ERROR)
|
|
1568
1593
|
);
|
|
1569
1594
|
}
|
|
@@ -1573,7 +1598,7 @@ async function runFixDrift(options) {
|
|
|
1573
1598
|
if (!dryRun && fixes.length > 0) {
|
|
1574
1599
|
const applyResult = await applyFixes(fixes, { dryRun: false });
|
|
1575
1600
|
if (!applyResult.ok) {
|
|
1576
|
-
return
|
|
1601
|
+
return Err(
|
|
1577
1602
|
new CLIError(`Failed to apply fixes: ${applyResult.error.message}`, ExitCode.ERROR)
|
|
1578
1603
|
);
|
|
1579
1604
|
}
|
|
@@ -1622,7 +1647,7 @@ async function runFixDrift(options) {
|
|
|
1622
1647
|
fixes: appliedFixes,
|
|
1623
1648
|
suggestions
|
|
1624
1649
|
};
|
|
1625
|
-
return
|
|
1650
|
+
return Ok(result);
|
|
1626
1651
|
}
|
|
1627
1652
|
function createFixDriftCommand() {
|
|
1628
1653
|
const command = new Command10("fix-drift").description("Auto-fix entropy issues (doc drift, dead code)").option("--no-dry-run", "Actually apply fixes (default is dry-run mode)").action(async (opts, cmd) => {
|
|
@@ -1691,14 +1716,11 @@ import { Command as Command13 } from "commander";
|
|
|
1691
1716
|
import { Command as Command11 } from "commander";
|
|
1692
1717
|
import * as path17 from "path";
|
|
1693
1718
|
import * as childProcess from "child_process";
|
|
1694
|
-
import { Ok as Ok12, Err as Err8 } from "@harness-engineering/core";
|
|
1695
|
-
import { requestPeerReview } from "@harness-engineering/core";
|
|
1696
1719
|
|
|
1697
1720
|
// src/persona/loader.ts
|
|
1698
1721
|
import * as fs6 from "fs";
|
|
1699
1722
|
import * as path14 from "path";
|
|
1700
1723
|
import YAML from "yaml";
|
|
1701
|
-
import { Ok as Ok11, Err as Err7 } from "@harness-engineering/core";
|
|
1702
1724
|
|
|
1703
1725
|
// src/persona/schema.ts
|
|
1704
1726
|
import { z as z3 } from "zod";
|
|
@@ -1784,24 +1806,24 @@ function normalizePersona(raw) {
|
|
|
1784
1806
|
function loadPersona(filePath) {
|
|
1785
1807
|
try {
|
|
1786
1808
|
if (!fs6.existsSync(filePath)) {
|
|
1787
|
-
return
|
|
1809
|
+
return Err(new Error(`Persona file not found: ${filePath}`));
|
|
1788
1810
|
}
|
|
1789
1811
|
const raw = fs6.readFileSync(filePath, "utf-8");
|
|
1790
1812
|
const parsed = YAML.parse(raw);
|
|
1791
1813
|
const result = PersonaSchema.safeParse(parsed);
|
|
1792
1814
|
if (!result.success) {
|
|
1793
|
-
return
|
|
1815
|
+
return Err(new Error(`Invalid persona ${filePath}: ${result.error.message}`));
|
|
1794
1816
|
}
|
|
1795
|
-
return
|
|
1817
|
+
return Ok(normalizePersona(result.data));
|
|
1796
1818
|
} catch (error) {
|
|
1797
|
-
return
|
|
1819
|
+
return Err(
|
|
1798
1820
|
new Error(`Failed to load persona: ${error instanceof Error ? error.message : String(error)}`)
|
|
1799
1821
|
);
|
|
1800
1822
|
}
|
|
1801
1823
|
}
|
|
1802
1824
|
function listPersonas(dir) {
|
|
1803
1825
|
try {
|
|
1804
|
-
if (!fs6.existsSync(dir)) return
|
|
1826
|
+
if (!fs6.existsSync(dir)) return Ok([]);
|
|
1805
1827
|
const entries = fs6.readdirSync(dir).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
|
|
1806
1828
|
const personas = [];
|
|
1807
1829
|
for (const entry of entries) {
|
|
@@ -1811,9 +1833,9 @@ function listPersonas(dir) {
|
|
|
1811
1833
|
personas.push({ name: result.value.name, description: result.value.description, filePath });
|
|
1812
1834
|
}
|
|
1813
1835
|
}
|
|
1814
|
-
return
|
|
1836
|
+
return Ok(personas);
|
|
1815
1837
|
} catch (error) {
|
|
1816
|
-
return
|
|
1838
|
+
return Err(
|
|
1817
1839
|
new Error(
|
|
1818
1840
|
`Failed to list personas: ${error instanceof Error ? error.message : String(error)}`
|
|
1819
1841
|
)
|
|
@@ -1904,8 +1926,8 @@ async function runPersona(persona, context) {
|
|
|
1904
1926
|
const result = await Promise.race([
|
|
1905
1927
|
context.commandExecutor(step.command),
|
|
1906
1928
|
new Promise(
|
|
1907
|
-
(
|
|
1908
|
-
() =>
|
|
1929
|
+
(resolve24) => setTimeout(
|
|
1930
|
+
() => resolve24({
|
|
1909
1931
|
ok: false,
|
|
1910
1932
|
error: new Error(TIMEOUT_ERROR_MESSAGE)
|
|
1911
1933
|
}),
|
|
@@ -1960,7 +1982,7 @@ async function runPersona(persona, context) {
|
|
|
1960
1982
|
const result = await Promise.race([
|
|
1961
1983
|
context.skillExecutor(step.skill, skillContext),
|
|
1962
1984
|
new Promise(
|
|
1963
|
-
(
|
|
1985
|
+
(resolve24) => setTimeout(() => resolve24(SKILL_TIMEOUT_RESULT), remainingTime)
|
|
1964
1986
|
)
|
|
1965
1987
|
]);
|
|
1966
1988
|
const durationMs = Date.now() - stepStart;
|
|
@@ -2142,7 +2164,7 @@ async function runAgentTask(task, options) {
|
|
|
2142
2164
|
};
|
|
2143
2165
|
const agentType = agentTypeMap[task];
|
|
2144
2166
|
if (!agentType) {
|
|
2145
|
-
return
|
|
2167
|
+
return Err(
|
|
2146
2168
|
new CLIError(
|
|
2147
2169
|
`Unknown task: ${task}. Available: ${Object.keys(agentTypeMap).join(", ")}`,
|
|
2148
2170
|
ExitCode.ERROR
|
|
@@ -2162,10 +2184,10 @@ async function runAgentTask(task, options) {
|
|
|
2162
2184
|
{ timeout }
|
|
2163
2185
|
);
|
|
2164
2186
|
if (!reviewResult.ok) {
|
|
2165
|
-
return
|
|
2187
|
+
return Err(new CLIError(`Agent task failed: ${reviewResult.error.message}`, ExitCode.ERROR));
|
|
2166
2188
|
}
|
|
2167
2189
|
const review = reviewResult.value;
|
|
2168
|
-
return
|
|
2190
|
+
return Ok({
|
|
2169
2191
|
success: review.approved,
|
|
2170
2192
|
output: review.approved ? `Agent task '${task}' completed successfully` : `Agent task '${task}' found issues:
|
|
2171
2193
|
${review.comments.map((c) => ` - ${c.message}`).join("\n")}`
|
|
@@ -2197,13 +2219,13 @@ function createRunCommand() {
|
|
|
2197
2219
|
const trigger = opts.trigger === "auto" ? "auto" : VALID_TRIGGERS.has(opts.trigger) ? opts.trigger : "manual";
|
|
2198
2220
|
const commandExecutor = async (command) => {
|
|
2199
2221
|
if (!ALLOWED_PERSONA_COMMANDS.has(command)) {
|
|
2200
|
-
return
|
|
2222
|
+
return Err(new Error(`Unknown harness command: ${command}`));
|
|
2201
2223
|
}
|
|
2202
2224
|
try {
|
|
2203
2225
|
childProcess.execFileSync("npx", ["harness", command], { stdio: "inherit" });
|
|
2204
|
-
return
|
|
2226
|
+
return Ok(null);
|
|
2205
2227
|
} catch (error) {
|
|
2206
|
-
return
|
|
2228
|
+
return Err(new Error(error instanceof Error ? error.message : String(error)));
|
|
2207
2229
|
}
|
|
2208
2230
|
};
|
|
2209
2231
|
const report = await runPersona(persona, {
|
|
@@ -2245,8 +2267,6 @@ function createRunCommand() {
|
|
|
2245
2267
|
// src/commands/agent/review.ts
|
|
2246
2268
|
import { Command as Command12 } from "commander";
|
|
2247
2269
|
import { execSync as execSync2 } from "child_process";
|
|
2248
|
-
import { Ok as Ok13, Err as Err9 } from "@harness-engineering/core";
|
|
2249
|
-
import { createSelfReview, parseDiff } from "@harness-engineering/core";
|
|
2250
2270
|
async function runAgentReview(options) {
|
|
2251
2271
|
const configResult = resolveConfig(options.configPath);
|
|
2252
2272
|
if (!configResult.ok) {
|
|
@@ -2260,17 +2280,17 @@ async function runAgentReview(options) {
|
|
|
2260
2280
|
diff = execSync2("git diff", { encoding: "utf-8" });
|
|
2261
2281
|
}
|
|
2262
2282
|
} catch {
|
|
2263
|
-
return
|
|
2283
|
+
return Err(new CLIError("Failed to get git diff", ExitCode.ERROR));
|
|
2264
2284
|
}
|
|
2265
2285
|
if (!diff) {
|
|
2266
|
-
return
|
|
2286
|
+
return Ok({
|
|
2267
2287
|
passed: true,
|
|
2268
2288
|
checklist: [{ check: "No changes to review", passed: true }]
|
|
2269
2289
|
});
|
|
2270
2290
|
}
|
|
2271
2291
|
const parsedDiffResult = parseDiff(diff);
|
|
2272
2292
|
if (!parsedDiffResult.ok) {
|
|
2273
|
-
return
|
|
2293
|
+
return Err(new CLIError(parsedDiffResult.error.message, ExitCode.ERROR));
|
|
2274
2294
|
}
|
|
2275
2295
|
const codeChanges = parsedDiffResult.value;
|
|
2276
2296
|
const review = await createSelfReview(codeChanges, {
|
|
@@ -2281,9 +2301,9 @@ async function runAgentReview(options) {
|
|
|
2281
2301
|
}
|
|
2282
2302
|
});
|
|
2283
2303
|
if (!review.ok) {
|
|
2284
|
-
return
|
|
2304
|
+
return Err(new CLIError(review.error.message, ExitCode.ERROR));
|
|
2285
2305
|
}
|
|
2286
|
-
return
|
|
2306
|
+
return Ok({
|
|
2287
2307
|
passed: review.value.passed,
|
|
2288
2308
|
checklist: review.value.items.map((item) => ({
|
|
2289
2309
|
check: item.check,
|
|
@@ -2339,7 +2359,6 @@ function createAgentCommand() {
|
|
|
2339
2359
|
import { Command as Command14 } from "commander";
|
|
2340
2360
|
import * as fs9 from "fs";
|
|
2341
2361
|
import * as path18 from "path";
|
|
2342
|
-
import { Ok as Ok14, Err as Err10 } from "@harness-engineering/core";
|
|
2343
2362
|
var LAYER_INDEX_TEMPLATE = (name) => `// ${name} layer
|
|
2344
2363
|
// Add your ${name} exports here
|
|
2345
2364
|
|
|
@@ -2369,7 +2388,7 @@ async function runAdd(componentType, name, options) {
|
|
|
2369
2388
|
const cwd = options.cwd ?? process.cwd();
|
|
2370
2389
|
const NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
|
|
2371
2390
|
if (!name || !NAME_PATTERN.test(name)) {
|
|
2372
|
-
return
|
|
2391
|
+
return Err(
|
|
2373
2392
|
new CLIError(
|
|
2374
2393
|
"Invalid name. Must start with a letter and contain only alphanumeric characters, hyphens, and underscores.",
|
|
2375
2394
|
ExitCode.ERROR
|
|
@@ -2398,7 +2417,7 @@ async function runAdd(componentType, name, options) {
|
|
|
2398
2417
|
case "module": {
|
|
2399
2418
|
const modulePath = path18.join(cwd, "src", `${name}.ts`);
|
|
2400
2419
|
if (fs9.existsSync(modulePath)) {
|
|
2401
|
-
return
|
|
2420
|
+
return Err(new CLIError(`Module ${name} already exists`, ExitCode.ERROR));
|
|
2402
2421
|
}
|
|
2403
2422
|
fs9.writeFileSync(modulePath, MODULE_TEMPLATE(name));
|
|
2404
2423
|
created.push(`src/${name}.ts`);
|
|
@@ -2411,7 +2430,7 @@ async function runAdd(componentType, name, options) {
|
|
|
2411
2430
|
}
|
|
2412
2431
|
const docPath = path18.join(docsDir, `${name}.md`);
|
|
2413
2432
|
if (fs9.existsSync(docPath)) {
|
|
2414
|
-
return
|
|
2433
|
+
return Err(new CLIError(`Doc ${name} already exists`, ExitCode.ERROR));
|
|
2415
2434
|
}
|
|
2416
2435
|
fs9.writeFileSync(docPath, DOC_TEMPLATE(name));
|
|
2417
2436
|
created.push(`docs/${name}.md`);
|
|
@@ -2435,7 +2454,7 @@ async function runAdd(componentType, name, options) {
|
|
|
2435
2454
|
}
|
|
2436
2455
|
const personaPath = path18.join(personasDir, `${name}.yaml`);
|
|
2437
2456
|
if (fs9.existsSync(personaPath)) {
|
|
2438
|
-
return
|
|
2457
|
+
return Err(new CLIError(`Persona ${name} already exists`, ExitCode.ERROR));
|
|
2439
2458
|
}
|
|
2440
2459
|
fs9.writeFileSync(
|
|
2441
2460
|
personaPath,
|
|
@@ -2451,7 +2470,7 @@ focus_areas: []
|
|
|
2451
2470
|
}
|
|
2452
2471
|
default: {
|
|
2453
2472
|
const _exhaustive = componentType;
|
|
2454
|
-
return
|
|
2473
|
+
return Err(
|
|
2455
2474
|
new CLIError(
|
|
2456
2475
|
`Unknown component type: ${String(_exhaustive)}. Use: layer, module, doc, skill, persona`,
|
|
2457
2476
|
ExitCode.ERROR
|
|
@@ -2459,9 +2478,9 @@ focus_areas: []
|
|
|
2459
2478
|
);
|
|
2460
2479
|
}
|
|
2461
2480
|
}
|
|
2462
|
-
return
|
|
2481
|
+
return Ok({ created });
|
|
2463
2482
|
} catch (error) {
|
|
2464
|
-
return
|
|
2483
|
+
return Err(
|
|
2465
2484
|
new CLIError(
|
|
2466
2485
|
`Failed to add ${componentType}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
2467
2486
|
ExitCode.ERROR
|
|
@@ -2495,7 +2514,346 @@ import { Command as Command17 } from "commander";
|
|
|
2495
2514
|
|
|
2496
2515
|
// src/commands/linter/generate.ts
|
|
2497
2516
|
import { Command as Command15 } from "commander";
|
|
2498
|
-
|
|
2517
|
+
|
|
2518
|
+
// ../linter-gen/dist/generator/orchestrator.js
|
|
2519
|
+
import * as fs12 from "fs/promises";
|
|
2520
|
+
import * as path21 from "path";
|
|
2521
|
+
|
|
2522
|
+
// ../linter-gen/dist/parser/config-parser.js
|
|
2523
|
+
import * as fs10 from "fs/promises";
|
|
2524
|
+
import * as yaml from "yaml";
|
|
2525
|
+
|
|
2526
|
+
// ../linter-gen/dist/schema/linter-config.js
|
|
2527
|
+
import { z as z4 } from "zod";
|
|
2528
|
+
var RuleConfigSchema = z4.object({
|
|
2529
|
+
/** Rule name in kebab-case (e.g., 'no-ui-in-services') */
|
|
2530
|
+
name: z4.string().regex(/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/, "Rule name must be kebab-case"),
|
|
2531
|
+
/** Rule type - determines which template to use */
|
|
2532
|
+
type: z4.string().min(1),
|
|
2533
|
+
/** ESLint severity level */
|
|
2534
|
+
severity: z4.enum(["error", "warn", "off"]).default("error"),
|
|
2535
|
+
/** Template-specific configuration */
|
|
2536
|
+
config: z4.record(z4.unknown())
|
|
2537
|
+
});
|
|
2538
|
+
var LinterConfigSchema = z4.object({
|
|
2539
|
+
/** Config version - currently only 1 is supported */
|
|
2540
|
+
version: z4.literal(1),
|
|
2541
|
+
/** Output directory for generated rules */
|
|
2542
|
+
output: z4.string().min(1),
|
|
2543
|
+
/** Optional explicit template path mappings (type → path) */
|
|
2544
|
+
templates: z4.record(z4.string()).optional(),
|
|
2545
|
+
/** Rules to generate */
|
|
2546
|
+
rules: z4.array(RuleConfigSchema).min(1, "At least one rule is required")
|
|
2547
|
+
});
|
|
2548
|
+
|
|
2549
|
+
// ../linter-gen/dist/parser/config-parser.js
|
|
2550
|
+
var ParseError = class extends Error {
|
|
2551
|
+
code;
|
|
2552
|
+
cause;
|
|
2553
|
+
constructor(message, code, cause) {
|
|
2554
|
+
super(message);
|
|
2555
|
+
this.code = code;
|
|
2556
|
+
this.cause = cause;
|
|
2557
|
+
this.name = "ParseError";
|
|
2558
|
+
}
|
|
2559
|
+
};
|
|
2560
|
+
async function parseConfig(configPath) {
|
|
2561
|
+
let content;
|
|
2562
|
+
try {
|
|
2563
|
+
content = await fs10.readFile(configPath, "utf-8");
|
|
2564
|
+
} catch (err) {
|
|
2565
|
+
if (err.code === "ENOENT") {
|
|
2566
|
+
return {
|
|
2567
|
+
success: false,
|
|
2568
|
+
error: new ParseError(`Config file not found: ${configPath}`, "FILE_NOT_FOUND", err)
|
|
2569
|
+
};
|
|
2570
|
+
}
|
|
2571
|
+
return {
|
|
2572
|
+
success: false,
|
|
2573
|
+
error: new ParseError(`Failed to read config file: ${configPath}`, "FILE_READ_ERROR", err)
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2576
|
+
let parsed;
|
|
2577
|
+
try {
|
|
2578
|
+
parsed = yaml.parse(content);
|
|
2579
|
+
} catch (err) {
|
|
2580
|
+
return {
|
|
2581
|
+
success: false,
|
|
2582
|
+
error: new ParseError(`Invalid YAML syntax in ${configPath}: ${err.message}`, "YAML_PARSE_ERROR", err)
|
|
2583
|
+
};
|
|
2584
|
+
}
|
|
2585
|
+
const result = LinterConfigSchema.safeParse(parsed);
|
|
2586
|
+
if (!result.success) {
|
|
2587
|
+
const issues = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
2588
|
+
return {
|
|
2589
|
+
success: false,
|
|
2590
|
+
error: new ParseError(`Invalid config: ${issues}`, "VALIDATION_ERROR", result.error)
|
|
2591
|
+
};
|
|
2592
|
+
}
|
|
2593
|
+
return {
|
|
2594
|
+
success: true,
|
|
2595
|
+
data: result.data,
|
|
2596
|
+
configPath
|
|
2597
|
+
};
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
// ../linter-gen/dist/engine/template-loader.js
|
|
2601
|
+
import * as fs11 from "fs/promises";
|
|
2602
|
+
import * as path19 from "path";
|
|
2603
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2604
|
+
var __filename2 = fileURLToPath2(import.meta.url);
|
|
2605
|
+
var __dirname2 = path19.dirname(__filename2);
|
|
2606
|
+
var TemplateLoadError = class extends Error {
|
|
2607
|
+
code;
|
|
2608
|
+
cause;
|
|
2609
|
+
constructor(message, code, cause) {
|
|
2610
|
+
super(message);
|
|
2611
|
+
this.code = code;
|
|
2612
|
+
this.cause = cause;
|
|
2613
|
+
this.name = "TemplateLoadError";
|
|
2614
|
+
}
|
|
2615
|
+
};
|
|
2616
|
+
var BUILTIN_TEMPLATES = ["import-restriction", "boundary-validation", "dependency-graph"];
|
|
2617
|
+
async function fileExists(filePath) {
|
|
2618
|
+
try {
|
|
2619
|
+
await fs11.access(filePath);
|
|
2620
|
+
return true;
|
|
2621
|
+
} catch {
|
|
2622
|
+
return false;
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
async function loadTemplateFile(filePath, type) {
|
|
2626
|
+
try {
|
|
2627
|
+
const content = await fs11.readFile(filePath, "utf-8");
|
|
2628
|
+
return {
|
|
2629
|
+
success: true,
|
|
2630
|
+
source: { type, path: filePath, content }
|
|
2631
|
+
};
|
|
2632
|
+
} catch (err) {
|
|
2633
|
+
return {
|
|
2634
|
+
success: false,
|
|
2635
|
+
error: new TemplateLoadError(`Failed to read template: ${filePath}`, "TEMPLATE_READ_ERROR", err)
|
|
2636
|
+
};
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
async function loadTemplate(ruleType, templatesConfig, configDir) {
|
|
2640
|
+
if (templatesConfig?.[ruleType]) {
|
|
2641
|
+
const explicitPath = path19.resolve(configDir, templatesConfig[ruleType]);
|
|
2642
|
+
if (await fileExists(explicitPath)) {
|
|
2643
|
+
return loadTemplateFile(explicitPath, "explicit");
|
|
2644
|
+
}
|
|
2645
|
+
return {
|
|
2646
|
+
success: false,
|
|
2647
|
+
error: new TemplateLoadError(`Explicit template not found: ${explicitPath}`, "TEMPLATE_NOT_FOUND")
|
|
2648
|
+
};
|
|
2649
|
+
}
|
|
2650
|
+
const conventionPath = path19.join(configDir, "templates", `${ruleType}.ts.hbs`);
|
|
2651
|
+
if (await fileExists(conventionPath)) {
|
|
2652
|
+
return loadTemplateFile(conventionPath, "convention");
|
|
2653
|
+
}
|
|
2654
|
+
if (BUILTIN_TEMPLATES.includes(ruleType)) {
|
|
2655
|
+
const builtinPath = path19.join(__dirname2, "..", "templates", `${ruleType}.ts.hbs`);
|
|
2656
|
+
if (await fileExists(builtinPath)) {
|
|
2657
|
+
return loadTemplateFile(builtinPath, "builtin");
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
return {
|
|
2661
|
+
success: false,
|
|
2662
|
+
error: new TemplateLoadError(`Template not found for type '${ruleType}'. Checked: explicit config, ./templates/${ruleType}.ts.hbs, built-in templates.`, "TEMPLATE_NOT_FOUND")
|
|
2663
|
+
};
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
// ../linter-gen/dist/generator/rule-generator.js
|
|
2667
|
+
import * as path20 from "path";
|
|
2668
|
+
|
|
2669
|
+
// ../linter-gen/dist/engine/context-builder.js
|
|
2670
|
+
var GENERATOR_VERSION = "0.1.0";
|
|
2671
|
+
function toCamelCase(str) {
|
|
2672
|
+
return str.replace(/-([a-z0-9])/g, (_, char) => char.toUpperCase());
|
|
2673
|
+
}
|
|
2674
|
+
function toPascalCase(str) {
|
|
2675
|
+
const camel = toCamelCase(str);
|
|
2676
|
+
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
|
2677
|
+
}
|
|
2678
|
+
function buildRuleContext(rule, configPath) {
|
|
2679
|
+
return {
|
|
2680
|
+
name: rule.name,
|
|
2681
|
+
nameCamel: toCamelCase(rule.name),
|
|
2682
|
+
namePascal: toPascalCase(rule.name),
|
|
2683
|
+
severity: rule.severity,
|
|
2684
|
+
config: rule.config,
|
|
2685
|
+
meta: {
|
|
2686
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2687
|
+
generatorVersion: GENERATOR_VERSION,
|
|
2688
|
+
configPath
|
|
2689
|
+
}
|
|
2690
|
+
};
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
// ../linter-gen/dist/engine/template-renderer.js
|
|
2694
|
+
import Handlebars2 from "handlebars";
|
|
2695
|
+
var TemplateError = class extends Error {
|
|
2696
|
+
cause;
|
|
2697
|
+
constructor(message, cause) {
|
|
2698
|
+
super(message);
|
|
2699
|
+
this.cause = cause;
|
|
2700
|
+
this.name = "TemplateError";
|
|
2701
|
+
}
|
|
2702
|
+
};
|
|
2703
|
+
function toCamelCase2(str) {
|
|
2704
|
+
return str.replace(/-([a-z0-9])/g, (_, char) => char.toUpperCase());
|
|
2705
|
+
}
|
|
2706
|
+
function toPascalCase2(str) {
|
|
2707
|
+
const camel = toCamelCase2(str);
|
|
2708
|
+
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
|
2709
|
+
}
|
|
2710
|
+
Handlebars2.registerHelper("json", (obj) => JSON.stringify(obj));
|
|
2711
|
+
Handlebars2.registerHelper("jsonPretty", (obj) => JSON.stringify(obj, null, 2));
|
|
2712
|
+
Handlebars2.registerHelper("camelCase", (str) => toCamelCase2(str));
|
|
2713
|
+
Handlebars2.registerHelper("pascalCase", (str) => toPascalCase2(str));
|
|
2714
|
+
function renderTemplate(templateSource, context) {
|
|
2715
|
+
try {
|
|
2716
|
+
const compiled = Handlebars2.compile(templateSource, { strict: true });
|
|
2717
|
+
const output = compiled(context);
|
|
2718
|
+
return { success: true, output };
|
|
2719
|
+
} catch (err) {
|
|
2720
|
+
return {
|
|
2721
|
+
success: false,
|
|
2722
|
+
error: new TemplateError(`Template rendering failed: ${err.message}`, err)
|
|
2723
|
+
};
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
// ../linter-gen/dist/generator/rule-generator.js
|
|
2728
|
+
function generateRule(rule, template, outputDir, configPath) {
|
|
2729
|
+
const context = buildRuleContext(rule, configPath);
|
|
2730
|
+
const renderResult = renderTemplate(template.content, context);
|
|
2731
|
+
if (!renderResult.success) {
|
|
2732
|
+
return {
|
|
2733
|
+
success: false,
|
|
2734
|
+
error: renderResult.error,
|
|
2735
|
+
ruleName: rule.name
|
|
2736
|
+
};
|
|
2737
|
+
}
|
|
2738
|
+
const outputPath = path20.join(outputDir, `${rule.name}.ts`);
|
|
2739
|
+
return {
|
|
2740
|
+
success: true,
|
|
2741
|
+
rule: {
|
|
2742
|
+
name: rule.name,
|
|
2743
|
+
outputPath,
|
|
2744
|
+
content: renderResult.output
|
|
2745
|
+
}
|
|
2746
|
+
};
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
// ../linter-gen/dist/generator/index-generator.js
|
|
2750
|
+
function toCamelCase3(str) {
|
|
2751
|
+
return str.replace(/-([a-z0-9])/g, (_, char) => char.toUpperCase());
|
|
2752
|
+
}
|
|
2753
|
+
function generateIndex(ruleNames) {
|
|
2754
|
+
const imports = ruleNames.map((name) => {
|
|
2755
|
+
const camel = toCamelCase3(name);
|
|
2756
|
+
return `import ${camel} from './${name}';`;
|
|
2757
|
+
}).join("\n");
|
|
2758
|
+
const rulesObject = ruleNames.map((name) => {
|
|
2759
|
+
const camel = toCamelCase3(name);
|
|
2760
|
+
return ` '${name}': ${camel},`;
|
|
2761
|
+
}).join("\n");
|
|
2762
|
+
const namedExports = ruleNames.map(toCamelCase3).join(", ");
|
|
2763
|
+
return `// Generated by @harness-engineering/linter-gen
|
|
2764
|
+
// Do not edit manually - regenerate from harness-linter.yml
|
|
2765
|
+
|
|
2766
|
+
${imports}
|
|
2767
|
+
|
|
2768
|
+
export const rules = {
|
|
2769
|
+
${rulesObject}
|
|
2770
|
+
};
|
|
2771
|
+
|
|
2772
|
+
export { ${namedExports} };
|
|
2773
|
+
`;
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2776
|
+
// ../linter-gen/dist/generator/orchestrator.js
|
|
2777
|
+
async function validate(options) {
|
|
2778
|
+
const parseResult = await parseConfig(options.configPath);
|
|
2779
|
+
if (!parseResult.success) {
|
|
2780
|
+
return { success: false, error: parseResult.error };
|
|
2781
|
+
}
|
|
2782
|
+
return { success: true, ruleCount: parseResult.data.rules.length };
|
|
2783
|
+
}
|
|
2784
|
+
async function generate(options) {
|
|
2785
|
+
const errors = [];
|
|
2786
|
+
const parseResult = await parseConfig(options.configPath);
|
|
2787
|
+
if (!parseResult.success) {
|
|
2788
|
+
return { success: false, errors: [{ type: "parse", error: parseResult.error }] };
|
|
2789
|
+
}
|
|
2790
|
+
const config = parseResult.data;
|
|
2791
|
+
const configDir = path21.dirname(path21.resolve(options.configPath));
|
|
2792
|
+
const outputDir = options.outputDir ? path21.resolve(options.outputDir) : path21.resolve(configDir, config.output);
|
|
2793
|
+
if (options.clean && !options.dryRun) {
|
|
2794
|
+
try {
|
|
2795
|
+
await fs12.rm(outputDir, { recursive: true, force: true });
|
|
2796
|
+
} catch {
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
if (!options.dryRun) {
|
|
2800
|
+
await fs12.mkdir(outputDir, { recursive: true });
|
|
2801
|
+
}
|
|
2802
|
+
const generatedRules = [];
|
|
2803
|
+
for (const rule of config.rules) {
|
|
2804
|
+
const templateResult = await loadTemplate(rule.type, config.templates, configDir);
|
|
2805
|
+
if (!templateResult.success) {
|
|
2806
|
+
errors.push({
|
|
2807
|
+
type: "template",
|
|
2808
|
+
error: templateResult.error,
|
|
2809
|
+
ruleName: rule.name
|
|
2810
|
+
});
|
|
2811
|
+
continue;
|
|
2812
|
+
}
|
|
2813
|
+
const ruleResult = generateRule(rule, templateResult.source, outputDir, options.configPath);
|
|
2814
|
+
if (!ruleResult.success) {
|
|
2815
|
+
errors.push({
|
|
2816
|
+
type: "render",
|
|
2817
|
+
error: ruleResult.error,
|
|
2818
|
+
ruleName: ruleResult.ruleName
|
|
2819
|
+
});
|
|
2820
|
+
continue;
|
|
2821
|
+
}
|
|
2822
|
+
if (!options.dryRun) {
|
|
2823
|
+
try {
|
|
2824
|
+
await fs12.writeFile(ruleResult.rule.outputPath, ruleResult.rule.content, "utf-8");
|
|
2825
|
+
} catch (err) {
|
|
2826
|
+
errors.push({
|
|
2827
|
+
type: "write",
|
|
2828
|
+
error: err,
|
|
2829
|
+
path: ruleResult.rule.outputPath
|
|
2830
|
+
});
|
|
2831
|
+
continue;
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
generatedRules.push(rule.name);
|
|
2835
|
+
}
|
|
2836
|
+
if (generatedRules.length > 0 && !options.dryRun) {
|
|
2837
|
+
const indexContent = generateIndex(generatedRules);
|
|
2838
|
+
const indexPath = path21.join(outputDir, "index.ts");
|
|
2839
|
+
try {
|
|
2840
|
+
await fs12.writeFile(indexPath, indexContent, "utf-8");
|
|
2841
|
+
} catch (err) {
|
|
2842
|
+
errors.push({ type: "write", error: err, path: indexPath });
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
if (errors.length > 0) {
|
|
2846
|
+
return { success: false, errors };
|
|
2847
|
+
}
|
|
2848
|
+
return {
|
|
2849
|
+
success: true,
|
|
2850
|
+
rulesGenerated: generatedRules,
|
|
2851
|
+
outputDir,
|
|
2852
|
+
dryRun: options.dryRun ?? false
|
|
2853
|
+
};
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
// src/commands/linter/generate.ts
|
|
2499
2857
|
function createGenerateCommand() {
|
|
2500
2858
|
return new Command15("generate").description("Generate ESLint rules from harness-linter.yml").option("-c, --config <path>", "Path to harness-linter.yml", "./harness-linter.yml").option("-o, --output <dir>", "Override output directory").option("--clean", "Remove existing files before generating").option("--dry-run", "Preview without writing files").option("--json", "Output as JSON").option("--verbose", "Show detailed output").action(async (options) => {
|
|
2501
2859
|
try {
|
|
@@ -2560,7 +2918,6 @@ Generated ${result.rulesGenerated.length} rules to ${result.outputDir}`);
|
|
|
2560
2918
|
|
|
2561
2919
|
// src/commands/linter/validate.ts
|
|
2562
2920
|
import { Command as Command16 } from "commander";
|
|
2563
|
-
import { validate } from "@harness-engineering/linter-gen";
|
|
2564
2921
|
function createValidateCommand2() {
|
|
2565
2922
|
return new Command16("validate").description("Validate harness-linter.yml config").option("-c, --config <path>", "Path to harness-linter.yml", "./harness-linter.yml").option("--json", "Output as JSON").action(async (options) => {
|
|
2566
2923
|
try {
|
|
@@ -2625,11 +2982,8 @@ function createListCommand() {
|
|
|
2625
2982
|
|
|
2626
2983
|
// src/commands/persona/generate.ts
|
|
2627
2984
|
import { Command as Command19 } from "commander";
|
|
2628
|
-
import * as
|
|
2629
|
-
import * as
|
|
2630
|
-
|
|
2631
|
-
// src/persona/generators/runtime.ts
|
|
2632
|
-
import { Ok as Ok15, Err as Err11 } from "@harness-engineering/core";
|
|
2985
|
+
import * as fs13 from "fs";
|
|
2986
|
+
import * as path22 from "path";
|
|
2633
2987
|
|
|
2634
2988
|
// src/utils/string.ts
|
|
2635
2989
|
function toKebabCase(name) {
|
|
@@ -2646,9 +3000,9 @@ function generateRuntime(persona) {
|
|
|
2646
3000
|
timeout: persona.config.timeout,
|
|
2647
3001
|
severity: persona.config.severity
|
|
2648
3002
|
};
|
|
2649
|
-
return
|
|
3003
|
+
return Ok(JSON.stringify(config, null, 2));
|
|
2650
3004
|
} catch (error) {
|
|
2651
|
-
return
|
|
3005
|
+
return Err(
|
|
2652
3006
|
new Error(
|
|
2653
3007
|
`Failed to generate runtime config: ${error instanceof Error ? error.message : String(error)}`
|
|
2654
3008
|
)
|
|
@@ -2657,7 +3011,6 @@ function generateRuntime(persona) {
|
|
|
2657
3011
|
}
|
|
2658
3012
|
|
|
2659
3013
|
// src/persona/generators/agents-md.ts
|
|
2660
|
-
import { Ok as Ok16, Err as Err12 } from "@harness-engineering/core";
|
|
2661
3014
|
function formatTrigger(trigger) {
|
|
2662
3015
|
switch (trigger.event) {
|
|
2663
3016
|
case "on_pr": {
|
|
@@ -2691,9 +3044,9 @@ function generateAgentsMd(persona) {
|
|
|
2691
3044
|
|
|
2692
3045
|
**When this agent flags an issue:** Fix violations before merging. Run ${allCommands} locally to validate.
|
|
2693
3046
|
`;
|
|
2694
|
-
return
|
|
3047
|
+
return Ok(fragment);
|
|
2695
3048
|
} catch (error) {
|
|
2696
|
-
return
|
|
3049
|
+
return Err(
|
|
2697
3050
|
new Error(
|
|
2698
3051
|
`Failed to generate AGENTS.md fragment: ${error instanceof Error ? error.message : String(error)}`
|
|
2699
3052
|
)
|
|
@@ -2703,7 +3056,6 @@ function generateAgentsMd(persona) {
|
|
|
2703
3056
|
|
|
2704
3057
|
// src/persona/generators/ci-workflow.ts
|
|
2705
3058
|
import YAML2 from "yaml";
|
|
2706
|
-
import { Ok as Ok17, Err as Err13 } from "@harness-engineering/core";
|
|
2707
3059
|
function buildGitHubTriggers(triggers) {
|
|
2708
3060
|
const on = {};
|
|
2709
3061
|
for (const trigger of triggers) {
|
|
@@ -2729,7 +3081,7 @@ function buildGitHubTriggers(triggers) {
|
|
|
2729
3081
|
}
|
|
2730
3082
|
function generateCIWorkflow(persona, platform) {
|
|
2731
3083
|
try {
|
|
2732
|
-
if (platform === "gitlab") return
|
|
3084
|
+
if (platform === "gitlab") return Err(new Error("GitLab CI generation is not yet supported"));
|
|
2733
3085
|
const severity = persona.config.severity;
|
|
2734
3086
|
const steps = [
|
|
2735
3087
|
{ uses: "actions/checkout@v4" },
|
|
@@ -2751,9 +3103,9 @@ function generateCIWorkflow(persona, platform) {
|
|
|
2751
3103
|
}
|
|
2752
3104
|
}
|
|
2753
3105
|
};
|
|
2754
|
-
return
|
|
3106
|
+
return Ok(YAML2.stringify(workflow, { lineWidth: 0 }));
|
|
2755
3107
|
} catch (error) {
|
|
2756
|
-
return
|
|
3108
|
+
return Err(
|
|
2757
3109
|
new Error(
|
|
2758
3110
|
`Failed to generate CI workflow: ${error instanceof Error ? error.message : String(error)}`
|
|
2759
3111
|
)
|
|
@@ -2766,40 +3118,40 @@ function createGenerateCommand2() {
|
|
|
2766
3118
|
return new Command19("generate").description("Generate artifacts from a persona config").argument("<name>", "Persona name (e.g., architecture-enforcer)").option("--output-dir <dir>", "Output directory", ".").option("--only <type>", "Generate only: ci, agents-md, runtime").action(async (name, opts, cmd) => {
|
|
2767
3119
|
const globalOpts = cmd.optsWithGlobals();
|
|
2768
3120
|
const personasDir = resolvePersonasDir();
|
|
2769
|
-
const filePath =
|
|
3121
|
+
const filePath = path22.join(personasDir, `${name}.yaml`);
|
|
2770
3122
|
const personaResult = loadPersona(filePath);
|
|
2771
3123
|
if (!personaResult.ok) {
|
|
2772
3124
|
logger.error(personaResult.error.message);
|
|
2773
3125
|
process.exit(ExitCode.ERROR);
|
|
2774
3126
|
}
|
|
2775
3127
|
const persona = personaResult.value;
|
|
2776
|
-
const outputDir =
|
|
3128
|
+
const outputDir = path22.resolve(opts.outputDir);
|
|
2777
3129
|
const slug = toKebabCase(persona.name);
|
|
2778
3130
|
const only = opts.only;
|
|
2779
3131
|
const generated = [];
|
|
2780
3132
|
if (!only || only === "runtime") {
|
|
2781
3133
|
const result = generateRuntime(persona);
|
|
2782
3134
|
if (result.ok) {
|
|
2783
|
-
const outPath =
|
|
2784
|
-
|
|
2785
|
-
|
|
3135
|
+
const outPath = path22.join(outputDir, `${slug}.runtime.json`);
|
|
3136
|
+
fs13.mkdirSync(path22.dirname(outPath), { recursive: true });
|
|
3137
|
+
fs13.writeFileSync(outPath, result.value);
|
|
2786
3138
|
generated.push(outPath);
|
|
2787
3139
|
}
|
|
2788
3140
|
}
|
|
2789
3141
|
if (!only || only === "agents-md") {
|
|
2790
3142
|
const result = generateAgentsMd(persona);
|
|
2791
3143
|
if (result.ok) {
|
|
2792
|
-
const outPath =
|
|
2793
|
-
|
|
3144
|
+
const outPath = path22.join(outputDir, `${slug}.agents.md`);
|
|
3145
|
+
fs13.writeFileSync(outPath, result.value);
|
|
2794
3146
|
generated.push(outPath);
|
|
2795
3147
|
}
|
|
2796
3148
|
}
|
|
2797
3149
|
if (!only || only === "ci") {
|
|
2798
3150
|
const result = generateCIWorkflow(persona, "github");
|
|
2799
3151
|
if (result.ok) {
|
|
2800
|
-
const outPath =
|
|
2801
|
-
|
|
2802
|
-
|
|
3152
|
+
const outPath = path22.join(outputDir, ".github", "workflows", `${slug}.yml`);
|
|
3153
|
+
fs13.mkdirSync(path22.dirname(outPath), { recursive: true });
|
|
3154
|
+
fs13.writeFileSync(outPath, result.value);
|
|
2803
3155
|
generated.push(outPath);
|
|
2804
3156
|
}
|
|
2805
3157
|
}
|
|
@@ -2824,26 +3176,26 @@ import { Command as Command25 } from "commander";
|
|
|
2824
3176
|
|
|
2825
3177
|
// src/commands/skill/list.ts
|
|
2826
3178
|
import { Command as Command21 } from "commander";
|
|
2827
|
-
import * as
|
|
2828
|
-
import * as
|
|
2829
|
-
import { parse as
|
|
3179
|
+
import * as fs14 from "fs";
|
|
3180
|
+
import * as path23 from "path";
|
|
3181
|
+
import { parse as parse4 } from "yaml";
|
|
2830
3182
|
function createListCommand2() {
|
|
2831
3183
|
return new Command21("list").description("List available skills").action(async (_opts, cmd) => {
|
|
2832
3184
|
const globalOpts = cmd.optsWithGlobals();
|
|
2833
3185
|
const skillsDir = resolveSkillsDir();
|
|
2834
|
-
if (!
|
|
3186
|
+
if (!fs14.existsSync(skillsDir)) {
|
|
2835
3187
|
logger.info("No skills directory found.");
|
|
2836
3188
|
process.exit(ExitCode.SUCCESS);
|
|
2837
3189
|
return;
|
|
2838
3190
|
}
|
|
2839
|
-
const entries =
|
|
3191
|
+
const entries = fs14.readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
2840
3192
|
const skills = [];
|
|
2841
3193
|
for (const name of entries) {
|
|
2842
|
-
const yamlPath =
|
|
2843
|
-
if (!
|
|
3194
|
+
const yamlPath = path23.join(skillsDir, name, "skill.yaml");
|
|
3195
|
+
if (!fs14.existsSync(yamlPath)) continue;
|
|
2844
3196
|
try {
|
|
2845
|
-
const raw =
|
|
2846
|
-
const parsed =
|
|
3197
|
+
const raw = fs14.readFileSync(yamlPath, "utf-8");
|
|
3198
|
+
const parsed = parse4(raw);
|
|
2847
3199
|
const result = SkillMetadataSchema.safeParse(parsed);
|
|
2848
3200
|
if (result.success) {
|
|
2849
3201
|
skills.push(result.data);
|
|
@@ -2873,12 +3225,12 @@ function createListCommand2() {
|
|
|
2873
3225
|
|
|
2874
3226
|
// src/commands/skill/run.ts
|
|
2875
3227
|
import { Command as Command22 } from "commander";
|
|
2876
|
-
import * as
|
|
2877
|
-
import * as
|
|
2878
|
-
import { parse as
|
|
3228
|
+
import * as fs15 from "fs";
|
|
3229
|
+
import * as path24 from "path";
|
|
3230
|
+
import { parse as parse5 } from "yaml";
|
|
2879
3231
|
|
|
2880
3232
|
// src/skill/complexity.ts
|
|
2881
|
-
import {
|
|
3233
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
2882
3234
|
function evaluateSignals(signals) {
|
|
2883
3235
|
if (signals.fileCount >= 3) return "full";
|
|
2884
3236
|
if (signals.newDir) return "full";
|
|
@@ -2890,17 +3242,17 @@ function evaluateSignals(signals) {
|
|
|
2890
3242
|
}
|
|
2891
3243
|
function detectComplexity(projectPath) {
|
|
2892
3244
|
try {
|
|
2893
|
-
const base =
|
|
3245
|
+
const base = execFileSync2("git", ["merge-base", "HEAD", "main"], {
|
|
2894
3246
|
cwd: projectPath,
|
|
2895
3247
|
encoding: "utf-8",
|
|
2896
3248
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2897
3249
|
}).trim();
|
|
2898
|
-
const diffFiles =
|
|
3250
|
+
const diffFiles = execFileSync2("git", ["diff", "--name-only", base], {
|
|
2899
3251
|
cwd: projectPath,
|
|
2900
3252
|
encoding: "utf-8",
|
|
2901
3253
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2902
3254
|
}).trim().split("\n").filter(Boolean);
|
|
2903
|
-
const diffStat =
|
|
3255
|
+
const diffStat = execFileSync2("git", ["diff", "--stat", base], {
|
|
2904
3256
|
cwd: projectPath,
|
|
2905
3257
|
encoding: "utf-8",
|
|
2906
3258
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -2964,18 +3316,18 @@ ${options.priorState}`);
|
|
|
2964
3316
|
function createRunCommand2() {
|
|
2965
3317
|
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) => {
|
|
2966
3318
|
const skillsDir = resolveSkillsDir();
|
|
2967
|
-
const skillDir =
|
|
2968
|
-
if (!
|
|
3319
|
+
const skillDir = path24.join(skillsDir, name);
|
|
3320
|
+
if (!fs15.existsSync(skillDir)) {
|
|
2969
3321
|
logger.error(`Skill not found: ${name}`);
|
|
2970
3322
|
process.exit(ExitCode.ERROR);
|
|
2971
3323
|
return;
|
|
2972
3324
|
}
|
|
2973
|
-
const yamlPath =
|
|
3325
|
+
const yamlPath = path24.join(skillDir, "skill.yaml");
|
|
2974
3326
|
let metadata = null;
|
|
2975
|
-
if (
|
|
3327
|
+
if (fs15.existsSync(yamlPath)) {
|
|
2976
3328
|
try {
|
|
2977
|
-
const raw =
|
|
2978
|
-
const parsed =
|
|
3329
|
+
const raw = fs15.readFileSync(yamlPath, "utf-8");
|
|
3330
|
+
const parsed = parse5(raw);
|
|
2979
3331
|
const result = SkillMetadataSchema.safeParse(parsed);
|
|
2980
3332
|
if (result.success) metadata = result.data;
|
|
2981
3333
|
} catch {
|
|
@@ -2985,17 +3337,17 @@ function createRunCommand2() {
|
|
|
2985
3337
|
if (metadata?.phases && metadata.phases.length > 0) {
|
|
2986
3338
|
const requested = opts.complexity ?? "auto";
|
|
2987
3339
|
if (requested === "auto") {
|
|
2988
|
-
const projectPath2 = opts.path ?
|
|
3340
|
+
const projectPath2 = opts.path ? path24.resolve(opts.path) : process.cwd();
|
|
2989
3341
|
complexity = detectComplexity(projectPath2);
|
|
2990
3342
|
} else {
|
|
2991
3343
|
complexity = requested;
|
|
2992
3344
|
}
|
|
2993
3345
|
}
|
|
2994
3346
|
let principles;
|
|
2995
|
-
const projectPath = opts.path ?
|
|
2996
|
-
const principlesPath =
|
|
2997
|
-
if (
|
|
2998
|
-
principles =
|
|
3347
|
+
const projectPath = opts.path ? path24.resolve(opts.path) : process.cwd();
|
|
3348
|
+
const principlesPath = path24.join(projectPath, "docs", "principles.md");
|
|
3349
|
+
if (fs15.existsSync(principlesPath)) {
|
|
3350
|
+
principles = fs15.readFileSync(principlesPath, "utf-8");
|
|
2999
3351
|
}
|
|
3000
3352
|
let priorState;
|
|
3001
3353
|
let stateWarning;
|
|
@@ -3010,16 +3362,16 @@ function createRunCommand2() {
|
|
|
3010
3362
|
}
|
|
3011
3363
|
if (metadata?.state.persistent && metadata.state.files.length > 0) {
|
|
3012
3364
|
for (const stateFilePath of metadata.state.files) {
|
|
3013
|
-
const fullPath =
|
|
3014
|
-
if (
|
|
3015
|
-
const stat =
|
|
3365
|
+
const fullPath = path24.join(projectPath, stateFilePath);
|
|
3366
|
+
if (fs15.existsSync(fullPath)) {
|
|
3367
|
+
const stat = fs15.statSync(fullPath);
|
|
3016
3368
|
if (stat.isDirectory()) {
|
|
3017
|
-
const files =
|
|
3369
|
+
const files = fs15.readdirSync(fullPath).map((f) => ({ name: f, mtime: fs15.statSync(path24.join(fullPath, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
|
|
3018
3370
|
if (files.length > 0) {
|
|
3019
|
-
priorState =
|
|
3371
|
+
priorState = fs15.readFileSync(path24.join(fullPath, files[0].name), "utf-8");
|
|
3020
3372
|
}
|
|
3021
3373
|
} else {
|
|
3022
|
-
priorState =
|
|
3374
|
+
priorState = fs15.readFileSync(fullPath, "utf-8");
|
|
3023
3375
|
}
|
|
3024
3376
|
break;
|
|
3025
3377
|
}
|
|
@@ -3038,17 +3390,17 @@ function createRunCommand2() {
|
|
|
3038
3390
|
...stateWarning !== void 0 && { stateWarning },
|
|
3039
3391
|
party: opts.party
|
|
3040
3392
|
});
|
|
3041
|
-
const skillMdPath =
|
|
3042
|
-
if (!
|
|
3393
|
+
const skillMdPath = path24.join(skillDir, "SKILL.md");
|
|
3394
|
+
if (!fs15.existsSync(skillMdPath)) {
|
|
3043
3395
|
logger.error(`SKILL.md not found for skill: ${name}`);
|
|
3044
3396
|
process.exit(ExitCode.ERROR);
|
|
3045
3397
|
return;
|
|
3046
3398
|
}
|
|
3047
|
-
let content =
|
|
3399
|
+
let content = fs15.readFileSync(skillMdPath, "utf-8");
|
|
3048
3400
|
if (metadata?.state.persistent && opts.path) {
|
|
3049
|
-
const stateFile =
|
|
3050
|
-
if (
|
|
3051
|
-
const stateContent =
|
|
3401
|
+
const stateFile = path24.join(projectPath, ".harness", "state.json");
|
|
3402
|
+
if (fs15.existsSync(stateFile)) {
|
|
3403
|
+
const stateContent = fs15.readFileSync(stateFile, "utf-8");
|
|
3052
3404
|
content += `
|
|
3053
3405
|
|
|
3054
3406
|
---
|
|
@@ -3066,9 +3418,9 @@ ${stateContent}
|
|
|
3066
3418
|
|
|
3067
3419
|
// src/commands/skill/validate.ts
|
|
3068
3420
|
import { Command as Command23 } from "commander";
|
|
3069
|
-
import * as
|
|
3070
|
-
import * as
|
|
3071
|
-
import { parse as
|
|
3421
|
+
import * as fs16 from "fs";
|
|
3422
|
+
import * as path25 from "path";
|
|
3423
|
+
import { parse as parse6 } from "yaml";
|
|
3072
3424
|
var REQUIRED_SECTIONS = [
|
|
3073
3425
|
"## When to Use",
|
|
3074
3426
|
"## Process",
|
|
@@ -3080,32 +3432,32 @@ function createValidateCommand3() {
|
|
|
3080
3432
|
return new Command23("validate").description("Validate all skill.yaml files and SKILL.md structure").action(async (_opts, cmd) => {
|
|
3081
3433
|
const globalOpts = cmd.optsWithGlobals();
|
|
3082
3434
|
const skillsDir = resolveSkillsDir();
|
|
3083
|
-
if (!
|
|
3435
|
+
if (!fs16.existsSync(skillsDir)) {
|
|
3084
3436
|
logger.info("No skills directory found.");
|
|
3085
3437
|
process.exit(ExitCode.SUCCESS);
|
|
3086
3438
|
return;
|
|
3087
3439
|
}
|
|
3088
|
-
const entries =
|
|
3440
|
+
const entries = fs16.readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
3089
3441
|
const errors = [];
|
|
3090
3442
|
let validated = 0;
|
|
3091
3443
|
for (const name of entries) {
|
|
3092
|
-
const skillDir =
|
|
3093
|
-
const yamlPath =
|
|
3094
|
-
const skillMdPath =
|
|
3095
|
-
if (!
|
|
3444
|
+
const skillDir = path25.join(skillsDir, name);
|
|
3445
|
+
const yamlPath = path25.join(skillDir, "skill.yaml");
|
|
3446
|
+
const skillMdPath = path25.join(skillDir, "SKILL.md");
|
|
3447
|
+
if (!fs16.existsSync(yamlPath)) {
|
|
3096
3448
|
errors.push(`${name}: missing skill.yaml`);
|
|
3097
3449
|
continue;
|
|
3098
3450
|
}
|
|
3099
3451
|
try {
|
|
3100
|
-
const raw =
|
|
3101
|
-
const parsed =
|
|
3452
|
+
const raw = fs16.readFileSync(yamlPath, "utf-8");
|
|
3453
|
+
const parsed = parse6(raw);
|
|
3102
3454
|
const result = SkillMetadataSchema.safeParse(parsed);
|
|
3103
3455
|
if (!result.success) {
|
|
3104
3456
|
errors.push(`${name}/skill.yaml: ${result.error.message}`);
|
|
3105
3457
|
continue;
|
|
3106
3458
|
}
|
|
3107
|
-
if (
|
|
3108
|
-
const mdContent =
|
|
3459
|
+
if (fs16.existsSync(skillMdPath)) {
|
|
3460
|
+
const mdContent = fs16.readFileSync(skillMdPath, "utf-8");
|
|
3109
3461
|
for (const section of REQUIRED_SECTIONS) {
|
|
3110
3462
|
if (!mdContent.includes(section)) {
|
|
3111
3463
|
errors.push(`${name}/SKILL.md: missing section "${section}"`);
|
|
@@ -3147,28 +3499,28 @@ function createValidateCommand3() {
|
|
|
3147
3499
|
|
|
3148
3500
|
// src/commands/skill/info.ts
|
|
3149
3501
|
import { Command as Command24 } from "commander";
|
|
3150
|
-
import * as
|
|
3151
|
-
import * as
|
|
3152
|
-
import { parse as
|
|
3502
|
+
import * as fs17 from "fs";
|
|
3503
|
+
import * as path26 from "path";
|
|
3504
|
+
import { parse as parse7 } from "yaml";
|
|
3153
3505
|
function createInfoCommand() {
|
|
3154
3506
|
return new Command24("info").description("Show metadata for a skill").argument("<name>", "Skill name (e.g., harness-tdd)").action(async (name, _opts, cmd) => {
|
|
3155
3507
|
const globalOpts = cmd.optsWithGlobals();
|
|
3156
3508
|
const skillsDir = resolveSkillsDir();
|
|
3157
|
-
const skillDir =
|
|
3158
|
-
if (!
|
|
3509
|
+
const skillDir = path26.join(skillsDir, name);
|
|
3510
|
+
if (!fs17.existsSync(skillDir)) {
|
|
3159
3511
|
logger.error(`Skill not found: ${name}`);
|
|
3160
3512
|
process.exit(ExitCode.ERROR);
|
|
3161
3513
|
return;
|
|
3162
3514
|
}
|
|
3163
|
-
const yamlPath =
|
|
3164
|
-
if (!
|
|
3515
|
+
const yamlPath = path26.join(skillDir, "skill.yaml");
|
|
3516
|
+
if (!fs17.existsSync(yamlPath)) {
|
|
3165
3517
|
logger.error(`skill.yaml not found for skill: ${name}`);
|
|
3166
3518
|
process.exit(ExitCode.ERROR);
|
|
3167
3519
|
return;
|
|
3168
3520
|
}
|
|
3169
3521
|
try {
|
|
3170
|
-
const raw =
|
|
3171
|
-
const parsed =
|
|
3522
|
+
const raw = fs17.readFileSync(yamlPath, "utf-8");
|
|
3523
|
+
const parsed = parse7(raw);
|
|
3172
3524
|
const result = SkillMetadataSchema.safeParse(parsed);
|
|
3173
3525
|
if (!result.success) {
|
|
3174
3526
|
logger.error(`Invalid skill.yaml: ${result.error.message}`);
|
|
@@ -3221,12 +3573,11 @@ import { Command as Command30 } from "commander";
|
|
|
3221
3573
|
|
|
3222
3574
|
// src/commands/state/show.ts
|
|
3223
3575
|
import { Command as Command26 } from "commander";
|
|
3224
|
-
import * as
|
|
3225
|
-
import { loadState } from "@harness-engineering/core";
|
|
3576
|
+
import * as path27 from "path";
|
|
3226
3577
|
function createShowCommand() {
|
|
3227
3578
|
return new Command26("show").description("Show current project state").option("--path <path>", "Project root path", ".").option("--stream <name>", "Target a specific stream").action(async (opts, cmd) => {
|
|
3228
3579
|
const globalOpts = cmd.optsWithGlobals();
|
|
3229
|
-
const projectPath =
|
|
3580
|
+
const projectPath = path27.resolve(opts.path);
|
|
3230
3581
|
const result = await loadState(projectPath, opts.stream);
|
|
3231
3582
|
if (!result.ok) {
|
|
3232
3583
|
logger.error(result.error.message);
|
|
@@ -3267,13 +3618,12 @@ Decisions: ${state.decisions.length}`);
|
|
|
3267
3618
|
|
|
3268
3619
|
// src/commands/state/reset.ts
|
|
3269
3620
|
import { Command as Command27 } from "commander";
|
|
3270
|
-
import * as
|
|
3271
|
-
import * as
|
|
3621
|
+
import * as fs18 from "fs";
|
|
3622
|
+
import * as path28 from "path";
|
|
3272
3623
|
import * as readline from "readline";
|
|
3273
|
-
import { resolveStreamPath } from "@harness-engineering/core";
|
|
3274
3624
|
function createResetCommand() {
|
|
3275
3625
|
return new Command27("reset").description("Reset project state (deletes .harness/state.json)").option("--path <path>", "Project root path", ".").option("--stream <name>", "Target a specific stream").option("--yes", "Skip confirmation prompt").action(async (opts, _cmd) => {
|
|
3276
|
-
const projectPath =
|
|
3626
|
+
const projectPath = path28.resolve(opts.path);
|
|
3277
3627
|
let statePath;
|
|
3278
3628
|
if (opts.stream) {
|
|
3279
3629
|
const streamResult = await resolveStreamPath(projectPath, { stream: opts.stream });
|
|
@@ -3282,19 +3632,19 @@ function createResetCommand() {
|
|
|
3282
3632
|
process.exit(ExitCode.ERROR);
|
|
3283
3633
|
return;
|
|
3284
3634
|
}
|
|
3285
|
-
statePath =
|
|
3635
|
+
statePath = path28.join(streamResult.value, "state.json");
|
|
3286
3636
|
} else {
|
|
3287
|
-
statePath =
|
|
3637
|
+
statePath = path28.join(projectPath, ".harness", "state.json");
|
|
3288
3638
|
}
|
|
3289
|
-
if (!
|
|
3639
|
+
if (!fs18.existsSync(statePath)) {
|
|
3290
3640
|
logger.info("No state file found. Nothing to reset.");
|
|
3291
3641
|
process.exit(ExitCode.SUCCESS);
|
|
3292
3642
|
return;
|
|
3293
3643
|
}
|
|
3294
3644
|
if (!opts.yes) {
|
|
3295
3645
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
3296
|
-
const answer = await new Promise((
|
|
3297
|
-
rl.question("Reset project state? This cannot be undone. [y/N] ",
|
|
3646
|
+
const answer = await new Promise((resolve24) => {
|
|
3647
|
+
rl.question("Reset project state? This cannot be undone. [y/N] ", resolve24);
|
|
3298
3648
|
});
|
|
3299
3649
|
rl.close();
|
|
3300
3650
|
if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
|
|
@@ -3304,7 +3654,7 @@ function createResetCommand() {
|
|
|
3304
3654
|
}
|
|
3305
3655
|
}
|
|
3306
3656
|
try {
|
|
3307
|
-
|
|
3657
|
+
fs18.unlinkSync(statePath);
|
|
3308
3658
|
logger.success("Project state reset.");
|
|
3309
3659
|
} catch (e) {
|
|
3310
3660
|
logger.error(`Failed to reset state: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -3317,11 +3667,10 @@ function createResetCommand() {
|
|
|
3317
3667
|
|
|
3318
3668
|
// src/commands/state/learn.ts
|
|
3319
3669
|
import { Command as Command28 } from "commander";
|
|
3320
|
-
import * as
|
|
3321
|
-
import { appendLearning } from "@harness-engineering/core";
|
|
3670
|
+
import * as path29 from "path";
|
|
3322
3671
|
function createLearnCommand() {
|
|
3323
3672
|
return new Command28("learn").description("Append a learning to .harness/learnings.md").argument("<message>", "The learning to record").option("--path <path>", "Project root path", ".").option("--stream <name>", "Target a specific stream").action(async (message, opts, _cmd) => {
|
|
3324
|
-
const projectPath =
|
|
3673
|
+
const projectPath = path29.resolve(opts.path);
|
|
3325
3674
|
const result = await appendLearning(projectPath, message, void 0, void 0, opts.stream);
|
|
3326
3675
|
if (!result.ok) {
|
|
3327
3676
|
logger.error(result.error.message);
|
|
@@ -3335,19 +3684,12 @@ function createLearnCommand() {
|
|
|
3335
3684
|
|
|
3336
3685
|
// src/commands/state/streams.ts
|
|
3337
3686
|
import { Command as Command29 } from "commander";
|
|
3338
|
-
import * as
|
|
3339
|
-
import {
|
|
3340
|
-
createStream,
|
|
3341
|
-
listStreams,
|
|
3342
|
-
archiveStream,
|
|
3343
|
-
setActiveStream,
|
|
3344
|
-
loadStreamIndex
|
|
3345
|
-
} from "@harness-engineering/core";
|
|
3687
|
+
import * as path30 from "path";
|
|
3346
3688
|
function createStreamsCommand() {
|
|
3347
3689
|
const command = new Command29("streams").description("Manage state streams");
|
|
3348
3690
|
command.command("list").description("List all known streams").option("--path <path>", "Project root path", ".").action(async (opts, cmd) => {
|
|
3349
3691
|
const globalOpts = cmd.optsWithGlobals();
|
|
3350
|
-
const projectPath =
|
|
3692
|
+
const projectPath = path30.resolve(opts.path);
|
|
3351
3693
|
const indexResult = await loadStreamIndex(projectPath);
|
|
3352
3694
|
const result = await listStreams(projectPath);
|
|
3353
3695
|
if (!result.ok) {
|
|
@@ -3371,7 +3713,7 @@ function createStreamsCommand() {
|
|
|
3371
3713
|
process.exit(ExitCode.SUCCESS);
|
|
3372
3714
|
});
|
|
3373
3715
|
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) => {
|
|
3374
|
-
const projectPath =
|
|
3716
|
+
const projectPath = path30.resolve(opts.path);
|
|
3375
3717
|
const result = await createStream(projectPath, name, opts.branch);
|
|
3376
3718
|
if (!result.ok) {
|
|
3377
3719
|
logger.error(result.error.message);
|
|
@@ -3382,7 +3724,7 @@ function createStreamsCommand() {
|
|
|
3382
3724
|
process.exit(ExitCode.SUCCESS);
|
|
3383
3725
|
});
|
|
3384
3726
|
command.command("archive <name>").description("Archive a stream").option("--path <path>", "Project root path", ".").action(async (name, opts) => {
|
|
3385
|
-
const projectPath =
|
|
3727
|
+
const projectPath = path30.resolve(opts.path);
|
|
3386
3728
|
const result = await archiveStream(projectPath, name);
|
|
3387
3729
|
if (!result.ok) {
|
|
3388
3730
|
logger.error(result.error.message);
|
|
@@ -3393,7 +3735,7 @@ function createStreamsCommand() {
|
|
|
3393
3735
|
process.exit(ExitCode.SUCCESS);
|
|
3394
3736
|
});
|
|
3395
3737
|
command.command("activate <name>").description("Set the active stream").option("--path <path>", "Project root path", ".").action(async (name, opts) => {
|
|
3396
|
-
const projectPath =
|
|
3738
|
+
const projectPath = path30.resolve(opts.path);
|
|
3397
3739
|
const result = await setActiveStream(projectPath, name);
|
|
3398
3740
|
if (!result.ok) {
|
|
3399
3741
|
logger.error(result.error.message);
|
|
@@ -3418,18 +3760,17 @@ function createStateCommand() {
|
|
|
3418
3760
|
|
|
3419
3761
|
// src/commands/check-phase-gate.ts
|
|
3420
3762
|
import { Command as Command31 } from "commander";
|
|
3421
|
-
import * as
|
|
3422
|
-
import * as
|
|
3423
|
-
import { Ok as Ok18 } from "@harness-engineering/core";
|
|
3763
|
+
import * as path31 from "path";
|
|
3764
|
+
import * as fs19 from "fs";
|
|
3424
3765
|
function resolveSpecPath(implFile, implPattern, specPattern, cwd) {
|
|
3425
|
-
const relImpl =
|
|
3766
|
+
const relImpl = path31.relative(cwd, implFile);
|
|
3426
3767
|
const implBase = (implPattern.split("*")[0] ?? "").replace(/\/+$/, "");
|
|
3427
3768
|
const afterBase = relImpl.startsWith(implBase + "/") ? relImpl.slice(implBase.length + 1) : relImpl;
|
|
3428
3769
|
const segments = afterBase.split("/");
|
|
3429
3770
|
const firstSegment = segments[0] ?? "";
|
|
3430
|
-
const feature = segments.length > 1 ? firstSegment :
|
|
3771
|
+
const feature = segments.length > 1 ? firstSegment : path31.basename(firstSegment, path31.extname(firstSegment));
|
|
3431
3772
|
const specRelative = specPattern.replace("{feature}", feature);
|
|
3432
|
-
return
|
|
3773
|
+
return path31.resolve(cwd, specRelative);
|
|
3433
3774
|
}
|
|
3434
3775
|
async function runCheckPhaseGate(options) {
|
|
3435
3776
|
const configResult = resolveConfig(options.configPath);
|
|
@@ -3437,9 +3778,9 @@ async function runCheckPhaseGate(options) {
|
|
|
3437
3778
|
return configResult;
|
|
3438
3779
|
}
|
|
3439
3780
|
const config = configResult.value;
|
|
3440
|
-
const cwd = options.cwd ?? (options.configPath ?
|
|
3781
|
+
const cwd = options.cwd ?? (options.configPath ? path31.dirname(path31.resolve(options.configPath)) : process.cwd());
|
|
3441
3782
|
if (!config.phaseGates?.enabled) {
|
|
3442
|
-
return
|
|
3783
|
+
return Ok({
|
|
3443
3784
|
pass: true,
|
|
3444
3785
|
skipped: true,
|
|
3445
3786
|
missingSpecs: [],
|
|
@@ -3454,16 +3795,16 @@ async function runCheckPhaseGate(options) {
|
|
|
3454
3795
|
for (const implFile of implFiles) {
|
|
3455
3796
|
checkedFiles++;
|
|
3456
3797
|
const expectedSpec = resolveSpecPath(implFile, mapping.implPattern, mapping.specPattern, cwd);
|
|
3457
|
-
if (!
|
|
3798
|
+
if (!fs19.existsSync(expectedSpec)) {
|
|
3458
3799
|
missingSpecs.push({
|
|
3459
|
-
implFile:
|
|
3460
|
-
expectedSpec:
|
|
3800
|
+
implFile: path31.relative(cwd, implFile),
|
|
3801
|
+
expectedSpec: path31.relative(cwd, expectedSpec)
|
|
3461
3802
|
});
|
|
3462
3803
|
}
|
|
3463
3804
|
}
|
|
3464
3805
|
}
|
|
3465
3806
|
const pass = missingSpecs.length === 0;
|
|
3466
|
-
return
|
|
3807
|
+
return Ok({
|
|
3467
3808
|
pass,
|
|
3468
3809
|
skipped: false,
|
|
3469
3810
|
severity: phaseGates.severity,
|
|
@@ -3527,15 +3868,15 @@ function createCheckPhaseGateCommand() {
|
|
|
3527
3868
|
|
|
3528
3869
|
// src/commands/generate-slash-commands.ts
|
|
3529
3870
|
import { Command as Command32 } from "commander";
|
|
3530
|
-
import
|
|
3531
|
-
import
|
|
3871
|
+
import fs22 from "fs";
|
|
3872
|
+
import path34 from "path";
|
|
3532
3873
|
import os2 from "os";
|
|
3533
3874
|
import readline2 from "readline";
|
|
3534
3875
|
|
|
3535
3876
|
// src/slash-commands/normalize.ts
|
|
3536
|
-
import
|
|
3537
|
-
import
|
|
3538
|
-
import { parse as
|
|
3877
|
+
import fs20 from "fs";
|
|
3878
|
+
import path32 from "path";
|
|
3879
|
+
import { parse as parse8 } from "yaml";
|
|
3539
3880
|
|
|
3540
3881
|
// src/slash-commands/normalize-name.ts
|
|
3541
3882
|
function normalizeName(skillName) {
|
|
@@ -3555,18 +3896,18 @@ function normalizeSkills(skillSources, platforms) {
|
|
|
3555
3896
|
const specs = [];
|
|
3556
3897
|
const nameMap = /* @__PURE__ */ new Map();
|
|
3557
3898
|
for (const { dir: skillsDir, source } of skillSources) {
|
|
3558
|
-
if (!
|
|
3559
|
-
const entries =
|
|
3899
|
+
if (!fs20.existsSync(skillsDir)) continue;
|
|
3900
|
+
const entries = fs20.readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
3560
3901
|
for (const entry of entries) {
|
|
3561
|
-
const yamlPath =
|
|
3562
|
-
if (!
|
|
3902
|
+
const yamlPath = path32.join(skillsDir, entry.name, "skill.yaml");
|
|
3903
|
+
if (!fs20.existsSync(yamlPath)) continue;
|
|
3563
3904
|
let raw;
|
|
3564
3905
|
try {
|
|
3565
|
-
raw =
|
|
3906
|
+
raw = fs20.readFileSync(yamlPath, "utf-8");
|
|
3566
3907
|
} catch {
|
|
3567
3908
|
continue;
|
|
3568
3909
|
}
|
|
3569
|
-
const parsed =
|
|
3910
|
+
const parsed = parse8(raw);
|
|
3570
3911
|
const result = SkillMetadataSchema.safeParse(parsed);
|
|
3571
3912
|
if (!result.success) {
|
|
3572
3913
|
console.warn(`Skipping ${entry.name}: invalid skill.yaml`);
|
|
@@ -3586,15 +3927,15 @@ function normalizeSkills(skillSources, platforms) {
|
|
|
3586
3927
|
continue;
|
|
3587
3928
|
}
|
|
3588
3929
|
nameMap.set(normalized, { skillName: meta.name, source });
|
|
3589
|
-
const skillMdPath =
|
|
3590
|
-
const skillMdContent =
|
|
3591
|
-
const skillMdRelative =
|
|
3930
|
+
const skillMdPath = path32.join(skillsDir, entry.name, "SKILL.md");
|
|
3931
|
+
const skillMdContent = fs20.existsSync(skillMdPath) ? fs20.readFileSync(skillMdPath, "utf-8") : "";
|
|
3932
|
+
const skillMdRelative = path32.relative(
|
|
3592
3933
|
process.cwd(),
|
|
3593
|
-
|
|
3934
|
+
path32.join(skillsDir, entry.name, "SKILL.md")
|
|
3594
3935
|
);
|
|
3595
|
-
const skillYamlRelative =
|
|
3936
|
+
const skillYamlRelative = path32.relative(
|
|
3596
3937
|
process.cwd(),
|
|
3597
|
-
|
|
3938
|
+
path32.join(skillsDir, entry.name, "skill.yaml")
|
|
3598
3939
|
);
|
|
3599
3940
|
const args = (meta.cli?.args ?? []).map((a) => ({
|
|
3600
3941
|
name: a.name,
|
|
@@ -3763,8 +4104,8 @@ function renderGemini(spec, skillMdContent, skillYamlContent) {
|
|
|
3763
4104
|
}
|
|
3764
4105
|
|
|
3765
4106
|
// src/slash-commands/sync.ts
|
|
3766
|
-
import
|
|
3767
|
-
import
|
|
4107
|
+
import fs21 from "fs";
|
|
4108
|
+
import path33 from "path";
|
|
3768
4109
|
|
|
3769
4110
|
// src/agent-definitions/constants.ts
|
|
3770
4111
|
var GENERATED_HEADER_AGENT = "<!-- Generated by harness generate-agent-definitions. Do not edit. -->";
|
|
@@ -3776,11 +4117,11 @@ function computeSyncPlan(outputDir, rendered) {
|
|
|
3776
4117
|
const removed = [];
|
|
3777
4118
|
const unchanged = [];
|
|
3778
4119
|
for (const [filename, content] of rendered) {
|
|
3779
|
-
const filePath =
|
|
3780
|
-
if (!
|
|
4120
|
+
const filePath = path33.join(outputDir, filename);
|
|
4121
|
+
if (!fs21.existsSync(filePath)) {
|
|
3781
4122
|
added.push(filename);
|
|
3782
4123
|
} else {
|
|
3783
|
-
const existing =
|
|
4124
|
+
const existing = fs21.readFileSync(filePath, "utf-8");
|
|
3784
4125
|
if (existing === content) {
|
|
3785
4126
|
unchanged.push(filename);
|
|
3786
4127
|
} else {
|
|
@@ -3788,14 +4129,14 @@ function computeSyncPlan(outputDir, rendered) {
|
|
|
3788
4129
|
}
|
|
3789
4130
|
}
|
|
3790
4131
|
}
|
|
3791
|
-
if (
|
|
3792
|
-
const existing =
|
|
3793
|
-
const stat =
|
|
4132
|
+
if (fs21.existsSync(outputDir)) {
|
|
4133
|
+
const existing = fs21.readdirSync(outputDir).filter((f) => {
|
|
4134
|
+
const stat = fs21.statSync(path33.join(outputDir, f));
|
|
3794
4135
|
return stat.isFile();
|
|
3795
4136
|
});
|
|
3796
4137
|
for (const filename of existing) {
|
|
3797
4138
|
if (rendered.has(filename)) continue;
|
|
3798
|
-
const content =
|
|
4139
|
+
const content = fs21.readFileSync(path33.join(outputDir, filename), "utf-8");
|
|
3799
4140
|
if (content.includes(GENERATED_HEADER_CLAUDE) || content.includes(GENERATED_HEADER_GEMINI) || content.includes(GENERATED_HEADER_AGENT)) {
|
|
3800
4141
|
removed.push(filename);
|
|
3801
4142
|
}
|
|
@@ -3804,18 +4145,18 @@ function computeSyncPlan(outputDir, rendered) {
|
|
|
3804
4145
|
return { added, updated, removed, unchanged };
|
|
3805
4146
|
}
|
|
3806
4147
|
function applySyncPlan(outputDir, rendered, plan, deleteOrphans) {
|
|
3807
|
-
|
|
4148
|
+
fs21.mkdirSync(outputDir, { recursive: true });
|
|
3808
4149
|
for (const filename of [...plan.added, ...plan.updated]) {
|
|
3809
4150
|
const content = rendered.get(filename);
|
|
3810
4151
|
if (content !== void 0) {
|
|
3811
|
-
|
|
4152
|
+
fs21.writeFileSync(path33.join(outputDir, filename), content);
|
|
3812
4153
|
}
|
|
3813
4154
|
}
|
|
3814
4155
|
if (deleteOrphans) {
|
|
3815
4156
|
for (const filename of plan.removed) {
|
|
3816
|
-
const filePath =
|
|
3817
|
-
if (
|
|
3818
|
-
|
|
4157
|
+
const filePath = path33.join(outputDir, filename);
|
|
4158
|
+
if (fs21.existsSync(filePath)) {
|
|
4159
|
+
fs21.unlinkSync(filePath);
|
|
3819
4160
|
}
|
|
3820
4161
|
}
|
|
3821
4162
|
}
|
|
@@ -3824,24 +4165,24 @@ function applySyncPlan(outputDir, rendered, plan, deleteOrphans) {
|
|
|
3824
4165
|
// src/commands/generate-slash-commands.ts
|
|
3825
4166
|
function resolveOutputDir(platform, opts) {
|
|
3826
4167
|
if (opts.output) {
|
|
3827
|
-
return
|
|
4168
|
+
return path34.join(opts.output, "harness");
|
|
3828
4169
|
}
|
|
3829
4170
|
if (opts.global) {
|
|
3830
4171
|
const home = os2.homedir();
|
|
3831
|
-
return platform === "claude-code" ?
|
|
4172
|
+
return platform === "claude-code" ? path34.join(home, ".claude", "commands", "harness") : path34.join(home, ".gemini", "commands", "harness");
|
|
3832
4173
|
}
|
|
3833
|
-
return platform === "claude-code" ?
|
|
4174
|
+
return platform === "claude-code" ? path34.join("agents", "commands", "claude-code", "harness") : path34.join("agents", "commands", "gemini-cli", "harness");
|
|
3834
4175
|
}
|
|
3835
4176
|
function fileExtension(platform) {
|
|
3836
4177
|
return platform === "claude-code" ? ".md" : ".toml";
|
|
3837
4178
|
}
|
|
3838
4179
|
async function confirmDeletion(files) {
|
|
3839
4180
|
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
3840
|
-
return new Promise((
|
|
4181
|
+
return new Promise((resolve24) => {
|
|
3841
4182
|
rl.question(`
|
|
3842
4183
|
Remove ${files.length} orphaned command(s)? (y/N) `, (answer) => {
|
|
3843
4184
|
rl.close();
|
|
3844
|
-
|
|
4185
|
+
resolve24(answer.toLowerCase() === "y");
|
|
3845
4186
|
});
|
|
3846
4187
|
});
|
|
3847
4188
|
}
|
|
@@ -3856,7 +4197,7 @@ function generateSlashCommands(opts) {
|
|
|
3856
4197
|
}
|
|
3857
4198
|
if (opts.includeGlobal || skillSources.length === 0) {
|
|
3858
4199
|
const globalDir = resolveGlobalSkillsDir();
|
|
3859
|
-
if (!projectDir ||
|
|
4200
|
+
if (!projectDir || path34.resolve(globalDir) !== path34.resolve(projectDir)) {
|
|
3860
4201
|
skillSources.push({ dir: globalDir, source: "global" });
|
|
3861
4202
|
}
|
|
3862
4203
|
}
|
|
@@ -3878,7 +4219,7 @@ function generateSlashCommands(opts) {
|
|
|
3878
4219
|
executionContext: spec.prompt.executionContext.split("\n").map((line) => {
|
|
3879
4220
|
if (line.startsWith("@")) {
|
|
3880
4221
|
const relPath = line.slice(1);
|
|
3881
|
-
return `@${
|
|
4222
|
+
return `@${path34.resolve(relPath)}`;
|
|
3882
4223
|
}
|
|
3883
4224
|
return line;
|
|
3884
4225
|
}).join("\n")
|
|
@@ -3886,10 +4227,10 @@ function generateSlashCommands(opts) {
|
|
|
3886
4227
|
} : spec;
|
|
3887
4228
|
rendered.set(filename, renderClaudeCode(renderSpec));
|
|
3888
4229
|
} else {
|
|
3889
|
-
const mdPath =
|
|
3890
|
-
const yamlPath =
|
|
3891
|
-
const mdContent =
|
|
3892
|
-
const yamlContent =
|
|
4230
|
+
const mdPath = path34.join(spec.skillsBaseDir, spec.sourceDir, "SKILL.md");
|
|
4231
|
+
const yamlPath = path34.join(spec.skillsBaseDir, spec.sourceDir, "skill.yaml");
|
|
4232
|
+
const mdContent = fs22.existsSync(mdPath) ? fs22.readFileSync(mdPath, "utf-8") : "";
|
|
4233
|
+
const yamlContent = fs22.existsSync(yamlPath) ? fs22.readFileSync(yamlPath, "utf-8") : "";
|
|
3893
4234
|
rendered.set(filename, renderGemini(spec, mdContent, yamlContent));
|
|
3894
4235
|
}
|
|
3895
4236
|
}
|
|
@@ -3915,9 +4256,9 @@ async function handleOrphanDeletion(results, opts) {
|
|
|
3915
4256
|
const shouldDelete = opts.yes || await confirmDeletion(result.removed);
|
|
3916
4257
|
if (shouldDelete) {
|
|
3917
4258
|
for (const filename of result.removed) {
|
|
3918
|
-
const filePath =
|
|
3919
|
-
if (
|
|
3920
|
-
|
|
4259
|
+
const filePath = path34.join(result.outputDir, filename);
|
|
4260
|
+
if (fs22.existsSync(filePath)) {
|
|
4261
|
+
fs22.unlinkSync(filePath);
|
|
3921
4262
|
}
|
|
3922
4263
|
}
|
|
3923
4264
|
}
|
|
@@ -3993,7 +4334,6 @@ import { Command as Command35 } from "commander";
|
|
|
3993
4334
|
|
|
3994
4335
|
// src/commands/ci/check.ts
|
|
3995
4336
|
import { Command as Command33 } from "commander";
|
|
3996
|
-
import { runCIChecks } from "@harness-engineering/core";
|
|
3997
4337
|
var VALID_CHECKS = ["validate", "deps", "docs", "entropy", "phase-gate"];
|
|
3998
4338
|
async function runCICheck(options) {
|
|
3999
4339
|
const configResult = resolveConfig(options.configPath);
|
|
@@ -4069,9 +4409,8 @@ function createCheckCommand() {
|
|
|
4069
4409
|
|
|
4070
4410
|
// src/commands/ci/init.ts
|
|
4071
4411
|
import { Command as Command34 } from "commander";
|
|
4072
|
-
import * as
|
|
4073
|
-
import * as
|
|
4074
|
-
import { Ok as Ok19, Err as Err14 } from "@harness-engineering/core";
|
|
4412
|
+
import * as fs23 from "fs";
|
|
4413
|
+
import * as path35 from "path";
|
|
4075
4414
|
var ALL_CHECKS = ["validate", "deps", "docs", "entropy", "phase-gate"];
|
|
4076
4415
|
function buildSkipFlag(checks) {
|
|
4077
4416
|
if (!checks) return "";
|
|
@@ -4155,16 +4494,16 @@ function generateCIConfig(options) {
|
|
|
4155
4494
|
};
|
|
4156
4495
|
const entry = generators[platform];
|
|
4157
4496
|
if (!entry) {
|
|
4158
|
-
return
|
|
4497
|
+
return Err(new CLIError(`Unknown platform: ${platform}`, ExitCode.ERROR));
|
|
4159
4498
|
}
|
|
4160
|
-
return
|
|
4499
|
+
return Ok({
|
|
4161
4500
|
filename: entry.filename,
|
|
4162
4501
|
content: entry.generate(skipFlag)
|
|
4163
4502
|
});
|
|
4164
4503
|
}
|
|
4165
4504
|
function detectPlatform() {
|
|
4166
|
-
if (
|
|
4167
|
-
if (
|
|
4505
|
+
if (fs23.existsSync(".github")) return "github";
|
|
4506
|
+
if (fs23.existsSync(".gitlab-ci.yml")) return "gitlab";
|
|
4168
4507
|
return null;
|
|
4169
4508
|
}
|
|
4170
4509
|
function createInitCommand2() {
|
|
@@ -4180,12 +4519,12 @@ function createInitCommand2() {
|
|
|
4180
4519
|
process.exit(result.error.exitCode);
|
|
4181
4520
|
}
|
|
4182
4521
|
const { filename, content } = result.value;
|
|
4183
|
-
const targetPath =
|
|
4184
|
-
const dir =
|
|
4185
|
-
|
|
4186
|
-
|
|
4522
|
+
const targetPath = path35.resolve(filename);
|
|
4523
|
+
const dir = path35.dirname(targetPath);
|
|
4524
|
+
fs23.mkdirSync(dir, { recursive: true });
|
|
4525
|
+
fs23.writeFileSync(targetPath, content);
|
|
4187
4526
|
if (platform === "generic") {
|
|
4188
|
-
|
|
4527
|
+
fs23.chmodSync(targetPath, "755");
|
|
4189
4528
|
}
|
|
4190
4529
|
if (globalOpts.json) {
|
|
4191
4530
|
console.log(JSON.stringify({ file: filename, platform }));
|
|
@@ -4206,7 +4545,7 @@ function createCICommand() {
|
|
|
4206
4545
|
|
|
4207
4546
|
// src/commands/update.ts
|
|
4208
4547
|
import { Command as Command36 } from "commander";
|
|
4209
|
-
import {
|
|
4548
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
4210
4549
|
import { realpathSync } from "fs";
|
|
4211
4550
|
import readline3 from "readline";
|
|
4212
4551
|
import chalk4 from "chalk";
|
|
@@ -4226,7 +4565,7 @@ function detectPackageManager() {
|
|
|
4226
4565
|
return "npm";
|
|
4227
4566
|
}
|
|
4228
4567
|
function getLatestVersion(pkg = "@harness-engineering/cli") {
|
|
4229
|
-
const output =
|
|
4568
|
+
const output = execFileSync3("npm", ["view", pkg, "dist-tags.latest"], {
|
|
4230
4569
|
encoding: "utf-8",
|
|
4231
4570
|
timeout: 15e3
|
|
4232
4571
|
});
|
|
@@ -4234,7 +4573,7 @@ function getLatestVersion(pkg = "@harness-engineering/cli") {
|
|
|
4234
4573
|
}
|
|
4235
4574
|
function getInstalledVersion(pm) {
|
|
4236
4575
|
try {
|
|
4237
|
-
const output =
|
|
4576
|
+
const output = execFileSync3(pm, ["list", "-g", "@harness-engineering/cli", "--json"], {
|
|
4238
4577
|
encoding: "utf-8",
|
|
4239
4578
|
timeout: 15e3
|
|
4240
4579
|
});
|
|
@@ -4247,7 +4586,7 @@ function getInstalledVersion(pm) {
|
|
|
4247
4586
|
}
|
|
4248
4587
|
function getInstalledPackages(pm) {
|
|
4249
4588
|
try {
|
|
4250
|
-
const output =
|
|
4589
|
+
const output = execFileSync3(pm, ["list", "-g", "--json"], {
|
|
4251
4590
|
encoding: "utf-8",
|
|
4252
4591
|
timeout: 15e3
|
|
4253
4592
|
});
|
|
@@ -4263,10 +4602,10 @@ function prompt(question) {
|
|
|
4263
4602
|
input: process.stdin,
|
|
4264
4603
|
output: process.stdout
|
|
4265
4604
|
});
|
|
4266
|
-
return new Promise((
|
|
4605
|
+
return new Promise((resolve24) => {
|
|
4267
4606
|
rl.question(question, (answer) => {
|
|
4268
4607
|
rl.close();
|
|
4269
|
-
|
|
4608
|
+
resolve24(answer.trim().toLowerCase());
|
|
4270
4609
|
});
|
|
4271
4610
|
});
|
|
4272
4611
|
}
|
|
@@ -4302,19 +4641,19 @@ function createUpdateCommand() {
|
|
|
4302
4641
|
if (globalOpts.verbose) {
|
|
4303
4642
|
logger.info(`Installed packages: ${packages.join(", ")}`);
|
|
4304
4643
|
}
|
|
4305
|
-
const
|
|
4644
|
+
const installPkgs = packages.map((pkg) => {
|
|
4306
4645
|
if (opts.version && pkg === "@harness-engineering/cli") {
|
|
4307
4646
|
return `${pkg}@${opts.version}`;
|
|
4308
4647
|
}
|
|
4309
4648
|
return `${pkg}@latest`;
|
|
4310
|
-
})
|
|
4311
|
-
const installCmd = `${pm} install -g ${
|
|
4649
|
+
});
|
|
4650
|
+
const installCmd = `${pm} install -g ${installPkgs.join(" ")}`;
|
|
4312
4651
|
if (globalOpts.verbose) {
|
|
4313
4652
|
logger.info(`Running: ${installCmd}`);
|
|
4314
4653
|
}
|
|
4315
4654
|
try {
|
|
4316
4655
|
logger.info("Updating packages...");
|
|
4317
|
-
|
|
4656
|
+
execFileSync3(pm, ["install", "-g", ...installPkgs], { stdio: "inherit", timeout: 12e4 });
|
|
4318
4657
|
console.log("");
|
|
4319
4658
|
logger.success("Update complete");
|
|
4320
4659
|
} catch {
|
|
@@ -4327,12 +4666,14 @@ function createUpdateCommand() {
|
|
|
4327
4666
|
const regenAnswer = await prompt("Regenerate slash commands and agent definitions? (y/N) ");
|
|
4328
4667
|
if (regenAnswer === "y" || regenAnswer === "yes") {
|
|
4329
4668
|
const scopeAnswer = await prompt("Generate for (g)lobal or (l)ocal project? (g/l) ");
|
|
4330
|
-
const
|
|
4669
|
+
const isGlobal = scopeAnswer === "g" || scopeAnswer === "global";
|
|
4331
4670
|
try {
|
|
4332
|
-
|
|
4671
|
+
execFileSync3("harness", ["generate", ...isGlobal ? ["--global"] : []], {
|
|
4672
|
+
stdio: "inherit"
|
|
4673
|
+
});
|
|
4333
4674
|
} catch {
|
|
4334
4675
|
logger.warn("Generation failed. Run manually:");
|
|
4335
|
-
console.log(` ${chalk4.cyan(`harness generate${
|
|
4676
|
+
console.log(` ${chalk4.cyan(`harness generate${isGlobal ? " --global" : ""}`)}`);
|
|
4336
4677
|
}
|
|
4337
4678
|
}
|
|
4338
4679
|
process.exit(ExitCode.SUCCESS);
|
|
@@ -4341,8 +4682,8 @@ function createUpdateCommand() {
|
|
|
4341
4682
|
|
|
4342
4683
|
// src/commands/generate-agent-definitions.ts
|
|
4343
4684
|
import { Command as Command37 } from "commander";
|
|
4344
|
-
import * as
|
|
4345
|
-
import * as
|
|
4685
|
+
import * as fs24 from "fs";
|
|
4686
|
+
import * as path36 from "path";
|
|
4346
4687
|
import * as os3 from "os";
|
|
4347
4688
|
|
|
4348
4689
|
// src/agent-definitions/generator.ts
|
|
@@ -4352,7 +4693,9 @@ var AGENT_DESCRIPTIONS = {
|
|
|
4352
4693
|
"parallel-coordinator": "Dispatch independent tasks across isolated agents for parallel execution. Use when multiple independent tasks need to run concurrently, splitting work across agents, or coordinating parallel implementation.",
|
|
4353
4694
|
"architecture-enforcer": "Validate architectural constraints and dependency rules. Use when checking layer boundaries, detecting circular dependencies, or verifying import direction compliance.",
|
|
4354
4695
|
"documentation-maintainer": "Keep documentation in sync with source code. Use when detecting documentation drift, validating doc coverage, or aligning docs with code changes.",
|
|
4355
|
-
"entropy-cleaner": "Detect and fix codebase entropy including drift, dead code, and pattern violations. Use when running cleanup, detecting dead code, or fixing pattern violations."
|
|
4696
|
+
"entropy-cleaner": "Detect and fix codebase entropy including drift, dead code, and pattern violations. Use when running cleanup, detecting dead code, or fixing pattern violations.",
|
|
4697
|
+
planner: "Create detailed implementation plans from specs with task breakdown, dependency ordering, and checkpoint placement. Use when planning a phase, breaking a spec into tasks, or creating an execution plan.",
|
|
4698
|
+
verifier: "Verify implementation completeness against spec and plan at three tiers (EXISTS, SUBSTANTIVE, WIRED). Use when checking if built code matches what was planned, validating phase completion, or auditing implementation quality."
|
|
4356
4699
|
};
|
|
4357
4700
|
var DEFAULT_TOOLS = ["Bash", "Read", "Write", "Edit", "Glob", "Grep"];
|
|
4358
4701
|
function generateAgentDefinition(persona, skillContents) {
|
|
@@ -4445,7 +4788,12 @@ function renderGeminiAgent(def) {
|
|
|
4445
4788
|
lines.push(`name: ${def.name}`);
|
|
4446
4789
|
lines.push(`description: >`);
|
|
4447
4790
|
lines.push(` ${def.description}`);
|
|
4448
|
-
|
|
4791
|
+
if (def.tools.length > 0) {
|
|
4792
|
+
lines.push("tools:");
|
|
4793
|
+
for (const tool of def.tools) {
|
|
4794
|
+
lines.push(` - ${tool}`);
|
|
4795
|
+
}
|
|
4796
|
+
}
|
|
4449
4797
|
lines.push("---");
|
|
4450
4798
|
lines.push("");
|
|
4451
4799
|
lines.push(GENERATED_HEADER_AGENT);
|
|
@@ -4478,19 +4826,19 @@ function renderGeminiAgent(def) {
|
|
|
4478
4826
|
// src/commands/generate-agent-definitions.ts
|
|
4479
4827
|
function resolveOutputDir2(platform, opts) {
|
|
4480
4828
|
if (opts.output) {
|
|
4481
|
-
return platform === "claude-code" ?
|
|
4829
|
+
return platform === "claude-code" ? path36.join(opts.output, "claude-code") : path36.join(opts.output, "gemini-cli");
|
|
4482
4830
|
}
|
|
4483
4831
|
if (opts.global) {
|
|
4484
4832
|
const home = os3.homedir();
|
|
4485
|
-
return platform === "claude-code" ?
|
|
4833
|
+
return platform === "claude-code" ? path36.join(home, ".claude", "agents") : path36.join(home, ".gemini", "agents");
|
|
4486
4834
|
}
|
|
4487
|
-
return platform === "claude-code" ?
|
|
4835
|
+
return platform === "claude-code" ? path36.join("agents", "agents", "claude-code") : path36.join("agents", "agents", "gemini-cli");
|
|
4488
4836
|
}
|
|
4489
4837
|
function loadSkillContent(skillName) {
|
|
4490
4838
|
const skillsDir = resolveSkillsDir();
|
|
4491
|
-
const skillMdPath =
|
|
4492
|
-
if (!
|
|
4493
|
-
return
|
|
4839
|
+
const skillMdPath = path36.join(skillsDir, skillName, "SKILL.md");
|
|
4840
|
+
if (!fs24.existsSync(skillMdPath)) return null;
|
|
4841
|
+
return fs24.readFileSync(skillMdPath, "utf-8");
|
|
4494
4842
|
}
|
|
4495
4843
|
function getRenderer(platform) {
|
|
4496
4844
|
return platform === "claude-code" ? renderClaudeCodeAgent : renderGeminiAgent;
|
|
@@ -4644,9 +4992,9 @@ function createGenerateCommand3() {
|
|
|
4644
4992
|
|
|
4645
4993
|
// src/commands/graph/scan.ts
|
|
4646
4994
|
import { Command as Command39 } from "commander";
|
|
4647
|
-
import * as
|
|
4995
|
+
import * as path37 from "path";
|
|
4648
4996
|
async function runScan(projectPath) {
|
|
4649
|
-
const { GraphStore, CodeIngestor, TopologicalLinker, KnowledgeIngestor, GitIngestor } = await import("
|
|
4997
|
+
const { GraphStore, CodeIngestor, TopologicalLinker, KnowledgeIngestor, GitIngestor } = await import("./dist-N4D4QWFV.js");
|
|
4650
4998
|
const store = new GraphStore();
|
|
4651
4999
|
const start = Date.now();
|
|
4652
5000
|
await new CodeIngestor(store).ingest(projectPath);
|
|
@@ -4657,13 +5005,13 @@ async function runScan(projectPath) {
|
|
|
4657
5005
|
await new GitIngestor(store).ingest(projectPath);
|
|
4658
5006
|
} catch {
|
|
4659
5007
|
}
|
|
4660
|
-
const graphDir =
|
|
5008
|
+
const graphDir = path37.join(projectPath, ".harness", "graph");
|
|
4661
5009
|
await store.save(graphDir);
|
|
4662
5010
|
return { nodeCount: store.nodeCount, edgeCount: store.edgeCount, durationMs: Date.now() - start };
|
|
4663
5011
|
}
|
|
4664
5012
|
function createScanCommand() {
|
|
4665
5013
|
return new Command39("scan").description("Scan project and build knowledge graph").argument("[path]", "Project root path", ".").action(async (inputPath, _opts, cmd) => {
|
|
4666
|
-
const projectPath =
|
|
5014
|
+
const projectPath = path37.resolve(inputPath);
|
|
4667
5015
|
const globalOpts = cmd.optsWithGlobals();
|
|
4668
5016
|
try {
|
|
4669
5017
|
const result = await runScan(projectPath);
|
|
@@ -4683,12 +5031,12 @@ function createScanCommand() {
|
|
|
4683
5031
|
|
|
4684
5032
|
// src/commands/graph/ingest.ts
|
|
4685
5033
|
import { Command as Command40 } from "commander";
|
|
4686
|
-
import * as
|
|
5034
|
+
import * as path38 from "path";
|
|
4687
5035
|
async function loadConnectorConfig(projectPath, source) {
|
|
4688
5036
|
try {
|
|
4689
|
-
const
|
|
4690
|
-
const configPath =
|
|
4691
|
-
const config = JSON.parse(await
|
|
5037
|
+
const fs25 = await import("fs/promises");
|
|
5038
|
+
const configPath = path38.join(projectPath, "harness.config.json");
|
|
5039
|
+
const config = JSON.parse(await fs25.readFile(configPath, "utf-8"));
|
|
4692
5040
|
const connector = config.graph?.connectors?.find(
|
|
4693
5041
|
(c) => c.source === source
|
|
4694
5042
|
);
|
|
@@ -4727,8 +5075,8 @@ async function runIngest(projectPath, source, opts) {
|
|
|
4727
5075
|
SyncManager,
|
|
4728
5076
|
JiraConnector,
|
|
4729
5077
|
SlackConnector
|
|
4730
|
-
} = await import("
|
|
4731
|
-
const graphDir =
|
|
5078
|
+
} = await import("./dist-N4D4QWFV.js");
|
|
5079
|
+
const graphDir = path38.join(projectPath, ".harness", "graph");
|
|
4732
5080
|
const store = new GraphStore();
|
|
4733
5081
|
await store.load(graphDir);
|
|
4734
5082
|
if (opts?.all) {
|
|
@@ -4795,7 +5143,7 @@ function createIngestCommand() {
|
|
|
4795
5143
|
process.exit(1);
|
|
4796
5144
|
}
|
|
4797
5145
|
const globalOpts = cmd.optsWithGlobals();
|
|
4798
|
-
const projectPath =
|
|
5146
|
+
const projectPath = path38.resolve(globalOpts.config ? path38.dirname(globalOpts.config) : ".");
|
|
4799
5147
|
try {
|
|
4800
5148
|
const result = await runIngest(projectPath, opts.source ?? "", {
|
|
4801
5149
|
full: opts.full,
|
|
@@ -4818,11 +5166,11 @@ function createIngestCommand() {
|
|
|
4818
5166
|
|
|
4819
5167
|
// src/commands/graph/query.ts
|
|
4820
5168
|
import { Command as Command41 } from "commander";
|
|
4821
|
-
import * as
|
|
5169
|
+
import * as path39 from "path";
|
|
4822
5170
|
async function runQuery(projectPath, rootNodeId, opts) {
|
|
4823
|
-
const { GraphStore, ContextQL } = await import("
|
|
5171
|
+
const { GraphStore, ContextQL } = await import("./dist-N4D4QWFV.js");
|
|
4824
5172
|
const store = new GraphStore();
|
|
4825
|
-
const graphDir =
|
|
5173
|
+
const graphDir = path39.join(projectPath, ".harness", "graph");
|
|
4826
5174
|
const loaded = await store.load(graphDir);
|
|
4827
5175
|
if (!loaded) throw new Error("No graph found. Run `harness scan` first.");
|
|
4828
5176
|
const params = {
|
|
@@ -4838,7 +5186,7 @@ async function runQuery(projectPath, rootNodeId, opts) {
|
|
|
4838
5186
|
function createQueryCommand() {
|
|
4839
5187
|
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) => {
|
|
4840
5188
|
const globalOpts = cmd.optsWithGlobals();
|
|
4841
|
-
const projectPath =
|
|
5189
|
+
const projectPath = path39.resolve(globalOpts.config ? path39.dirname(globalOpts.config) : ".");
|
|
4842
5190
|
try {
|
|
4843
5191
|
const result = await runQuery(projectPath, rootNodeId, {
|
|
4844
5192
|
depth: parseInt(opts.depth),
|
|
@@ -4867,18 +5215,18 @@ function createQueryCommand() {
|
|
|
4867
5215
|
import { Command as Command42 } from "commander";
|
|
4868
5216
|
|
|
4869
5217
|
// src/commands/graph/status.ts
|
|
4870
|
-
import * as
|
|
5218
|
+
import * as path40 from "path";
|
|
4871
5219
|
async function runGraphStatus(projectPath) {
|
|
4872
|
-
const { GraphStore } = await import("
|
|
4873
|
-
const graphDir =
|
|
5220
|
+
const { GraphStore } = await import("./dist-N4D4QWFV.js");
|
|
5221
|
+
const graphDir = path40.join(projectPath, ".harness", "graph");
|
|
4874
5222
|
const store = new GraphStore();
|
|
4875
5223
|
const loaded = await store.load(graphDir);
|
|
4876
5224
|
if (!loaded) return { status: "no_graph", message: "No graph found. Run `harness scan` first." };
|
|
4877
|
-
const
|
|
4878
|
-
const metaPath =
|
|
5225
|
+
const fs25 = await import("fs/promises");
|
|
5226
|
+
const metaPath = path40.join(graphDir, "metadata.json");
|
|
4879
5227
|
let lastScan = "unknown";
|
|
4880
5228
|
try {
|
|
4881
|
-
const meta = JSON.parse(await
|
|
5229
|
+
const meta = JSON.parse(await fs25.readFile(metaPath, "utf-8"));
|
|
4882
5230
|
lastScan = meta.lastScanTimestamp;
|
|
4883
5231
|
} catch {
|
|
4884
5232
|
}
|
|
@@ -4889,8 +5237,8 @@ async function runGraphStatus(projectPath) {
|
|
|
4889
5237
|
}
|
|
4890
5238
|
let connectorSyncStatus = {};
|
|
4891
5239
|
try {
|
|
4892
|
-
const syncMetaPath =
|
|
4893
|
-
const syncMeta = JSON.parse(await
|
|
5240
|
+
const syncMetaPath = path40.join(graphDir, "sync-metadata.json");
|
|
5241
|
+
const syncMeta = JSON.parse(await fs25.readFile(syncMetaPath, "utf-8"));
|
|
4894
5242
|
for (const [name, data] of Object.entries(syncMeta.connectors ?? {})) {
|
|
4895
5243
|
connectorSyncStatus[name] = data.lastSyncTimestamp;
|
|
4896
5244
|
}
|
|
@@ -4907,10 +5255,10 @@ async function runGraphStatus(projectPath) {
|
|
|
4907
5255
|
}
|
|
4908
5256
|
|
|
4909
5257
|
// src/commands/graph/export.ts
|
|
4910
|
-
import * as
|
|
5258
|
+
import * as path41 from "path";
|
|
4911
5259
|
async function runGraphExport(projectPath, format) {
|
|
4912
|
-
const { GraphStore } = await import("
|
|
4913
|
-
const graphDir =
|
|
5260
|
+
const { GraphStore } = await import("./dist-N4D4QWFV.js");
|
|
5261
|
+
const graphDir = path41.join(projectPath, ".harness", "graph");
|
|
4914
5262
|
const store = new GraphStore();
|
|
4915
5263
|
const loaded = await store.load(graphDir);
|
|
4916
5264
|
if (!loaded) throw new Error("No graph found. Run `harness scan` first.");
|
|
@@ -4939,13 +5287,13 @@ async function runGraphExport(projectPath, format) {
|
|
|
4939
5287
|
}
|
|
4940
5288
|
|
|
4941
5289
|
// src/commands/graph/index.ts
|
|
4942
|
-
import * as
|
|
5290
|
+
import * as path42 from "path";
|
|
4943
5291
|
function createGraphCommand() {
|
|
4944
5292
|
const graph = new Command42("graph").description("Knowledge graph management");
|
|
4945
5293
|
graph.command("status").description("Show graph statistics").action(async (_opts, cmd) => {
|
|
4946
5294
|
try {
|
|
4947
5295
|
const globalOpts = cmd.optsWithGlobals();
|
|
4948
|
-
const projectPath =
|
|
5296
|
+
const projectPath = path42.resolve(globalOpts.config ? path42.dirname(globalOpts.config) : ".");
|
|
4949
5297
|
const result = await runGraphStatus(projectPath);
|
|
4950
5298
|
if (globalOpts.json) {
|
|
4951
5299
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -4972,7 +5320,7 @@ function createGraphCommand() {
|
|
|
4972
5320
|
});
|
|
4973
5321
|
graph.command("export").description("Export graph").requiredOption("--format <format>", "Output format (json, mermaid)").action(async (opts, cmd) => {
|
|
4974
5322
|
const globalOpts = cmd.optsWithGlobals();
|
|
4975
|
-
const projectPath =
|
|
5323
|
+
const projectPath = path42.resolve(globalOpts.config ? path42.dirname(globalOpts.config) : ".");
|
|
4976
5324
|
try {
|
|
4977
5325
|
const output = await runGraphExport(projectPath, opts.format);
|
|
4978
5326
|
console.log(output);
|