@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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Intense Visions, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # @harness-engineering/cli
2
+
3
+ CLI for Harness Engineering toolkit.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @harness-engineering/cli
9
+ ```
10
+
11
+ ## Commands
12
+
13
+ ### `harness init`
14
+
15
+ Initialize a new harness-engineering project.
16
+
17
+ ```bash
18
+ harness init
19
+ harness init --name my-project
20
+ ```
21
+
22
+ ### `harness validate`
23
+
24
+ Run all validation checks.
25
+
26
+ ```bash
27
+ harness validate
28
+ harness validate --json
29
+ harness validate --verbose
30
+ ```
31
+
32
+ ### `harness check-deps`
33
+
34
+ Validate dependency layers and detect circular dependencies.
35
+
36
+ ```bash
37
+ harness check-deps
38
+ ```
39
+
40
+ ### `harness check-docs`
41
+
42
+ Check documentation coverage.
43
+
44
+ ```bash
45
+ harness check-docs
46
+ harness check-docs --min-coverage 90
47
+ ```
48
+
49
+ ### `harness cleanup`
50
+
51
+ Detect entropy issues.
52
+
53
+ ```bash
54
+ harness cleanup
55
+ harness cleanup --type drift
56
+ harness cleanup --type dead-code
57
+ ```
58
+
59
+ ### `harness fix-drift`
60
+
61
+ Auto-fix entropy issues.
62
+
63
+ ```bash
64
+ harness fix-drift # Dry run
65
+ harness fix-drift --no-dry-run # Apply fixes
66
+ ```
67
+
68
+ ### `harness add`
69
+
70
+ Add components to the project.
71
+
72
+ ```bash
73
+ harness add layer services
74
+ harness add module user
75
+ harness add doc architecture
76
+ ```
77
+
78
+ ### `harness agent`
79
+
80
+ Agent orchestration commands.
81
+
82
+ ```bash
83
+ harness agent run review
84
+ harness agent review
85
+ ```
86
+
87
+ ## Global Options
88
+
89
+ - `--config <path>` - Path to config file
90
+ - `--json` - Output as JSON
91
+ - `--verbose` - Verbose output
92
+ - `--quiet` - Minimal output
93
+
94
+ ## Configuration
95
+
96
+ Create `harness.config.json` in your project root:
97
+
98
+ ```json
99
+ {
100
+ "version": 1,
101
+ "name": "my-project",
102
+ "layers": [
103
+ { "name": "types", "pattern": "src/types/**", "allowedDependencies": [] },
104
+ { "name": "domain", "pattern": "src/domain/**", "allowedDependencies": ["types"] }
105
+ ],
106
+ "agentsMapPath": "./AGENTS.md",
107
+ "docsDir": "./docs"
108
+ }
109
+ ```
110
+
111
+ ## Exit Codes
112
+
113
+ - `0` - Success
114
+ - `1` - Validation failed
115
+ - `2` - Error
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ createProgram
4
+ } from "../chunk-5JDJNUEO.js";
5
+ import {
6
+ handleError
7
+ } from "../chunk-EFZOLZFB.js";
8
+
9
+ // src/bin/harness.ts
10
+ async function main() {
11
+ const program = createProgram();
12
+ try {
13
+ await program.parseAsync(process.argv);
14
+ } catch (error) {
15
+ handleError(error);
16
+ }
17
+ }
18
+ void main();
@@ -0,0 +1,526 @@
1
+ // src/index.ts
2
+ import { Command as Command4 } 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/index.ts
506
+ function createProgram() {
507
+ const program = new Command4();
508
+ 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");
509
+ program.addCommand(createValidateCommand());
510
+ program.addCommand(createCheckDepsCommand());
511
+ program.addCommand(createCheckDocsCommand());
512
+ return program;
513
+ }
514
+
515
+ export {
516
+ ExitCode,
517
+ CLIError,
518
+ handleError,
519
+ findConfigFile,
520
+ loadConfig,
521
+ resolveConfig,
522
+ OutputMode,
523
+ OutputFormatter,
524
+ logger,
525
+ createProgram
526
+ };