@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,935 @@
1
+ // src/index.ts
2
+ import { Command as Command7 } 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]
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 Err3 } 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 Err3(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 Err4 } 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 Err4(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 Err4(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 Err5, 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 Err5(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 Err5(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 Err6,
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 Err6(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 Err6(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 Err6(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 Err6(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 Err6(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/index.ts
912
+ function createProgram() {
913
+ const program = new Command7();
914
+ 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");
915
+ program.addCommand(createValidateCommand());
916
+ program.addCommand(createCheckDepsCommand());
917
+ program.addCommand(createCheckDocsCommand());
918
+ program.addCommand(createInitCommand());
919
+ program.addCommand(createCleanupCommand());
920
+ program.addCommand(createFixDriftCommand());
921
+ return program;
922
+ }
923
+
924
+ export {
925
+ ExitCode,
926
+ CLIError,
927
+ handleError,
928
+ findConfigFile,
929
+ loadConfig,
930
+ resolveConfig,
931
+ OutputMode,
932
+ OutputFormatter,
933
+ logger,
934
+ createProgram
935
+ };