agent-gauntlet 0.1.10 → 0.1.11

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.
Files changed (48) hide show
  1. package/README.md +1 -1
  2. package/package.json +4 -2
  3. package/src/cli-adapters/claude.ts +139 -108
  4. package/src/cli-adapters/codex.ts +141 -117
  5. package/src/cli-adapters/cursor.ts +152 -0
  6. package/src/cli-adapters/gemini.ts +171 -139
  7. package/src/cli-adapters/github-copilot.ts +153 -0
  8. package/src/cli-adapters/index.ts +77 -48
  9. package/src/commands/check.test.ts +24 -20
  10. package/src/commands/check.ts +65 -59
  11. package/src/commands/detect.test.ts +38 -32
  12. package/src/commands/detect.ts +74 -61
  13. package/src/commands/health.test.ts +67 -53
  14. package/src/commands/health.ts +167 -145
  15. package/src/commands/help.test.ts +37 -37
  16. package/src/commands/help.ts +30 -22
  17. package/src/commands/index.ts +9 -9
  18. package/src/commands/init.test.ts +118 -107
  19. package/src/commands/init.ts +514 -417
  20. package/src/commands/list.test.ts +87 -70
  21. package/src/commands/list.ts +28 -24
  22. package/src/commands/rerun.ts +142 -119
  23. package/src/commands/review.test.ts +26 -20
  24. package/src/commands/review.ts +65 -59
  25. package/src/commands/run.test.ts +22 -20
  26. package/src/commands/run.ts +64 -58
  27. package/src/commands/shared.ts +44 -35
  28. package/src/config/loader.test.ts +112 -90
  29. package/src/config/loader.ts +132 -123
  30. package/src/config/schema.ts +49 -47
  31. package/src/config/types.ts +15 -13
  32. package/src/config/validator.ts +521 -454
  33. package/src/core/change-detector.ts +122 -104
  34. package/src/core/entry-point.test.ts +60 -62
  35. package/src/core/entry-point.ts +76 -67
  36. package/src/core/job.ts +69 -59
  37. package/src/core/runner.ts +261 -230
  38. package/src/gates/check.ts +78 -69
  39. package/src/gates/result.ts +7 -7
  40. package/src/gates/review.test.ts +174 -138
  41. package/src/gates/review.ts +716 -561
  42. package/src/index.ts +16 -15
  43. package/src/output/console.ts +253 -214
  44. package/src/output/logger.ts +64 -52
  45. package/src/templates/run_gauntlet.template.md +18 -0
  46. package/src/utils/diff-parser.ts +64 -62
  47. package/src/utils/log-parser.ts +227 -206
  48. package/src/utils/sanitizer.ts +1 -1
@@ -1,79 +1,108 @@
1
1
  export interface CLIAdapterHealth {
2
- available: boolean;
3
- status: 'healthy' | 'missing' | 'unhealthy';
4
- message?: string;
2
+ available: boolean;
3
+ status: "healthy" | "missing" | "unhealthy";
4
+ message?: string;
5
5
  }
6
6
 
7
7
  export function isUsageLimit(output: string): boolean {
8
- const lower = output.toLowerCase();
9
- return lower.includes('usage limit') ||
10
- lower.includes('quota exceeded') ||
11
- lower.includes('quota will reset') ||
12
- lower.includes('credit balance is too low') ||
13
- lower.includes('out of extra usage') ||
14
- lower.includes('out of usage');
8
+ const lower = output.toLowerCase();
9
+ return (
10
+ lower.includes("usage limit") ||
11
+ lower.includes("quota exceeded") ||
12
+ lower.includes("quota will reset") ||
13
+ lower.includes("credit balance is too low") ||
14
+ lower.includes("out of extra usage") ||
15
+ lower.includes("out of usage")
16
+ );
15
17
  }
16
18
 
