verify-grid 0.1.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/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # verify-grid
2
+
3
+ Run matrix of tasks across services with parallel execution and status reporting.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install verify-grid
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ Create `qa.config.js` or `qa.config.mjs`:
14
+
15
+ ```javascript
16
+ export default {
17
+ tasks: {
18
+ unit: { command: 'npm', args: ['test'] },
19
+ lint: { command: 'npm', args: ['run', 'lint'] },
20
+ },
21
+ services: [
22
+ { name: 'service1', cwd: './services/service1' },
23
+ { name: 'service2', cwd: './services/service2' },
24
+ ],
25
+ };
26
+ ```
27
+
28
+ Run tasks:
29
+
30
+ ```bash
31
+ qa run --tasks unit,lint --services all
32
+ qa run --tasks all --services all --fail-fast
33
+ qa run --tasks unit --services all --watch
34
+ ```
35
+
36
+ ## Features
37
+
38
+ - Parallel execution with configurable concurrency
39
+ - Fail-fast mode to stop on first error
40
+ - Watch mode with live updates
41
+ - Multiple output formats (table, minimal, json)
42
+ - Proper exit codes for CI/CD (0=success, 1=errors, 2=config error)
43
+ - Task overrides per service
44
+ - Timeout support
45
+
46
+ ## License
47
+
48
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,641 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { cac } from "cac";
5
+ import { cpus } from "os";
6
+
7
+ // src/config/loader.ts
8
+ import { pathToFileURL } from "url";
9
+ import { existsSync } from "fs";
10
+ import { resolve, dirname } from "path";
11
+
12
+ // src/config/schema.ts
13
+ import { z } from "zod";
14
+ var TaskDefSchema = z.object({
15
+ name: z.string(),
16
+ command: z.string(),
17
+ args: z.array(z.string()).optional(),
18
+ shell: z.boolean().optional(),
19
+ timeout: z.number().positive().optional(),
20
+ env: z.record(z.string()).optional()
21
+ });
22
+ var ServiceDefSchema = z.object({
23
+ name: z.string(),
24
+ cwd: z.string(),
25
+ tasks: z.record(z.object({
26
+ command: z.string().optional(),
27
+ args: z.array(z.string()).optional(),
28
+ shell: z.boolean().optional(),
29
+ timeout: z.number().positive().optional(),
30
+ env: z.record(z.string()).optional()
31
+ })).optional(),
32
+ env: z.record(z.string()).optional()
33
+ });
34
+ var ConfigSchema = z.object({
35
+ services: z.array(ServiceDefSchema),
36
+ tasks: z.record(TaskDefSchema.omit({ name: true }))
37
+ });
38
+
39
+ // src/config/loader.ts
40
+ var CONFIG_FILES = ["qa.config.ts", "qa.config.js", "qa.config.mjs"];
41
+ async function findConfig(startDir = process.cwd()) {
42
+ let currentDir = resolve(startDir);
43
+ const root = resolve("/");
44
+ while (currentDir !== root) {
45
+ for (const configFile of CONFIG_FILES) {
46
+ const configPath = resolve(currentDir, configFile);
47
+ if (existsSync(configPath)) {
48
+ return configPath;
49
+ }
50
+ }
51
+ currentDir = dirname(currentDir);
52
+ }
53
+ for (const configFile of CONFIG_FILES) {
54
+ const configPath = resolve(root, configFile);
55
+ if (existsSync(configPath)) {
56
+ return configPath;
57
+ }
58
+ }
59
+ return null;
60
+ }
61
+ async function loadConfig(configPath) {
62
+ const path = configPath || await findConfig();
63
+ if (!path) {
64
+ throw new Error(
65
+ "No config file found. Create qa.config.ts or qa.config.js in your project root."
66
+ );
67
+ }
68
+ if (!existsSync(path)) {
69
+ throw new Error(`Config file not found: ${path}`);
70
+ }
71
+ try {
72
+ const fileUrl = pathToFileURL(path).href;
73
+ const module = await import(fileUrl);
74
+ const configData = module.default || module;
75
+ const result = ConfigSchema.safeParse(configData);
76
+ if (!result.success) {
77
+ const errors = result.error.errors.map(
78
+ (err) => ` - ${err.path.join(".")}: ${err.message}`
79
+ ).join("\n");
80
+ throw new Error(`Invalid config:
81
+ ${errors}`);
82
+ }
83
+ const serviceNames = /* @__PURE__ */ new Set();
84
+ for (const service of result.data.services) {
85
+ if (serviceNames.has(service.name)) {
86
+ throw new Error(`Duplicate service name: ${service.name}`);
87
+ }
88
+ serviceNames.add(service.name);
89
+ }
90
+ const config = {
91
+ services: result.data.services,
92
+ tasks: Object.entries(result.data.tasks).reduce((acc, [name, def]) => {
93
+ acc[name] = { name, ...def };
94
+ return acc;
95
+ }, {})
96
+ };
97
+ return config;
98
+ } catch (error) {
99
+ if (error instanceof Error) {
100
+ throw error;
101
+ }
102
+ throw new Error(`Failed to load config: ${String(error)}`);
103
+ }
104
+ }
105
+
106
+ // src/runner/generator.ts
107
+ import { existsSync as existsSync2 } from "fs";
108
+ function generateJobs(config, taskNames, serviceNames) {
109
+ const jobs = [];
110
+ const resolvedServiceNames = serviceNames.includes("all") ? config.services.map((s) => s.name) : serviceNames;
111
+ const resolvedTaskNames = taskNames.includes("all") ? Object.keys(config.tasks) : taskNames;
112
+ for (const taskName of resolvedTaskNames) {
113
+ const globalTask = config.tasks[taskName];
114
+ if (!globalTask) {
115
+ throw new Error(`Unknown task: ${taskName}`);
116
+ }
117
+ for (const serviceName of resolvedServiceNames) {
118
+ const service = config.services.find((s) => s.name === serviceName);
119
+ if (!service) {
120
+ throw new Error(`Unknown service: ${serviceName}`);
121
+ }
122
+ if (!existsSync2(service.cwd)) {
123
+ jobs.push({
124
+ id: `${taskName}:${serviceName}`,
125
+ taskName,
126
+ serviceName,
127
+ cwd: service.cwd,
128
+ command: "",
129
+ status: "skipped"
130
+ });
131
+ continue;
132
+ }
133
+ const taskOverride = service.tasks?.[taskName];
134
+ if (taskOverride && taskOverride.command === void 0 && !globalTask.command) {
135
+ jobs.push({
136
+ id: `${taskName}:${serviceName}`,
137
+ taskName,
138
+ serviceName,
139
+ cwd: service.cwd,
140
+ command: "",
141
+ status: "skipped"
142
+ });
143
+ continue;
144
+ }
145
+ const mergedTask = {
146
+ name: taskName,
147
+ command: taskOverride?.command ?? globalTask.command,
148
+ args: taskOverride?.args ?? globalTask.args,
149
+ shell: taskOverride?.shell ?? globalTask.shell ?? true,
150
+ timeout: taskOverride?.timeout ?? globalTask.timeout,
151
+ env: {
152
+ ...globalTask.env,
153
+ ...service.env,
154
+ ...taskOverride?.env
155
+ }
156
+ };
157
+ jobs.push({
158
+ id: `${taskName}:${serviceName}`,
159
+ taskName,
160
+ serviceName,
161
+ cwd: service.cwd,
162
+ command: mergedTask.command,
163
+ args: mergedTask.args,
164
+ shell: mergedTask.shell,
165
+ timeout: mergedTask.timeout,
166
+ env: mergedTask.env,
167
+ status: "queued"
168
+ });
169
+ }
170
+ }
171
+ return jobs;
172
+ }
173
+
174
+ // src/runner/runner.ts
175
+ import PQueue from "p-queue";
176
+ import { EventEmitter } from "events";
177
+
178
+ // src/runner/executor.ts
179
+ import { execa } from "execa";
180
+
181
+ // src/utils/log.ts
182
+ var MAX_LOG_LINES = 200;
183
+ function extractErrorSnippet(stderr, stdout, maxLines = 10) {
184
+ const errorLog = stderr || stdout;
185
+ const lines = errorLog.split("\n").filter((line) => line.trim());
186
+ if (lines.length === 0) {
187
+ return "No error output";
188
+ }
189
+ const relevantLines = lines.slice(-maxLines);
190
+ return relevantLines.join("\n");
191
+ }
192
+ var LogBuffer = class {
193
+ lines = [];
194
+ maxLines;
195
+ constructor(maxLines = MAX_LOG_LINES) {
196
+ this.maxLines = maxLines;
197
+ }
198
+ append(data) {
199
+ const newLines = data.split("\n");
200
+ this.lines.push(...newLines);
201
+ if (this.lines.length > this.maxLines) {
202
+ this.lines = this.lines.slice(-this.maxLines);
203
+ }
204
+ }
205
+ getContent() {
206
+ return this.lines.join("\n");
207
+ }
208
+ clear() {
209
+ this.lines = [];
210
+ }
211
+ };
212
+
213
+ // src/runner/executor.ts
214
+ async function executeJob(job) {
215
+ const startTime = Date.now();
216
+ const stdoutBuffer = new LogBuffer();
217
+ const stderrBuffer = new LogBuffer();
218
+ try {
219
+ const args = job.args || [];
220
+ const options = {
221
+ cwd: job.cwd,
222
+ env: { ...process.env, ...job.env },
223
+ shell: job.shell,
224
+ timeout: job.timeout,
225
+ reject: false,
226
+ all: true
227
+ };
228
+ const result = await execa(job.command, args, options);
229
+ stdoutBuffer.append(result.stdout || "");
230
+ stderrBuffer.append(result.stderr || "");
231
+ const durationMs = Date.now() - startTime;
232
+ if (result.exitCode === 0) {
233
+ return {
234
+ job,
235
+ exitCode: 0,
236
+ stdout: stdoutBuffer.getContent(),
237
+ stderr: stderrBuffer.getContent(),
238
+ durationMs
239
+ };
240
+ }
241
+ return {
242
+ job,
243
+ exitCode: result.exitCode,
244
+ stdout: stdoutBuffer.getContent(),
245
+ stderr: stderrBuffer.getContent(),
246
+ durationMs,
247
+ errorType: result.signal ? "signal" : "exit",
248
+ signal: result.signal || void 0
249
+ };
250
+ } catch (error) {
251
+ const durationMs = Date.now() - startTime;
252
+ const execaError = error;
253
+ if (execaError.stdout) stdoutBuffer.append(execaError.stdout);
254
+ if (execaError.stderr) stderrBuffer.append(execaError.stderr);
255
+ let errorType = "exit";
256
+ if (execaError.timedOut) {
257
+ errorType = "timeout";
258
+ } else if (execaError.signal) {
259
+ errorType = "signal";
260
+ } else if (execaError.exitCode === 127 || error.code === "ENOENT") {
261
+ errorType = "spawn";
262
+ }
263
+ return {
264
+ job,
265
+ exitCode: execaError.exitCode,
266
+ stdout: stdoutBuffer.getContent(),
267
+ stderr: stderrBuffer.getContent(),
268
+ durationMs,
269
+ errorType,
270
+ signal: execaError.signal || void 0
271
+ };
272
+ }
273
+ }
274
+
275
+ // src/runner/runner.ts
276
+ var Runner = class extends EventEmitter {
277
+ queue;
278
+ state;
279
+ shouldStop = false;
280
+ options;
281
+ constructor(options) {
282
+ super();
283
+ this.options = options;
284
+ this.queue = new PQueue({ concurrency: options.concurrency });
285
+ this.state = {
286
+ jobs: /* @__PURE__ */ new Map(),
287
+ results: /* @__PURE__ */ new Map(),
288
+ errors: [],
289
+ startedAt: Date.now()
290
+ };
291
+ process.on("SIGINT", () => this.handleInterrupt());
292
+ process.on("SIGTERM", () => this.handleInterrupt());
293
+ }
294
+ handleInterrupt() {
295
+ if (this.shouldStop) {
296
+ process.exit(130);
297
+ }
298
+ this.shouldStop = true;
299
+ this.queue.clear();
300
+ for (const [id, job] of this.state.jobs) {
301
+ if (job.status === "queued") {
302
+ job.status = "canceled";
303
+ this.state.jobs.set(id, job);
304
+ }
305
+ }
306
+ this.emit("runCanceled");
307
+ }
308
+ async run(jobs) {
309
+ for (const job of jobs) {
310
+ this.state.jobs.set(job.id, job);
311
+ }
312
+ const executableJobs = jobs.filter((j) => j.status === "queued");
313
+ for (const job of executableJobs) {
314
+ if (this.shouldStop) {
315
+ job.status = "canceled";
316
+ this.state.jobs.set(job.id, job);
317
+ continue;
318
+ }
319
+ this.queue.add(async () => {
320
+ if (this.shouldStop) {
321
+ job.status = "canceled";
322
+ this.state.jobs.set(job.id, job);
323
+ return;
324
+ }
325
+ job.status = "running";
326
+ job.startedAt = Date.now();
327
+ this.state.jobs.set(job.id, job);
328
+ this.emit("jobStart", job);
329
+ const result = await executeJob(job);
330
+ job.status = result.exitCode === 0 ? "done" : "error";
331
+ job.endedAt = Date.now();
332
+ this.state.jobs.set(job.id, job);
333
+ this.state.results.set(job.id, result);
334
+ if (result.exitCode !== 0) {
335
+ this.emit("jobError", result);
336
+ if (this.options.failFast) {
337
+ this.shouldStop = true;
338
+ this.queue.clear();
339
+ for (const [id, j] of this.state.jobs) {
340
+ if (j.status === "queued") {
341
+ j.status = "canceled";
342
+ this.state.jobs.set(id, j);
343
+ }
344
+ }
345
+ }
346
+ } else {
347
+ this.emit("jobComplete", result);
348
+ }
349
+ });
350
+ }
351
+ await this.queue.onIdle();
352
+ this.state.endedAt = Date.now();
353
+ this.emit("runComplete", this.state);
354
+ return this.state;
355
+ }
356
+ getState() {
357
+ return this.state;
358
+ }
359
+ };
360
+
361
+ // src/render/table.ts
362
+ import Table from "cli-table3";
363
+
364
+ // src/utils/colors.ts
365
+ import kleur from "kleur";
366
+ var colorsEnabled = true;
367
+ function setColorsEnabled(enabled) {
368
+ colorsEnabled = enabled;
369
+ kleur.enabled = enabled;
370
+ }
371
+ function getStatusColor(status) {
372
+ if (!colorsEnabled) {
373
+ return (text) => text;
374
+ }
375
+ switch (status) {
376
+ case "done":
377
+ return kleur.green;
378
+ case "error":
379
+ return kleur.red;
380
+ case "running":
381
+ return kleur.blue;
382
+ case "queued":
383
+ return kleur.yellow;
384
+ case "skipped":
385
+ case "canceled":
386
+ return kleur.gray;
387
+ default:
388
+ return (text) => text;
389
+ }
390
+ }
391
+ var colors = {
392
+ error: kleur.red,
393
+ success: kleur.green,
394
+ warning: kleur.yellow,
395
+ info: kleur.blue,
396
+ dim: kleur.gray,
397
+ bold: kleur.bold
398
+ };
399
+
400
+ // src/render/table.ts
401
+ function renderTable(state) {
402
+ const table = new Table({
403
+ head: ["Task", "Service", "Status"],
404
+ style: {
405
+ head: [],
406
+ border: []
407
+ }
408
+ });
409
+ const taskGroups = groupJobsByTask(state);
410
+ for (const [taskName, jobs] of taskGroups) {
411
+ let isFirstRow = true;
412
+ for (const job of jobs) {
413
+ const statusColor = getStatusColor(job.status);
414
+ const statusText = statusColor(job.status);
415
+ table.push([
416
+ isFirstRow ? taskName : "",
417
+ job.serviceName,
418
+ statusText
419
+ ]);
420
+ isFirstRow = false;
421
+ }
422
+ }
423
+ return table.toString();
424
+ }
425
+ function renderMinimal(state) {
426
+ const lines = [];
427
+ for (const [, job] of state.jobs) {
428
+ const statusColor = getStatusColor(job.status);
429
+ const statusText = statusColor(job.status);
430
+ lines.push(`${job.taskName} ${job.serviceName} ${statusText}`);
431
+ }
432
+ return lines.join("\n");
433
+ }
434
+ function renderJson(state) {
435
+ const matrix = {};
436
+ for (const [, job] of state.jobs) {
437
+ if (!matrix[job.taskName]) {
438
+ matrix[job.taskName] = {};
439
+ }
440
+ const result = state.results.get(job.id);
441
+ const taskMatrix = matrix[job.taskName];
442
+ if (taskMatrix) {
443
+ taskMatrix[job.serviceName] = {
444
+ status: job.status,
445
+ exitCode: result?.exitCode,
446
+ durationMs: result?.durationMs
447
+ };
448
+ }
449
+ }
450
+ const errors = Array.from(state.results.values()).filter((r) => r.job.status === "error").map((r) => ({
451
+ taskName: r.job.taskName,
452
+ serviceName: r.job.serviceName,
453
+ exitCode: r.exitCode,
454
+ durationMs: r.durationMs,
455
+ errorType: r.errorType
456
+ }));
457
+ const output = {
458
+ startedAt: state.startedAt,
459
+ endedAt: state.endedAt,
460
+ durationMs: state.endedAt ? state.endedAt - state.startedAt : void 0,
461
+ matrix,
462
+ errors
463
+ };
464
+ return JSON.stringify(output, null, 2);
465
+ }
466
+ function groupJobsByTask(state) {
467
+ const groups = /* @__PURE__ */ new Map();
468
+ for (const [, job] of state.jobs) {
469
+ if (!groups.has(job.taskName)) {
470
+ groups.set(job.taskName, []);
471
+ }
472
+ groups.get(job.taskName).push(job);
473
+ }
474
+ return groups;
475
+ }
476
+
477
+ // src/render/watch.ts
478
+ import logUpdate from "log-update";
479
+ var WatchRenderer = class {
480
+ updateInterval = null;
481
+ state = null;
482
+ start(state) {
483
+ this.state = state;
484
+ this.render();
485
+ this.updateInterval = setInterval(() => {
486
+ this.render();
487
+ }, 100);
488
+ }
489
+ update(state) {
490
+ this.state = state;
491
+ }
492
+ stop() {
493
+ if (this.updateInterval) {
494
+ clearInterval(this.updateInterval);
495
+ this.updateInterval = null;
496
+ }
497
+ logUpdate.done();
498
+ }
499
+ render() {
500
+ if (!this.state) return;
501
+ const output = renderTable(this.state);
502
+ logUpdate(output);
503
+ }
504
+ };
505
+
506
+ // src/report/aggregator.ts
507
+ function aggregateErrors(state) {
508
+ const errors = [];
509
+ for (const [, result] of state.results) {
510
+ if (result.job.status === "error") {
511
+ const errorMessage = getErrorMessage(result);
512
+ const logSnippet = extractErrorSnippet(result.stderr, result.stdout);
513
+ errors.push({
514
+ taskName: result.job.taskName,
515
+ serviceName: result.job.serviceName,
516
+ exitCode: result.exitCode,
517
+ durationMs: result.durationMs,
518
+ message: errorMessage,
519
+ logSnippet,
520
+ errorType: result.errorType
521
+ });
522
+ }
523
+ }
524
+ errors.sort((a, b) => {
525
+ if (a.taskName !== b.taskName) {
526
+ return a.taskName.localeCompare(b.taskName);
527
+ }
528
+ return a.serviceName.localeCompare(b.serviceName);
529
+ });
530
+ return errors;
531
+ }
532
+ function getErrorMessage(result) {
533
+ if (result.errorType === "timeout") {
534
+ return `Timeout after ${result.durationMs}ms`;
535
+ }
536
+ if (result.errorType === "spawn") {
537
+ return `Command not found: ${result.job.command}`;
538
+ }
539
+ if (result.errorType === "signal") {
540
+ return `Killed by signal: ${result.signal}`;
541
+ }
542
+ return `Exit code ${result.exitCode}`;
543
+ }
544
+ function formatErrors(errors) {
545
+ if (errors.length === 0) {
546
+ return "";
547
+ }
548
+ const lines = ["", "Errors:"];
549
+ for (const error of errors) {
550
+ lines.push(`- ${error.serviceName} has error in ${error.taskName} test:`);
551
+ const snippet = error.logSnippet.trim();
552
+ if (snippet) {
553
+ const snippetLines = snippet.split("\n").slice(0, 10);
554
+ for (const line of snippetLines) {
555
+ lines.push(` ${line}`);
556
+ }
557
+ } else {
558
+ lines.push(` ${error.message}`);
559
+ }
560
+ lines.push("");
561
+ }
562
+ return lines.join("\n");
563
+ }
564
+
565
+ // src/cli.ts
566
+ var cli = cac("qa");
567
+ cli.command("run", "Run tasks across services").option("--tasks <tasks>", 'Comma-separated list of tasks or "all"', { default: "all" }).option("--services <services>", 'Comma-separated list of services or "all"', { default: "all" }).option("--concurrency <n>", "Maximum parallel jobs", { default: cpus().length }).option("--parallel", "Alias for concurrency").option("--fail-fast", "Stop on first error", { default: false }).option("--watch", "Watch mode with live updates", { default: false }).option("--format <format>", "Output format: table, minimal, json", { default: "table" }).option("--output <path>", "Save JSON output to file").option("--verbose", "Verbose output", { default: false }).option("--quiet", "Minimal output", { default: false }).option("--timeout <ms>", "Timeout per task in milliseconds").option("--no-color", "Disable colors").option("--config <path>", "Path to config file").action(async (options) => {
568
+ try {
569
+ const isTTY = process.stdout.isTTY;
570
+ const colorsEnabled2 = options.color !== false && isTTY;
571
+ setColorsEnabled(colorsEnabled2);
572
+ const config = await loadConfig(options.config);
573
+ const tasks = options.tasks === "all" ? ["all"] : options.tasks.split(",").map((t) => t.trim());
574
+ const services = options.services === "all" ? ["all"] : options.services.split(",").map((s) => s.trim());
575
+ const runOptions = {
576
+ tasks,
577
+ services,
578
+ concurrency: Number(options.concurrency),
579
+ failFast: options.failFast,
580
+ watch: options.watch && isTTY,
581
+ format: options.format,
582
+ output: options.output,
583
+ verbose: options.verbose,
584
+ quiet: options.quiet,
585
+ timeout: options.timeout ? Number(options.timeout) : void 0,
586
+ noColor: !colorsEnabled2
587
+ };
588
+ const jobs = generateJobs(config, tasks, services);
589
+ if (jobs.length === 0) {
590
+ console.error("No jobs to run");
591
+ process.exit(2);
592
+ }
593
+ const runner = new Runner(runOptions);
594
+ let watchRenderer = null;
595
+ if (runOptions.watch) {
596
+ watchRenderer = new WatchRenderer();
597
+ watchRenderer.start(runner.getState());
598
+ runner.on("jobStart", () => {
599
+ watchRenderer?.update(runner.getState());
600
+ });
601
+ runner.on("jobComplete", () => {
602
+ watchRenderer?.update(runner.getState());
603
+ });
604
+ runner.on("jobError", () => {
605
+ watchRenderer?.update(runner.getState());
606
+ });
607
+ }
608
+ const state = await runner.run(jobs);
609
+ if (watchRenderer) {
610
+ watchRenderer.stop();
611
+ }
612
+ if (!runOptions.quiet) {
613
+ let output;
614
+ if (runOptions.format === "json") {
615
+ output = renderJson(state);
616
+ } else if (runOptions.format === "minimal") {
617
+ output = renderMinimal(state);
618
+ } else {
619
+ output = renderTable(state);
620
+ }
621
+ console.log(output);
622
+ const errors = aggregateErrors(state);
623
+ if (errors.length > 0 && runOptions.format !== "json") {
624
+ console.log(formatErrors(errors));
625
+ }
626
+ }
627
+ if (options.output) {
628
+ const { writeFileSync } = await import("fs");
629
+ writeFileSync(options.output, renderJson(state), "utf-8");
630
+ }
631
+ const hasErrors = Array.from(state.jobs.values()).some((j) => j.status === "error");
632
+ process.exit(hasErrors ? 1 : 0);
633
+ } catch (error) {
634
+ console.error("Error:", error instanceof Error ? error.message : String(error));
635
+ process.exit(2);
636
+ }
637
+ });
638
+ cli.help();
639
+ cli.version("0.1.0");
640
+ cli.parse();
641
+ //# sourceMappingURL=cli.js.map