agent-gauntlet 0.1.10 → 0.1.12

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 (55) hide show
  1. package/README.md +55 -87
  2. package/package.json +4 -2
  3. package/src/bun-plugins.d.ts +4 -0
  4. package/src/cli-adapters/claude.ts +139 -108
  5. package/src/cli-adapters/codex.ts +141 -117
  6. package/src/cli-adapters/cursor.ts +152 -0
  7. package/src/cli-adapters/gemini.ts +171 -139
  8. package/src/cli-adapters/github-copilot.ts +153 -0
  9. package/src/cli-adapters/index.ts +77 -48
  10. package/src/commands/check.test.ts +24 -20
  11. package/src/commands/check.ts +86 -59
  12. package/src/commands/ci/index.ts +15 -0
  13. package/src/commands/ci/init.ts +96 -0
  14. package/src/commands/ci/list-jobs.ts +78 -0
  15. package/src/commands/detect.test.ts +38 -32
  16. package/src/commands/detect.ts +89 -61
  17. package/src/commands/health.test.ts +67 -53
  18. package/src/commands/health.ts +167 -145
  19. package/src/commands/help.test.ts +37 -37
  20. package/src/commands/help.ts +31 -22
  21. package/src/commands/index.ts +10 -9
  22. package/src/commands/init.test.ts +120 -107
  23. package/src/commands/init.ts +514 -417
  24. package/src/commands/list.test.ts +87 -70
  25. package/src/commands/list.ts +28 -24
  26. package/src/commands/rerun.ts +157 -119
  27. package/src/commands/review.test.ts +26 -20
  28. package/src/commands/review.ts +86 -59
  29. package/src/commands/run.test.ts +22 -20
  30. package/src/commands/run.ts +85 -58
  31. package/src/commands/shared.ts +44 -35
  32. package/src/config/ci-loader.ts +33 -0
  33. package/src/config/ci-schema.ts +52 -0
  34. package/src/config/loader.test.ts +112 -90
  35. package/src/config/loader.ts +132 -123
  36. package/src/config/schema.ts +48 -47
  37. package/src/config/types.ts +28 -13
  38. package/src/config/validator.ts +521 -454
  39. package/src/core/change-detector.ts +122 -104
  40. package/src/core/entry-point.test.ts +60 -62
  41. package/src/core/entry-point.ts +120 -74
  42. package/src/core/job.ts +69 -59
  43. package/src/core/runner.ts +264 -230
  44. package/src/gates/check.ts +78 -69
  45. package/src/gates/result.ts +7 -7
  46. package/src/gates/review.test.ts +277 -138
  47. package/src/gates/review.ts +724 -561
  48. package/src/index.ts +18 -15
  49. package/src/output/console.ts +253 -214
  50. package/src/output/logger.ts +66 -52
  51. package/src/templates/run_gauntlet.template.md +18 -0
  52. package/src/templates/workflow.yml +77 -0
  53. package/src/utils/diff-parser.ts +64 -62
  54. package/src/utils/log-parser.ts +227 -206
  55. package/src/utils/sanitizer.ts +1 -1
