@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 +21 -0
- package/README.md +115 -0
- package/dist/bin/harness.d.ts +1 -0
- package/dist/bin/harness.js +18 -0
- package/dist/chunk-4RCIE5YB.js +526 -0
- package/dist/chunk-5JDJNUEO.js +2639 -0
- package/dist/chunk-6I25KNGR.js +1209 -0
- package/dist/chunk-77B7VOJM.js +1304 -0
- package/dist/chunk-ATN2MXAI.js +2798 -0
- package/dist/chunk-CJ2ZAYCV.js +2639 -0
- package/dist/chunk-EFZOLZFB.js +265 -0
- package/dist/chunk-EQKDZSPA.js +226 -0
- package/dist/chunk-FQB2ZTRA.js +1209 -0
- package/dist/chunk-G2SHRCBP.js +935 -0
- package/dist/chunk-GPYYJN6Z.js +2634 -0
- package/dist/chunk-H2LQ7ELQ.js +2795 -0
- package/dist/chunk-L64MEJOI.js +2512 -0
- package/dist/chunk-LFA7JNFB.js +2633 -0
- package/dist/chunk-NDZWBEZS.js +317 -0
- package/dist/chunk-RZHIR5XA.js +640 -0
- package/dist/chunk-SJJ37KLV.js +317 -0
- package/dist/chunk-SPR56MPD.js +2798 -0
- package/dist/chunk-TLZO4QIN.js +2850 -0
- package/dist/chunk-TUMCTRNV.js +2637 -0
- package/dist/chunk-Z7MYWXIH.js +2852 -0
- package/dist/chunk-ZOOWDP6S.js +2857 -0
- package/dist/create-skill-4GKJZB5R.js +8 -0
- package/dist/index.d.ts +543 -0
- package/dist/index.js +42 -0
- package/dist/validate-cross-check-N75UV2CO.js +69 -0
- package/dist/validate-cross-check-ZB2OZDOK.js +69 -0
- package/package.json +59 -0
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { Command as Command5 } 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/index.ts
|
|
619
|
+
function createProgram() {
|
|
620
|
+
const program = new Command5();
|
|
621
|
+
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");
|
|
622
|
+
program.addCommand(createValidateCommand());
|
|
623
|
+
program.addCommand(createCheckDepsCommand());
|
|
624
|
+
program.addCommand(createCheckDocsCommand());
|
|
625
|
+
program.addCommand(createInitCommand());
|
|
626
|
+
return program;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
export {
|
|
630
|
+
ExitCode,
|
|
631
|
+
CLIError,
|
|
632
|
+
handleError,
|
|
633
|
+
findConfigFile,
|
|
634
|
+
loadConfig,
|
|
635
|
+
resolveConfig,
|
|
636
|
+
OutputMode,
|
|
637
|
+
OutputFormatter,
|
|
638
|
+
logger,
|
|
639
|
+
createProgram
|
|
640
|
+
};
|