@harness-engineering/cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,317 @@
1
+ // src/index.ts
2
+ import { Command as Command2 } 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 cwd = options.cwd ?? process.cwd();
217
+ const configResult = resolveConfig(options.configPath);
218
+ if (!configResult.ok) {
219
+ return configResult;
220
+ }
221
+ const config = configResult.value;
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/index.ts
299
+ function createProgram() {
300
+ const program = new Command2();
301
+ 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");
302
+ program.addCommand(createValidateCommand());
303
+ return program;
304
+ }
305
+
306
+ export {
307
+ ExitCode,
308
+ CLIError,
309
+ handleError,
310
+ findConfigFile,
311
+ loadConfig,
312
+ resolveConfig,
313
+ OutputMode,
314
+ OutputFormatter,
315
+ logger,
316
+ createProgram
317
+ };