@@ -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,99 @@
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(
17
+ "-b, --base-branch <branch>",
18
+ "Override base branch for change detection",
19
+ )
20
+ .option("-g, --gate <name>", "Run specific check gate only")
21
+ .option("-c, --commit <sha>", "Use diff for a specific commit")
22
+ .option(
23
+ "-u, --uncommitted",
24
+ "Use diff for current uncommitted changes (staged and unstaged)",
25
+ )
26
+ .action(async (options) => {
27
+ try {
28
+ const config = await loadConfig();
22
29
 
23
- // Rotate logs before starting
24
- await rotateLogs(config.project.log_dir);
30
+ // Rotate logs before starting
31
+ await rotateLogs(config.project.log_dir);
25
32
 
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.`));
33
+ // Determine effective base branch
34
+ // Priority: CLI override > CI env var > config
35
+ const effectiveBaseBranch =
36
+ options.baseBranch ||
37
+ (process.env.GITHUB_BASE_REF &&
38
+ (process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true")
39
+ ? process.env.GITHUB_BASE_REF
40
+ : null) ||
41
+ config.project.base_branch;
42
42
 
43
- const entryPoints = await expander.expand(config.project.entry_points, changes);
44
- let jobs = jobGen.generateJobs(entryPoints);
43
+ const changeDetector = new ChangeDetector(effectiveBaseBranch, {
44
+ commit: options.commit,
45
+ uncommitted: options.uncommitted,
46
+ });
47
+ const expander = new EntryPointExpander();
48
+ const jobGen = new JobGenerator(config);
45
49
 
46
- // Filter to only checks
47
- jobs = jobs.filter(j => j.type === 'check');
50
+ console.log(chalk.dim("Detecting changes..."));
51
+ const changes = await changeDetector.getChangedFiles();
48
52
 
49
- if (options.gate) {
50
- jobs = jobs.filter(j => j.name === options.gate);
51
- }
53
+ if (changes.length === 0) {
54
+ console.log(chalk.green("No changes detected."));
55
+ process.exit(0);
56
+ }
52
57
 
53
- if (jobs.length === 0) {
54
- console.log(chalk.yellow('No applicable checks for these changes.'));
55
- process.exit(0);
56
- }
58
+ console.log(chalk.dim(`Found ${changes.length} changed files.`));
57
59
 
58
- console.log(chalk.dim(`Running ${jobs.length} check(s)...`));
60
+ const entryPoints = await expander.expand(
61
+ config.project.entry_points,
62
+ changes,
63
+ );
64
+ let jobs = jobGen.generateJobs(entryPoints);
59
65
 
60
- const logger = new Logger(config.project.log_dir);
61
- const reporter = new ConsoleReporter();
62
- const runner = new Runner(config, logger, reporter);
66
+ // Filter to only checks
67
+ jobs = jobs.filter((j) => j.type === "check");
63
68
 
64
- const success = await runner.run(jobs);
65
- process.exit(success ? 0 : 1);
69
+ if (options.gate) {
70
+ jobs = jobs.filter((j) => j.name === options.gate);
71
+ }
66
72
 
67
- } catch (error: any) {
68
- console.error(chalk.red('Error:'), error.message);
69
- process.exit(1);
70
- }
71
- });
73
+ if (jobs.length === 0) {
74
+ console.log(chalk.yellow("No applicable checks for these changes."));
75
+ process.exit(0);
76
+ }
77
+
78
+ console.log(chalk.dim(`Running ${jobs.length} check(s)...`));
79
+
80
+ const logger = new Logger(config.project.log_dir);
81
+ const reporter = new ConsoleReporter();
82
+ const runner = new Runner(
83
+ config,
84
+ logger,
85
+ reporter,
86
+ undefined,
87
+ undefined,
88
+ effectiveBaseBranch,
89
+ );
90
+
91
+ const success = await runner.run(jobs);
92
+ process.exit(success ? 0 : 1);
93
+ } catch (error: unknown) {
94
+ const err = error as { message?: string };
95
+ console.error(chalk.red("Error:"), err.message);
96
+ process.exit(1);
97
+ }
98
+ });
72
99
  }
@@ -0,0 +1,15 @@
1
+ import type { Command } from "commander";
2
+ import { initCI } from "./init.js";
3
+ import { listJobs } from "./list-jobs.js";
4
+
5
+ export function registerCICommand(program: Command): void {
6
+ const ci = program.command("ci").description("Manage CI integration");
7
+
8
+ ci.command("init")
9
+ .description("Initialize CI workflow and configuration")
10
+ .action(initCI);
11
+
12
+ ci.command("list-jobs")
13
+ .description("List CI jobs (used by workflow)")
14
+ .action(listJobs);
15
+ }
@@ -0,0 +1,96 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import chalk from "chalk";
4
+ import YAML from "yaml";
5
+ import { loadCIConfig } from "../../config/ci-loader.js";
6
+ import type { CIConfig } from "../../config/types.js";
7
+ import workflowTemplate from "../../templates/workflow.yml" with {
8
+ type: "text",
9
+ };
10
+
11
+ export async function initCI(): Promise<void> {
12
+ const workflowDir = path.join(process.cwd(), ".github", "workflows");
13
+ const workflowPath = path.join(workflowDir, "gauntlet.yml");
14
+ const gauntletDir = path.join(process.cwd(), ".gauntlet");
15
+ const ciConfigPath = path.join(gauntletDir, "ci.yml");
16
+
17
+ // 1. Ensure .gauntlet/ci.yml exists
18
+ if (!(await fileExists(ciConfigPath))) {
19
+ console.log(chalk.yellow("Creating starter .gauntlet/ci.yml..."));
20
+ await fs.mkdir(gauntletDir, { recursive: true });
21
+ const starterContent = `# CI Configuration for Agent Gauntlet
22
+ # Define runtimes, services, and which checks to run in CI.
23
+
24
+ runtimes:
25
+ # ruby:
26
+ # version: "3.3"
27
+ # bundler_cache: true
28
+
29
+ services:
30
+ # postgres:
31
+ # image: postgres:16
32
+ # ports: ["5432:5432"]
33
+
34
+ setup:
35
+ # - name: Global Setup
36
+ # run: echo "Setting up..."
37
+
38
+ checks:
39
+ # - name: linter
40
+ # requires_runtimes: [ruby]
41
+ `;
42
+ await fs.writeFile(ciConfigPath, starterContent);
43
+ } else {
44
+ console.log(chalk.dim("Found existing .gauntlet/ci.yml"));
45
+ }
46
+
47
+ // 2. Load CI config to get services
48
+ let ciConfig: CIConfig | undefined;
49
+ try {
50
+ ciConfig = await loadCIConfig();
51
+ } catch (_e) {
52
+ console.warn(
53
+ chalk.yellow(
54
+ "Could not load CI config to inject services. Workflow will have no services defined.",
55
+ ),
56
+ );
57
+ }
58
+
59
+ // 3. Generate workflow file
60
+ console.log(chalk.dim(`Generating ${workflowPath}...`));
61
+ await fs.mkdir(workflowDir, { recursive: true });
62
+
63
+ let templateContent = workflowTemplate;
64
+
65
+ // Inject services
66
+ if (ciConfig?.services && Object.keys(ciConfig.services).length > 0) {
67
+ const servicesYaml = YAML.stringify({ services: ciConfig.services });
68
+ // Indent services
69
+ const indentedServices = servicesYaml
70
+ .split("\n")
71
+ .map((line) => (line.trim() ? ` ${line}` : line))
72
+ .join("\n");
73
+
74
+ templateContent = templateContent.replace(
75
+ "# Services will be injected here by agent-gauntlet",
76
+ indentedServices,
77
+ );
78
+ } else {
79
+ templateContent = templateContent.replace(
80
+ " # Services will be injected here by agent-gauntlet\n",
81
+ "",
82
+ );
83
+ }
84
+
85
+ await fs.writeFile(workflowPath, templateContent);
86
+ console.log(chalk.green("Successfully generated GitHub Actions workflow!"));
87
+ }
88
+
89
+ async function fileExists(path: string): Promise<boolean> {
90
+ try {
91
+ const stat = await fs.stat(path);
92
+ return stat.isFile();
93
+ } catch {
94
+ return false;
95
+ }
96
+ }
@@ -0,0 +1,78 @@
1
+ import { loadCIConfig } from "../../config/ci-loader.js";
2
+ import { loadConfig } from "../../config/loader.js";
3
+ import type { CISetupStep } from "../../config/types.js";
4
+ import { EntryPointExpander } from "../../core/entry-point.js";
5
+
6
+ export async function listJobs(): Promise<void> {
7
+ try {
8
+ const config = await loadConfig();
9
+ const ciConfig = await loadCIConfig();
10
+ const expander = new EntryPointExpander();
11
+ const expandedEntryPoints = await expander.expandAll(
12
+ config.project.entry_points,
13
+ );
14
+
15
+ const matrixJobs = [];
16
+
17
+ const globalSetup = formatSetup(ciConfig.setup || undefined);
18
+
19
+ if (ciConfig.checks) {
20
+ for (const ep of expandedEntryPoints) {
21
+ // Get checks enabled for this entry point
22
+ const allowedChecks = new Set(ep.config.checks || []);
23
+
24
+ for (const check of ciConfig.checks) {
25
+ if (allowedChecks.has(check.name)) {
26
+ // Check definition from .gauntlet/checks/*.yml
27
+ const checkDef = config.checks[check.name];
28
+ if (!checkDef) {
29
+ console.warn(
30
+ `Warning: Check '${check.name}' found in CI config but not defined in checks/*.yml`,
31
+ );
32
+ continue;
33
+ }
34
+
35
+ const id = `${check.name}-${ep.path.replace(/\//g, "-")}`;
36
+
37
+ matrixJobs.push({
38
+ id,
39
+ name: check.name,
40
+ entry_point: ep.path,
41
+ working_directory: checkDef.working_directory || ep.path,
42
+ command: checkDef.command,
43
+ runtimes: check.requires_runtimes || [],
44
+ services: check.requires_services || [],
45
+ setup: formatSetup(check.setup || undefined),
46
+ global_setup: globalSetup,
47
+ });
48
+ }
49
+ }
50
+ }
51
+ }
52
+
53
+ const output = {
54
+ matrix: matrixJobs,
55
+ services: ciConfig.services || {},
56
+ runtimes: ciConfig.runtimes || {},
57
+ };
58
+
59
+ console.log(JSON.stringify(output));
60
+ } catch (e) {
61
+ console.error("Error generating CI jobs:", e);
62
+ process.exit(1);
63
+ }
64
+ }
65
+
66
+ const formatSetup = (steps: CISetupStep[] | null | undefined): string => {
67
+ if (!steps || steps.length === 0) return "";
68
+ return steps
69
+ .map((s) => {
70
+ const cmd = s.working_directory
71
+ ? `(cd "${s.working_directory}" && ${s.run})`
72
+ : s.run;
73
+ return `echo "::group::${s.name}"
74
+ ${cmd}
75
+ echo "::endgroup::"`;
76
+ })
77
+ .join("\n");
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,97 @@
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(
15
+ "-b, --base-branch <branch>",
16
+ "Override base branch for change detection",
17
+ )
18
+ .option("-c, --commit <sha>", "Use diff for a specific commit")
19
+ .option(
20
+ "-u, --uncommitted",
21
+ "Use diff for current uncommitted changes (staged and unstaged)",
22
+ )
23
+ .action(async (options) => {
24
+ try {
25
+ const config = await loadConfig();
35
26
 
36
- const entryPoints = await expander.expand(config.project.entry_points, changes);
37
- const jobs = jobGen.generateJobs(entryPoints);
27
+ // Determine effective base branch
28
+ // Priority: CLI override > CI env var > config
29
+ const effectiveBaseBranch =
30
+ options.baseBranch ||
31
+ (process.env.GITHUB_BASE_REF &&
32
+ (process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true")
33
+ ? process.env.GITHUB_BASE_REF
34
+ : null) ||
35
+ config.project.base_branch;
38
36
 
39
- if (jobs.length === 0) {
40
- console.log(chalk.yellow('No applicable gates for these changes.'));
41
- return;
42
- }
37
+ const changeDetector = new ChangeDetector(effectiveBaseBranch, {
38
+ commit: options.commit,
39
+ uncommitted: options.uncommitted,
40
+ });
41
+ const expander = new EntryPointExpander();
42
+ const jobGen = new JobGenerator(config);
43
43
 
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
- }
44
+ console.log(chalk.dim("Detecting changes..."));
45
+ const changes = await changeDetector.getChangedFiles();
54
46
 
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
- }
47
+ if (changes.length === 0) {
48
+ console.log(chalk.green("No changes detected."));
49
+ return;
50
+ }
63
51
 
64
- } catch (error: any) {
65
- console.error(chalk.red('Error:'), error.message);
66
- process.exit(1);
67
- }
68
- });
52
+ console.log(chalk.dim(`Found ${changes.length} changed files:`));
53
+ changes.forEach((file) => {
54
+ console.log(chalk.dim(` - ${file}`));
55
+ });
56
+ console.log();
57
+
58
+ const entryPoints = await expander.expand(
59
+ config.project.entry_points,
60
+ changes,
61
+ );
62
+ const jobs = jobGen.generateJobs(entryPoints);
63
+
64
+ if (jobs.length === 0) {
65
+ console.log(chalk.yellow("No applicable gates for these changes."));
66
+ return;
67
+ }
68
+
69
+ console.log(chalk.bold(`Would run ${jobs.length} gate(s):\n`));
70
+
71
+ // Group jobs by entry point for better display
72
+ const jobsByEntryPoint = new Map<string, Job[]>();
73
+ for (const job of jobs) {
74
+ if (!jobsByEntryPoint.has(job.entryPoint)) {
75
+ jobsByEntryPoint.set(job.entryPoint, []);
76
+ }
77
+ jobsByEntryPoint.get(job.entryPoint)?.push(job);
78
+ }
79
+
80
+ for (const [entryPoint, entryJobs] of jobsByEntryPoint.entries()) {
81
+ console.log(chalk.cyan(`Entry point: ${entryPoint}`));
82
+ for (const job of entryJobs) {
83
+ const typeLabel =
84
+ job.type === "check"
85
+ ? chalk.yellow("check")
86
+ : chalk.blue("review");
87
+ console.log(` ${typeLabel} ${chalk.bold(job.name)}`);
88
+ }
89
+ console.log();
90
+ }
91
+ } catch (error: unknown) {
92
+ const err = error as { message?: string };
93
+ console.error(chalk.red("Error:"), err.message);
94
+ process.exit(1);
95
+ }
96
+ });
69
97
  }