17
19
  export interface CLIAdapter {
18
- name: string;
19
- isAvailable(): Promise<boolean>;
20
- checkHealth(options?: { checkUsageLimit?: boolean }): Promise<CLIAdapterHealth>;
21
- execute(opts: { prompt: string; diff: string; model?: string; timeoutMs?: number }): Promise<string>;
22
- /**
23
- * Returns the project-scoped command directory path (relative to project root).
24
- * Returns null if the CLI only supports user-level commands.
25
- */
26
- getProjectCommandDir(): string | null;
27
- /**
28
- * Returns the user-level command directory path (absolute path).
29
- * Returns null if the CLI doesn't support user-level commands.
30
- */
31
- getUserCommandDir(): string | null;
32
- /**
33
- * Returns the command file extension used by this CLI.
34
- */
35
- getCommandExtension(): string;
36
- /**
37
- * Returns true if this adapter can use symlinks (same format as source Markdown).
38
- */
39
- canUseSymlink(): boolean;
40
- /**
41
- * Transforms gauntlet command content to this CLI's format.
42
- * The source content is always Markdown with YAML frontmatter.
43
- */
44
- transformCommand(markdownContent: string): string;
20
+ name: string;
21
+ isAvailable(): Promise<boolean>;
22
+ checkHealth(options?: {
23
+ checkUsageLimit?: boolean;
24
+ }): Promise<CLIAdapterHealth>;
25
+ execute(opts: {
26
+ prompt: string;
27
+ diff: string;
28
+ model?: string;
29
+ timeoutMs?: number;
30
+ }): Promise<string>;
31
+ /**
32
+ * Returns the project-scoped command directory path (relative to project root).
33
+ * Returns null if the CLI only supports user-level commands.
34
+ */
35
+ getProjectCommandDir(): string | null;
36
+ /**
37
+ * Returns the user-level command directory path (absolute path).
38
+ * Returns null if the CLI doesn't support user-level commands.
39
+ */
40
+ getUserCommandDir(): string | null;
41
+ /**
42
+ * Returns the command file extension used by this CLI.
43
+ */
44
+ getCommandExtension(): string;
45
+ /**
46
+ * Returns true if this adapter can use symlinks (same format as source Markdown).
47
+ */
48
+ canUseSymlink(): boolean;
49
+ /**
50
+ * Transforms gauntlet command content to this CLI's format.
51
+ * The source content is always Markdown with YAML frontmatter.
52
+ */
53
+ transformCommand(markdownContent: string): string;
45
54
  }
46
55
 
47
- import { GeminiAdapter } from './gemini.js';
48
- import { CodexAdapter } from './codex.js';
49
- import { ClaudeAdapter } from './claude.js';
56
+ import { ClaudeAdapter } from "./claude.js";
57
+ import { CodexAdapter } from "./codex.js";
58
+ import { CursorAdapter } from "./cursor.js";
59
+ import { GeminiAdapter } from "./gemini.js";
60
+ import { GitHubCopilotAdapter } from "./github-copilot.js";
50
61
 
51
- export { GeminiAdapter, CodexAdapter, ClaudeAdapter };
62
+ export {
63
+ GeminiAdapter,
64
+ CodexAdapter,
65
+ ClaudeAdapter,
66
+ GitHubCopilotAdapter,
67
+ CursorAdapter,
68
+ };
52
69
 
70
+ // Adapter registry: keys should use lowercase with hyphens for multi-word names
53
71
  const adapters: Record<string, CLIAdapter> = {
54
- gemini: new GeminiAdapter(),
55
- codex: new CodexAdapter(),
56
- claude: new ClaudeAdapter(),
72
+ gemini: new GeminiAdapter(),
73
+ codex: new CodexAdapter(),
74
+ claude: new ClaudeAdapter(),
75
+ "github-copilot": new GitHubCopilotAdapter(),
76
+ cursor: new CursorAdapter(),
57
77
  };
58
78
 
59
79
  export function getAdapter(name: string): CLIAdapter | undefined {
60
- return adapters[name];
80
+ return adapters[name];
61
81
  }
62
82
 
63
83
  export function getAllAdapters(): CLIAdapter[] {
64
- return Object.values(adapters);
84
+ return Object.values(adapters);
65
85
  }
66
86
 
67
87
  /**
68
88
  * Returns all adapters that support project-scoped commands.
69
89
  */
70
90
  export function getProjectCommandAdapters(): CLIAdapter[] {
71
- return Object.values(adapters).filter(a => a.getProjectCommandDir() !== null);
91
+ return Object.values(adapters).filter(
92
+ (a) => a.getProjectCommandDir() !== null,
93
+ );
72
94
  }
73
95
 
74
96
  /**
75
97
  * Returns all adapters that support user-level commands.
76
98
  */
