@harness-engineering/cli 1.8.2 → 1.10.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/dist/agents/skills/claude-code/cleanup-dead-code/SKILL.md +3 -3
- package/dist/agents/skills/claude-code/harness-autopilot/SKILL.md +20 -3
- package/dist/agents/skills/claude-code/harness-brainstorming/SKILL.md +55 -5
- package/dist/agents/skills/claude-code/harness-code-review/SKILL.md +36 -15
- package/dist/agents/skills/claude-code/harness-codebase-cleanup/SKILL.md +1 -1
- package/dist/agents/skills/claude-code/harness-execution/SKILL.md +70 -13
- package/dist/agents/skills/claude-code/harness-planning/SKILL.md +41 -3
- package/dist/agents/skills/claude-code/harness-pre-commit-review/SKILL.md +28 -3
- package/dist/agents/skills/claude-code/harness-release-readiness/SKILL.md +14 -2
- package/dist/agents/skills/claude-code/harness-verification/SKILL.md +18 -2
- package/dist/agents/skills/gemini-cli/cleanup-dead-code/SKILL.md +3 -3
- package/dist/agents/skills/gemini-cli/harness-autopilot/SKILL.md +20 -3
- package/dist/agents/skills/gemini-cli/harness-brainstorming/SKILL.md +55 -5
- package/dist/agents/skills/gemini-cli/harness-code-review/SKILL.md +36 -15
- package/dist/agents/skills/gemini-cli/harness-codebase-cleanup/SKILL.md +1 -1
- package/dist/agents/skills/gemini-cli/harness-execution/SKILL.md +70 -13
- package/dist/agents/skills/gemini-cli/harness-planning/SKILL.md +41 -3
- package/dist/agents/skills/gemini-cli/harness-pre-commit-review/SKILL.md +28 -3
- package/dist/agents/skills/gemini-cli/harness-release-readiness/SKILL.md +14 -2
- package/dist/agents/skills/gemini-cli/harness-verification/SKILL.md +18 -2
- package/dist/agents-md-EMRFLNBC.js +8 -0
- package/dist/architecture-5JNN5L3M.js +13 -0
- package/dist/bin/harness-mcp.d.ts +1 -0
- package/dist/bin/harness-mcp.js +28 -0
- package/dist/bin/harness.js +42 -8
- package/dist/check-phase-gate-WOKIYGAM.js +12 -0
- package/dist/chunk-46YA6FI3.js +293 -0
- package/dist/chunk-4PFMY3H7.js +248 -0
- package/dist/{chunk-LB4GRDDV.js → chunk-72GHBOL2.js} +1 -1
- package/dist/chunk-7X7ZAYMY.js +373 -0
- package/dist/chunk-B7HFEHWP.js +35 -0
- package/dist/chunk-BM3PWGXQ.js +14 -0
- package/dist/chunk-C2ERUR3L.js +255 -0
- package/dist/chunk-CWZ4Y2PO.js +189 -0
- package/dist/{chunk-ULSRSP53.js → chunk-ECUJQS3B.js} +11 -112
- package/dist/chunk-EOLRW32Q.js +72 -0
- package/dist/chunk-F3YDAJFQ.js +125 -0
- package/dist/chunk-F4PTVZWA.js +116 -0
- package/dist/chunk-FPIPT36X.js +187 -0
- package/dist/chunk-FX7SQHGD.js +103 -0
- package/dist/chunk-HIOXKZYF.js +15 -0
- package/dist/chunk-IDZNPTYD.js +16 -0
- package/dist/chunk-JSTQ3AWB.js +31 -0
- package/dist/chunk-K6XAPGML.js +27 -0
- package/dist/chunk-KET4QQZB.js +8 -0
- package/dist/chunk-LXU5M77O.js +4028 -0
- package/dist/chunk-MDUK2J2O.js +67 -0
- package/dist/chunk-MHBMTPW7.js +29 -0
- package/dist/chunk-MO4YQOMB.js +85 -0
- package/dist/chunk-NKDM3FMH.js +52 -0
- package/dist/{chunk-SAB3VXOW.js → chunk-NX6DSZSM.js} +144 -111
- package/dist/chunk-OPXH4CQN.js +62 -0
- package/dist/{chunk-Y7U5AYAL.js → chunk-PAHHT2IK.js} +471 -2719
- package/dist/chunk-PMTFPOCT.js +122 -0
- package/dist/chunk-PSXF277V.js +89 -0
- package/dist/chunk-Q6AB7W5Z.js +135 -0
- package/dist/chunk-QPEH2QPG.js +347 -0
- package/dist/chunk-TEFCFC4H.js +15 -0
- package/dist/chunk-TRAPF4IX.js +185 -0
- package/dist/chunk-VUCPTQ6G.js +67 -0
- package/dist/chunk-W6Y7ZW3Y.js +13 -0
- package/dist/chunk-ZOAWBDWU.js +72 -0
- package/dist/ci-workflow-ZBBUNTHQ.js +8 -0
- package/dist/constants-5JGUXPEK.js +6 -0
- package/dist/create-skill-LUWO46WF.js +11 -0
- package/dist/dist-D4RYGUZE.js +14 -0
- package/dist/{dist-K6KTTN3I.js → dist-I7DB5VKB.js} +237 -0
- package/dist/dist-L7LAAQAS.js +18 -0
- package/dist/{dist-ZODQVGC4.js → dist-PBTNVK6K.js} +8 -6
- package/dist/docs-PTJGD6XI.js +12 -0
- package/dist/engine-SCMZ3G3E.js +8 -0
- package/dist/entropy-YIUBGKY7.js +12 -0
- package/dist/feedback-WEVQSLAA.js +18 -0
- package/dist/generate-agent-definitions-BU5LOJTI.js +15 -0
- package/dist/glob-helper-5OHBUQAI.js +52 -0
- package/dist/graph-loader-RLO3KRIX.js +8 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.js +84 -33
- package/dist/loader-6S6PVGSF.js +10 -0
- package/dist/mcp-BNLBTCXZ.js +34 -0
- package/dist/performance-5TVW6SA6.js +24 -0
- package/dist/review-pipeline-4JTQAWKW.js +9 -0
- package/dist/runner-VMYLHWOC.js +6 -0
- package/dist/runtime-PXIM7UV6.js +9 -0
- package/dist/security-URYTKLGK.js +9 -0
- package/dist/skill-executor-KVS47DAU.js +8 -0
- package/dist/validate-KSDUUK2M.js +12 -0
- package/dist/validate-cross-check-WZAX357V.js +8 -0
- package/dist/version-KFFPOQAX.js +6 -0
- package/package.json +7 -5
- package/dist/create-skill-UZOHMXRU.js +0 -8
- package/dist/validate-cross-check-DLNK423G.js +0 -7
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import {
|
|
2
|
+
logger
|
|
3
|
+
} from "./chunk-HIOXKZYF.js";
|
|
4
|
+
import {
|
|
5
|
+
CLIError,
|
|
6
|
+
ExitCode
|
|
7
|
+
} from "./chunk-B7HFEHWP.js";
|
|
8
|
+
import {
|
|
9
|
+
Err,
|
|
10
|
+
Ok
|
|
11
|
+
} from "./chunk-MHBMTPW7.js";
|
|
12
|
+
|
|
13
|
+
// src/commands/check-phase-gate.ts
|
|
14
|
+
import { Command } from "commander";
|
|
15
|
+
import * as path2 from "path";
|
|
16
|
+
import * as fs2 from "fs";
|
|
17
|
+
|
|
18
|
+
// src/config/loader.ts
|
|
19
|
+
import * as fs from "fs";
|
|
20
|
+
import * as path from "path";
|
|
21
|
+
|
|
22
|
+
// src/config/schema.ts
|
|
23
|
+
import { z } from "zod";
|
|
24
|
+
var LayerSchema = z.object({
|
|
25
|
+
name: z.string(),
|
|
26
|
+
pattern: z.string(),
|
|
27
|
+
allowedDependencies: z.array(z.string())
|
|
28
|
+
});
|
|
29
|
+
var ForbiddenImportSchema = z.object({
|
|
30
|
+
from: z.string(),
|
|
31
|
+
disallow: z.array(z.string()),
|
|
32
|
+
message: z.string().optional()
|
|
33
|
+
});
|
|
34
|
+
var BoundaryConfigSchema = z.object({
|
|
35
|
+
requireSchema: z.array(z.string())
|
|
36
|
+
});
|
|
37
|
+
var AgentConfigSchema = z.object({
|
|
38
|
+
executor: z.enum(["subprocess", "cloud", "noop"]).default("subprocess"),
|
|
39
|
+
timeout: z.number().default(3e5),
|
|
40
|
+
skills: z.array(z.string()).optional()
|
|
41
|
+
});
|
|
42
|
+
var EntropyConfigSchema = z.object({
|
|
43
|
+
excludePatterns: z.array(z.string()).default(["**/node_modules/**", "**/*.test.ts"]),
|
|
44
|
+
autoFix: z.boolean().default(false)
|
|
45
|
+
});
|
|
46
|
+
var PhaseGateMappingSchema = z.object({
|
|
47
|
+
implPattern: z.string(),
|
|
48
|
+
specPattern: z.string()
|
|
49
|
+
});
|
|
50
|
+
var PhaseGatesConfigSchema = z.object({
|
|
51
|
+
enabled: z.boolean().default(false),
|
|
52
|
+
severity: z.enum(["error", "warning"]).default("error"),
|
|
53
|
+
mappings: z.array(PhaseGateMappingSchema).default([{ implPattern: "src/**/*.ts", specPattern: "docs/changes/{feature}/proposal.md" }])
|
|
54
|
+
});
|
|
55
|
+
var SecurityConfigSchema = z.object({
|
|
56
|
+
enabled: z.boolean().default(true),
|
|
57
|
+
strict: z.boolean().default(false),
|
|
58
|
+
rules: z.record(z.string(), z.enum(["off", "error", "warning", "info"])).optional(),
|
|
59
|
+
exclude: z.array(z.string()).optional()
|
|
60
|
+
}).passthrough();
|
|
61
|
+
var PerformanceConfigSchema = z.object({
|
|
62
|
+
complexity: z.record(z.unknown()).optional(),
|
|
63
|
+
coupling: z.record(z.unknown()).optional(),
|
|
64
|
+
sizeBudget: z.record(z.unknown()).optional()
|
|
65
|
+
}).passthrough();
|
|
66
|
+
var DesignConfigSchema = z.object({
|
|
67
|
+
strictness: z.enum(["strict", "standard", "permissive"]).default("standard"),
|
|
68
|
+
platforms: z.array(z.enum(["web", "mobile"])).default([]),
|
|
69
|
+
tokenPath: z.string().optional(),
|
|
70
|
+
aestheticIntent: z.string().optional()
|
|
71
|
+
});
|
|
72
|
+
var I18nCoverageConfigSchema = z.object({
|
|
73
|
+
minimumPercent: z.number().min(0).max(100).default(100),
|
|
74
|
+
requirePlurals: z.boolean().default(true),
|
|
75
|
+
detectUntranslated: z.boolean().default(true)
|
|
76
|
+
});
|
|
77
|
+
var I18nMcpConfigSchema = z.object({
|
|
78
|
+
server: z.string(),
|
|
79
|
+
projectId: z.string().optional()
|
|
80
|
+
});
|
|
81
|
+
var I18nConfigSchema = z.object({
|
|
82
|
+
enabled: z.boolean().default(false),
|
|
83
|
+
strictness: z.enum(["strict", "standard", "permissive"]).default("standard"),
|
|
84
|
+
sourceLocale: z.string().default("en"),
|
|
85
|
+
targetLocales: z.array(z.string()).default([]),
|
|
86
|
+
framework: z.enum([
|
|
87
|
+
"auto",
|
|
88
|
+
"i18next",
|
|
89
|
+
"react-intl",
|
|
90
|
+
"vue-i18n",
|
|
91
|
+
"flutter-intl",
|
|
92
|
+
"apple",
|
|
93
|
+
"android",
|
|
94
|
+
"custom"
|
|
95
|
+
]).default("auto"),
|
|
96
|
+
format: z.string().default("json"),
|
|
97
|
+
messageFormat: z.enum(["icu", "i18next", "custom"]).default("icu"),
|
|
98
|
+
keyConvention: z.enum(["dot-notation", "snake_case", "camelCase", "custom"]).default("dot-notation"),
|
|
99
|
+
translationPaths: z.record(z.string(), z.string()).optional(),
|
|
100
|
+
platforms: z.array(z.enum(["web", "mobile", "backend"])).default([]),
|
|
101
|
+
industry: z.string().optional(),
|
|
102
|
+
coverage: I18nCoverageConfigSchema.optional(),
|
|
103
|
+
pseudoLocale: z.string().optional(),
|
|
104
|
+
mcp: I18nMcpConfigSchema.optional()
|
|
105
|
+
});
|
|
106
|
+
var ModelTierConfigSchema = z.object({
|
|
107
|
+
fast: z.string().optional(),
|
|
108
|
+
standard: z.string().optional(),
|
|
109
|
+
strong: z.string().optional()
|
|
110
|
+
});
|
|
111
|
+
var ReviewConfigSchema = z.object({
|
|
112
|
+
model_tiers: ModelTierConfigSchema.optional()
|
|
113
|
+
});
|
|
114
|
+
var HarnessConfigSchema = z.object({
|
|
115
|
+
version: z.literal(1),
|
|
116
|
+
name: z.string().optional(),
|
|
117
|
+
rootDir: z.string().default("."),
|
|
118
|
+
layers: z.array(LayerSchema).optional(),
|
|
119
|
+
forbiddenImports: z.array(ForbiddenImportSchema).optional(),
|
|
120
|
+
boundaries: BoundaryConfigSchema.optional(),
|
|
121
|
+
agentsMapPath: z.string().default("./AGENTS.md"),
|
|
122
|
+
docsDir: z.string().default("./docs"),
|
|
123
|
+
agent: AgentConfigSchema.optional(),
|
|
124
|
+
entropy: EntropyConfigSchema.optional(),
|
|
125
|
+
security: SecurityConfigSchema.optional(),
|
|
126
|
+
performance: PerformanceConfigSchema.optional(),
|
|
127
|
+
template: z.object({
|
|
128
|
+
level: z.enum(["basic", "intermediate", "advanced"]),
|
|
129
|
+
framework: z.string().optional(),
|
|
130
|
+
version: z.number()
|
|
131
|
+
}).optional(),
|
|
132
|
+
phaseGates: PhaseGatesConfigSchema.optional(),
|
|
133
|
+
design: DesignConfigSchema.optional(),
|
|
134
|
+
i18n: I18nConfigSchema.optional(),
|
|
135
|
+
review: ReviewConfigSchema.optional(),
|
|
136
|
+
updateCheckInterval: z.number().int().min(0).optional()
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// src/config/loader.ts
|
|
140
|
+
var CONFIG_FILENAMES = ["harness.config.json"];
|
|
141
|
+
function findConfigFile(startDir = process.cwd()) {
|
|
142
|
+
let currentDir = path.resolve(startDir);
|
|
143
|
+
const root = path.parse(currentDir).root;
|
|
144
|
+
while (currentDir !== root) {
|
|
145
|
+
for (const filename of CONFIG_FILENAMES) {
|
|
146
|
+
const configPath = path.join(currentDir, filename);
|
|
147
|
+
if (fs.existsSync(configPath)) {
|
|
148
|
+
return Ok(configPath);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
currentDir = path.dirname(currentDir);
|
|
152
|
+
}
|
|
153
|
+
return Err(
|
|
154
|
+
new CLIError('No harness.config.json found. Run "harness init" to create one.', ExitCode.ERROR)
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
function loadConfig(configPath) {
|
|
158
|
+
if (!fs.existsSync(configPath)) {
|
|
159
|
+
return Err(new CLIError(`Config file not found: ${configPath}`, ExitCode.ERROR));
|
|
160
|
+
}
|
|
161
|
+
let rawConfig;
|
|
162
|
+
try {
|
|
163
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
164
|
+
rawConfig = JSON.parse(content);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
return Err(
|
|
167
|
+
new CLIError(
|
|
168
|
+
`Failed to parse config: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
169
|
+
ExitCode.ERROR
|
|
170
|
+
)
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
const parsed = HarnessConfigSchema.safeParse(rawConfig);
|
|
174
|
+
if (!parsed.success) {
|
|
175
|
+
const issues = parsed.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
176
|
+
return Err(new CLIError(`Invalid config:
|
|
177
|
+
${issues}`, ExitCode.ERROR));
|
|
178
|
+
}
|
|
179
|
+
return Ok(parsed.data);
|
|
180
|
+
}
|
|
181
|
+
function resolveConfig(configPath) {
|
|
182
|
+
if (configPath) {
|
|
183
|
+
return loadConfig(configPath);
|
|
184
|
+
}
|
|
185
|
+
const findResult = findConfigFile();
|
|
186
|
+
if (!findResult.ok) {
|
|
187
|
+
return findResult;
|
|
188
|
+
}
|
|
189
|
+
return loadConfig(findResult.value);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/utils/files.ts
|
|
193
|
+
import { glob } from "glob";
|
|
194
|
+
async function findFiles(pattern, cwd = process.cwd()) {
|
|
195
|
+
return glob(pattern, { cwd, absolute: true });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/output/formatter.ts
|
|
199
|
+
import chalk from "chalk";
|
|
200
|
+
var OutputMode = {
|
|
201
|
+
JSON: "json",
|
|
202
|
+
TEXT: "text",
|
|
203
|
+
QUIET: "quiet",
|
|
204
|
+
VERBOSE: "verbose"
|
|
205
|
+
};
|
|
206
|
+
var OutputFormatter = class {
|
|
207
|
+
constructor(mode = OutputMode.TEXT) {
|
|
208
|
+
this.mode = mode;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Format raw data (for JSON mode)
|
|
212
|
+
*/
|
|
213
|
+
format(data) {
|
|
214
|
+
if (this.mode === OutputMode.JSON) {
|
|
215
|
+
return JSON.stringify(data, null, 2);
|
|
216
|
+
}
|
|
217
|
+
return String(data);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Format validation result
|
|
221
|
+
*/
|
|
222
|
+
formatValidation(result) {
|
|
223
|
+
if (this.mode === OutputMode.JSON) {
|
|
224
|
+
return JSON.stringify(result, null, 2);
|
|
225
|
+
}
|
|
226
|
+
if (this.mode === OutputMode.QUIET) {
|
|
227
|
+
if (result.valid) return "";
|
|
228
|
+
return result.issues.map((i) => `${i.file ?? ""}: ${i.message}`).join("\n");
|
|
229
|
+
}
|
|
230
|
+
const lines = [];
|
|
231
|
+
if (result.valid) {
|
|
232
|
+
lines.push(chalk.green("v validation passed"));
|
|
233
|
+
} else {
|
|
234
|
+
lines.push(chalk.red(`x Validation failed (${result.issues.length} issues)`));
|
|
235
|
+
lines.push("");
|
|
236
|
+
for (const issue of result.issues) {
|
|
237
|
+
const location = issue.file ? issue.line ? `${issue.file}:${issue.line}` : issue.file : "unknown";
|
|
238
|
+
lines.push(` ${chalk.yellow("*")} ${chalk.dim(location)}`);
|
|
239
|
+
lines.push(` ${issue.message}`);
|
|
240
|
+
if (issue.suggestion && this.mode === OutputMode.VERBOSE) {
|
|
241
|
+
lines.push(` ${chalk.dim("->")} ${issue.suggestion}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return lines.join("\n");
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Format a summary line
|
|
249
|
+
*/
|
|
250
|
+
formatSummary(label, value, success) {
|
|
251
|
+
if (this.mode === OutputMode.JSON || this.mode === OutputMode.QUIET) {
|
|
252
|
+
return "";
|
|
253
|
+
}
|
|
254
|
+
const icon = success ? chalk.green("v") : chalk.red("x");
|
|
255
|
+
return `${icon} ${label}: ${value}`;
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// src/commands/check-phase-gate.ts
|
|
260
|
+
function resolveSpecPath(implFile, implPattern, specPattern, cwd) {
|
|
261
|
+
const relImpl = path2.relative(cwd, implFile).replace(/\\/g, "/");
|
|
262
|
+
const implBase = (implPattern.split("*")[0] ?? "").replace(/\/+$/, "");
|
|
263
|
+
const afterBase = relImpl.startsWith(implBase + "/") ? relImpl.slice(implBase.length + 1) : relImpl;
|
|
264
|
+
const segments = afterBase.split("/");
|
|
265
|
+
const firstSegment = segments[0] ?? "";
|
|
266
|
+
const feature = segments.length > 1 ? firstSegment : path2.basename(firstSegment, path2.extname(firstSegment));
|
|
267
|
+
const specRelative = specPattern.replace("{feature}", feature);
|
|
268
|
+
return path2.resolve(cwd, specRelative);
|
|
269
|
+
}
|
|
270
|
+
async function runCheckPhaseGate(options) {
|
|
271
|
+
const configResult = resolveConfig(options.configPath);
|
|
272
|
+
if (!configResult.ok) {
|
|
273
|
+
return configResult;
|
|
274
|
+
}
|
|
275
|
+
const config = configResult.value;
|
|
276
|
+
const cwd = options.cwd ?? (options.configPath ? path2.dirname(path2.resolve(options.configPath)) : process.cwd());
|
|
277
|
+
if (!config.phaseGates?.enabled) {
|
|
278
|
+
return Ok({
|
|
279
|
+
pass: true,
|
|
280
|
+
skipped: true,
|
|
281
|
+
missingSpecs: [],
|
|
282
|
+
checkedFiles: 0
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
const phaseGates = config.phaseGates;
|
|
286
|
+
const missingSpecs = [];
|
|
287
|
+
let checkedFiles = 0;
|
|
288
|
+
for (const mapping of phaseGates.mappings) {
|
|
289
|
+
const implFiles = await findFiles(mapping.implPattern, cwd);
|
|
290
|
+
for (const implFile of implFiles) {
|
|
291
|
+
checkedFiles++;
|
|
292
|
+
const expectedSpec = resolveSpecPath(implFile, mapping.implPattern, mapping.specPattern, cwd);
|
|
293
|
+
if (!fs2.existsSync(expectedSpec)) {
|
|
294
|
+
missingSpecs.push({
|
|
295
|
+
implFile: path2.relative(cwd, implFile).replace(/\\/g, "/"),
|
|
296
|
+
expectedSpec: path2.relative(cwd, expectedSpec).replace(/\\/g, "/")
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const pass = missingSpecs.length === 0;
|
|
302
|
+
return Ok({
|
|
303
|
+
pass,
|
|
304
|
+
skipped: false,
|
|
305
|
+
severity: phaseGates.severity,
|
|
306
|
+
missingSpecs,
|
|
307
|
+
checkedFiles
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
function createCheckPhaseGateCommand() {
|
|
311
|
+
const command = new Command("check-phase-gate").description("Verify that implementation files have matching spec documents").action(async (_opts, cmd) => {
|
|
312
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
313
|
+
const mode = globalOpts.json ? OutputMode.JSON : globalOpts.quiet ? OutputMode.QUIET : globalOpts.verbose ? OutputMode.VERBOSE : OutputMode.TEXT;
|
|
314
|
+
const formatter = new OutputFormatter(mode);
|
|
315
|
+
const result = await runCheckPhaseGate({
|
|
316
|
+
configPath: globalOpts.config,
|
|
317
|
+
json: globalOpts.json,
|
|
318
|
+
verbose: globalOpts.verbose,
|
|
319
|
+
quiet: globalOpts.quiet
|
|
320
|
+
});
|
|
321
|
+
if (!result.ok) {
|
|
322
|
+
if (mode === OutputMode.JSON) {
|
|
323
|
+
console.log(JSON.stringify({ error: result.error.message }));
|
|
324
|
+
} else {
|
|
325
|
+
logger.error(result.error.message);
|
|
326
|
+
}
|
|
327
|
+
process.exit(result.error.exitCode);
|
|
328
|
+
}
|
|
329
|
+
const value = result.value;
|
|
330
|
+
if (value.skipped) {
|
|
331
|
+
if (mode === OutputMode.JSON) {
|
|
332
|
+
console.log(formatter.format(value));
|
|
333
|
+
} else if (mode !== OutputMode.QUIET) {
|
|
334
|
+
logger.dim("Phase gates not enabled, skipping.");
|
|
335
|
+
}
|
|
336
|
+
process.exit(ExitCode.SUCCESS);
|
|
337
|
+
}
|
|
338
|
+
const output = formatter.formatValidation({
|
|
339
|
+
valid: value.pass,
|
|
340
|
+
issues: value.missingSpecs.map((m) => ({
|
|
341
|
+
file: m.implFile,
|
|
342
|
+
message: `Missing spec: ${m.expectedSpec}`
|
|
343
|
+
}))
|
|
344
|
+
});
|
|
345
|
+
if (output) {
|
|
346
|
+
console.log(output);
|
|
347
|
+
}
|
|
348
|
+
const summary = formatter.formatSummary(
|
|
349
|
+
"Phase gate check",
|
|
350
|
+
`${value.checkedFiles} files checked, ${value.missingSpecs.length} missing specs`,
|
|
351
|
+
value.pass
|
|
352
|
+
);
|
|
353
|
+
if (summary) {
|
|
354
|
+
console.log(summary);
|
|
355
|
+
}
|
|
356
|
+
if (!value.pass && value.severity === "error") {
|
|
357
|
+
process.exit(ExitCode.VALIDATION_FAILED);
|
|
358
|
+
}
|
|
359
|
+
process.exit(ExitCode.SUCCESS);
|
|
360
|
+
});
|
|
361
|
+
return command;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export {
|
|
365
|
+
findConfigFile,
|
|
366
|
+
loadConfig,
|
|
367
|
+
resolveConfig,
|
|
368
|
+
OutputMode,
|
|
369
|
+
OutputFormatter,
|
|
370
|
+
findFiles,
|
|
371
|
+
runCheckPhaseGate,
|
|
372
|
+
createCheckPhaseGateCommand
|
|
373
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// src/utils/errors.ts
|
|
2
|
+
var ExitCode = {
|
|
3
|
+
SUCCESS: 0,
|
|
4
|
+
VALIDATION_FAILED: 1,
|
|
5
|
+
ERROR: 2
|
|
6
|
+
};
|
|
7
|
+
var CLIError = class extends Error {
|
|
8
|
+
exitCode;
|
|
9
|
+
constructor(message, exitCode = ExitCode.ERROR) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "CLIError";
|
|
12
|
+
this.exitCode = exitCode;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
function formatError(error) {
|
|
16
|
+
if (error instanceof CLIError) {
|
|
17
|
+
return `Error: ${error.message}`;
|
|
18
|
+
}
|
|
19
|
+
if (error instanceof Error) {
|
|
20
|
+
return `Error: ${error.message}`;
|
|
21
|
+
}
|
|
22
|
+
return `Error: ${String(error)}`;
|
|
23
|
+
}
|
|
24
|
+
function handleError(error) {
|
|
25
|
+
const message = formatError(error);
|
|
26
|
+
console.error(message);
|
|
27
|
+
const exitCode = error instanceof CLIError ? error.exitCode : ExitCode.ERROR;
|
|
28
|
+
process.exit(exitCode);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
ExitCode,
|
|
33
|
+
CLIError,
|
|
34
|
+
handleError
|
|
35
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// src/version.ts
|
|
2
|
+
import { createRequire } from "module";
|
|
3
|
+
var require_ = createRequire(import.meta.url);
|
|
4
|
+
var resolved;
|
|
5
|
+
try {
|
|
6
|
+
resolved = require_("../package.json").version ?? "0.0.0";
|
|
7
|
+
} catch {
|
|
8
|
+
resolved = "0.0.0";
|
|
9
|
+
}
|
|
10
|
+
var CLI_VERSION = resolved;
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
CLI_VERSION
|
|
14
|
+
};
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Err,
|
|
3
|
+
Ok
|
|
4
|
+
} from "./chunk-MHBMTPW7.js";
|
|
5
|
+
|
|
6
|
+
// src/templates/engine.ts
|
|
7
|
+
import * as fs from "fs";
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
import Handlebars from "handlebars";
|
|
10
|
+
|
|
11
|
+
// src/templates/schema.ts
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
var MergeStrategySchema = z.object({
|
|
14
|
+
json: z.enum(["deep-merge", "overlay-wins"]).default("deep-merge"),
|
|
15
|
+
files: z.literal("overlay-wins").default("overlay-wins")
|
|
16
|
+
});
|
|
17
|
+
var TemplateMetadataSchema = z.object({
|
|
18
|
+
name: z.string(),
|
|
19
|
+
description: z.string(),
|
|
20
|
+
level: z.enum(["basic", "intermediate", "advanced"]).optional(),
|
|
21
|
+
framework: z.string().optional(),
|
|
22
|
+
extends: z.string().optional(),
|
|
23
|
+
mergeStrategy: MergeStrategySchema.default({}),
|
|
24
|
+
version: z.literal(1)
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// src/templates/merger.ts
|
|
28
|
+
function isPlainObject(val) {
|
|
29
|
+
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
30
|
+
}
|
|
31
|
+
function deepMergeJson(base, overlay) {
|
|
32
|
+
const result = { ...base };
|
|
33
|
+
for (const key of Object.keys(overlay)) {
|
|
34
|
+
if (isPlainObject(result[key]) && isPlainObject(overlay[key])) {
|
|
35
|
+
result[key] = deepMergeJson(
|
|
36
|
+
result[key],
|
|
37
|
+
overlay[key]
|
|
38
|
+
);
|
|
39
|
+
} else {
|
|
40
|
+
result[key] = overlay[key];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
var CONCAT_KEYS = /* @__PURE__ */ new Set(["dependencies", "devDependencies", "peerDependencies"]);
|
|
46
|
+
function mergePackageJson(base, overlay) {
|
|
47
|
+
const result = { ...base };
|
|
48
|
+
for (const key of Object.keys(overlay)) {
|
|
49
|
+
if (CONCAT_KEYS.has(key) && isPlainObject(result[key]) && isPlainObject(overlay[key])) {
|
|
50
|
+
result[key] = {
|
|
51
|
+
...result[key],
|
|
52
|
+
...overlay[key]
|
|
53
|
+
};
|
|
54
|
+
} else if (isPlainObject(result[key]) && isPlainObject(overlay[key])) {
|
|
55
|
+
result[key] = deepMergeJson(
|
|
56
|
+
result[key],
|
|
57
|
+
overlay[key]
|
|
58
|
+
);
|
|
59
|
+
} else {
|
|
60
|
+
result[key] = overlay[key];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/templates/engine.ts
|
|
67
|
+
var TemplateEngine = class {
|
|
68
|
+
constructor(templatesDir) {
|
|
69
|
+
this.templatesDir = templatesDir;
|
|
70
|
+
}
|
|
71
|
+
listTemplates() {
|
|
72
|
+
try {
|
|
73
|
+
const entries = fs.readdirSync(this.templatesDir, { withFileTypes: true });
|
|
74
|
+
const templates = [];
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
if (!entry.isDirectory()) continue;
|
|
77
|
+
const metaPath = path.join(this.templatesDir, entry.name, "template.json");
|
|
78
|
+
if (!fs.existsSync(metaPath)) continue;
|
|
79
|
+
const raw = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
|
|
80
|
+
const parsed = TemplateMetadataSchema.safeParse(raw);
|
|
81
|
+
if (parsed.success) templates.push(parsed.data);
|
|
82
|
+
}
|
|
83
|
+
return Ok(templates);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
return Err(
|
|
86
|
+
new Error(
|
|
87
|
+
`Failed to list templates: ${error instanceof Error ? error.message : String(error)}`
|
|
88
|
+
)
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
resolveTemplate(level, framework) {
|
|
93
|
+
const levelDir = this.findTemplateDir(level, "level");
|
|
94
|
+
if (!levelDir) return Err(new Error(`Template not found for level: ${level}`));
|
|
95
|
+
const metaPath = path.join(levelDir, "template.json");
|
|
96
|
+
const metaRaw = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
|
|
97
|
+
const metaResult = TemplateMetadataSchema.safeParse(metaRaw);
|
|
98
|
+
if (!metaResult.success)
|
|
99
|
+
return Err(new Error(`Invalid template.json in ${level}: ${metaResult.error.message}`));
|
|
100
|
+
const metadata = metaResult.data;
|
|
101
|
+
let files = [];
|
|
102
|
+
if (metadata.extends) {
|
|
103
|
+
const baseDir = path.join(this.templatesDir, metadata.extends);
|
|
104
|
+
if (fs.existsSync(baseDir)) files = this.collectFiles(baseDir, metadata.extends);
|
|
105
|
+
}
|
|
106
|
+
const levelFiles = this.collectFiles(levelDir, level);
|
|
107
|
+
files = this.mergeFileLists(files, levelFiles);
|
|
108
|
+
let overlayMetadata;
|
|
109
|
+
if (framework) {
|
|
110
|
+
const frameworkDir = this.findTemplateDir(framework, "framework");
|
|
111
|
+
if (!frameworkDir) return Err(new Error(`Framework template not found: ${framework}`));
|
|
112
|
+
const fMetaPath = path.join(frameworkDir, "template.json");
|
|
113
|
+
const fMetaRaw = JSON.parse(fs.readFileSync(fMetaPath, "utf-8"));
|
|
114
|
+
const fMetaResult = TemplateMetadataSchema.safeParse(fMetaRaw);
|
|
115
|
+
if (fMetaResult.success) overlayMetadata = fMetaResult.data;
|
|
116
|
+
const frameworkFiles = this.collectFiles(frameworkDir, framework);
|
|
117
|
+
files = this.mergeFileLists(files, frameworkFiles);
|
|
118
|
+
}
|
|
119
|
+
files = files.filter((f) => f.relativePath !== "template.json");
|
|
120
|
+
const resolved = { metadata, files };
|
|
121
|
+
if (overlayMetadata !== void 0) resolved.overlayMetadata = overlayMetadata;
|
|
122
|
+
return Ok(resolved);
|
|
123
|
+
}
|
|
124
|
+
render(template, context) {
|
|
125
|
+
const rendered = [];
|
|
126
|
+
const jsonBuffers = /* @__PURE__ */ new Map();
|
|
127
|
+
for (const file of template.files) {
|
|
128
|
+
const outputPath = file.relativePath.replace(/\.hbs$/, "");
|
|
129
|
+
if (file.isHandlebars) {
|
|
130
|
+
try {
|
|
131
|
+
const raw = fs.readFileSync(file.absolutePath, "utf-8");
|
|
132
|
+
const compiled = Handlebars.compile(raw, { strict: true });
|
|
133
|
+
const content = compiled(context);
|
|
134
|
+
if (outputPath.endsWith(".json") && file.relativePath.endsWith(".json.hbs")) {
|
|
135
|
+
if (!jsonBuffers.has(outputPath)) jsonBuffers.set(outputPath, []);
|
|
136
|
+
jsonBuffers.get(outputPath).push(JSON.parse(content));
|
|
137
|
+
} else {
|
|
138
|
+
rendered.push({ relativePath: outputPath, content });
|
|
139
|
+
}
|
|
140
|
+
} catch (error) {
|
|
141
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
142
|
+
return Err(
|
|
143
|
+
new Error(
|
|
144
|
+
`Template render failed in ${file.sourceTemplate}/${file.relativePath}: ${msg}`
|
|
145
|
+
)
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
try {
|
|
150
|
+
const content = fs.readFileSync(file.absolutePath, "utf-8");
|
|
151
|
+
rendered.push({ relativePath: file.relativePath, content });
|
|
152
|
+
} catch (error) {
|
|
153
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
154
|
+
return Err(
|
|
155
|
+
new Error(
|
|
156
|
+
`Template render failed in ${file.sourceTemplate}/${file.relativePath}: ${msg}`
|
|
157
|
+
)
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
for (const [outputPath, jsons] of jsonBuffers) {
|
|
164
|
+
let merged = {};
|
|
165
|
+
for (const json of jsons) {
|
|
166
|
+
merged = outputPath === "package.json" ? mergePackageJson(merged, json) : deepMergeJson(merged, json);
|
|
167
|
+
}
|
|
168
|
+
rendered.push({ relativePath: outputPath, content: JSON.stringify(merged, null, 2) });
|
|
169
|
+
}
|
|
170
|
+
} catch (error) {
|
|
171
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
172
|
+
return Err(new Error(`JSON merge failed: ${msg}`));
|
|
173
|
+
}
|
|
174
|
+
return Ok({ files: rendered });
|
|
175
|
+
}
|
|
176
|
+
write(files, targetDir, options) {
|
|
177
|
+
try {
|
|
178
|
+
const written = [];
|
|
179
|
+
for (const file of files.files) {
|
|
180
|
+
const targetPath = path.join(targetDir, file.relativePath);
|
|
181
|
+
const dir = path.dirname(targetPath);
|
|
182
|
+
if (!options.overwrite && fs.existsSync(targetPath)) continue;
|
|
183
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
184
|
+
fs.writeFileSync(targetPath, file.content);
|
|
185
|
+
written.push(file.relativePath);
|
|
186
|
+
}
|
|
187
|
+
return Ok(written);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
return Err(
|
|
190
|
+
new Error(
|
|
191
|
+
`Failed to write files: ${error instanceof Error ? error.message : String(error)}`
|
|
192
|
+
)
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
findTemplateDir(name, type) {
|
|
197
|
+
const entries = fs.readdirSync(this.templatesDir, { withFileTypes: true });
|
|
198
|
+
for (const entry of entries) {
|
|
199
|
+
if (!entry.isDirectory()) continue;
|
|
200
|
+
const metaPath = path.join(this.templatesDir, entry.name, "template.json");
|
|
201
|
+
if (!fs.existsSync(metaPath)) continue;
|
|
202
|
+
const raw = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
|
|
203
|
+
const parsed = TemplateMetadataSchema.safeParse(raw);
|
|
204
|
+
if (!parsed.success) continue;
|
|
205
|
+
if (type === "level" && parsed.data.level === name)
|
|
206
|
+
return path.join(this.templatesDir, entry.name);
|
|
207
|
+
if (type === "framework" && parsed.data.framework === name)
|
|
208
|
+
return path.join(this.templatesDir, entry.name);
|
|
209
|
+
if (parsed.data.name === name) return path.join(this.templatesDir, entry.name);
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
collectFiles(dir, sourceName) {
|
|
214
|
+
const files = [];
|
|
215
|
+
const walk = (currentDir) => {
|
|
216
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
217
|
+
for (const entry of entries) {
|
|
218
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
219
|
+
if (entry.isDirectory()) {
|
|
220
|
+
walk(fullPath);
|
|
221
|
+
} else {
|
|
222
|
+
files.push({
|
|
223
|
+
relativePath: path.relative(dir, fullPath).replace(/\\/g, "/"),
|
|
224
|
+
absolutePath: fullPath,
|
|
225
|
+
isHandlebars: entry.name.endsWith(".hbs"),
|
|
226
|
+
sourceTemplate: sourceName
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
walk(dir);
|
|
232
|
+
return files;
|
|
233
|
+
}
|
|
234
|
+
mergeFileLists(base, overlay) {
|
|
235
|
+
const map = /* @__PURE__ */ new Map();
|
|
236
|
+
for (const file of base) map.set(file.relativePath, file);
|
|
237
|
+
for (const file of overlay) {
|
|
238
|
+
if (file.relativePath.endsWith(".json.hbs")) {
|
|
239
|
+
const baseKey = base.find((f) => f.relativePath === file.relativePath);
|
|
240
|
+
if (baseKey) {
|
|
241
|
+
map.set(`__overlay__${file.relativePath}`, file);
|
|
242
|
+
} else {
|
|
243
|
+
map.set(file.relativePath, file);
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
map.set(file.relativePath, file);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return Array.from(map.values());
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
export {
|
|
254
|
+
TemplateEngine
|
|
255
|
+
};
|