@halecraft/verify 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 +18 -0
- package/README.md +314 -0
- package/bin/verify.mjs +176 -0
- package/dist/index.d.ts +532 -0
- package/dist/index.js +1796 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1796 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// src/config.ts
|
|
9
|
+
import { existsSync } from "fs";
|
|
10
|
+
import { join, resolve } from "path";
|
|
11
|
+
import { pathToFileURL } from "url";
|
|
12
|
+
function defineConfig(config) {
|
|
13
|
+
return config;
|
|
14
|
+
}
|
|
15
|
+
function defineTask(task) {
|
|
16
|
+
return task;
|
|
17
|
+
}
|
|
18
|
+
var CONFIG_FILES = [
|
|
19
|
+
"verify.config.ts",
|
|
20
|
+
"verify.config.mts",
|
|
21
|
+
"verify.config.js",
|
|
22
|
+
"verify.config.mjs"
|
|
23
|
+
];
|
|
24
|
+
function findConfigFile(cwd) {
|
|
25
|
+
for (const filename of CONFIG_FILES) {
|
|
26
|
+
const filepath = join(cwd, filename);
|
|
27
|
+
if (existsSync(filepath)) {
|
|
28
|
+
return filepath;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
async function loadConfig(configPath) {
|
|
34
|
+
const absolutePath = resolve(configPath);
|
|
35
|
+
if (!existsSync(absolutePath)) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const fileUrl = pathToFileURL(absolutePath).href;
|
|
39
|
+
const module = await import(fileUrl);
|
|
40
|
+
if (!module.default) {
|
|
41
|
+
throw new Error(`Config file ${configPath} must have a default export`);
|
|
42
|
+
}
|
|
43
|
+
return module.default;
|
|
44
|
+
}
|
|
45
|
+
async function loadConfigFromCwd(cwd, configPath) {
|
|
46
|
+
if (configPath) {
|
|
47
|
+
return loadConfig(configPath);
|
|
48
|
+
}
|
|
49
|
+
const foundPath = findConfigFile(cwd);
|
|
50
|
+
if (!foundPath) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return loadConfig(foundPath);
|
|
54
|
+
}
|
|
55
|
+
function mergeOptions(configOptions, cliOptions) {
|
|
56
|
+
return {
|
|
57
|
+
logs: cliOptions?.logs ?? configOptions?.logs ?? "failed",
|
|
58
|
+
format: cliOptions?.format ?? configOptions?.format ?? "human",
|
|
59
|
+
filter: cliOptions?.filter ?? configOptions?.filter,
|
|
60
|
+
cwd: cliOptions?.cwd ?? configOptions?.cwd ?? process.cwd(),
|
|
61
|
+
noColor: cliOptions?.noColor ?? configOptions?.noColor ?? false,
|
|
62
|
+
topLevelOnly: cliOptions?.topLevelOnly ?? configOptions?.topLevelOnly ?? false,
|
|
63
|
+
noTty: cliOptions?.noTty ?? configOptions?.noTty ?? false
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/discovery.ts
|
|
68
|
+
import { existsSync as existsSync2, readdirSync, statSync } from "fs";
|
|
69
|
+
import { join as join2, relative } from "path";
|
|
70
|
+
var DEFAULT_PATTERNS = ["packages/*", "apps/*"];
|
|
71
|
+
function findMatchingDirs(rootDir, patterns) {
|
|
72
|
+
const results = [];
|
|
73
|
+
for (const pattern of patterns) {
|
|
74
|
+
if (pattern.endsWith("/*")) {
|
|
75
|
+
const parentDir = pattern.slice(0, -2);
|
|
76
|
+
const parentPath = join2(rootDir, parentDir);
|
|
77
|
+
if (existsSync2(parentPath) && statSync(parentPath).isDirectory()) {
|
|
78
|
+
const entries = readdirSync(parentPath);
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
const entryPath = join2(parentPath, entry);
|
|
81
|
+
if (statSync(entryPath).isDirectory()) {
|
|
82
|
+
results.push(entryPath);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
const dirPath = join2(rootDir, pattern);
|
|
88
|
+
if (existsSync2(dirPath) && statSync(dirPath).isDirectory()) {
|
|
89
|
+
results.push(dirPath);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return results;
|
|
94
|
+
}
|
|
95
|
+
function getPackageName(packageDir) {
|
|
96
|
+
const packageJsonPath = join2(packageDir, "package.json");
|
|
97
|
+
if (!existsSync2(packageJsonPath)) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const content = __require(packageJsonPath);
|
|
102
|
+
return content.name ?? null;
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async function discoverPackages(rootDir, options = {}) {
|
|
108
|
+
const patterns = options.patterns ?? DEFAULT_PATTERNS;
|
|
109
|
+
const matchingDirs = findMatchingDirs(rootDir, patterns);
|
|
110
|
+
const packages = [];
|
|
111
|
+
for (const dir of matchingDirs) {
|
|
112
|
+
const name = getPackageName(dir);
|
|
113
|
+
if (!name) continue;
|
|
114
|
+
if (options.filter && options.filter.length > 0) {
|
|
115
|
+
const matches = options.filter.some(
|
|
116
|
+
(f) => name === f || name.includes(f) || relative(rootDir, dir).includes(f)
|
|
117
|
+
);
|
|
118
|
+
if (!matches) continue;
|
|
119
|
+
}
|
|
120
|
+
const configPath = findConfigFile(dir);
|
|
121
|
+
const config = configPath ? await loadConfig(configPath) : null;
|
|
122
|
+
packages.push({
|
|
123
|
+
name,
|
|
124
|
+
path: relative(rootDir, dir),
|
|
125
|
+
absolutePath: dir,
|
|
126
|
+
config
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return packages;
|
|
130
|
+
}
|
|
131
|
+
async function hasPackageChanged(_packagePath, _baseBranch = "main") {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/init/detect.ts
|
|
136
|
+
import { existsSync as existsSync3, readFileSync } from "fs";
|
|
137
|
+
import { join as join3 } from "path";
|
|
138
|
+
var TOOL_PATTERNS = [
|
|
139
|
+
// Biome
|
|
140
|
+
{
|
|
141
|
+
pattern: /\bbiome\s+(check|lint|format)/,
|
|
142
|
+
binary: "biome",
|
|
143
|
+
getArgs: (match, content) => {
|
|
144
|
+
const biomeMatch = content.match(/biome\s+([^&|;]+)/);
|
|
145
|
+
return biomeMatch ? biomeMatch[1].trim() : "check .";
|
|
146
|
+
},
|
|
147
|
+
parser: "biome"
|
|
148
|
+
},
|
|
149
|
+
// ESLint
|
|
150
|
+
{
|
|
151
|
+
pattern: /\beslint\b/,
|
|
152
|
+
binary: "eslint",
|
|
153
|
+
getArgs: (_, content) => {
|
|
154
|
+
const eslintMatch = content.match(/eslint\s+([^&|;]+)/);
|
|
155
|
+
return eslintMatch ? eslintMatch[1].trim() : ".";
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
// Prettier
|
|
159
|
+
{
|
|
160
|
+
pattern: /\bprettier\b/,
|
|
161
|
+
binary: "prettier",
|
|
162
|
+
getArgs: (_, content) => {
|
|
163
|
+
const prettierMatch = content.match(/prettier\s+([^&|;]+)/);
|
|
164
|
+
return prettierMatch ? prettierMatch[1].trim() : "--check .";
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
// TypeScript
|
|
168
|
+
{
|
|
169
|
+
pattern: /\btsc\b/,
|
|
170
|
+
binary: "tsc",
|
|
171
|
+
getArgs: (_, content) => {
|
|
172
|
+
const tscMatch = content.match(/tsc\s+([^&|;]+)/);
|
|
173
|
+
return tscMatch ? tscMatch[1].trim() : "--noEmit";
|
|
174
|
+
},
|
|
175
|
+
parser: "tsc"
|
|
176
|
+
},
|
|
177
|
+
// tsgo
|
|
178
|
+
{
|
|
179
|
+
pattern: /\btsgo\b/,
|
|
180
|
+
binary: "tsgo",
|
|
181
|
+
getArgs: (_, content) => {
|
|
182
|
+
const tsgoMatch = content.match(/tsgo\s+([^&|;]+)/);
|
|
183
|
+
return tsgoMatch ? tsgoMatch[1].trim() : "--noEmit";
|
|
184
|
+
},
|
|
185
|
+
parser: "tsc"
|
|
186
|
+
},
|
|
187
|
+
// Vitest
|
|
188
|
+
{
|
|
189
|
+
pattern: /\bvitest\b/,
|
|
190
|
+
binary: "vitest",
|
|
191
|
+
getArgs: (_, content) => {
|
|
192
|
+
if (content.includes("vitest run")) return "run";
|
|
193
|
+
if (content.includes("vitest watch")) return "run";
|
|
194
|
+
return "run";
|
|
195
|
+
},
|
|
196
|
+
parser: "vitest"
|
|
197
|
+
},
|
|
198
|
+
// Jest
|
|
199
|
+
{
|
|
200
|
+
pattern: /\bjest\b/,
|
|
201
|
+
binary: "jest",
|
|
202
|
+
getArgs: () => ""
|
|
203
|
+
},
|
|
204
|
+
// Mocha
|
|
205
|
+
{
|
|
206
|
+
pattern: /\bmocha\b/,
|
|
207
|
+
binary: "mocha",
|
|
208
|
+
getArgs: () => ""
|
|
209
|
+
},
|
|
210
|
+
// tsup
|
|
211
|
+
{
|
|
212
|
+
pattern: /\btsup\b/,
|
|
213
|
+
binary: "tsup",
|
|
214
|
+
getArgs: (_, content) => {
|
|
215
|
+
const tsupMatch = content.match(/tsup\s+([^&|;]+)/);
|
|
216
|
+
return tsupMatch ? tsupMatch[1].trim() : "";
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
// esbuild
|
|
220
|
+
{
|
|
221
|
+
pattern: /\besbuild\b/,
|
|
222
|
+
binary: "esbuild",
|
|
223
|
+
getArgs: (_, content) => {
|
|
224
|
+
const esbuildMatch = content.match(/esbuild\s+([^&|;]+)/);
|
|
225
|
+
return esbuildMatch ? esbuildMatch[1].trim() : "";
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
];
|
|
229
|
+
var SCRIPT_NAME_PATTERNS = [
|
|
230
|
+
// Format/Lint patterns
|
|
231
|
+
{
|
|
232
|
+
pattern: /^(lint|eslint|biome|prettier|format)$/i,
|
|
233
|
+
key: "format",
|
|
234
|
+
name: "Format & Lint",
|
|
235
|
+
category: "format"
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
pattern: /^(lint:fix|format:fix|fix)$/i,
|
|
239
|
+
key: "format",
|
|
240
|
+
name: "Format & Lint",
|
|
241
|
+
category: "format"
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
pattern: /^verify:format$/i,
|
|
245
|
+
key: "format",
|
|
246
|
+
name: "Format",
|
|
247
|
+
category: "format"
|
|
248
|
+
},
|
|
249
|
+
// Type checking patterns
|
|
250
|
+
{
|
|
251
|
+
pattern: /^(typecheck|type-check|tsc|types|check-types)$/i,
|
|
252
|
+
key: "types",
|
|
253
|
+
name: "Type Check",
|
|
254
|
+
category: "types"
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
pattern: /^verify:types$/i,
|
|
258
|
+
key: "types",
|
|
259
|
+
name: "Types",
|
|
260
|
+
category: "types"
|
|
261
|
+
},
|
|
262
|
+
// Test patterns
|
|
263
|
+
{
|
|
264
|
+
pattern: /^(test|tests|vitest|jest|mocha|ava)$/i,
|
|
265
|
+
key: "test",
|
|
266
|
+
name: "Tests",
|
|
267
|
+
category: "logic"
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
pattern: /^test:(unit|integration|e2e)$/i,
|
|
271
|
+
key: "test",
|
|
272
|
+
name: "Tests",
|
|
273
|
+
category: "logic"
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
pattern: /^verify:logic$/i,
|
|
277
|
+
key: "logic",
|
|
278
|
+
name: "Logic Tests",
|
|
279
|
+
category: "logic"
|
|
280
|
+
},
|
|
281
|
+
// Build patterns
|
|
282
|
+
{
|
|
283
|
+
pattern: /^(build|compile)$/i,
|
|
284
|
+
key: "build",
|
|
285
|
+
name: "Build",
|
|
286
|
+
category: "build"
|
|
287
|
+
}
|
|
288
|
+
];
|
|
289
|
+
function readPackageJson(cwd) {
|
|
290
|
+
const packageJsonPath = join3(cwd, "package.json");
|
|
291
|
+
if (!existsSync3(packageJsonPath)) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
const content = readFileSync(packageJsonPath, "utf-8");
|
|
296
|
+
return JSON.parse(content);
|
|
297
|
+
} catch {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function binaryExists(cwd, binary) {
|
|
302
|
+
return existsSync3(join3(cwd, "node_modules", ".bin", binary));
|
|
303
|
+
}
|
|
304
|
+
function extractOptimizedCommand(cwd, scriptContent) {
|
|
305
|
+
for (const tool of TOOL_PATTERNS) {
|
|
306
|
+
const match = scriptContent.match(tool.pattern);
|
|
307
|
+
if (match && binaryExists(cwd, tool.binary)) {
|
|
308
|
+
const args = tool.getArgs(match, scriptContent);
|
|
309
|
+
const command = args ? `./node_modules/.bin/${tool.binary} ${args}` : `./node_modules/.bin/${tool.binary}`;
|
|
310
|
+
return { command, parser: tool.parser };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
function detectFromPackageJson(cwd) {
|
|
316
|
+
const pkg = readPackageJson(cwd);
|
|
317
|
+
if (!pkg?.scripts) {
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
const detected = [];
|
|
321
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
322
|
+
for (const [scriptName, scriptContent] of Object.entries(pkg.scripts)) {
|
|
323
|
+
if (scriptContent.includes("run-s") || scriptContent.includes("run-p") || scriptContent.includes("npm-run-all")) {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
for (const { pattern, key, name, category } of SCRIPT_NAME_PATTERNS) {
|
|
327
|
+
if (pattern.test(scriptName)) {
|
|
328
|
+
const uniqueKey = seenKeys.has(key) ? `${key}-${scriptName}` : key;
|
|
329
|
+
if (!seenKeys.has(uniqueKey)) {
|
|
330
|
+
seenKeys.add(uniqueKey);
|
|
331
|
+
const optimized = extractOptimizedCommand(cwd, scriptContent);
|
|
332
|
+
detected.push({
|
|
333
|
+
key: uniqueKey,
|
|
334
|
+
name,
|
|
335
|
+
scriptName,
|
|
336
|
+
command: optimized?.command ?? `npm run ${scriptName}`,
|
|
337
|
+
category,
|
|
338
|
+
parser: optimized?.parser
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const categoryOrder = {
|
|
346
|
+
format: 0,
|
|
347
|
+
types: 1,
|
|
348
|
+
logic: 2,
|
|
349
|
+
build: 3,
|
|
350
|
+
other: 4
|
|
351
|
+
};
|
|
352
|
+
detected.sort((a, b) => categoryOrder[a.category] - categoryOrder[b.category]);
|
|
353
|
+
const hasFormatTask = detected.some((t) => t.category === "format");
|
|
354
|
+
if (hasFormatTask) {
|
|
355
|
+
for (const task of detected) {
|
|
356
|
+
if (task.category !== "format" && task.category !== "other") {
|
|
357
|
+
task.reportingDependsOn = ["format"];
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return detected;
|
|
362
|
+
}
|
|
363
|
+
function detectPackageManager(cwd) {
|
|
364
|
+
if (existsSync3(join3(cwd, "pnpm-lock.yaml"))) {
|
|
365
|
+
return "pnpm";
|
|
366
|
+
}
|
|
367
|
+
if (existsSync3(join3(cwd, "yarn.lock"))) {
|
|
368
|
+
return "yarn";
|
|
369
|
+
}
|
|
370
|
+
return "npm";
|
|
371
|
+
}
|
|
372
|
+
function getRunCommand(packageManager, scriptName) {
|
|
373
|
+
switch (packageManager) {
|
|
374
|
+
case "pnpm":
|
|
375
|
+
return `pnpm ${scriptName}`;
|
|
376
|
+
case "yarn":
|
|
377
|
+
return `yarn ${scriptName}`;
|
|
378
|
+
default:
|
|
379
|
+
return `npm run ${scriptName}`;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
function detectTasks(cwd) {
|
|
383
|
+
const packageManager = detectPackageManager(cwd);
|
|
384
|
+
const tasks = detectFromPackageJson(cwd);
|
|
385
|
+
return tasks.map((task) => {
|
|
386
|
+
if (task.command.startsWith("./")) {
|
|
387
|
+
return task;
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
...task,
|
|
391
|
+
command: getRunCommand(packageManager, task.scriptName)
|
|
392
|
+
};
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/init/generate.ts
|
|
397
|
+
function getOutputFormat(filePath) {
|
|
398
|
+
if (filePath.endsWith(".mts")) return "mts";
|
|
399
|
+
if (filePath.endsWith(".mjs")) return "mjs";
|
|
400
|
+
if (filePath.endsWith(".js")) return "js";
|
|
401
|
+
return "ts";
|
|
402
|
+
}
|
|
403
|
+
function generateImport(format) {
|
|
404
|
+
return `import { defineConfig } from "@halecraft/verify"`;
|
|
405
|
+
}
|
|
406
|
+
function generateTask(task, indent) {
|
|
407
|
+
const parts = [`key: "${task.key}"`, `run: "${task.command}"`];
|
|
408
|
+
if (task.parser) {
|
|
409
|
+
parts.push(`parser: "${task.parser}"`);
|
|
410
|
+
}
|
|
411
|
+
if (task.reportingDependsOn && task.reportingDependsOn.length > 0) {
|
|
412
|
+
const deps = task.reportingDependsOn.map((d) => `"${d}"`).join(", ");
|
|
413
|
+
parts.push(`reportingDependsOn: [${deps}]`);
|
|
414
|
+
}
|
|
415
|
+
return `${indent}{ ${parts.join(", ")} }`;
|
|
416
|
+
}
|
|
417
|
+
function generateSkeleton(format) {
|
|
418
|
+
const importStatement = generateImport(format);
|
|
419
|
+
return `${importStatement}
|
|
420
|
+
|
|
421
|
+
export default defineConfig({
|
|
422
|
+
tasks: [
|
|
423
|
+
// Add your verification tasks here
|
|
424
|
+
// Example:
|
|
425
|
+
// { key: "format", run: "pnpm lint" },
|
|
426
|
+
// { key: "types", run: "pnpm typecheck" },
|
|
427
|
+
// { key: "test", run: "pnpm test" },
|
|
428
|
+
],
|
|
429
|
+
})
|
|
430
|
+
`;
|
|
431
|
+
}
|
|
432
|
+
function generateConfigContent(tasks, format) {
|
|
433
|
+
if (tasks.length === 0) {
|
|
434
|
+
return generateSkeleton(format);
|
|
435
|
+
}
|
|
436
|
+
const importStatement = generateImport(format);
|
|
437
|
+
const indent = " ";
|
|
438
|
+
const taskLines = tasks.map((task) => generateTask(task, indent));
|
|
439
|
+
return `${importStatement}
|
|
440
|
+
|
|
441
|
+
export default defineConfig({
|
|
442
|
+
tasks: [
|
|
443
|
+
${taskLines.join(",\n")},
|
|
444
|
+
],
|
|
445
|
+
})
|
|
446
|
+
`;
|
|
447
|
+
}
|
|
448
|
+
function getDefaultConfigPath() {
|
|
449
|
+
return "verify.config.ts";
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// src/init/prompts.ts
|
|
453
|
+
function shouldSkipPrompts(options) {
|
|
454
|
+
return options.yes || !options.isTTY;
|
|
455
|
+
}
|
|
456
|
+
async function promptForTasks(detectedTasks, options) {
|
|
457
|
+
if (detectedTasks.length === 0) {
|
|
458
|
+
if (!shouldSkipPrompts(options)) {
|
|
459
|
+
console.log(
|
|
460
|
+
"\n\u26A0\uFE0F No verification scripts detected in package.json.\n A skeleton config will be created.\n"
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
return { tasks: [], cancelled: false };
|
|
464
|
+
}
|
|
465
|
+
if (shouldSkipPrompts(options)) {
|
|
466
|
+
console.log(`
|
|
467
|
+
\u2713 Auto-selecting ${detectedTasks.length} detected task(s)
|
|
468
|
+
`);
|
|
469
|
+
return { tasks: detectedTasks, cancelled: false };
|
|
470
|
+
}
|
|
471
|
+
try {
|
|
472
|
+
const { checkbox } = await import("@inquirer/prompts");
|
|
473
|
+
console.log("\n\u{1F50D} Detected verification scripts in package.json:\n");
|
|
474
|
+
const choices = detectedTasks.map((task) => ({
|
|
475
|
+
name: `${task.name} (${task.command})`,
|
|
476
|
+
value: task,
|
|
477
|
+
checked: true
|
|
478
|
+
// Pre-select all by default
|
|
479
|
+
}));
|
|
480
|
+
const selected = await checkbox({
|
|
481
|
+
message: "Select tasks to include in your config:",
|
|
482
|
+
choices,
|
|
483
|
+
instructions: false
|
|
484
|
+
});
|
|
485
|
+
if (selected.length === 0) {
|
|
486
|
+
console.log(
|
|
487
|
+
"\n\u26A0\uFE0F No tasks selected. A skeleton config will be created.\n"
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
return { tasks: selected, cancelled: false };
|
|
491
|
+
} catch (error) {
|
|
492
|
+
if (error instanceof Error && (error.message.includes("User force closed") || error.name === "ExitPromptError")) {
|
|
493
|
+
return { tasks: [], cancelled: true };
|
|
494
|
+
}
|
|
495
|
+
throw error;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// src/init/write.ts
|
|
500
|
+
import { existsSync as existsSync4, writeFileSync } from "fs";
|
|
501
|
+
import { resolve as resolve2 } from "path";
|
|
502
|
+
function checkConfigExists(cwd, configPath) {
|
|
503
|
+
const absolutePath = resolve2(cwd, configPath);
|
|
504
|
+
return {
|
|
505
|
+
exists: existsSync4(absolutePath),
|
|
506
|
+
path: absolutePath
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
function writeConfigFile(cwd, configPath, content) {
|
|
510
|
+
const absolutePath = resolve2(cwd, configPath);
|
|
511
|
+
writeFileSync(absolutePath, content, "utf-8");
|
|
512
|
+
}
|
|
513
|
+
function printExistsWarning(path) {
|
|
514
|
+
console.error(`
|
|
515
|
+
\u26A0\uFE0F Config file already exists: ${path}`);
|
|
516
|
+
console.error(" Use --force to overwrite.\n");
|
|
517
|
+
}
|
|
518
|
+
function printSuccess(options) {
|
|
519
|
+
const { configPath, tasks, hasOptimizedCommands, removableScripts } = options;
|
|
520
|
+
console.log(`
|
|
521
|
+
\u2705 Created ${configPath}`);
|
|
522
|
+
console.log("");
|
|
523
|
+
console.log(" Quick start:");
|
|
524
|
+
console.log(" $ verify # Run all verifications");
|
|
525
|
+
console.log(" $ verify --top-level # Show only top-level tasks");
|
|
526
|
+
console.log(" $ verify format # Run only 'format' task");
|
|
527
|
+
console.log("");
|
|
528
|
+
if (hasOptimizedCommands) {
|
|
529
|
+
console.log(
|
|
530
|
+
" \u26A1 Performance: Using direct tool paths for faster execution"
|
|
531
|
+
);
|
|
532
|
+
console.log(
|
|
533
|
+
" (avoids ~250ms overhead per command from package manager)"
|
|
534
|
+
);
|
|
535
|
+
console.log("");
|
|
536
|
+
}
|
|
537
|
+
if (removableScripts.length > 0) {
|
|
538
|
+
console.log(" \u{1F4A1} Optional cleanup:");
|
|
539
|
+
console.log(
|
|
540
|
+
" You can remove these scripts from package.json if you only"
|
|
541
|
+
);
|
|
542
|
+
console.log(" run them via 'verify' (keeps package.json cleaner):");
|
|
543
|
+
for (const script of removableScripts) {
|
|
544
|
+
console.log(` - "${script}"`);
|
|
545
|
+
}
|
|
546
|
+
console.log("");
|
|
547
|
+
}
|
|
548
|
+
const tasksWithParsers = tasks.filter((t) => t.parser);
|
|
549
|
+
if (tasksWithParsers.length > 0) {
|
|
550
|
+
console.log(" \u{1F4CA} Rich output: Parsers detected for detailed summaries:");
|
|
551
|
+
for (const task of tasksWithParsers) {
|
|
552
|
+
console.log(` - ${task.key}: ${task.parser}`);
|
|
553
|
+
}
|
|
554
|
+
console.log("");
|
|
555
|
+
}
|
|
556
|
+
console.log(" \u{1F4D6} Docs: https://github.com/halecraft/verify");
|
|
557
|
+
console.log("");
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// src/init/index.ts
|
|
561
|
+
async function runInit(options) {
|
|
562
|
+
const configPath = options.config ?? getDefaultConfigPath();
|
|
563
|
+
const format = getOutputFormat(configPath);
|
|
564
|
+
const fileCheck = checkConfigExists(options.cwd, configPath);
|
|
565
|
+
if (fileCheck.exists && !options.force) {
|
|
566
|
+
printExistsWarning(fileCheck.path);
|
|
567
|
+
return {
|
|
568
|
+
success: false,
|
|
569
|
+
error: "Config file already exists. Use --force to overwrite."
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
const detectedTasks = detectTasks(options.cwd);
|
|
573
|
+
const isTTY = process.stdout.isTTY ?? false;
|
|
574
|
+
const promptOptions = {
|
|
575
|
+
yes: options.yes,
|
|
576
|
+
isTTY
|
|
577
|
+
};
|
|
578
|
+
if (!shouldSkipPrompts(promptOptions)) {
|
|
579
|
+
console.log("\n\u{1F680} Initializing @halecraft/verify config...\n");
|
|
580
|
+
}
|
|
581
|
+
const promptResult = await promptForTasks(detectedTasks, promptOptions);
|
|
582
|
+
if (promptResult.cancelled) {
|
|
583
|
+
console.log("\n\u274C Cancelled.\n");
|
|
584
|
+
return {
|
|
585
|
+
success: false,
|
|
586
|
+
error: "User cancelled"
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
const content = generateConfigContent(promptResult.tasks, format);
|
|
590
|
+
const hasOptimizedCommands = promptResult.tasks.some(
|
|
591
|
+
(t) => t.command.startsWith("./node_modules/.bin/")
|
|
592
|
+
);
|
|
593
|
+
const removableScripts = promptResult.tasks.filter((t) => t.command.startsWith("./node_modules/.bin/")).map((t) => t.scriptName);
|
|
594
|
+
try {
|
|
595
|
+
writeConfigFile(options.cwd, configPath, content);
|
|
596
|
+
printSuccess({
|
|
597
|
+
configPath,
|
|
598
|
+
tasks: promptResult.tasks,
|
|
599
|
+
hasOptimizedCommands,
|
|
600
|
+
removableScripts
|
|
601
|
+
});
|
|
602
|
+
return {
|
|
603
|
+
success: true,
|
|
604
|
+
configPath
|
|
605
|
+
};
|
|
606
|
+
} catch (error) {
|
|
607
|
+
const message = error instanceof Error ? error.message : "Failed to write config file";
|
|
608
|
+
console.error(`
|
|
609
|
+
\u274C Error: ${message}
|
|
610
|
+
`);
|
|
611
|
+
return {
|
|
612
|
+
success: false,
|
|
613
|
+
error: message
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// src/parsers/biome.ts
|
|
619
|
+
var biomeParser = {
|
|
620
|
+
id: "biome",
|
|
621
|
+
parse(output, exitCode) {
|
|
622
|
+
const filesMatch = output.match(
|
|
623
|
+
/Checked\s+(\d+)\s+files?\s+in\s+[\d.]+(?:ms|s)/i
|
|
624
|
+
);
|
|
625
|
+
const fileCount = filesMatch ? Number.parseInt(filesMatch[1], 10) : void 0;
|
|
626
|
+
const warningMatch = output.match(/Found\s+(\d+)\s+warning/i);
|
|
627
|
+
const warnings = warningMatch ? Number.parseInt(warningMatch[1], 10) : 0;
|
|
628
|
+
if (exitCode === 0) {
|
|
629
|
+
const filesPart = fileCount ? `passed ${fileCount} files` : "passed";
|
|
630
|
+
const warningSuffix = warnings > 0 ? `, ${warnings} warning${warnings === 1 ? "" : "s"}` : "";
|
|
631
|
+
return {
|
|
632
|
+
summary: `${filesPart}${warningSuffix}`,
|
|
633
|
+
metrics: { errors: 0, warnings, total: fileCount }
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
const summaryMatch = output.match(
|
|
637
|
+
/Found\s+(\d+)\s+error(?:s)?(?:\s+and\s+(\d+)\s+warning(?:s)?)?/i
|
|
638
|
+
);
|
|
639
|
+
if (summaryMatch) {
|
|
640
|
+
const errors2 = Number.parseInt(summaryMatch[1], 10);
|
|
641
|
+
const parsedWarnings = summaryMatch[2] ? Number.parseInt(summaryMatch[2], 10) : warnings;
|
|
642
|
+
const fileSuffix = fileCount ? ` in ${fileCount} files` : "";
|
|
643
|
+
return {
|
|
644
|
+
summary: `${errors2} error${errors2 === 1 ? "" : "s"}${parsedWarnings > 0 ? `, ${parsedWarnings} warning${parsedWarnings === 1 ? "" : "s"}` : ""}${fileSuffix}`,
|
|
645
|
+
metrics: { errors: errors2, warnings: parsedWarnings, total: fileCount }
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
const errorLines = output.match(/^\s*error\[/gm);
|
|
649
|
+
const warningLines = output.match(/^\s*warning\[/gm);
|
|
650
|
+
const errors = errorLines ? errorLines.length : 0;
|
|
651
|
+
const countedWarnings = warningLines ? warningLines.length : warnings;
|
|
652
|
+
if (errors > 0 || countedWarnings > 0) {
|
|
653
|
+
const fileSuffix = fileCount ? ` in ${fileCount} files` : "";
|
|
654
|
+
return {
|
|
655
|
+
summary: `${errors} error${errors === 1 ? "" : "s"}${countedWarnings > 0 ? `, ${countedWarnings} warning${countedWarnings === 1 ? "" : "s"}` : ""}${fileSuffix}`,
|
|
656
|
+
metrics: { errors, warnings: countedWarnings, total: fileCount }
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
if (fileCount) {
|
|
660
|
+
return {
|
|
661
|
+
summary: `passed ${fileCount} files`,
|
|
662
|
+
metrics: { errors: 0, warnings: 0, total: fileCount }
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
// src/parsers/generic.ts
|
|
670
|
+
var genericParser = {
|
|
671
|
+
id: "generic",
|
|
672
|
+
parse(_output, exitCode) {
|
|
673
|
+
return {
|
|
674
|
+
summary: exitCode === 0 ? "passed" : `failed (exit code ${exitCode})`
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
// src/parsers/gotest.ts
|
|
680
|
+
var gotestParser = {
|
|
681
|
+
id: "gotest",
|
|
682
|
+
parse(output, exitCode) {
|
|
683
|
+
const okMatches = output.match(/^ok\s+\S+/gm);
|
|
684
|
+
const failMatches = output.match(/^FAIL\s+\S+/gm);
|
|
685
|
+
const passed = okMatches ? okMatches.length : 0;
|
|
686
|
+
const failed = failMatches ? failMatches.length : 0;
|
|
687
|
+
const total = passed + failed;
|
|
688
|
+
if (total === 0) {
|
|
689
|
+
if (output.includes("no test files")) {
|
|
690
|
+
return {
|
|
691
|
+
summary: "no test files",
|
|
692
|
+
metrics: { passed: 0, failed: 0, total: 0 }
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
const durationMatch = output.match(/(?:PASS|FAIL)\s*$[\s\S]*?(\d+\.?\d*s)/m);
|
|
698
|
+
const duration = durationMatch ? durationMatch[1] : void 0;
|
|
699
|
+
if (exitCode === 0) {
|
|
700
|
+
return {
|
|
701
|
+
summary: `${passed} package${passed === 1 ? "" : "s"} passed${duration ? ` in ${duration}` : ""}`,
|
|
702
|
+
metrics: { passed, failed: 0, total: passed, duration }
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
return {
|
|
706
|
+
summary: `${failed}/${total} package${total === 1 ? "" : "s"} failed`,
|
|
707
|
+
metrics: { passed, failed, total, duration }
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
// src/parsers/tsc.ts
|
|
713
|
+
var tscParser = {
|
|
714
|
+
id: "tsc",
|
|
715
|
+
parse(output, exitCode) {
|
|
716
|
+
const filesMatch = output.match(/^Files:\s+(\d+)/m);
|
|
717
|
+
const fileCount = filesMatch ? Number.parseInt(filesMatch[1], 10) : void 0;
|
|
718
|
+
if (exitCode === 0) {
|
|
719
|
+
const filesPart = fileCount ? `passed ${fileCount} files` : "passed";
|
|
720
|
+
return {
|
|
721
|
+
summary: filesPart,
|
|
722
|
+
metrics: { errors: 0, total: fileCount }
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
const errorMatches = output.match(/error TS\d+:/g);
|
|
726
|
+
const errorCount = errorMatches ? errorMatches.length : 0;
|
|
727
|
+
if (errorCount === 0) {
|
|
728
|
+
return null;
|
|
729
|
+
}
|
|
730
|
+
const fileSuffix = fileCount ? ` in ${fileCount} files` : "";
|
|
731
|
+
return {
|
|
732
|
+
summary: `${errorCount} type error${errorCount === 1 ? "" : "s"}${fileSuffix}`,
|
|
733
|
+
metrics: { errors: errorCount, total: fileCount }
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
// src/parsers/vitest.ts
|
|
739
|
+
function stripAnsi(str) {
|
|
740
|
+
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
741
|
+
}
|
|
742
|
+
var vitestParser = {
|
|
743
|
+
id: "vitest",
|
|
744
|
+
parse(output, exitCode) {
|
|
745
|
+
const cleanOutput = stripAnsi(output);
|
|
746
|
+
const testsMatch = cleanOutput.match(/Tests\s+(\d+)\s+passed\s*\((\d+)\)/m);
|
|
747
|
+
const durationMatch = cleanOutput.match(/Duration\s+([\d.]+(?:ms|s))\b/m);
|
|
748
|
+
if (!testsMatch) {
|
|
749
|
+
return null;
|
|
750
|
+
}
|
|
751
|
+
const passed = Number.parseInt(testsMatch[1], 10);
|
|
752
|
+
const total = Number.parseInt(testsMatch[2], 10);
|
|
753
|
+
const duration = durationMatch ? durationMatch[1] : void 0;
|
|
754
|
+
return {
|
|
755
|
+
summary: exitCode === 0 ? `passed ${passed}/${total} tests` : `passed ${passed}/${total} tests (some failed)`,
|
|
756
|
+
metrics: {
|
|
757
|
+
passed,
|
|
758
|
+
total,
|
|
759
|
+
failed: total - passed,
|
|
760
|
+
duration
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
// src/parsers/index.ts
|
|
767
|
+
var ParserRegistry = class {
|
|
768
|
+
parsers = /* @__PURE__ */ new Map();
|
|
769
|
+
constructor() {
|
|
770
|
+
this.register(genericParser);
|
|
771
|
+
this.register(vitestParser);
|
|
772
|
+
this.register(tscParser);
|
|
773
|
+
this.register(biomeParser);
|
|
774
|
+
this.register(gotestParser);
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Register a custom parser
|
|
778
|
+
*/
|
|
779
|
+
register(parser) {
|
|
780
|
+
this.parsers.set(parser.id, parser);
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Get a parser by ID
|
|
784
|
+
*/
|
|
785
|
+
get(id) {
|
|
786
|
+
return this.parsers.get(id);
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Auto-detect parser based on command
|
|
790
|
+
*/
|
|
791
|
+
detectParser(cmd) {
|
|
792
|
+
const cmdLower = cmd.toLowerCase();
|
|
793
|
+
if (cmdLower.includes("vitest") || cmdLower.includes("jest")) {
|
|
794
|
+
return "vitest";
|
|
795
|
+
}
|
|
796
|
+
if (cmdLower.includes("tsc") || cmdLower.includes("tsgo")) {
|
|
797
|
+
return "tsc";
|
|
798
|
+
}
|
|
799
|
+
if (cmdLower.includes("biome") || cmdLower.includes("eslint")) {
|
|
800
|
+
return "biome";
|
|
801
|
+
}
|
|
802
|
+
if (cmdLower.includes("go test") || cmdLower.includes("go") && cmdLower.includes("test")) {
|
|
803
|
+
return "gotest";
|
|
804
|
+
}
|
|
805
|
+
return "generic";
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Parse output using the specified or auto-detected parser
|
|
809
|
+
*/
|
|
810
|
+
parse(output, exitCode, parserId, cmd) {
|
|
811
|
+
const id = parserId ?? (cmd ? this.detectParser(cmd) : "generic");
|
|
812
|
+
const parser = this.parsers.get(id) ?? genericParser;
|
|
813
|
+
const result = parser.parse(output, exitCode);
|
|
814
|
+
if (result) {
|
|
815
|
+
return result;
|
|
816
|
+
}
|
|
817
|
+
const fallback = genericParser.parse(output, exitCode);
|
|
818
|
+
if (!fallback) {
|
|
819
|
+
throw new Error("genericParser unexpectedly returned null");
|
|
820
|
+
}
|
|
821
|
+
return fallback;
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
var defaultRegistry = new ParserRegistry();
|
|
825
|
+
|
|
826
|
+
// src/spinner.ts
|
|
827
|
+
var SPINNER_FRAMES = ["\u25DC", "\u25E0", "\u25DD", "\u25DE", "\u25E1", "\u25DF"];
|
|
828
|
+
var SPINNER_INTERVAL = 80;
|
|
829
|
+
var SpinnerManager = class {
|
|
830
|
+
frames = SPINNER_FRAMES;
|
|
831
|
+
frameIndex = 0;
|
|
832
|
+
interval = null;
|
|
833
|
+
/**
|
|
834
|
+
* Start the spinner animation
|
|
835
|
+
* @param onTick - Callback called on each frame update
|
|
836
|
+
*/
|
|
837
|
+
start(onTick) {
|
|
838
|
+
if (this.interval) return;
|
|
839
|
+
this.interval = setInterval(() => {
|
|
840
|
+
this.frameIndex = (this.frameIndex + 1) % this.frames.length;
|
|
841
|
+
onTick();
|
|
842
|
+
}, SPINNER_INTERVAL);
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Stop the spinner animation
|
|
846
|
+
*/
|
|
847
|
+
stop() {
|
|
848
|
+
if (this.interval) {
|
|
849
|
+
clearInterval(this.interval);
|
|
850
|
+
this.interval = null;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Get the current spinner frame character
|
|
855
|
+
*/
|
|
856
|
+
getFrame() {
|
|
857
|
+
return this.frames[this.frameIndex];
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Check if spinner is currently running
|
|
861
|
+
*/
|
|
862
|
+
isRunning() {
|
|
863
|
+
return this.interval !== null;
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
|
|
867
|
+
// src/reporter.ts
|
|
868
|
+
var ansi = {
|
|
869
|
+
reset: "\x1B[0m",
|
|
870
|
+
dim: "\x1B[2m",
|
|
871
|
+
red: "\x1B[31m",
|
|
872
|
+
green: "\x1B[32m",
|
|
873
|
+
yellow: "\x1B[33m",
|
|
874
|
+
cyan: "\x1B[36m",
|
|
875
|
+
bold: "\x1B[1m"
|
|
876
|
+
};
|
|
877
|
+
var cursor = {
|
|
878
|
+
hide: "\x1B[?25l",
|
|
879
|
+
show: "\x1B[?25h",
|
|
880
|
+
moveUp: (n) => `\x1B[${n}A`,
|
|
881
|
+
moveToStart: "\x1B[0G",
|
|
882
|
+
clearLine: "\x1B[2K"
|
|
883
|
+
};
|
|
884
|
+
function shouldUseColors(options) {
|
|
885
|
+
if (options.noColor) return false;
|
|
886
|
+
if (options.format === "json") return false;
|
|
887
|
+
if (!process.stdout.isTTY) return false;
|
|
888
|
+
if ("NO_COLOR" in process.env) return false;
|
|
889
|
+
if (process.env.TERM === "dumb") return false;
|
|
890
|
+
return true;
|
|
891
|
+
}
|
|
892
|
+
var BaseReporter = class {
|
|
893
|
+
colorEnabled;
|
|
894
|
+
stream;
|
|
895
|
+
taskDepths = /* @__PURE__ */ new Map();
|
|
896
|
+
constructor(options = {}) {
|
|
897
|
+
this.colorEnabled = shouldUseColors(options);
|
|
898
|
+
this.stream = options.format === "json" ? process.stderr : process.stdout;
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Apply ANSI color code to string (if colors enabled)
|
|
902
|
+
*/
|
|
903
|
+
c(code, s) {
|
|
904
|
+
return this.colorEnabled ? `${code}${s}${ansi.reset}` : s;
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Get success mark (✓ or OK)
|
|
908
|
+
*/
|
|
909
|
+
okMark() {
|
|
910
|
+
return this.colorEnabled ? this.c(ansi.green, "\u2713") : "OK";
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Get failure mark (✗ or FAIL)
|
|
914
|
+
*/
|
|
915
|
+
failMark() {
|
|
916
|
+
return this.colorEnabled ? this.c(ansi.red, "\u2717") : "FAIL";
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Get suppressed mark (⊘ or SUPPRESSED)
|
|
920
|
+
*/
|
|
921
|
+
suppressedMark() {
|
|
922
|
+
return this.colorEnabled ? this.c(ansi.yellow, "\u2298") : "SUPPRESSED";
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Get arrow symbol (→ or ->)
|
|
926
|
+
*/
|
|
927
|
+
arrow() {
|
|
928
|
+
return this.colorEnabled ? this.c(ansi.cyan, "\u2192") : "->";
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Get indentation string for a given depth
|
|
932
|
+
*/
|
|
933
|
+
getIndent(depth) {
|
|
934
|
+
return " ".repeat(depth);
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Get task depth from path
|
|
938
|
+
*/
|
|
939
|
+
getTaskDepth(path) {
|
|
940
|
+
return this.taskDepths.get(path) ?? 0;
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Recursively collect task depths from verification tree
|
|
944
|
+
*/
|
|
945
|
+
collectTaskDepths(nodes, parentPath, depth) {
|
|
946
|
+
for (const node of nodes) {
|
|
947
|
+
const path = parentPath ? `${parentPath}:${node.key}` : node.key;
|
|
948
|
+
this.taskDepths.set(path, depth);
|
|
949
|
+
if (node.children) {
|
|
950
|
+
this.collectTaskDepths(node.children, path, depth + 1);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Extract summary from task result
|
|
956
|
+
*/
|
|
957
|
+
extractSummary(result) {
|
|
958
|
+
if (result.summaryLine) {
|
|
959
|
+
const colonIndex = result.summaryLine.indexOf(": ");
|
|
960
|
+
if (colonIndex !== -1) {
|
|
961
|
+
return result.summaryLine.slice(colonIndex + 2);
|
|
962
|
+
}
|
|
963
|
+
return result.summaryLine;
|
|
964
|
+
}
|
|
965
|
+
return result.ok ? "passed" : "failed";
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Flatten nested task results into a single array
|
|
969
|
+
*/
|
|
970
|
+
flattenResults(results) {
|
|
971
|
+
const flat = [];
|
|
972
|
+
for (const r of results) {
|
|
973
|
+
flat.push(r);
|
|
974
|
+
if (r.children) {
|
|
975
|
+
flat.push(...this.flattenResults(r.children));
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
return flat;
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Output task logs
|
|
982
|
+
*/
|
|
983
|
+
outputLogs(results, logsMode) {
|
|
984
|
+
if (logsMode === "none") return;
|
|
985
|
+
const flatResults = this.flattenResults(results);
|
|
986
|
+
for (const r of flatResults) {
|
|
987
|
+
if (r.children) continue;
|
|
988
|
+
if (logsMode === "failed" && r.ok) continue;
|
|
989
|
+
if (r.suppressed) continue;
|
|
990
|
+
const status = r.ok ? this.c(ansi.green, "OK") : this.c(ansi.red, "FAIL");
|
|
991
|
+
this.stream.write(
|
|
992
|
+
`
|
|
993
|
+
${this.c(ansi.bold, "====")} ${this.c(ansi.bold, r.path.toUpperCase())} ${status} ${this.c(ansi.bold, "====")}
|
|
994
|
+
`
|
|
995
|
+
);
|
|
996
|
+
this.stream.write(r.output || "(no output)\n");
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Output final summary
|
|
1001
|
+
*/
|
|
1002
|
+
outputSummary(result) {
|
|
1003
|
+
const finalMessage = result.ok ? this.c(ansi.green, "\n== verification: All correct ==") : this.c(ansi.red, "\n== verification: Failed ==");
|
|
1004
|
+
this.stream.write(`${finalMessage}
|
|
1005
|
+
`);
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
var LiveDashboardReporter = class extends BaseReporter {
|
|
1009
|
+
topLevelOnly;
|
|
1010
|
+
tasks = /* @__PURE__ */ new Map();
|
|
1011
|
+
taskOrder = [];
|
|
1012
|
+
spinner;
|
|
1013
|
+
lineCount = 0;
|
|
1014
|
+
constructor(options = {}) {
|
|
1015
|
+
super(options);
|
|
1016
|
+
this.topLevelOnly = options.topLevelOnly ?? false;
|
|
1017
|
+
this.spinner = new SpinnerManager();
|
|
1018
|
+
const cleanup = () => {
|
|
1019
|
+
this.spinner.stop();
|
|
1020
|
+
this.stream.write(cursor.show);
|
|
1021
|
+
process.exit(130);
|
|
1022
|
+
};
|
|
1023
|
+
process.on("SIGINT", cleanup);
|
|
1024
|
+
process.on("SIGTERM", cleanup);
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Initialize task list from verification nodes
|
|
1028
|
+
*/
|
|
1029
|
+
onStart(tasks) {
|
|
1030
|
+
this.collectTasks(tasks, "", 0);
|
|
1031
|
+
this.stream.write(cursor.hide);
|
|
1032
|
+
this.spinner.start(() => this.redraw());
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Recursively collect tasks from verification tree
|
|
1036
|
+
*/
|
|
1037
|
+
collectTasks(nodes, parentPath, depth) {
|
|
1038
|
+
for (const node of nodes) {
|
|
1039
|
+
const path = parentPath ? `${parentPath}:${node.key}` : node.key;
|
|
1040
|
+
this.tasks.set(path, {
|
|
1041
|
+
key: node.key,
|
|
1042
|
+
path,
|
|
1043
|
+
depth,
|
|
1044
|
+
status: "pending"
|
|
1045
|
+
});
|
|
1046
|
+
this.taskOrder.push(path);
|
|
1047
|
+
this.taskDepths.set(path, depth);
|
|
1048
|
+
if (node.children) {
|
|
1049
|
+
this.collectTasks(node.children, path, depth + 1);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Get display key - shows :key for nested, key for root
|
|
1055
|
+
*/
|
|
1056
|
+
getDisplayKey(task) {
|
|
1057
|
+
if (task.depth === 0) {
|
|
1058
|
+
return task.key;
|
|
1059
|
+
}
|
|
1060
|
+
return `:${task.key}`;
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Check if task should be displayed based on topLevelOnly flag
|
|
1064
|
+
*/
|
|
1065
|
+
shouldDisplay(task) {
|
|
1066
|
+
if (this.topLevelOnly) return task.depth === 0;
|
|
1067
|
+
return true;
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Format a single task line
|
|
1071
|
+
*/
|
|
1072
|
+
formatLine(task) {
|
|
1073
|
+
const indent = this.getIndent(task.depth);
|
|
1074
|
+
const displayKey = this.getDisplayKey(task);
|
|
1075
|
+
if (task.status === "running") {
|
|
1076
|
+
const spinnerChar = this.c(ansi.dim, `(${this.spinner.getFrame()})`);
|
|
1077
|
+
return `${indent}${this.arrow()} verifying ${this.c(ansi.bold, displayKey)} ${spinnerChar}`;
|
|
1078
|
+
}
|
|
1079
|
+
if (task.status === "completed" && task.result) {
|
|
1080
|
+
const duration = this.c(ansi.dim, `${task.result.durationMs}ms`);
|
|
1081
|
+
if (task.result.suppressed) {
|
|
1082
|
+
const reason = task.result.suppressedBy ? `${task.result.suppressedBy} failed` : "dependency failed";
|
|
1083
|
+
return `${indent}${this.suppressedMark()} suppressed ${this.c(ansi.bold, displayKey)} ${this.c(ansi.dim, `(${reason}, ${duration})`)}`;
|
|
1084
|
+
}
|
|
1085
|
+
const summary = this.extractSummary(task.result);
|
|
1086
|
+
if (task.result.ok) {
|
|
1087
|
+
return ` ${indent}${this.okMark()} verified ${this.c(ansi.bold, displayKey)} ${this.c(ansi.dim, `(${summary}, ${duration})`)}`;
|
|
1088
|
+
}
|
|
1089
|
+
return `${indent}${this.failMark()} failed ${this.c(ansi.bold, displayKey)} ${this.c(ansi.dim, `(${summary}, ${duration})`)}`;
|
|
1090
|
+
}
|
|
1091
|
+
return "";
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Redraw all visible task lines
|
|
1095
|
+
*/
|
|
1096
|
+
redraw() {
|
|
1097
|
+
if (this.lineCount > 0) {
|
|
1098
|
+
this.stream.write(cursor.moveUp(this.lineCount));
|
|
1099
|
+
}
|
|
1100
|
+
const lines = [];
|
|
1101
|
+
for (const path of this.taskOrder) {
|
|
1102
|
+
const task = this.tasks.get(path);
|
|
1103
|
+
if (!task) continue;
|
|
1104
|
+
if (!this.shouldDisplay(task)) continue;
|
|
1105
|
+
if (task.status === "pending") continue;
|
|
1106
|
+
const line = this.formatLine(task);
|
|
1107
|
+
if (line) {
|
|
1108
|
+
lines.push(line);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
for (const line of lines) {
|
|
1112
|
+
this.stream.write(`${cursor.clearLine}${cursor.moveToStart}${line}
|
|
1113
|
+
`);
|
|
1114
|
+
}
|
|
1115
|
+
this.lineCount = lines.length;
|
|
1116
|
+
}
|
|
1117
|
+
onTaskStart(path, _key) {
|
|
1118
|
+
const task = this.tasks.get(path);
|
|
1119
|
+
if (task) {
|
|
1120
|
+
task.status = "running";
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
onTaskComplete(result) {
|
|
1124
|
+
const task = this.tasks.get(result.path);
|
|
1125
|
+
if (task) {
|
|
1126
|
+
task.status = "completed";
|
|
1127
|
+
task.result = result;
|
|
1128
|
+
}
|
|
1129
|
+
this.redraw();
|
|
1130
|
+
}
|
|
1131
|
+
onFinish() {
|
|
1132
|
+
this.spinner.stop();
|
|
1133
|
+
this.redraw();
|
|
1134
|
+
this.stream.write(cursor.show);
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
var SequentialReporter = class extends BaseReporter {
|
|
1138
|
+
topLevelOnly;
|
|
1139
|
+
constructor(options = {}) {
|
|
1140
|
+
super(options);
|
|
1141
|
+
this.topLevelOnly = options.topLevelOnly ?? false;
|
|
1142
|
+
}
|
|
1143
|
+
onStart(tasks) {
|
|
1144
|
+
this.collectTaskDepths(tasks, "", 0);
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Check if task should be displayed based on topLevelOnly flag
|
|
1148
|
+
*/
|
|
1149
|
+
shouldDisplay(path) {
|
|
1150
|
+
if (this.topLevelOnly) return this.getTaskDepth(path) === 0;
|
|
1151
|
+
return true;
|
|
1152
|
+
}
|
|
1153
|
+
onTaskStart(path, _key) {
|
|
1154
|
+
if (!this.shouldDisplay(path)) return;
|
|
1155
|
+
this.stream.write(`${this.arrow()} verifying ${this.c(ansi.bold, path)}
|
|
1156
|
+
`);
|
|
1157
|
+
}
|
|
1158
|
+
onTaskComplete(result) {
|
|
1159
|
+
if (!this.shouldDisplay(result.path)) return;
|
|
1160
|
+
const duration = this.c(ansi.dim, `${result.durationMs}ms`);
|
|
1161
|
+
if (result.suppressed) {
|
|
1162
|
+
const reason = result.suppressedBy ? `${result.suppressedBy} failed` : "dependency failed";
|
|
1163
|
+
this.stream.write(
|
|
1164
|
+
`${this.suppressedMark()} suppressed ${this.c(ansi.bold, result.path)} ${this.c(ansi.dim, `(${reason}, ${duration})`)}
|
|
1165
|
+
`
|
|
1166
|
+
);
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
const mark = result.ok ? this.okMark() : this.failMark();
|
|
1170
|
+
const verb = result.ok ? "verified" : "failed";
|
|
1171
|
+
const summary = this.extractSummary(result);
|
|
1172
|
+
this.stream.write(
|
|
1173
|
+
`${mark} ${verb} ${this.c(ansi.bold, result.path)} ${this.c(ansi.dim, `(${summary}, ${duration})`)}
|
|
1174
|
+
`
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
onFinish() {
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
var JSONReporter = class {
|
|
1181
|
+
onStart(_tasks) {
|
|
1182
|
+
}
|
|
1183
|
+
onTaskStart(_path, _key) {
|
|
1184
|
+
}
|
|
1185
|
+
onTaskComplete(_result) {
|
|
1186
|
+
}
|
|
1187
|
+
onFinish() {
|
|
1188
|
+
}
|
|
1189
|
+
outputLogs(_results, _logsMode) {
|
|
1190
|
+
}
|
|
1191
|
+
outputSummary(result) {
|
|
1192
|
+
const summary = {
|
|
1193
|
+
ok: result.ok,
|
|
1194
|
+
startedAt: result.startedAt,
|
|
1195
|
+
finishedAt: result.finishedAt,
|
|
1196
|
+
durationMs: result.durationMs,
|
|
1197
|
+
tasks: this.serializeTasks(result.tasks)
|
|
1198
|
+
};
|
|
1199
|
+
process.stdout.write(`${JSON.stringify(summary)}
|
|
1200
|
+
`);
|
|
1201
|
+
}
|
|
1202
|
+
serializeTasks(tasks) {
|
|
1203
|
+
return tasks.map((t) => ({
|
|
1204
|
+
key: t.key,
|
|
1205
|
+
path: t.path,
|
|
1206
|
+
ok: t.ok,
|
|
1207
|
+
code: t.code,
|
|
1208
|
+
durationMs: t.durationMs,
|
|
1209
|
+
summaryLine: t.summaryLine,
|
|
1210
|
+
...t.suppressed ? { suppressed: t.suppressed } : {},
|
|
1211
|
+
...t.suppressedBy ? { suppressedBy: t.suppressedBy } : {},
|
|
1212
|
+
...t.children ? { children: this.serializeTasks(t.children) } : {}
|
|
1213
|
+
}));
|
|
1214
|
+
}
|
|
1215
|
+
};
|
|
1216
|
+
var QuietReporter = class extends BaseReporter {
|
|
1217
|
+
onStart(_tasks) {
|
|
1218
|
+
}
|
|
1219
|
+
onTaskStart(_path, _key) {
|
|
1220
|
+
}
|
|
1221
|
+
onTaskComplete(_result) {
|
|
1222
|
+
}
|
|
1223
|
+
onFinish() {
|
|
1224
|
+
}
|
|
1225
|
+
outputLogs(_results, _logsMode) {
|
|
1226
|
+
}
|
|
1227
|
+
outputSummary(result) {
|
|
1228
|
+
const message = result.ok ? this.c(ansi.green, "\u2713 All verifications passed") : this.c(ansi.red, "\u2717 Some verifications failed");
|
|
1229
|
+
process.stdout.write(`${message}
|
|
1230
|
+
`);
|
|
1231
|
+
}
|
|
1232
|
+
};
|
|
1233
|
+
function createReporter(options) {
|
|
1234
|
+
if (options.format === "json") {
|
|
1235
|
+
return new JSONReporter();
|
|
1236
|
+
}
|
|
1237
|
+
if (process.stdout.isTTY && !options.noTty) {
|
|
1238
|
+
return new LiveDashboardReporter(options);
|
|
1239
|
+
}
|
|
1240
|
+
return new SequentialReporter(options);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// src/runner.ts
|
|
1244
|
+
import { spawn } from "child_process";
|
|
1245
|
+
import treeKill from "tree-kill";
|
|
1246
|
+
function normalizeCommand(run) {
|
|
1247
|
+
if (typeof run === "string") {
|
|
1248
|
+
const parts = run.split(/\s+/);
|
|
1249
|
+
return {
|
|
1250
|
+
cmd: parts[0],
|
|
1251
|
+
args: parts.slice(1)
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
if (Array.isArray(run)) {
|
|
1255
|
+
return {
|
|
1256
|
+
cmd: run[0],
|
|
1257
|
+
args: run[1]
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
return run;
|
|
1261
|
+
}
|
|
1262
|
+
var ReportingDependencyTracker = class {
|
|
1263
|
+
/** Map of task path/key to their results */
|
|
1264
|
+
results = /* @__PURE__ */ new Map();
|
|
1265
|
+
/** Map of task path/key to waiters (callbacks to resolve when result is available) */
|
|
1266
|
+
waiters = /* @__PURE__ */ new Map();
|
|
1267
|
+
/** Map of task path to its key (for key-based lookups) */
|
|
1268
|
+
pathToKey = /* @__PURE__ */ new Map();
|
|
1269
|
+
/** Map of task key to its path (for key-based lookups) */
|
|
1270
|
+
keyToPath = /* @__PURE__ */ new Map();
|
|
1271
|
+
/** Map of task path to its reportingDependsOn array */
|
|
1272
|
+
dependencies = /* @__PURE__ */ new Map();
|
|
1273
|
+
/** Reverse map: task path → list of tasks that depend on it */
|
|
1274
|
+
reverseDeps = /* @__PURE__ */ new Map();
|
|
1275
|
+
/** Map of task path to its running ChildProcess */
|
|
1276
|
+
processes = /* @__PURE__ */ new Map();
|
|
1277
|
+
/** Set of task paths that have been killed */
|
|
1278
|
+
killedPaths = /* @__PURE__ */ new Set();
|
|
1279
|
+
/**
|
|
1280
|
+
* Initialize the tracker with all tasks from the verification tree.
|
|
1281
|
+
* Also validates for circular dependencies and builds reverse dependency map.
|
|
1282
|
+
*/
|
|
1283
|
+
initialize(nodes, parentPath = "") {
|
|
1284
|
+
for (const node of nodes) {
|
|
1285
|
+
const path = parentPath ? `${parentPath}:${node.key}` : node.key;
|
|
1286
|
+
this.pathToKey.set(path, node.key);
|
|
1287
|
+
this.keyToPath.set(node.key, path);
|
|
1288
|
+
if (node.reportingDependsOn && node.reportingDependsOn.length > 0) {
|
|
1289
|
+
this.dependencies.set(path, node.reportingDependsOn);
|
|
1290
|
+
}
|
|
1291
|
+
if (node.children) {
|
|
1292
|
+
this.initialize(node.children, path);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
if (parentPath === "") {
|
|
1296
|
+
this.validateNoCycles();
|
|
1297
|
+
this.buildReverseDeps();
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* Build reverse dependency map (task → tasks that depend on it)
|
|
1302
|
+
*/
|
|
1303
|
+
buildReverseDeps() {
|
|
1304
|
+
for (const [path, deps] of this.dependencies.entries()) {
|
|
1305
|
+
for (const dep of deps) {
|
|
1306
|
+
const resolvedDep = this.resolveDependency(dep);
|
|
1307
|
+
if (resolvedDep) {
|
|
1308
|
+
const existing = this.reverseDeps.get(resolvedDep) ?? [];
|
|
1309
|
+
existing.push(path);
|
|
1310
|
+
this.reverseDeps.set(resolvedDep, existing);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
/**
|
|
1316
|
+
* Validate that there are no circular dependencies using DFS with coloring.
|
|
1317
|
+
* Throws an error with the cycle path if a cycle is detected.
|
|
1318
|
+
*/
|
|
1319
|
+
validateNoCycles() {
|
|
1320
|
+
const WHITE = 0;
|
|
1321
|
+
const GRAY = 1;
|
|
1322
|
+
const BLACK = 2;
|
|
1323
|
+
const colors = /* @__PURE__ */ new Map();
|
|
1324
|
+
const parent = /* @__PURE__ */ new Map();
|
|
1325
|
+
for (const path of this.pathToKey.keys()) {
|
|
1326
|
+
colors.set(path, WHITE);
|
|
1327
|
+
}
|
|
1328
|
+
const dfs = (path) => {
|
|
1329
|
+
colors.set(path, GRAY);
|
|
1330
|
+
const deps = this.dependencies.get(path) ?? [];
|
|
1331
|
+
for (const dep of deps) {
|
|
1332
|
+
const depPath = this.resolveDependency(dep);
|
|
1333
|
+
if (!depPath) continue;
|
|
1334
|
+
const color = colors.get(depPath) ?? WHITE;
|
|
1335
|
+
if (color === GRAY) {
|
|
1336
|
+
const cycle = [depPath];
|
|
1337
|
+
let current = path;
|
|
1338
|
+
while (current !== depPath) {
|
|
1339
|
+
cycle.unshift(current);
|
|
1340
|
+
current = parent.get(current) ?? "";
|
|
1341
|
+
}
|
|
1342
|
+
cycle.unshift(depPath);
|
|
1343
|
+
return cycle.join(" \u2192 ");
|
|
1344
|
+
}
|
|
1345
|
+
if (color === WHITE) {
|
|
1346
|
+
parent.set(depPath, path);
|
|
1347
|
+
const cyclePath = dfs(depPath);
|
|
1348
|
+
if (cyclePath) return cyclePath;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
colors.set(path, BLACK);
|
|
1352
|
+
return null;
|
|
1353
|
+
};
|
|
1354
|
+
for (const path of this.pathToKey.keys()) {
|
|
1355
|
+
if (colors.get(path) === WHITE) {
|
|
1356
|
+
const cyclePath = dfs(path);
|
|
1357
|
+
if (cyclePath) {
|
|
1358
|
+
throw new Error(
|
|
1359
|
+
`Circular reporting dependency detected: ${cyclePath}`
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
/**
|
|
1366
|
+
* Resolve a dependency identifier to a task path.
|
|
1367
|
+
* Tries exact path match first, then key match.
|
|
1368
|
+
*/
|
|
1369
|
+
resolveDependency(dep) {
|
|
1370
|
+
if (this.pathToKey.has(dep)) {
|
|
1371
|
+
return dep;
|
|
1372
|
+
}
|
|
1373
|
+
if (this.keyToPath.has(dep)) {
|
|
1374
|
+
return this.keyToPath.get(dep) ?? null;
|
|
1375
|
+
}
|
|
1376
|
+
return null;
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* Record a task result and notify any waiters.
|
|
1380
|
+
* If the task failed, kills all dependent processes for early termination.
|
|
1381
|
+
*/
|
|
1382
|
+
recordResult(result) {
|
|
1383
|
+
this.results.set(result.path, result);
|
|
1384
|
+
if (!result.ok) {
|
|
1385
|
+
this.killDependents(result.path);
|
|
1386
|
+
}
|
|
1387
|
+
const pathWaiters = this.waiters.get(result.path) ?? [];
|
|
1388
|
+
for (const waiter of pathWaiters) {
|
|
1389
|
+
waiter();
|
|
1390
|
+
}
|
|
1391
|
+
this.waiters.delete(result.path);
|
|
1392
|
+
const key = result.key;
|
|
1393
|
+
if (key !== result.path) {
|
|
1394
|
+
const keyWaiters = this.waiters.get(key) ?? [];
|
|
1395
|
+
for (const waiter of keyWaiters) {
|
|
1396
|
+
waiter();
|
|
1397
|
+
}
|
|
1398
|
+
this.waiters.delete(key);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* Wait for all dependencies of a task to complete.
|
|
1403
|
+
*/
|
|
1404
|
+
async waitForDependencies(path) {
|
|
1405
|
+
const deps = this.dependencies.get(path);
|
|
1406
|
+
if (!deps || deps.length === 0) {
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
const waitPromises = deps.map((dep) => this.waitForResult(dep));
|
|
1410
|
+
await Promise.all(waitPromises);
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Wait for a specific task result to be available.
|
|
1414
|
+
*/
|
|
1415
|
+
waitForResult(pathOrKey) {
|
|
1416
|
+
const resolvedPath = this.resolveDependency(pathOrKey);
|
|
1417
|
+
if (resolvedPath && this.results.has(resolvedPath)) {
|
|
1418
|
+
return Promise.resolve();
|
|
1419
|
+
}
|
|
1420
|
+
if (this.results.has(pathOrKey)) {
|
|
1421
|
+
return Promise.resolve();
|
|
1422
|
+
}
|
|
1423
|
+
return new Promise((resolve3) => {
|
|
1424
|
+
const waiters = this.waiters.get(pathOrKey) ?? [];
|
|
1425
|
+
waiters.push(resolve3);
|
|
1426
|
+
this.waiters.set(pathOrKey, waiters);
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
/**
|
|
1430
|
+
* Check if any dependency of a task has failed.
|
|
1431
|
+
* Returns the path of the first failed dependency, or null if all passed.
|
|
1432
|
+
*/
|
|
1433
|
+
getFailedDependency(path) {
|
|
1434
|
+
const deps = this.dependencies.get(path);
|
|
1435
|
+
if (!deps || deps.length === 0) {
|
|
1436
|
+
return null;
|
|
1437
|
+
}
|
|
1438
|
+
for (const dep of deps) {
|
|
1439
|
+
const resolvedPath = this.resolveDependency(dep);
|
|
1440
|
+
if (!resolvedPath) continue;
|
|
1441
|
+
const result = this.results.get(resolvedPath);
|
|
1442
|
+
if (result && !result.ok) {
|
|
1443
|
+
return resolvedPath;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
return null;
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* Check if a task has any reporting dependencies.
|
|
1450
|
+
*/
|
|
1451
|
+
hasDependencies(path) {
|
|
1452
|
+
const deps = this.dependencies.get(path);
|
|
1453
|
+
return deps !== void 0 && deps.length > 0;
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* Register a running process for a task.
|
|
1457
|
+
*/
|
|
1458
|
+
registerProcess(path, proc) {
|
|
1459
|
+
this.processes.set(path, proc);
|
|
1460
|
+
}
|
|
1461
|
+
/**
|
|
1462
|
+
* Unregister a process (called when it completes naturally).
|
|
1463
|
+
*/
|
|
1464
|
+
unregisterProcess(path) {
|
|
1465
|
+
this.processes.delete(path);
|
|
1466
|
+
}
|
|
1467
|
+
/**
|
|
1468
|
+
* Check if a task was killed.
|
|
1469
|
+
*/
|
|
1470
|
+
wasKilled(path) {
|
|
1471
|
+
return this.killedPaths.has(path);
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* Kill all processes that depend on the failed task.
|
|
1475
|
+
* Called when a task fails to terminate dependent tasks early.
|
|
1476
|
+
*/
|
|
1477
|
+
killDependents(failedPath) {
|
|
1478
|
+
const dependents = this.reverseDeps.get(failedPath) ?? [];
|
|
1479
|
+
for (const depPath of dependents) {
|
|
1480
|
+
const proc = this.processes.get(depPath);
|
|
1481
|
+
if (proc?.pid) {
|
|
1482
|
+
this.killedPaths.add(depPath);
|
|
1483
|
+
treeKill(proc.pid, "SIGTERM", (err) => {
|
|
1484
|
+
if (err) {
|
|
1485
|
+
}
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
};
|
|
1491
|
+
async function executeCommand(command, cwd, tracker, path) {
|
|
1492
|
+
const start = Date.now();
|
|
1493
|
+
return new Promise((resolve3) => {
|
|
1494
|
+
const proc = spawn(command.cmd, command.args, {
|
|
1495
|
+
shell: process.platform === "win32",
|
|
1496
|
+
cwd: command.cwd ?? cwd,
|
|
1497
|
+
env: { ...process.env, NO_COLOR: "1", ...command.env }
|
|
1498
|
+
});
|
|
1499
|
+
if (tracker && path) {
|
|
1500
|
+
tracker.registerProcess(path, proc);
|
|
1501
|
+
}
|
|
1502
|
+
let output = "";
|
|
1503
|
+
proc.stdout.on("data", (data) => {
|
|
1504
|
+
output += data.toString();
|
|
1505
|
+
});
|
|
1506
|
+
proc.stderr.on("data", (data) => {
|
|
1507
|
+
output += data.toString();
|
|
1508
|
+
});
|
|
1509
|
+
proc.on("close", (code, signal) => {
|
|
1510
|
+
if (tracker && path) {
|
|
1511
|
+
tracker.unregisterProcess(path);
|
|
1512
|
+
}
|
|
1513
|
+
const durationMs = Date.now() - start;
|
|
1514
|
+
const killed = signal === "SIGTERM" || code === 143 || (tracker?.wasKilled(path ?? "") ?? false);
|
|
1515
|
+
resolve3({
|
|
1516
|
+
code: code ?? 1,
|
|
1517
|
+
output,
|
|
1518
|
+
durationMs,
|
|
1519
|
+
killed
|
|
1520
|
+
});
|
|
1521
|
+
});
|
|
1522
|
+
proc.on("error", (err) => {
|
|
1523
|
+
if (tracker && path) {
|
|
1524
|
+
tracker.unregisterProcess(path);
|
|
1525
|
+
}
|
|
1526
|
+
const durationMs = Date.now() - start;
|
|
1527
|
+
resolve3({
|
|
1528
|
+
code: 1,
|
|
1529
|
+
output: `Failed to execute command: ${err.message}`,
|
|
1530
|
+
durationMs,
|
|
1531
|
+
killed: false
|
|
1532
|
+
});
|
|
1533
|
+
});
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
function buildPath(parentPath, key) {
|
|
1537
|
+
return parentPath ? `${parentPath}:${key}` : key;
|
|
1538
|
+
}
|
|
1539
|
+
function matchesFilter(path, filters) {
|
|
1540
|
+
if (!filters || filters.length === 0) {
|
|
1541
|
+
return true;
|
|
1542
|
+
}
|
|
1543
|
+
return filters.some((filter) => {
|
|
1544
|
+
return path === filter || path.startsWith(`${filter}:`);
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
function hasMatchingDescendant(node, parentPath, filters) {
|
|
1548
|
+
const path = buildPath(parentPath, node.key);
|
|
1549
|
+
if (matchesFilter(path, filters)) {
|
|
1550
|
+
return true;
|
|
1551
|
+
}
|
|
1552
|
+
if (node.children) {
|
|
1553
|
+
return node.children.some(
|
|
1554
|
+
(child) => hasMatchingDescendant(child, path, filters)
|
|
1555
|
+
);
|
|
1556
|
+
}
|
|
1557
|
+
return false;
|
|
1558
|
+
}
|
|
1559
|
+
var VerificationRunner = class {
|
|
1560
|
+
registry;
|
|
1561
|
+
options;
|
|
1562
|
+
callbacks;
|
|
1563
|
+
dependencyTracker;
|
|
1564
|
+
constructor(options = {}, registry = defaultRegistry, callbacks = {}) {
|
|
1565
|
+
this.options = options;
|
|
1566
|
+
this.registry = registry;
|
|
1567
|
+
this.callbacks = callbacks;
|
|
1568
|
+
this.dependencyTracker = new ReportingDependencyTracker();
|
|
1569
|
+
}
|
|
1570
|
+
/**
|
|
1571
|
+
* Run all verification tasks
|
|
1572
|
+
*/
|
|
1573
|
+
async run(tasks) {
|
|
1574
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1575
|
+
const wallStart = Date.now();
|
|
1576
|
+
this.dependencyTracker.initialize(tasks);
|
|
1577
|
+
const results = await this.runNodes(tasks, "");
|
|
1578
|
+
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1579
|
+
const durationMs = Date.now() - wallStart;
|
|
1580
|
+
const allOk = results.every((r) => r.ok);
|
|
1581
|
+
return {
|
|
1582
|
+
ok: allOk,
|
|
1583
|
+
startedAt,
|
|
1584
|
+
finishedAt,
|
|
1585
|
+
durationMs,
|
|
1586
|
+
tasks: results
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
/**
|
|
1590
|
+
* Run a list of nodes with the appropriate strategy
|
|
1591
|
+
*/
|
|
1592
|
+
async runNodes(nodes, parentPath, strategy = "parallel") {
|
|
1593
|
+
const filteredNodes = nodes.filter(
|
|
1594
|
+
(node) => hasMatchingDescendant(node, parentPath, this.options.filter)
|
|
1595
|
+
);
|
|
1596
|
+
if (filteredNodes.length === 0) {
|
|
1597
|
+
return [];
|
|
1598
|
+
}
|
|
1599
|
+
switch (strategy) {
|
|
1600
|
+
case "parallel":
|
|
1601
|
+
return Promise.all(
|
|
1602
|
+
filteredNodes.map((node) => this.runNode(node, parentPath))
|
|
1603
|
+
);
|
|
1604
|
+
case "sequential": {
|
|
1605
|
+
const results = [];
|
|
1606
|
+
for (const node of filteredNodes) {
|
|
1607
|
+
results.push(await this.runNode(node, parentPath));
|
|
1608
|
+
}
|
|
1609
|
+
return results;
|
|
1610
|
+
}
|
|
1611
|
+
case "fail-fast": {
|
|
1612
|
+
const results = [];
|
|
1613
|
+
for (const node of filteredNodes) {
|
|
1614
|
+
const result = await this.runNode(node, parentPath);
|
|
1615
|
+
results.push(result);
|
|
1616
|
+
if (!result.ok) {
|
|
1617
|
+
break;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
return results;
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
/**
|
|
1625
|
+
* Run a single node (leaf or group)
|
|
1626
|
+
*/
|
|
1627
|
+
async runNode(node, parentPath) {
|
|
1628
|
+
const path = buildPath(parentPath, node.key);
|
|
1629
|
+
this.callbacks.onTaskStart?.(path, node.key);
|
|
1630
|
+
if (node.children && node.children.length > 0) {
|
|
1631
|
+
const start = Date.now();
|
|
1632
|
+
const childResults = await this.runNodes(
|
|
1633
|
+
node.children,
|
|
1634
|
+
path,
|
|
1635
|
+
node.strategy ?? "parallel"
|
|
1636
|
+
);
|
|
1637
|
+
const durationMs2 = Date.now() - start;
|
|
1638
|
+
const allOk = childResults.every((r) => r.ok || r.suppressed);
|
|
1639
|
+
const allSuppressed = childResults.length > 0 && childResults.every((r) => r.suppressed);
|
|
1640
|
+
const anySuppressed = childResults.some((r) => r.suppressed);
|
|
1641
|
+
const result2 = {
|
|
1642
|
+
key: node.key,
|
|
1643
|
+
path,
|
|
1644
|
+
ok: allOk,
|
|
1645
|
+
code: allOk ? 0 : 1,
|
|
1646
|
+
durationMs: durationMs2,
|
|
1647
|
+
output: "",
|
|
1648
|
+
summaryLine: allOk ? node.successLabel ?? `${node.key}: all passed` : node.failureLabel ?? `${node.key}: some failed`,
|
|
1649
|
+
children: childResults
|
|
1650
|
+
};
|
|
1651
|
+
if (allSuppressed) {
|
|
1652
|
+
result2.suppressed = true;
|
|
1653
|
+
result2.suppressedBy = childResults[0].suppressedBy;
|
|
1654
|
+
} else if (anySuppressed && !allOk) {
|
|
1655
|
+
}
|
|
1656
|
+
this.dependencyTracker.recordResult(result2);
|
|
1657
|
+
this.callbacks.onTaskComplete?.(result2);
|
|
1658
|
+
return result2;
|
|
1659
|
+
}
|
|
1660
|
+
if (!node.run) {
|
|
1661
|
+
const result2 = {
|
|
1662
|
+
key: node.key,
|
|
1663
|
+
path,
|
|
1664
|
+
ok: true,
|
|
1665
|
+
code: 0,
|
|
1666
|
+
durationMs: 0,
|
|
1667
|
+
output: "",
|
|
1668
|
+
summaryLine: `${node.key}: no command specified`
|
|
1669
|
+
};
|
|
1670
|
+
this.dependencyTracker.recordResult(result2);
|
|
1671
|
+
this.callbacks.onTaskComplete?.(result2);
|
|
1672
|
+
return result2;
|
|
1673
|
+
}
|
|
1674
|
+
const command = normalizeCommand(node.run);
|
|
1675
|
+
const cwd = this.options.cwd ?? process.cwd();
|
|
1676
|
+
const { code, output, durationMs, killed } = await executeCommand(
|
|
1677
|
+
command,
|
|
1678
|
+
cwd,
|
|
1679
|
+
this.dependencyTracker,
|
|
1680
|
+
path
|
|
1681
|
+
);
|
|
1682
|
+
const ok = code === 0;
|
|
1683
|
+
if (killed) {
|
|
1684
|
+
await this.dependencyTracker.waitForDependencies(path);
|
|
1685
|
+
const failedDep = this.dependencyTracker.getFailedDependency(path);
|
|
1686
|
+
const result2 = {
|
|
1687
|
+
key: node.key,
|
|
1688
|
+
path,
|
|
1689
|
+
ok: false,
|
|
1690
|
+
code,
|
|
1691
|
+
durationMs,
|
|
1692
|
+
output,
|
|
1693
|
+
summaryLine: `${node.key}: terminated`,
|
|
1694
|
+
suppressed: true,
|
|
1695
|
+
suppressedBy: failedDep ?? "unknown"
|
|
1696
|
+
};
|
|
1697
|
+
this.dependencyTracker.recordResult(result2);
|
|
1698
|
+
this.callbacks.onTaskComplete?.(result2);
|
|
1699
|
+
return result2;
|
|
1700
|
+
}
|
|
1701
|
+
const cmdString = `${command.cmd} ${command.args.join(" ")}`;
|
|
1702
|
+
const parsed = this.registry.parse(
|
|
1703
|
+
output,
|
|
1704
|
+
code,
|
|
1705
|
+
node.parser,
|
|
1706
|
+
cmdString
|
|
1707
|
+
);
|
|
1708
|
+
let summaryLine;
|
|
1709
|
+
if (ok) {
|
|
1710
|
+
summaryLine = node.successLabel ? `${node.key}: ${node.successLabel}` : `${node.key}: ${parsed.summary}`;
|
|
1711
|
+
} else {
|
|
1712
|
+
summaryLine = node.failureLabel ? `${node.key}: ${node.failureLabel}` : `${node.key}: ${parsed.summary}`;
|
|
1713
|
+
}
|
|
1714
|
+
let result = {
|
|
1715
|
+
key: node.key,
|
|
1716
|
+
path,
|
|
1717
|
+
ok,
|
|
1718
|
+
code,
|
|
1719
|
+
durationMs,
|
|
1720
|
+
output,
|
|
1721
|
+
summaryLine,
|
|
1722
|
+
metrics: parsed.metrics
|
|
1723
|
+
};
|
|
1724
|
+
if (this.dependencyTracker.hasDependencies(path)) {
|
|
1725
|
+
await this.dependencyTracker.waitForDependencies(path);
|
|
1726
|
+
if (!ok) {
|
|
1727
|
+
const failedDep = this.dependencyTracker.getFailedDependency(path);
|
|
1728
|
+
if (failedDep) {
|
|
1729
|
+
result = {
|
|
1730
|
+
...result,
|
|
1731
|
+
suppressed: true,
|
|
1732
|
+
suppressedBy: failedDep
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
this.dependencyTracker.recordResult(result);
|
|
1738
|
+
this.callbacks.onTaskComplete?.(result);
|
|
1739
|
+
return result;
|
|
1740
|
+
}
|
|
1741
|
+
};
|
|
1742
|
+
|
|
1743
|
+
// src/index.ts
|
|
1744
|
+
async function verify(config, cliOptions) {
|
|
1745
|
+
const options = mergeOptions(config.options, cliOptions);
|
|
1746
|
+
const reporter = createReporter(options);
|
|
1747
|
+
reporter.onStart?.(config.tasks);
|
|
1748
|
+
const runner = new VerificationRunner(options, void 0, {
|
|
1749
|
+
onTaskStart: (path, key) => reporter.onTaskStart(path, key),
|
|
1750
|
+
onTaskComplete: (result2) => reporter.onTaskComplete(result2)
|
|
1751
|
+
});
|
|
1752
|
+
const result = await runner.run(config.tasks);
|
|
1753
|
+
reporter.onFinish?.();
|
|
1754
|
+
reporter.outputLogs(result.tasks, options.logs ?? "failed");
|
|
1755
|
+
reporter.outputSummary(result);
|
|
1756
|
+
return result;
|
|
1757
|
+
}
|
|
1758
|
+
async function verifyFromConfig(cwd = process.cwd(), cliOptions) {
|
|
1759
|
+
const config = await loadConfigFromCwd(cwd, cliOptions?.cwd);
|
|
1760
|
+
if (!config) {
|
|
1761
|
+
throw new Error(
|
|
1762
|
+
`No verify config found in ${cwd}. Create a verify.config.ts file.`
|
|
1763
|
+
);
|
|
1764
|
+
}
|
|
1765
|
+
return verify(config, { ...cliOptions, cwd });
|
|
1766
|
+
}
|
|
1767
|
+
export {
|
|
1768
|
+
JSONReporter,
|
|
1769
|
+
LiveDashboardReporter,
|
|
1770
|
+
ParserRegistry,
|
|
1771
|
+
QuietReporter,
|
|
1772
|
+
SequentialReporter,
|
|
1773
|
+
SequentialReporter as TTYReporter,
|
|
1774
|
+
VerificationRunner,
|
|
1775
|
+
biomeParser,
|
|
1776
|
+
createReporter,
|
|
1777
|
+
defaultRegistry,
|
|
1778
|
+
defineConfig,
|
|
1779
|
+
defineTask,
|
|
1780
|
+
detectTasks,
|
|
1781
|
+
discoverPackages,
|
|
1782
|
+
findConfigFile,
|
|
1783
|
+
generateConfigContent,
|
|
1784
|
+
genericParser,
|
|
1785
|
+
gotestParser,
|
|
1786
|
+
hasPackageChanged,
|
|
1787
|
+
loadConfig,
|
|
1788
|
+
loadConfigFromCwd,
|
|
1789
|
+
mergeOptions,
|
|
1790
|
+
runInit,
|
|
1791
|
+
tscParser,
|
|
1792
|
+
verify,
|
|
1793
|
+
verifyFromConfig,
|
|
1794
|
+
vitestParser
|
|
1795
|
+
};
|
|
1796
|
+
//# sourceMappingURL=index.js.map
|