77
99
  export function getUserCommandAdapters(): CLIAdapter[] {
78
- return Object.values(adapters).filter(a => a.getUserCommandDir() !== null);
100
+ return Object.values(adapters).filter((a) => a.getUserCommandDir() !== null);
101
+ }
102
+
103
+ /**
104
+ * Returns all valid CLI tool names (adapter registry keys).
105
+ */
106
+ export function getValidCLITools(): string[] {
107
+ return Object.keys(adapters);
79
108
  }
@@ -1,25 +1,29 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
2
- import { Command } from 'commander';
3
- import { registerCheckCommand } from './check.js';
1
+ import { beforeEach, describe, expect, it } from "bun:test";
2
+ import { Command } from "commander";
3
+ import { registerCheckCommand } from "./check.js";
4
4
 
5
- describe('Check Command', () => {
6
- let program: Command;
5
+ describe("Check Command", () => {
6
+ let program: Command;
7
7
 
8
- beforeEach(() => {
9
- program = new Command();
10
- registerCheckCommand(program);
11
- });
8
+ beforeEach(() => {
9
+ program = new Command();
10
+ registerCheckCommand(program);
11
+ });
12
12
 
13
- it('should register the check command', () => {
14
- const checkCmd = program.commands.find(cmd => cmd.name() === 'check');
15
- expect(checkCmd).toBeDefined();
16
- expect(checkCmd?.description()).toBe('Run only applicable checks for detected changes');
17
- });
13
+ it("should register the check command", () => {
14
+ const checkCmd = program.commands.find((cmd) => cmd.name() === "check");
15
+ expect(checkCmd).toBeDefined();
16
+ expect(checkCmd?.description()).toBe(
17
+ "Run only applicable checks for detected changes",
18
+ );
19
+ });
18
20
 
19
- it('should have correct options', () => {
20
- const checkCmd = program.commands.find(cmd => cmd.name() === 'check');
21
- expect(checkCmd?.options.some(opt => opt.long === '--gate')).toBe(true);
22
- expect(checkCmd?.options.some(opt => opt.long === '--commit')).toBe(true);
23
- expect(checkCmd?.options.some(opt => opt.long === '--uncommitted')).toBe(true);
24
- });
21
+ it("should have correct options", () => {
22
+ const checkCmd = program.commands.find((cmd) => cmd.name() === "check");
23
+ expect(checkCmd?.options.some((opt) => opt.long === "--gate")).toBe(true);
24
+ expect(checkCmd?.options.some((opt) => opt.long === "--commit")).toBe(true);
25
+ expect(checkCmd?.options.some((opt) => opt.long === "--uncommitted")).toBe(
26
+ true,
27
+ );
28
+ });
25
29
  });
@@ -1,72 +1,78 @@
1
- import type { Command } from 'commander';
2
- import chalk from 'chalk';
3
- import { loadConfig } from '../config/loader.js';
4
- import { ChangeDetector } from '../core/change-detector.js';
5
- import { EntryPointExpander } from '../core/entry-point.js';
6
- import { JobGenerator } from '../core/job.js';
7
- import { Runner } from '../core/runner.js';
8
- import { Logger } from '../output/logger.js';
9
- import { ConsoleReporter } from '../output/console.js';
10
- import { rotateLogs } from './shared.js';
1
+ import chalk from "chalk";
2
+ import type { Command } from "commander";
3
+ import { loadConfig } from "../config/loader.js";
4
+ import { ChangeDetector } from "../core/change-detector.js";
5
+ import { EntryPointExpander } from "../core/entry-point.js";
6
+ import { JobGenerator } from "../core/job.js";
7
+ import { Runner } from "../core/runner.js";
8
+ import { ConsoleReporter } from "../output/console.js";
9
+ import { Logger } from "../output/logger.js";
10
+ import { rotateLogs } from "./shared.js";
11
11
 
12
12
  export function registerCheckCommand(program: Command): void {
13
- program
14
- .command('check')
15
- .description('Run only applicable checks for detected changes')
16
- .option('-g, --gate <name>', 'Run specific check gate only')
17
- .option('-c, --commit <sha>', 'Use diff for a specific commit')
18
- .option('-u, --uncommitted', 'Use diff for current uncommitted changes (staged and unstaged)')
19
- .action(async (options) => {
20
- try {
21
- const config = await loadConfig();
13
+ program
14
+ .command("check")
15
+ .description("Run only applicable checks for detected changes")
16
+ .option("-g, --gate <name>", "Run specific check gate only")
17
+ .option("-c, --commit <sha>", "Use diff for a specific commit")
18
+ .option(
19
+ "-u, --uncommitted",
20
+ "Use diff for current uncommitted changes (staged and unstaged)",
21
+ )
22
+ .action(async (options) => {
23
+ try {
24
+ const config = await loadConfig();
22
25
 
23
- // Rotate logs before starting
24
- await rotateLogs(config.project.log_dir);
26
+ // Rotate logs before starting
27
+ await rotateLogs(config.project.log_dir);
25
28
 
26
- const changeDetector = new ChangeDetector(config.project.base_branch, {
27
- commit: options.commit,
28
- uncommitted: options.uncommitted
29
- });
30
- const expander = new EntryPointExpander();
31
- const jobGen = new JobGenerator(config);
32
-
33
- console.log(chalk.dim('Detecting changes...'));
34
- const changes = await changeDetector.getChangedFiles();
35
-
36
- if (changes.length === 0) {
37
- console.log(chalk.green('No changes detected.'));
38
- process.exit(0);
39
- }
40
-
41
- console.log(chalk.dim(`Found ${changes.length} changed files.`));
29
+ const changeDetector = new ChangeDetector(config.project.base_branch, {
30
+ commit: options.commit,
31
+ uncommitted: options.uncommitted,
32
+ });
33
+ const expander = new EntryPointExpander();
34
+ const jobGen = new JobGenerator(config);
42
35
 
43
- const entryPoints = await expander.expand(config.project.entry_points, changes);
44
- let jobs = jobGen.generateJobs(entryPoints);
36
+ console.log(chalk.dim("Detecting changes..."));
37
+ const changes = await changeDetector.getChangedFiles();
45
38
 
46
- // Filter to only checks
47
- jobs = jobs.filter(j => j.type === 'check');
39
+ if (changes.length === 0) {
40
+ console.log(chalk.green("No changes detected."));
41
+ process.exit(0);
42
+ }
48
43
 
49
- if (options.gate) {
50
- jobs = jobs.filter(j => j.name === options.gate);
51
- }
44
+ console.log(chalk.dim(`Found ${changes.length} changed files.`));
52
45
 
53
- if (jobs.length === 0) {
54
- console.log(chalk.yellow('No applicable checks for these changes.'));
55
- process.exit(0);
56
- }
46
+ const entryPoints = await expander.expand(
47
+ config.project.entry_points,
48
+ changes,
49
+ );
50
+ let jobs = jobGen.generateJobs(entryPoints);
57
51
 
58
- console.log(chalk.dim(`Running ${jobs.length} check(s)...`));
52
+ // Filter to only checks
53
+ jobs = jobs.filter((j) => j.type === "check");
59
54
 
60
- const logger = new Logger(config.project.log_dir);
61
- const reporter = new ConsoleReporter();
62
- const runner = new Runner(config, logger, reporter);
55
+ if (options.gate) {
56
+ jobs = jobs.filter((j) => j.name === options.gate);
57
+ }
63
58
 
64
- const success = await runner.run(jobs);
65
- process.exit(success ? 0 : 1);
59
+ if (jobs.length === 0) {
60
+ console.log(chalk.yellow("No applicable checks for these changes."));
61
+ process.exit(0);
62
+ }
66
63
 
67
- } catch (error: any) {
68
- console.error(chalk.red('Error:'), error.message);
69
- process.exit(1);
70
- }
71
- });
64
+ console.log(chalk.dim(`Running ${jobs.length} check(s)...`));
65
+
66
+ const logger = new Logger(config.project.log_dir);
67
+ const reporter = new ConsoleReporter();
68
+ const runner = new Runner(config, logger, reporter);
69
+
70
+ const success = await runner.run(jobs);
71
+ process.exit(success ? 0 : 1);
72
+ } catch (error: unknown) {
73
+ const err = error as { message?: string };
74
+ console.error(chalk.red("Error:"), err.message);
75
+ process.exit(1);
76
+ }
77
+ });
72
78
  }
@@ -1,37 +1,43 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
2
- import { Command } from 'commander';
3
- import { registerDetectCommand } from './detect.js';
1
+ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
+ import { Command } from "commander";
3
+ import { registerDetectCommand } from "./detect.js";
4
4
 
5
- describe('Detect Command', () => {
6
- let program: Command;
7
- const originalConsoleLog = console.log;
8
- const originalConsoleError = console.error;
9
- let logs: string[];
10
- let errors: string[];
5
+ describe("Detect Command", () => {
6
+ let program: Command;
7
+ const originalConsoleLog = console.log;
8
+ const originalConsoleError = console.error;
9
+ let logs: string[];
10
+ let errors: string[];
11
11
 
12
- beforeEach(() => {
13
- program = new Command();
14
- registerDetectCommand(program);
15
- logs = [];
16
- errors = [];
17
- console.log = (...args: any[]) => {
18
- logs.push(args.join(' '));
19
- };
20
- console.error = (...args: any[]) => {
21
- errors.push(args.join(' '));
22
- };
23
- });
12
+ beforeEach(() => {
13
+ program = new Command();
14
+ registerDetectCommand(program);
15
+ logs = [];
16
+ errors = [];
17
+ console.log = (...args: unknown[]) => {
18
+ logs.push(args.join(" "));
19
+ };
20
+ console.error = (...args: unknown[]) => {
21
+ errors.push(args.join(" "));
22
+ };
23
+ });
24
24
 
25
- afterEach(() => {
26
- console.log = originalConsoleLog;
27
- console.error = originalConsoleError;
28
- });
25
+ afterEach(() => {
26
+ console.log = originalConsoleLog;
27
+ console.error = originalConsoleError;
28
+ });
29
29
 
30
- it('should register the detect command', () => {
31
- const detectCmd = program.commands.find(cmd => cmd.name() === 'detect');
32
- expect(detectCmd).toBeDefined();
33
- expect(detectCmd?.description()).toBe('Show what gates would run for detected changes (without executing them)');
34
- expect(detectCmd?.options.some(opt => opt.long === '--commit')).toBe(true);
35
- expect(detectCmd?.options.some(opt => opt.long === '--uncommitted')).toBe(true);
36
- });
30
+ it("should register the detect command", () => {
31
+ const detectCmd = program.commands.find((cmd) => cmd.name() === "detect");
32
+ expect(detectCmd).toBeDefined();
33
+ expect(detectCmd?.description()).toBe(
34
+ "Show what gates would run for detected changes (without executing them)",
35
+ );
36
+ expect(detectCmd?.options.some((opt) => opt.long === "--commit")).toBe(
37
+ true,
38
+ );
39
+ expect(detectCmd?.options.some((opt) => opt.long === "--uncommitted")).toBe(
40
+ true,
41
+ );
42
+ });
37
43
  });
@@ -1,69 +1,82 @@
1
- import type { Command } from 'commander';
2
- import chalk from 'chalk';
3
- import { loadConfig } from '../config/loader.js';
4
- import { ChangeDetector } from '../core/change-detector.js';
5
- import { EntryPointExpander } from '../core/entry-point.js';
6
- import { JobGenerator, type Job } from '../core/job.js';
1
+ import chalk from "chalk";
2
+ import type { Command } from "commander";
3
+ import { loadConfig } from "../config/loader.js";
4
+ import { ChangeDetector } from "../core/change-detector.js";
5
+ import { EntryPointExpander } from "../core/entry-point.js";
6
+ import { type Job, JobGenerator } from "../core/job.js";
7
7
 
8
8
  export function registerDetectCommand(program: Command): void {
9
- program
10
- .command('detect')
11
- .description('Show what gates would run for detected changes (without executing them)')
12
- .option('-c, --commit <sha>', 'Use diff for a specific commit')
13
- .option('-u, --uncommitted', 'Use diff for current uncommitted changes (staged and unstaged)')
14
- .action(async (options) => {
15
- try {
16
- const config = await loadConfig();
17
- const changeDetector = new ChangeDetector(config.project.base_branch, {
18
- commit: options.commit,
19
- uncommitted: options.uncommitted
20
- });
21
- const expander = new EntryPointExpander();
22
- const jobGen = new JobGenerator(config);
23
-
24
- console.log(chalk.dim('Detecting changes...'));
25
- const changes = await changeDetector.getChangedFiles();
26
-
27
- if (changes.length === 0) {
28
- console.log(chalk.green('No changes detected.'));
29
- return;
30
- }
31
-
32
- console.log(chalk.dim(`Found ${changes.length} changed files:`));
33
- changes.forEach(file => console.log(chalk.dim(` - ${file}`)));
34
- console.log();
9
+ program
10
+ .command("detect")
11
+ .description(
12
+ "Show what gates would run for detected changes (without executing them)",
13
+ )
14
+ .option("-c, --commit <sha>", "Use diff for a specific commit")
15
+ .option(
16
+ "-u, --uncommitted",
17
+ "Use diff for current uncommitted changes (staged and unstaged)",
18
+ )
19
+ .action(async (options) => {
20
+ try {
21
+ const config = await loadConfig();
22
+ const changeDetector = new ChangeDetector(config.project.base_branch, {
23
+ commit: options.commit,
24
+ uncommitted: options.uncommitted,
25
+ });
26
+ const expander = new EntryPointExpander();
27
+ const jobGen = new JobGenerator(config);
35
28
 
36
- const entryPoints = await expander.expand(config.project.entry_points, changes);
37
- const jobs = jobGen.generateJobs(entryPoints);
29
+ console.log(chalk.dim("Detecting changes..."));
30
+ const changes = await changeDetector.getChangedFiles();
38
31
 
39
- if (jobs.length === 0) {
40
- console.log(chalk.yellow('No applicable gates for these changes.'));
41
- return;
42
- }
32
+ if (changes.length === 0) {
33
+ console.log(chalk.green("No changes detected."));
34
+ return;
35
+ }
43
36
 
44
- console.log(chalk.bold(`Would run ${jobs.length} gate(s):\n`));
45
-
46
- // Group jobs by entry point for better display
47
- const jobsByEntryPoint = new Map<string, Job[]>();
48
- for (const job of jobs) {
49
- if (!jobsByEntryPoint.has(job.entryPoint)) {
50
- jobsByEntryPoint.set(job.entryPoint, []);
51
- }
52
- jobsByEntryPoint.get(job.entryPoint)!.push(job);
53
- }
37
+ console.log(chalk.dim(`Found ${changes.length} changed files:`));
38
+ changes.forEach((file) => {
39
+ console.log(chalk.dim(` - ${file}`));
40
+ });
41
+ console.log();
54
42
 
55
- for (const [entryPoint, entryJobs] of jobsByEntryPoint.entries()) {
56
- console.log(chalk.cyan(`Entry point: ${entryPoint}`));
57
- for (const job of entryJobs) {
58
- const typeLabel = job.type === 'check' ? chalk.yellow('check') : chalk.blue('review');
59
- console.log(` ${typeLabel} ${chalk.bold(job.name)}`);
60
- }
61
- console.log();
62
- }
43
+ const entryPoints = await expander.expand(
44
+ config.project.entry_points,
45
+ changes,
46
+ );
47
+ const jobs = jobGen.generateJobs(entryPoints);
63
48
 
64
- } catch (error: any) {
65
- console.error(chalk.red('Error:'), error.message);
66
- process.exit(1);
67
- }
68
- });
49
+ if (jobs.length === 0) {
50
+ console.log(chalk.yellow("No applicable gates for these changes."));
51
+ return;
52
+ }
53
+
54
+ console.log(chalk.bold(`Would run ${jobs.length} gate(s):\n`));
55
+
56
+ // Group jobs by entry point for better display
57
+ const jobsByEntryPoint = new Map<string, Job[]>();
58
+ for (const job of jobs) {
59
+ if (!jobsByEntryPoint.has(job.entryPoint)) {
60
+ jobsByEntryPoint.set(job.entryPoint, []);
61
+ }
62
+ jobsByEntryPoint.get(job.entryPoint)?.push(job);
63
+ }
64
+
65
+ for (const [entryPoint, entryJobs] of jobsByEntryPoint.entries()) {
66
+ console.log(chalk.cyan(`Entry point: ${entryPoint}`));
67
+ for (const job of entryJobs) {
68
+ const typeLabel =
69
+ job.type === "check"
70
+ ? chalk.yellow("check")
71
+ : chalk.blue("review");
72
+ console.log(` ${typeLabel} ${chalk.bold(job.name)}`);
73
+ }
74
+ console.log();
75
+ }
76
+ } catch (error: unknown) {
77
+ const err = error as { message?: string };
78
+ console.error(chalk.red("Error:"), err.message);
79
+ process.exit(1);
80
+ }
81
+ });
69
82
  }