@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
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
|
+
};
|