@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,1209 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { Command as Command11 } 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/index.ts
|
|
1184
|
+
function createProgram() {
|
|
1185
|
+
const program = new Command11();
|
|
1186
|
+
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");
|
|
1187
|
+
program.addCommand(createValidateCommand());
|
|
1188
|
+
program.addCommand(createCheckDepsCommand());
|
|
1189
|
+
program.addCommand(createCheckDocsCommand());
|
|
1190
|
+
program.addCommand(createInitCommand());
|
|
1191
|
+
program.addCommand(createCleanupCommand());
|
|
1192
|
+
program.addCommand(createFixDriftCommand());
|
|
1193
|
+
program.addCommand(createAgentCommand());
|
|
1194
|
+
program.addCommand(createAddCommand());
|
|
1195
|
+
return program;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
export {
|
|
1199
|
+
ExitCode,
|
|
1200
|
+
CLIError,
|
|
1201
|
+
handleError,
|
|
1202
|
+
findConfigFile,
|
|
1203
|
+
loadConfig,
|
|
1204
|
+
resolveConfig,
|
|
1205
|
+
OutputMode,
|
|
1206
|
+
OutputFormatter,
|
|
1207
|
+
logger,
|
|
1208
|
+
createProgram
|
|
1209
|
+
};
|