@harness-engineering/cli 1.0.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.
@@ -0,0 +1,1304 @@
1
+ // src/index.ts
2
+ import { Command as Command14 } from "commander";
3
+ import { VERSION } from "@harness-engineering/core";
4
+
5
+ // src/commands/validate.ts
6
+ import { Command } from "commander";
7
+ import * as path2 from "path";
8
+ import { Ok as Ok2 } from "@harness-engineering/core";
9
+ import { validateAgentsMap, validateKnowledgeMap } from "@harness-engineering/core";
10
+
11
+ // src/config/loader.ts
12
+ import * as fs from "fs";
13
+ import * as path from "path";
14
+ import { Ok, Err } from "@harness-engineering/core";
15
+
16
+ // src/config/schema.ts
17
+ import { z } from "zod";
18
+ var LayerSchema = z.object({
19
+ name: z.string(),
20
+ pattern: z.string(),
21
+ allowedDependencies: z.array(z.string())
22
+ });
23
+ var ForbiddenImportSchema = z.object({
24
+ from: z.string(),
25
+ disallow: z.array(z.string()),
26
+ message: z.string().optional()
27
+ });
28
+ var BoundaryConfigSchema = z.object({
29
+ requireSchema: z.array(z.string())
30
+ });
31
+ var AgentConfigSchema = z.object({
32
+ executor: z.enum(["subprocess", "cloud", "noop"]).default("subprocess"),
33
+ timeout: z.number().default(3e5),
34
+ skills: z.array(z.string()).optional()
35
+ });
36
+ var EntropyConfigSchema = z.object({
37
+ excludePatterns: z.array(z.string()).default(["**/node_modules/**", "**/*.test.ts"]),
38
+ autoFix: z.boolean().default(false)
39
+ });
40
+ var HarnessConfigSchema = z.object({
41
+ version: z.literal(1),
42
+ name: z.string().optional(),
43
+ rootDir: z.string().default("."),
44
+ layers: z.array(LayerSchema).optional(),
45
+ forbiddenImports: z.array(ForbiddenImportSchema).optional(),
46
+ boundaries: BoundaryConfigSchema.optional(),
47
+ agentsMapPath: z.string().default("./AGENTS.md"),
48
+ docsDir: z.string().default("./docs"),
49
+ agent: AgentConfigSchema.optional(),
50
+ entropy: EntropyConfigSchema.optional()
51
+ });
52
+
53
+ // src/utils/errors.ts
54
+ var ExitCode = {
55
+ SUCCESS: 0,
56
+ VALIDATION_FAILED: 1,
57
+ ERROR: 2
58
+ };
59
+ var CLIError = class extends Error {
60
+ exitCode;
61
+ constructor(message, exitCode = ExitCode.ERROR) {
62
+ super(message);
63
+ this.name = "CLIError";
64
+ this.exitCode = exitCode;
65
+ }
66
+ };
67
+ function formatError(error) {
68
+ if (error instanceof CLIError) {
69
+ return `Error: ${error.message}`;
70
+ }
71
+ if (error instanceof Error) {
72
+ return `Error: ${error.message}`;
73
+ }
74
+ return `Error: ${String(error)}`;
75
+ }
76
+ function handleError(error) {
77
+ const message = formatError(error);
78
+ console.error(message);
79
+ const exitCode = error instanceof CLIError ? error.exitCode : ExitCode.ERROR;
80
+ process.exit(exitCode);
81
+ }
82
+
83
+ // src/config/loader.ts
84
+ var CONFIG_FILENAMES = ["harness.config.json"];
85
+ function findConfigFile(startDir = process.cwd()) {
86
+ let currentDir = path.resolve(startDir);
87
+ const root = path.parse(currentDir).root;
88
+ while (currentDir !== root) {
89
+ for (const filename of CONFIG_FILENAMES) {
90
+ const configPath = path.join(currentDir, filename);
91
+ if (fs.existsSync(configPath)) {
92
+ return Ok(configPath);
93
+ }
94
+ }
95
+ currentDir = path.dirname(currentDir);
96
+ }
97
+ return Err(new CLIError(
98
+ 'No harness.config.json found. Run "harness init" to create one.',
99
+ ExitCode.ERROR
100
+ ));
101
+ }
102
+ function loadConfig(configPath) {
103
+ if (!fs.existsSync(configPath)) {
104
+ return Err(new CLIError(
105
+ `Config file not found: ${configPath}`,
106
+ ExitCode.ERROR
107
+ ));
108
+ }
109
+ let rawConfig;
110
+ try {
111
+ const content = fs.readFileSync(configPath, "utf-8");
112
+ rawConfig = JSON.parse(content);
113
+ } catch (error) {
114
+ return Err(new CLIError(
115
+ `Failed to parse config: ${error instanceof Error ? error.message : "Unknown error"}`,
116
+ ExitCode.ERROR
117
+ ));
118
+ }
119
+ const parsed = HarnessConfigSchema.safeParse(rawConfig);
120
+ if (!parsed.success) {
121
+ const issues = parsed.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
122
+ return Err(new CLIError(
123
+ `Invalid config:
124
+ ${issues}`,
125
+ ExitCode.ERROR
126
+ ));
127
+ }
128
+ return Ok(parsed.data);
129
+ }
130
+ function resolveConfig(configPath) {
131
+ if (configPath) {
132
+ return loadConfig(configPath);
133
+ }
134
+ const findResult = findConfigFile();
135
+ if (!findResult.ok) {
136
+ return findResult;
137
+ }
138
+ return loadConfig(findResult.value);
139
+ }
140
+
141
+ // src/output/formatter.ts
142
+ import chalk from "chalk";
143
+ var OutputMode = {
144
+ JSON: "json",
145
+ TEXT: "text",
146
+ QUIET: "quiet",
147
+ VERBOSE: "verbose"
148
+ };
149
+ var OutputFormatter = class {
150
+ constructor(mode = OutputMode.TEXT) {
151
+ this.mode = mode;
152
+ }
153
+ /**
154
+ * Format raw data (for JSON mode)
155
+ */
156
+ format(data) {
157
+ if (this.mode === OutputMode.JSON) {
158
+ return JSON.stringify(data, null, 2);
159
+ }
160
+ return String(data);
161
+ }
162
+ /**
163
+ * Format validation result
164
+ */
165
+ formatValidation(result) {
166
+ if (this.mode === OutputMode.JSON) {
167
+ return JSON.stringify(result, null, 2);
168
+ }
169
+ if (this.mode === OutputMode.QUIET) {
170
+ if (result.valid) return "";
171
+ return result.issues.map((i) => `${i.file ?? ""}: ${i.message}`).join("\n");
172
+ }
173
+ const lines = [];
174
+ if (result.valid) {
175
+ lines.push(chalk.green("v validation passed"));
176
+ } else {
177
+ lines.push(chalk.red(`x Validation failed (${result.issues.length} issues)`));
178
+ lines.push("");
179
+ for (const issue of result.issues) {
180
+ const location = issue.file ? issue.line ? `${issue.file}:${issue.line}` : issue.file : "unknown";
181
+ lines.push(` ${chalk.yellow("*")} ${chalk.dim(location)}`);
182
+ lines.push(` ${issue.message}`);
183
+ if (issue.suggestion && this.mode === OutputMode.VERBOSE) {
184
+ lines.push(` ${chalk.dim("->")} ${issue.suggestion}`);
185
+ }
186
+ }
187
+ }
188
+ return lines.join("\n");
189
+ }
190
+ /**
191
+ * Format a summary line
192
+ */
193
+ formatSummary(label, value, success) {
194
+ if (this.mode === OutputMode.JSON || this.mode === OutputMode.QUIET) {
195
+ return "";
196
+ }
197
+ const icon = success ? chalk.green("v") : chalk.red("x");
198
+ return `${icon} ${label}: ${value}`;
199
+ }
200
+ };
201
+
202
+ // src/output/logger.ts
203
+ import chalk2 from "chalk";
204
+ var logger = {
205
+ info: (message) => console.log(chalk2.blue("i"), message),
206
+ success: (message) => console.log(chalk2.green("v"), message),
207
+ warn: (message) => console.log(chalk2.yellow("!"), message),
208
+ error: (message) => console.error(chalk2.red("x"), message),
209
+ dim: (message) => console.log(chalk2.dim(message)),
210
+ // For JSON output mode
211
+ raw: (data) => console.log(JSON.stringify(data, null, 2))
212
+ };
213
+
214
+ // src/commands/validate.ts
215
+ async function runValidate(options) {
216
+ const configResult = resolveConfig(options.configPath);
217
+ if (!configResult.ok) {
218
+ return configResult;
219
+ }
220
+ const config = configResult.value;
221
+ const cwd = options.cwd ?? (options.configPath ? path2.dirname(path2.resolve(options.configPath)) : process.cwd());
222
+ const result = {
223
+ valid: true,
224
+ checks: {
225
+ agentsMap: false,
226
+ fileStructure: false,
227
+ knowledgeMap: false
228
+ },
229
+ issues: []
230
+ };
231
+ const agentsMapPath = path2.resolve(cwd, config.agentsMapPath);
232
+ const agentsResult = await validateAgentsMap(agentsMapPath);
233
+ if (agentsResult.ok) {
234
+ result.checks.agentsMap = true;
235
+ } else {
236
+ result.valid = false;
237
+ result.issues.push({
238
+ check: "agentsMap",
239
+ file: config.agentsMapPath,
240
+ message: agentsResult.error.message,
241
+ suggestion: agentsResult.error.suggestions?.[0] ?? void 0
242
+ });
243
+ }
244
+ const knowledgeResult = await validateKnowledgeMap(cwd);
245
+ if (knowledgeResult.ok && knowledgeResult.value.brokenLinks.length === 0) {
246
+ result.checks.knowledgeMap = true;
247
+ } else if (knowledgeResult.ok) {
248
+ result.valid = false;
249
+ for (const broken of knowledgeResult.value.brokenLinks) {
250
+ result.issues.push({
251
+ check: "knowledgeMap",
252
+ file: broken.path,
253
+ message: `Broken link: ${broken.path}`,
254
+ suggestion: broken.suggestion || "Remove or fix the broken link"
255
+ });
256
+ }
257
+ } else {
258
+ result.valid = false;
259
+ result.issues.push({
260
+ check: "knowledgeMap",
261
+ message: knowledgeResult.error.message
262
+ });
263
+ }
264
+ result.checks.fileStructure = true;
265
+ return Ok2(result);
266
+ }
267
+ function createValidateCommand() {
268
+ const command = new Command("validate").description("Run all validation checks").action(async (_opts, cmd) => {
269
+ const globalOpts = cmd.optsWithGlobals();
270
+ const mode = globalOpts.json ? OutputMode.JSON : globalOpts.quiet ? OutputMode.QUIET : globalOpts.verbose ? OutputMode.VERBOSE : OutputMode.TEXT;
271
+ const formatter = new OutputFormatter(mode);
272
+ const result = await runValidate({
273
+ configPath: globalOpts.config,
274
+ json: globalOpts.json,
275
+ verbose: globalOpts.verbose,
276
+ quiet: globalOpts.quiet
277
+ });
278
+ if (!result.ok) {
279
+ if (mode === OutputMode.JSON) {
280
+ console.log(JSON.stringify({ error: result.error.message }));
281
+ } else {
282
+ logger.error(result.error.message);
283
+ }
284
+ process.exit(result.error.exitCode);
285
+ }
286
+ const output = formatter.formatValidation({
287
+ valid: result.value.valid,
288
+ issues: result.value.issues
289
+ });
290
+ if (output) {
291
+ console.log(output);
292
+ }
293
+ process.exit(result.value.valid ? ExitCode.SUCCESS : ExitCode.VALIDATION_FAILED);
294
+ });
295
+ return command;
296
+ }
297
+
298
+ // src/commands/check-deps.ts
299
+ import { Command as Command2 } from "commander";
300
+ import * as path3 from "path";
301
+ import { Ok as Ok3 } from "@harness-engineering/core";
302
+ import {
303
+ validateDependencies,
304
+ detectCircularDepsInFiles,
305
+ defineLayer,
306
+ TypeScriptParser
307
+ } from "@harness-engineering/core";
308
+
309
+ // src/utils/files.ts
310
+ import { glob } from "glob";
311
+ async function findFiles(pattern, cwd = process.cwd()) {
312
+ return glob(pattern, { cwd, absolute: true });
313
+ }
314
+
315
+ // src/commands/check-deps.ts
316
+ async function runCheckDeps(options) {
317
+ const cwd = options.cwd ?? process.cwd();
318
+ const configResult = resolveConfig(options.configPath);
319
+ if (!configResult.ok) {
320
+ return configResult;
321
+ }
322
+ const config = configResult.value;
323
+ const result = {
324
+ valid: true,
325
+ layerViolations: [],
326
+ circularDeps: []
327
+ };
328
+ if (!config.layers || config.layers.length === 0) {
329
+ return Ok3(result);
330
+ }
331
+ const rootDir = path3.resolve(cwd, config.rootDir);
332
+ const parser = new TypeScriptParser();
333
+ const layers = config.layers.map(
334
+ (l) => defineLayer(l.name, [l.pattern], l.allowedDependencies)
335
+ );
336
+ const layerConfig = {
337
+ layers,
338
+ rootDir,
339
+ parser,
340
+ fallbackBehavior: "warn"
341
+ };
342
+ const depsResult = await validateDependencies(layerConfig);
343
+ if (depsResult.ok) {
344
+ for (const violation of depsResult.value.violations) {
345
+ result.valid = false;
346
+ result.layerViolations.push({
347
+ file: violation.file,
348
+ imports: violation.imports,
349
+ fromLayer: violation.fromLayer ?? "unknown",
350
+ toLayer: violation.toLayer ?? "unknown",
351
+ message: violation.reason
352
+ });
353
+ }
354
+ }
355
+ const allFiles = [];
356
+ for (const layer of config.layers) {
357
+ const files = await findFiles(layer.pattern, rootDir);
358
+ allFiles.push(...files);
359
+ }
360
+ const uniqueFiles = [...new Set(allFiles)];
361
+ if (uniqueFiles.length > 0) {
362
+ const circularResult = await detectCircularDepsInFiles(uniqueFiles, parser);
363
+ if (circularResult.ok && circularResult.value.hasCycles) {
364
+ result.valid = false;
365
+ for (const cycle of circularResult.value.cycles) {
366
+ result.circularDeps.push({ cycle: cycle.cycle });
367
+ }
368
+ }
369
+ }
370
+ return Ok3(result);
371
+ }
372
+ function createCheckDepsCommand() {
373
+ const command = new Command2("check-deps").description("Validate dependency layers and detect circular dependencies").action(async (_opts, cmd) => {
374
+ const globalOpts = cmd.optsWithGlobals();
375
+ const mode = globalOpts.json ? OutputMode.JSON : globalOpts.quiet ? OutputMode.QUIET : globalOpts.verbose ? OutputMode.VERBOSE : OutputMode.TEXT;
376
+ const formatter = new OutputFormatter(mode);
377
+ const result = await runCheckDeps({
378
+ configPath: globalOpts.config,
379
+ json: globalOpts.json,
380
+ verbose: globalOpts.verbose,
381
+ quiet: globalOpts.quiet
382
+ });
383
+ if (!result.ok) {
384
+ if (mode === OutputMode.JSON) {
385
+ console.log(JSON.stringify({ error: result.error.message }));
386
+ } else {
387
+ logger.error(result.error.message);
388
+ }
389
+ process.exit(result.error.exitCode);
390
+ }
391
+ const issues = [
392
+ ...result.value.layerViolations.map((v) => ({
393
+ file: v.file,
394
+ message: `Layer violation: ${v.fromLayer} -> ${v.toLayer} (${v.message})`
395
+ })),
396
+ ...result.value.circularDeps.map((c) => ({
397
+ message: `Circular dependency: ${c.cycle.join(" -> ")}`
398
+ }))
399
+ ];
400
+ const output = formatter.formatValidation({
401
+ valid: result.value.valid,
402
+ issues
403
+ });
404
+ if (output) {
405
+ console.log(output);
406
+ }
407
+ process.exit(result.value.valid ? ExitCode.SUCCESS : ExitCode.VALIDATION_FAILED);
408
+ });
409
+ return command;
410
+ }
411
+
412
+ // src/commands/check-docs.ts
413
+ import { Command as Command3 } from "commander";
414
+ import * as path4 from "path";
415
+ import { Ok as Ok4, Err as Err2 } from "@harness-engineering/core";
416
+ import { checkDocCoverage, validateKnowledgeMap as validateKnowledgeMap2 } from "@harness-engineering/core";
417
+ async function runCheckDocs(options) {
418
+ const cwd = options.cwd ?? process.cwd();
419
+ const minCoverage = options.minCoverage ?? 80;
420
+ const configResult = resolveConfig(options.configPath);
421
+ if (!configResult.ok) {
422
+ return configResult;
423
+ }
424
+ const config = configResult.value;
425
+ const docsDir = path4.resolve(cwd, config.docsDir);
426
+ const sourceDir = path4.resolve(cwd, config.rootDir);
427
+ const coverageResult = await checkDocCoverage("project", {
428
+ docsDir,
429
+ sourceDir,
430
+ excludePatterns: ["**/*.test.ts", "**/*.spec.ts", "**/node_modules/**"]
431
+ });
432
+ if (!coverageResult.ok) {
433
+ return Err2(new CLIError(
434
+ `Documentation coverage check failed: ${coverageResult.error.message}`,
435
+ ExitCode.ERROR
436
+ ));
437
+ }
438
+ const knowledgeResult = await validateKnowledgeMap2(cwd);
439
+ let brokenLinks = [];
440
+ if (knowledgeResult.ok) {
441
+ brokenLinks = knowledgeResult.value.brokenLinks.map((b) => b.path);
442
+ } else {
443
+ logger.warn(`Knowledge map validation failed: ${knowledgeResult.error.message}`);
444
+ }
445
+ const coveragePercent = coverageResult.value.coveragePercentage;
446
+ const result = {
447
+ valid: coveragePercent >= minCoverage && brokenLinks.length === 0,
448
+ coveragePercent,
449
+ documented: coverageResult.value.documented,
450
+ undocumented: coverageResult.value.undocumented,
451
+ brokenLinks
452
+ };
453
+ return Ok4(result);
454
+ }
455
+ function createCheckDocsCommand() {
456
+ const command = new Command3("check-docs").description("Check documentation coverage").option("--min-coverage <percent>", "Minimum coverage percentage", "80").action(async (opts, cmd) => {
457
+ const globalOpts = cmd.optsWithGlobals();
458
+ const mode = globalOpts.json ? OutputMode.JSON : globalOpts.quiet ? OutputMode.QUIET : globalOpts.verbose ? OutputMode.VERBOSE : OutputMode.TEXT;
459
+ const formatter = new OutputFormatter(mode);
460
+ const result = await runCheckDocs({
461
+ configPath: globalOpts.config,
462
+ minCoverage: parseInt(opts.minCoverage, 10),
463
+ json: globalOpts.json,
464
+ verbose: globalOpts.verbose,
465
+ quiet: globalOpts.quiet
466
+ });
467
+ if (!result.ok) {
468
+ if (mode === OutputMode.JSON) {
469
+ console.log(JSON.stringify({ error: result.error.message }));
470
+ } else {
471
+ logger.error(result.error.message);
472
+ }
473
+ process.exit(result.error.exitCode);
474
+ }
475
+ if (mode === OutputMode.JSON) {
476
+ console.log(JSON.stringify(result.value, null, 2));
477
+ } else if (mode !== OutputMode.QUIET) {
478
+ const { value } = result;
479
+ console.log(formatter.formatSummary(
480
+ "Documentation coverage",
481
+ `${value.coveragePercent.toFixed(1)}%`,
482
+ value.valid
483
+ ));
484
+ if (value.undocumented.length > 0 && (mode === OutputMode.VERBOSE || !value.valid)) {
485
+ console.log("\nUndocumented files:");
486
+ for (const file of value.undocumented.slice(0, 10)) {
487
+ console.log(` - ${file}`);
488
+ }
489
+ if (value.undocumented.length > 10) {
490
+ console.log(` ... and ${value.undocumented.length - 10} more`);
491
+ }
492
+ }
493
+ if (value.brokenLinks.length > 0) {
494
+ console.log("\nBroken links:");
495
+ for (const link of value.brokenLinks) {
496
+ console.log(` - ${link}`);
497
+ }
498
+ }
499
+ }
500
+ process.exit(result.value.valid ? ExitCode.SUCCESS : ExitCode.VALIDATION_FAILED);
501
+ });
502
+ return command;
503
+ }
504
+
505
+ // src/commands/init.ts
506
+ import { Command as Command4 } from "commander";
507
+ import * as fs2 from "fs";
508
+ import * as path5 from "path";
509
+ import { Ok as Ok5, Err as Err3 } from "@harness-engineering/core";
510
+
511
+ // src/templates/basic.ts
512
+ var CONFIG_TEMPLATE = (name) => ({
513
+ version: 1,
514
+ name,
515
+ layers: [
516
+ { name: "types", pattern: "src/types/**", allowedDependencies: [] },
517
+ { name: "domain", pattern: "src/domain/**", allowedDependencies: ["types"] },
518
+ { name: "services", pattern: "src/services/**", allowedDependencies: ["types", "domain"] },
519
+ { name: "api", pattern: "src/api/**", allowedDependencies: ["types", "domain", "services"] }
520
+ ],
521
+ agentsMapPath: "./AGENTS.md",
522
+ docsDir: "./docs"
523
+ });
524
+ var AGENTS_MD_TEMPLATE = (name) => `# ${name} Knowledge Map
525
+
526
+ ## About This Project
527
+
528
+ ${name} - A project using Harness Engineering practices.
529
+
530
+ ## Documentation
531
+
532
+ - Main docs: \`docs/\`
533
+
534
+ ## Source Code
535
+
536
+ - Entry point: \`src/index.ts\`
537
+
538
+ ## Architecture
539
+
540
+ See \`docs/architecture.md\` for architectural decisions.
541
+ `;
542
+ var DOCS_INDEX_TEMPLATE = (name) => `# ${name} Documentation
543
+
544
+ Welcome to the ${name} documentation.
545
+
546
+ ## Getting Started
547
+
548
+ TODO: Add getting started guide.
549
+
550
+ ## Architecture
551
+
552
+ TODO: Add architecture documentation.
553
+ `;
554
+
555
+ // src/commands/init.ts
556
+ async function runInit(options) {
557
+ const cwd = options.cwd ?? process.cwd();
558
+ const name = options.name ?? path5.basename(cwd);
559
+ const force = options.force ?? false;
560
+ const configPath = path5.join(cwd, "harness.config.json");
561
+ const agentsPath = path5.join(cwd, "AGENTS.md");
562
+ const docsDir = path5.join(cwd, "docs");
563
+ if (!force && fs2.existsSync(configPath)) {
564
+ return Err3(new CLIError(
565
+ "Project already initialized. Use --force to overwrite.",
566
+ ExitCode.ERROR
567
+ ));
568
+ }
569
+ const filesCreated = [];
570
+ try {
571
+ const config = CONFIG_TEMPLATE(name);
572
+ fs2.writeFileSync(configPath, JSON.stringify(config, null, 2));
573
+ filesCreated.push("harness.config.json");
574
+ if (!fs2.existsSync(agentsPath) || force) {
575
+ fs2.writeFileSync(agentsPath, AGENTS_MD_TEMPLATE(name));
576
+ filesCreated.push("AGENTS.md");
577
+ }
578
+ if (!fs2.existsSync(docsDir)) {
579
+ fs2.mkdirSync(docsDir, { recursive: true });
580
+ fs2.writeFileSync(path5.join(docsDir, "index.md"), DOCS_INDEX_TEMPLATE(name));
581
+ filesCreated.push("docs/index.md");
582
+ }
583
+ return Ok5({ filesCreated });
584
+ } catch (error) {
585
+ return Err3(new CLIError(
586
+ `Failed to initialize: ${error instanceof Error ? error.message : "Unknown error"}`,
587
+ ExitCode.ERROR
588
+ ));
589
+ }
590
+ }
591
+ function createInitCommand() {
592
+ const command = new Command4("init").description("Initialize a new harness-engineering project").option("-n, --name <name>", "Project name").option("-f, --force", "Overwrite existing files").action(async (opts, cmd) => {
593
+ const globalOpts = cmd.optsWithGlobals();
594
+ const result = await runInit({
595
+ name: opts.name,
596
+ force: opts.force
597
+ });
598
+ if (!result.ok) {
599
+ logger.error(result.error.message);
600
+ process.exit(result.error.exitCode);
601
+ }
602
+ if (!globalOpts.quiet) {
603
+ logger.success("Project initialized!");
604
+ logger.info("Created files:");
605
+ for (const file of result.value.filesCreated) {
606
+ console.log(` - ${file}`);
607
+ }
608
+ console.log("\nNext steps:");
609
+ console.log(" 1. Review harness.config.json");
610
+ console.log(" 2. Update AGENTS.md with your project structure");
611
+ console.log(' 3. Run "harness validate" to check your setup');
612
+ }
613
+ process.exit(ExitCode.SUCCESS);
614
+ });
615
+ return command;
616
+ }
617
+
618
+ // src/commands/cleanup.ts
619
+ import { Command as Command5 } from "commander";
620
+ import * as path6 from "path";
621
+ import { Ok as Ok6, Err as Err4, EntropyAnalyzer } from "@harness-engineering/core";
622
+ async function runCleanup(options) {
623
+ const cwd = options.cwd ?? process.cwd();
624
+ const type = options.type ?? "all";
625
+ const configResult = resolveConfig(options.configPath);
626
+ if (!configResult.ok) {
627
+ return Err4(configResult.error);
628
+ }
629
+ const config = configResult.value;
630
+ const result = {
631
+ driftIssues: [],
632
+ deadCode: [],
633
+ patternViolations: [],
634
+ totalIssues: 0
635
+ };
636
+ const rootDir = path6.resolve(cwd, config.rootDir);
637
+ const docsDir = path6.resolve(cwd, config.docsDir);
638
+ const entropyConfig = {
639
+ rootDir,
640
+ entryPoints: [path6.join(rootDir, "src/index.ts")],
641
+ docPaths: [docsDir],
642
+ analyze: {
643
+ drift: type === "all" || type === "drift",
644
+ deadCode: type === "all" || type === "dead-code",
645
+ patterns: type === "all" || type === "patterns" ? { patterns: [] } : false
646
+ },
647
+ exclude: config.entropy?.excludePatterns ?? ["**/node_modules/**", "**/*.test.ts"]
648
+ };
649
+ const analyzer = new EntropyAnalyzer(entropyConfig);
650
+ const analysisResult = await analyzer.analyze();
651
+ if (!analysisResult.ok) {
652
+ return Err4(new CLIError(
653
+ `Entropy analysis failed: ${analysisResult.error.message}`,
654
+ ExitCode.ERROR
655
+ ));
656
+ }
657
+ const report = analysisResult.value;
658
+ if (report.drift) {
659
+ result.driftIssues = report.drift.drifts.map((d) => ({
660
+ file: d.docFile,
661
+ issue: `${d.issue}: ${d.details}`
662
+ }));
663
+ }
664
+ if (report.deadCode) {
665
+ result.deadCode = [
666
+ ...report.deadCode.deadFiles.map((f) => ({ file: f.path })),
667
+ ...report.deadCode.deadExports.map((e) => ({ file: e.file, symbol: e.name }))
668
+ ];
669
+ }
670
+ if (report.patterns) {
671
+ result.patternViolations = report.patterns.violations.map((v) => ({
672
+ file: v.file,
673
+ pattern: v.pattern,
674
+ message: v.message
675
+ }));
676
+ }
677
+ result.totalIssues = result.driftIssues.length + result.deadCode.length + result.patternViolations.length;
678
+ return Ok6(result);
679
+ }
680
+ function createCleanupCommand() {
681
+ const command = new Command5("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) => {
682
+ const globalOpts = cmd.optsWithGlobals();
683
+ const mode = globalOpts.json ? OutputMode.JSON : globalOpts.quiet ? OutputMode.QUIET : globalOpts.verbose ? OutputMode.VERBOSE : OutputMode.TEXT;
684
+ const formatter = new OutputFormatter(mode);
685
+ const result = await runCleanup({
686
+ configPath: globalOpts.config,
687
+ type: opts.type,
688
+ json: globalOpts.json,
689
+ verbose: globalOpts.verbose,
690
+ quiet: globalOpts.quiet
691
+ });
692
+ if (!result.ok) {
693
+ if (mode === OutputMode.JSON) {
694
+ console.log(JSON.stringify({ error: result.error.message }));
695
+ } else {
696
+ logger.error(result.error.message);
697
+ }
698
+ process.exit(result.error.exitCode);
699
+ }
700
+ if (mode === OutputMode.JSON) {
701
+ console.log(JSON.stringify(result.value, null, 2));
702
+ } else if (mode !== OutputMode.QUIET || result.value.totalIssues > 0) {
703
+ console.log(formatter.formatSummary(
704
+ "Entropy issues",
705
+ result.value.totalIssues.toString(),
706
+ result.value.totalIssues === 0
707
+ ));
708
+ if (result.value.driftIssues.length > 0) {
709
+ console.log("\nDocumentation drift:");
710
+ for (const issue of result.value.driftIssues) {
711
+ console.log(` - ${issue.file}: ${issue.issue}`);
712
+ }
713
+ }
714
+ if (result.value.deadCode.length > 0) {
715
+ console.log("\nDead code:");
716
+ for (const item of result.value.deadCode.slice(0, 10)) {
717
+ console.log(` - ${item.file}${item.symbol ? `: ${item.symbol}` : ""}`);
718
+ }
719
+ if (result.value.deadCode.length > 10) {
720
+ console.log(` ... and ${result.value.deadCode.length - 10} more`);
721
+ }
722
+ }
723
+ if (result.value.patternViolations.length > 0) {
724
+ console.log("\nPattern violations:");
725
+ for (const violation of result.value.patternViolations.slice(0, 10)) {
726
+ console.log(` - ${violation.file} [${violation.pattern}]: ${violation.message}`);
727
+ }
728
+ if (result.value.patternViolations.length > 10) {
729
+ console.log(` ... and ${result.value.patternViolations.length - 10} more`);
730
+ }
731
+ }
732
+ }
733
+ process.exit(result.value.totalIssues === 0 ? ExitCode.SUCCESS : ExitCode.VALIDATION_FAILED);
734
+ });
735
+ return command;
736
+ }
737
+
738
+ // src/commands/fix-drift.ts
739
+ import { Command as Command6 } from "commander";
740
+ import * as path7 from "path";
741
+ import {
742
+ Ok as Ok7,
743
+ Err as Err5,
744
+ buildSnapshot,
745
+ detectDocDrift,
746
+ detectDeadCode,
747
+ createFixes,
748
+ applyFixes,
749
+ generateSuggestions
750
+ } from "@harness-engineering/core";
751
+ async function runFixDrift(options) {
752
+ const cwd = options.cwd ?? process.cwd();
753
+ const dryRun = options.dryRun !== false;
754
+ const configResult = resolveConfig(options.configPath);
755
+ if (!configResult.ok) {
756
+ return Err5(configResult.error);
757
+ }
758
+ const config = configResult.value;
759
+ const rootDir = path7.resolve(cwd, config.rootDir);
760
+ const docsDir = path7.resolve(cwd, config.docsDir);
761
+ const entropyConfig = {
762
+ rootDir,
763
+ entryPoints: [path7.join(rootDir, "src/index.ts")],
764
+ docPaths: [docsDir],
765
+ analyze: {
766
+ drift: true,
767
+ deadCode: true,
768
+ patterns: false
769
+ },
770
+ exclude: config.entropy?.excludePatterns ?? ["**/node_modules/**", "**/*.test.ts"]
771
+ };
772
+ const snapshotResult = await buildSnapshot(entropyConfig);
773
+ if (!snapshotResult.ok) {
774
+ return Err5(new CLIError(
775
+ `Failed to build snapshot: ${snapshotResult.error.message}`,
776
+ ExitCode.ERROR
777
+ ));
778
+ }
779
+ const snapshot = snapshotResult.value;
780
+ const driftResult = await detectDocDrift(snapshot);
781
+ if (!driftResult.ok) {
782
+ return Err5(new CLIError(
783
+ `Failed to detect drift: ${driftResult.error.message}`,
784
+ ExitCode.ERROR
785
+ ));
786
+ }
787
+ const driftReport = driftResult.value;
788
+ const deadCodeResult = await detectDeadCode(snapshot);
789
+ if (!deadCodeResult.ok) {
790
+ return Err5(new CLIError(
791
+ `Failed to detect dead code: ${deadCodeResult.error.message}`,
792
+ ExitCode.ERROR
793
+ ));
794
+ }
795
+ const deadCodeReport = deadCodeResult.value;
796
+ const fixes = createFixes(deadCodeReport);
797
+ const appliedFixes = [];
798
+ if (!dryRun && fixes.length > 0) {
799
+ const applyResult = await applyFixes(fixes, { dryRun: false });
800
+ if (!applyResult.ok) {
801
+ return Err5(new CLIError(
802
+ `Failed to apply fixes: ${applyResult.error.message}`,
803
+ ExitCode.ERROR
804
+ ));
805
+ }
806
+ for (const fix of applyResult.value.applied) {
807
+ appliedFixes.push({
808
+ file: fix.file,
809
+ action: fix.action,
810
+ applied: true
811
+ });
812
+ }
813
+ for (const fix of applyResult.value.skipped) {
814
+ appliedFixes.push({
815
+ file: fix.file,
816
+ action: fix.action,
817
+ applied: false
818
+ });
819
+ }
820
+ for (const { fix } of applyResult.value.errors) {
821
+ appliedFixes.push({
822
+ file: fix.file,
823
+ action: fix.action,
824
+ applied: false
825
+ });
826
+ }
827
+ } else {
828
+ for (const fix of fixes) {
829
+ appliedFixes.push({
830
+ file: fix.file,
831
+ action: fix.action,
832
+ applied: false
833
+ });
834
+ }
835
+ }
836
+ const suggestionReport = generateSuggestions(deadCodeReport, driftReport);
837
+ const suggestions = [];
838
+ for (const suggestion of suggestionReport.suggestions) {
839
+ for (const file of suggestion.files) {
840
+ suggestions.push({
841
+ file,
842
+ suggestion: suggestion.title
843
+ });
844
+ }
845
+ }
846
+ const result = {
847
+ dryRun,
848
+ fixes: appliedFixes,
849
+ suggestions
850
+ };
851
+ return Ok7(result);
852
+ }
853
+ function createFixDriftCommand() {
854
+ const command = new Command6("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) => {
855
+ const globalOpts = cmd.optsWithGlobals();
856
+ const mode = globalOpts.json ? OutputMode.JSON : globalOpts.quiet ? OutputMode.QUIET : globalOpts.verbose ? OutputMode.VERBOSE : OutputMode.TEXT;
857
+ const formatter = new OutputFormatter(mode);
858
+ const result = await runFixDrift({
859
+ configPath: globalOpts.config,
860
+ dryRun: opts.dryRun,
861
+ json: globalOpts.json,
862
+ verbose: globalOpts.verbose,
863
+ quiet: globalOpts.quiet
864
+ });
865
+ if (!result.ok) {
866
+ if (mode === OutputMode.JSON) {
867
+ console.log(JSON.stringify({ error: result.error.message }));
868
+ } else {
869
+ logger.error(result.error.message);
870
+ }
871
+ process.exit(result.error.exitCode);
872
+ }
873
+ if (mode === OutputMode.JSON) {
874
+ console.log(JSON.stringify(result.value, null, 2));
875
+ } else if (mode !== OutputMode.QUIET || result.value.fixes.length > 0 || result.value.suggestions.length > 0) {
876
+ const { value } = result;
877
+ const statusMessage = value.dryRun ? "(dry-run)" : "";
878
+ console.log(formatter.formatSummary(
879
+ `Fix drift ${statusMessage}`,
880
+ `${value.fixes.length} fixes, ${value.suggestions.length} suggestions`,
881
+ value.fixes.length === 0 && value.suggestions.length === 0
882
+ ));
883
+ if (value.fixes.length > 0) {
884
+ console.log("\nFixes:");
885
+ for (const fix of value.fixes.slice(0, 10)) {
886
+ const status = fix.applied ? "[applied]" : "[pending]";
887
+ console.log(` ${status} ${fix.action}: ${fix.file}`);
888
+ }
889
+ if (value.fixes.length > 10) {
890
+ console.log(` ... and ${value.fixes.length - 10} more`);
891
+ }
892
+ }
893
+ if (value.suggestions.length > 0 && (mode === OutputMode.VERBOSE || value.fixes.length === 0)) {
894
+ console.log("\nSuggestions:");
895
+ for (const suggestion of value.suggestions.slice(0, 10)) {
896
+ console.log(` - ${suggestion.file}: ${suggestion.suggestion}`);
897
+ }
898
+ if (value.suggestions.length > 10) {
899
+ console.log(` ... and ${value.suggestions.length - 10} more`);
900
+ }
901
+ }
902
+ if (value.dryRun && value.fixes.length > 0) {
903
+ console.log("\nRun with --no-dry-run to apply fixes.");
904
+ }
905
+ }
906
+ process.exit(ExitCode.SUCCESS);
907
+ });
908
+ return command;
909
+ }
910
+
911
+ // src/commands/agent/index.ts
912
+ import { Command as Command9 } from "commander";
913
+
914
+ // src/commands/agent/run.ts
915
+ import { Command as Command7 } from "commander";
916
+ import { Ok as Ok8, Err as Err6 } from "@harness-engineering/core";
917
+ import { requestPeerReview } from "@harness-engineering/core";
918
+ async function runAgentTask(task, options) {
919
+ const configResult = resolveConfig(options.configPath);
920
+ if (!configResult.ok) {
921
+ return configResult;
922
+ }
923
+ const agentTypeMap = {
924
+ review: "architecture-enforcer",
925
+ "doc-review": "documentation-maintainer",
926
+ "test-review": "test-reviewer"
927
+ };
928
+ const agentType = agentTypeMap[task];
929
+ if (!agentType) {
930
+ return Err6(
931
+ new CLIError(
932
+ `Unknown task: ${task}. Available: ${Object.keys(agentTypeMap).join(", ")}`,
933
+ ExitCode.ERROR
934
+ )
935
+ );
936
+ }
937
+ const config = configResult.value;
938
+ const timeout = options.timeout ?? config.agent?.timeout ?? 3e5;
939
+ const reviewResult = await requestPeerReview(
940
+ agentType,
941
+ {
942
+ files: [],
943
+ diff: "",
944
+ commitMessage: task,
945
+ metadata: { task, timeout }
946
+ },
947
+ { timeout }
948
+ );
949
+ if (!reviewResult.ok) {
950
+ return Err6(
951
+ new CLIError(`Agent task failed: ${reviewResult.error.message}`, ExitCode.ERROR)
952
+ );
953
+ }
954
+ const review = reviewResult.value;
955
+ return Ok8({
956
+ success: review.approved,
957
+ output: review.approved ? `Agent task '${task}' completed successfully` : `Agent task '${task}' found issues:
958
+ ${review.comments.map((c) => ` - ${c.message}`).join("\n")}`
959
+ });
960
+ }
961
+ function createRunCommand() {
962
+ return new Command7("run").description("Run an agent task").argument("<task>", "Task to run (review, doc-review, test-review)").option("--timeout <ms>", "Timeout in milliseconds", "300000").action(async (task, opts, cmd) => {
963
+ const globalOpts = cmd.optsWithGlobals();
964
+ const result = await runAgentTask(task, {
965
+ configPath: globalOpts.config,
966
+ timeout: parseInt(opts.timeout, 10)
967
+ });
968
+ if (!result.ok) {
969
+ logger.error(result.error.message);
970
+ process.exit(result.error.exitCode);
971
+ }
972
+ logger.success(result.value.output);
973
+ process.exit(ExitCode.SUCCESS);
974
+ });
975
+ }
976
+
977
+ // src/commands/agent/review.ts
978
+ import { Command as Command8 } from "commander";
979
+ import { execSync } from "child_process";
980
+ import { Ok as Ok9, Err as Err7 } from "@harness-engineering/core";
981
+ import { createSelfReview, parseDiff } from "@harness-engineering/core";
982
+ async function runAgentReview(options) {
983
+ const configResult = resolveConfig(options.configPath);
984
+ if (!configResult.ok) {
985
+ return configResult;
986
+ }
987
+ const config = configResult.value;
988
+ let diff;
989
+ try {
990
+ diff = execSync("git diff --cached", { encoding: "utf-8" });
991
+ if (!diff) {
992
+ diff = execSync("git diff", { encoding: "utf-8" });
993
+ }
994
+ } catch {
995
+ return Err7(new CLIError("Failed to get git diff", ExitCode.ERROR));
996
+ }
997
+ if (!diff) {
998
+ return Ok9({
999
+ passed: true,
1000
+ checklist: [{ check: "No changes to review", passed: true }]
1001
+ });
1002
+ }
1003
+ const parsedDiffResult = parseDiff(diff);
1004
+ if (!parsedDiffResult.ok) {
1005
+ return Err7(new CLIError(parsedDiffResult.error.message, ExitCode.ERROR));
1006
+ }
1007
+ const codeChanges = parsedDiffResult.value;
1008
+ const review = await createSelfReview(codeChanges, {
1009
+ rootDir: config.rootDir,
1010
+ diffAnalysis: {
1011
+ enabled: true,
1012
+ checkTestCoverage: true
1013
+ }
1014
+ });
1015
+ if (!review.ok) {
1016
+ return Err7(new CLIError(review.error.message, ExitCode.ERROR));
1017
+ }
1018
+ return Ok9({
1019
+ passed: review.value.passed,
1020
+ checklist: review.value.items.map((item) => ({
1021
+ check: item.check,
1022
+ passed: item.passed,
1023
+ details: item.details
1024
+ }))
1025
+ });
1026
+ }
1027
+ function createReviewCommand() {
1028
+ return new Command8("review").description("Run self-review on current changes").action(async (_opts, cmd) => {
1029
+ const globalOpts = cmd.optsWithGlobals();
1030
+ const mode = globalOpts.json ? OutputMode.JSON : globalOpts.quiet ? OutputMode.QUIET : OutputMode.TEXT;
1031
+ const result = await runAgentReview({
1032
+ configPath: globalOpts.config,
1033
+ json: globalOpts.json,
1034
+ verbose: globalOpts.verbose,
1035
+ quiet: globalOpts.quiet
1036
+ });
1037
+ if (!result.ok) {
1038
+ if (mode === OutputMode.JSON) {
1039
+ console.log(JSON.stringify({ error: result.error.message }));
1040
+ } else {
1041
+ logger.error(result.error.message);
1042
+ }
1043
+ process.exit(result.error.exitCode);
1044
+ }
1045
+ if (mode === OutputMode.JSON) {
1046
+ console.log(JSON.stringify(result.value, null, 2));
1047
+ } else if (mode !== OutputMode.QUIET) {
1048
+ console.log(result.value.passed ? "v Self-review passed" : "x Self-review found issues");
1049
+ console.log("");
1050
+ for (const item of result.value.checklist) {
1051
+ const icon = item.passed ? "v" : "x";
1052
+ console.log(` ${icon} ${item.check}`);
1053
+ if (item.details && !item.passed) {
1054
+ console.log(` ${item.details}`);
1055
+ }
1056
+ }
1057
+ }
1058
+ process.exit(result.value.passed ? ExitCode.SUCCESS : ExitCode.VALIDATION_FAILED);
1059
+ });
1060
+ }
1061
+
1062
+ // src/commands/agent/index.ts
1063
+ function createAgentCommand() {
1064
+ const command = new Command9("agent").description("Agent orchestration commands");
1065
+ command.addCommand(createRunCommand());
1066
+ command.addCommand(createReviewCommand());
1067
+ return command;
1068
+ }
1069
+
1070
+ // src/commands/add.ts
1071
+ import { Command as Command10 } from "commander";
1072
+ import * as fs3 from "fs";
1073
+ import * as path8 from "path";
1074
+ import { Ok as Ok10, Err as Err8 } from "@harness-engineering/core";
1075
+ var LAYER_INDEX_TEMPLATE = (name) => `// ${name} layer
1076
+ // Add your ${name} exports here
1077
+
1078
+ export {};
1079
+ `;
1080
+ var MODULE_TEMPLATE = (name) => `/**
1081
+ * ${name} module
1082
+ */
1083
+
1084
+ export function ${name}() {
1085
+ // TODO: Implement
1086
+ }
1087
+ `;
1088
+ var DOC_TEMPLATE = (name) => `# ${name}
1089
+
1090
+ ## Overview
1091
+
1092
+ TODO: Add overview
1093
+
1094
+ ## Usage
1095
+
1096
+ TODO: Add usage examples
1097
+ `;
1098
+ async function runAdd(componentType, name, options) {
1099
+ const cwd = options.cwd ?? process.cwd();
1100
+ const NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
1101
+ if (!name || !NAME_PATTERN.test(name)) {
1102
+ return Err8(new CLIError(
1103
+ "Invalid name. Must start with a letter and contain only alphanumeric characters, hyphens, and underscores.",
1104
+ ExitCode.ERROR
1105
+ ));
1106
+ }
1107
+ const configResult = resolveConfig(options.configPath);
1108
+ if (!configResult.ok) {
1109
+ }
1110
+ const created = [];
1111
+ try {
1112
+ switch (componentType) {
1113
+ case "layer": {
1114
+ const layerDir = path8.join(cwd, "src", name);
1115
+ if (!fs3.existsSync(layerDir)) {
1116
+ fs3.mkdirSync(layerDir, { recursive: true });
1117
+ created.push(`src/${name}/`);
1118
+ }
1119
+ const indexPath = path8.join(layerDir, "index.ts");
1120
+ if (!fs3.existsSync(indexPath)) {
1121
+ fs3.writeFileSync(indexPath, LAYER_INDEX_TEMPLATE(name));
1122
+ created.push(`src/${name}/index.ts`);
1123
+ }
1124
+ break;
1125
+ }
1126
+ case "module": {
1127
+ const modulePath = path8.join(cwd, "src", `${name}.ts`);
1128
+ if (fs3.existsSync(modulePath)) {
1129
+ return Err8(new CLIError(`Module ${name} already exists`, ExitCode.ERROR));
1130
+ }
1131
+ fs3.writeFileSync(modulePath, MODULE_TEMPLATE(name));
1132
+ created.push(`src/${name}.ts`);
1133
+ break;
1134
+ }
1135
+ case "doc": {
1136
+ const docsDir = path8.join(cwd, "docs");
1137
+ if (!fs3.existsSync(docsDir)) {
1138
+ fs3.mkdirSync(docsDir, { recursive: true });
1139
+ }
1140
+ const docPath = path8.join(docsDir, `${name}.md`);
1141
+ if (fs3.existsSync(docPath)) {
1142
+ return Err8(new CLIError(`Doc ${name} already exists`, ExitCode.ERROR));
1143
+ }
1144
+ fs3.writeFileSync(docPath, DOC_TEMPLATE(name));
1145
+ created.push(`docs/${name}.md`);
1146
+ break;
1147
+ }
1148
+ default:
1149
+ return Err8(new CLIError(
1150
+ `Unknown component type: ${componentType}. Use: layer, module, doc`,
1151
+ ExitCode.ERROR
1152
+ ));
1153
+ }
1154
+ return Ok10({ created });
1155
+ } catch (error) {
1156
+ return Err8(new CLIError(
1157
+ `Failed to add ${componentType}: ${error instanceof Error ? error.message : "Unknown error"}`,
1158
+ ExitCode.ERROR
1159
+ ));
1160
+ }
1161
+ }
1162
+ function createAddCommand() {
1163
+ const command = new Command10("add").description("Add a component to the project").argument("<type>", "Component type (layer, module, doc)").argument("<name>", "Component name").action(async (type, name, _opts, cmd) => {
1164
+ const globalOpts = cmd.optsWithGlobals();
1165
+ const result = await runAdd(type, name, {
1166
+ configPath: globalOpts.config
1167
+ });
1168
+ if (!result.ok) {
1169
+ logger.error(result.error.message);
1170
+ process.exit(result.error.exitCode);
1171
+ }
1172
+ if (!globalOpts.quiet) {
1173
+ logger.success(`Added ${type}: ${name}`);
1174
+ for (const file of result.value.created) {
1175
+ console.log(` + ${file}`);
1176
+ }
1177
+ }
1178
+ process.exit(ExitCode.SUCCESS);
1179
+ });
1180
+ return command;
1181
+ }
1182
+
1183
+ // src/commands/linter/index.ts
1184
+ import { Command as Command13 } from "commander";
1185
+
1186
+ // src/commands/linter/generate.ts
1187
+ import { Command as Command11 } from "commander";
1188
+ import { generate } from "@harness-engineering/linter-gen";
1189
+ function createGenerateCommand() {
1190
+ return new Command11("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) => {
1191
+ const formatter = new OutputFormatter(
1192
+ options.json ? OutputMode.JSON : OutputMode.TEXT
1193
+ );
1194
+ try {
1195
+ if (options.verbose) {
1196
+ logger.info(`Parsing config: ${options.config}`);
1197
+ }
1198
+ const result = await generate({
1199
+ configPath: options.config,
1200
+ outputDir: options.output,
1201
+ clean: options.clean,
1202
+ dryRun: options.dryRun
1203
+ });
1204
+ if (!result.success) {
1205
+ const errorMessages = result.errors.map((e) => {
1206
+ switch (e.type) {
1207
+ case "parse":
1208
+ return `Config error: ${e.error.message}`;
1209
+ case "template":
1210
+ return `Template error for '${e.ruleName}': ${e.error.message}`;
1211
+ case "render":
1212
+ return `Render error for '${e.ruleName}': ${e.error.message}`;
1213
+ case "write":
1214
+ return `Write error for '${e.path}': ${e.error.message}`;
1215
+ }
1216
+ });
1217
+ if (options.json) {
1218
+ console.log(JSON.stringify({ success: false, errors: errorMessages }, null, 2));
1219
+ } else {
1220
+ errorMessages.forEach((msg) => logger.error(msg));
1221
+ }
1222
+ process.exit(ExitCode.VALIDATION_FAILED);
1223
+ }
1224
+ if (options.json) {
1225
+ console.log(JSON.stringify({
1226
+ success: true,
1227
+ outputDir: result.outputDir,
1228
+ rulesGenerated: result.rulesGenerated,
1229
+ dryRun: result.dryRun
1230
+ }, null, 2));
1231
+ } else {
1232
+ if (result.dryRun) {
1233
+ logger.info("Dry run - no files written");
1234
+ }
1235
+ result.rulesGenerated.forEach((name) => {
1236
+ logger.success(`Generated ${name}.ts`);
1237
+ });
1238
+ logger.success(`Generated index.ts`);
1239
+ logger.info(`
1240
+ Generated ${result.rulesGenerated.length} rules to ${result.outputDir}`);
1241
+ }
1242
+ } catch (err) {
1243
+ throw new CLIError(`Generation failed: ${err.message}`, ExitCode.ERROR);
1244
+ }
1245
+ });
1246
+ }
1247
+
1248
+ // src/commands/linter/validate.ts
1249
+ import { Command as Command12 } from "commander";
1250
+ import { validate } from "@harness-engineering/linter-gen";
1251
+ function createValidateCommand2() {
1252
+ return new Command12("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) => {
1253
+ try {
1254
+ const result = await validate({ configPath: options.config });
1255
+ if (options.json) {
1256
+ console.log(JSON.stringify(result, null, 2));
1257
+ } else if (result.success) {
1258
+ logger.success(`Config valid: ${result.ruleCount} rules defined`);
1259
+ } else {
1260
+ logger.error(`Config invalid: ${result.error.message}`);
1261
+ process.exit(ExitCode.VALIDATION_FAILED);
1262
+ }
1263
+ } catch (err) {
1264
+ throw new CLIError(`Validation failed: ${err.message}`, ExitCode.ERROR);
1265
+ }
1266
+ });
1267
+ }
1268
+
1269
+ // src/commands/linter/index.ts
1270
+ function createLinterCommand() {
1271
+ const linter = new Command13("linter").description("Generate and validate ESLint rules from YAML config");
1272
+ linter.addCommand(createGenerateCommand());
1273
+ linter.addCommand(createValidateCommand2());
1274
+ return linter;
1275
+ }
1276
+
1277
+ // src/index.ts
1278
+ function createProgram() {
1279
+ const program = new Command14();
1280
+ program.name("harness").description("CLI for Harness Engineering toolkit").version(VERSION).option("-c, --config <path>", "Path to config file").option("--json", "Output as JSON").option("--verbose", "Verbose output").option("--quiet", "Minimal output");
1281
+ program.addCommand(createValidateCommand());
1282
+ program.addCommand(createCheckDepsCommand());
1283
+ program.addCommand(createCheckDocsCommand());
1284
+ program.addCommand(createInitCommand());
1285
+ program.addCommand(createCleanupCommand());
1286
+ program.addCommand(createFixDriftCommand());
1287
+ program.addCommand(createAgentCommand());
1288
+ program.addCommand(createAddCommand());
1289
+ program.addCommand(createLinterCommand());
1290
+ return program;
1291
+ }
1292
+
1293
+ export {
1294
+ ExitCode,
1295
+ CLIError,
1296
+ handleError,
1297
+ findConfigFile,
1298
+ loadConfig,
1299
+ resolveConfig,
1300
+ OutputMode,
1301
+ OutputFormatter,
1302
+ logger,
1303
+ createProgram
1304
+ };