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.
- package/README.md +55 -87
- package/package.json +4 -2
- package/src/bun-plugins.d.ts +4 -0
- package/src/cli-adapters/claude.ts +139 -108
- package/src/cli-adapters/codex.ts +141 -117
- package/src/cli-adapters/cursor.ts +152 -0
- package/src/cli-adapters/gemini.ts +171 -139
- package/src/cli-adapters/github-copilot.ts +153 -0
- package/src/cli-adapters/index.ts +77 -48
- package/src/commands/check.test.ts +24 -20
- package/src/commands/check.ts +86 -59
- package/src/commands/ci/index.ts +15 -0
- package/src/commands/ci/init.ts +96 -0
- package/src/commands/ci/list-jobs.ts +78 -0
- package/src/commands/detect.test.ts +38 -32
- package/src/commands/detect.ts +89 -61
- package/src/commands/health.test.ts +67 -53
- package/src/commands/health.ts +167 -145
- package/src/commands/help.test.ts +37 -37
- package/src/commands/help.ts +31 -22
- package/src/commands/index.ts +10 -9
- package/src/commands/init.test.ts +120 -107
- package/src/commands/init.ts +514 -417
- package/src/commands/list.test.ts +87 -70
- package/src/commands/list.ts +28 -24
- package/src/commands/rerun.ts +157 -119
- package/src/commands/review.test.ts +26 -20
- package/src/commands/review.ts +86 -59
- package/src/commands/run.test.ts +22 -20
- package/src/commands/run.ts +85 -58
- package/src/commands/shared.ts +44 -35
- package/src/config/ci-loader.ts +33 -0
- package/src/config/ci-schema.ts +52 -0
- package/src/config/loader.test.ts +112 -90
- package/src/config/loader.ts +132 -123
- package/src/config/schema.ts +48 -47
- package/src/config/types.ts +28 -13
- package/src/config/validator.ts +521 -454
- package/src/core/change-detector.ts +122 -104
- package/src/core/entry-point.test.ts +60 -62
- package/src/core/entry-point.ts +120 -74
- package/src/core/job.ts +69 -59
- package/src/core/runner.ts +264 -230
- package/src/gates/check.ts +78 -69
- package/src/gates/result.ts +7 -7
- package/src/gates/review.test.ts +277 -138
- package/src/gates/review.ts +724 -561
- package/src/index.ts +18 -15
- package/src/output/console.ts +253 -214
- package/src/output/logger.ts +66 -52
- package/src/templates/run_gauntlet.template.md +18 -0
- package/src/templates/workflow.yml +77 -0
- package/src/utils/diff-parser.ts +64 -62
- package/src/utils/log-parser.ts +227 -206
- package/src/utils/sanitizer.ts +1 -1
|
@@ -1,112 +1,130 @@
|
|
|
1
|
-
import { exec } from
|
|
2
|
-
import { promisify } from
|
|
1
|
+
import { exec } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
3
|
|
|
4
4
|
const execAsync = promisify(exec);
|
|
5
5
|
|
|
6
6
|
export interface ChangeDetectorOptions {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
commit?: string; // If provided, get diff for this commit vs its parent
|
|
8
|
+
uncommitted?: boolean; // If true, only get uncommitted changes (staged + unstaged)
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export class ChangeDetector {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
12
|
+
constructor(
|
|
13
|
+
private baseBranch: string = "origin/main",
|
|
14
|
+
private options: ChangeDetectorOptions = {},
|
|
15
|
+
) {}
|
|
16
|
+
|
|
17
|
+
async getChangedFiles(): Promise<string[]> {
|
|
18
|
+
// If commit option is provided, use that
|
|
19
|
+
if (this.options.commit) {
|
|
20
|
+
return this.getCommitChangedFiles(this.options.commit);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// If uncommitted option is provided, only get uncommitted changes
|
|
24
|
+
if (this.options.uncommitted) {
|
|
25
|
+
return this.getUncommittedChangedFiles();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const isCI =
|
|
29
|
+
process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true";
|
|
30
|
+
|
|
31
|
+
if (isCI) {
|
|
32
|
+
return this.getCIChangedFiles();
|
|
33
|
+
} else {
|
|
34
|
+
return this.getLocalChangedFiles();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private async getCIChangedFiles(): Promise<string[]> {
|
|
39
|
+
// In GitHub Actions, GITHUB_SHA is the commit being built
|
|
40
|
+
// Base branch priority is already resolved by caller
|
|
41
|
+
const baseRef = this.baseBranch;
|
|
42
|
+
const headRef = process.env.GITHUB_SHA || "HEAD";
|
|
43
|
+
|
|
44
|
+
// We might need to fetch first in some shallow clones, but assuming strictly for now
|
|
45
|
+
// git diff --name-only base...head
|
|
46
|
+
try {
|
|
47
|
+
const { stdout } = await execAsync(
|
|
48
|
+
`git diff --name-only ${baseRef}...${headRef}`,
|
|
49
|
+
);
|
|
50
|
+
return this.parseOutput(stdout);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.warn(
|
|
53
|
+
"Failed to detect changes via git diff in CI, falling back to HEAD^...HEAD",
|
|
54
|
+
error,
|
|
55
|
+
);
|
|
56
|
+
// Fallback for push events where base ref might not be available
|
|
57
|
+
const { stdout } = await execAsync("git diff --name-only HEAD^...HEAD");
|
|
58
|
+
return this.parseOutput(stdout);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private async getLocalChangedFiles(): Promise<string[]> {
|
|
63
|
+
// 1. Committed changes relative to base branch
|
|
64
|
+
const { stdout: committed } = await execAsync(
|
|
65
|
+
`git diff --name-only ${this.baseBranch}...HEAD`,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// 2. Uncommitted changes (staged and unstaged)
|
|
69
|
+
const { stdout: uncommitted } = await execAsync(
|
|
70
|
+
"git diff --name-only HEAD",
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// 3. Untracked files
|
|
74
|
+
const { stdout: untracked } = await execAsync(
|
|
75
|
+
"git ls-files --others --exclude-standard",
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const files = new Set([
|
|
79
|
+
...this.parseOutput(committed),
|
|
80
|
+
...this.parseOutput(uncommitted),
|
|
81
|
+
...this.parseOutput(untracked),
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
return Array.from(files);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private async getCommitChangedFiles(commit: string): Promise<string[]> {
|
|
88
|
+
// Get diff for commit vs its parent
|
|
89
|
+
try {
|
|
90
|
+
const { stdout } = await execAsync(
|
|
91
|
+
`git diff --name-only ${commit}^..${commit}`,
|
|
92
|
+
);
|
|
93
|
+
return this.parseOutput(stdout);
|
|
94
|
+
} catch (_error) {
|
|
95
|
+
// If commit has no parent (initial commit), just get files in that commit
|
|
96
|
+
try {
|
|
97
|
+
const { stdout } = await execAsync(
|
|
98
|
+
`git diff --name-only --root ${commit}`,
|
|
99
|
+
);
|
|
100
|
+
return this.parseOutput(stdout);
|
|
101
|
+
} catch {
|
|
102
|
+
throw new Error(`Failed to get changes for commit ${commit}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private async getUncommittedChangedFiles(): Promise<string[]> {
|
|
108
|
+
// Get uncommitted changes (staged + unstaged) and untracked files
|
|
109
|
+
const { stdout: staged } = await execAsync("git diff --name-only --cached");
|
|
110
|
+
const { stdout: unstaged } = await execAsync("git diff --name-only");
|
|
111
|
+
const { stdout: untracked } = await execAsync(
|
|
112
|
+
"git ls-files --others --exclude-standard",
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const files = new Set([
|
|
116
|
+
...this.parseOutput(staged),
|
|
117
|
+
...this.parseOutput(unstaged),
|
|
118
|
+
...this.parseOutput(untracked),
|
|
119
|
+
]);
|
|
120
|
+
|
|
121
|
+
return Array.from(files);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private parseOutput(stdout: string): string[] {
|
|
125
|
+
return stdout
|
|
126
|
+
.split("\n")
|
|
127
|
+
.map((line) => line.trim())
|
|
128
|
+
.filter((line) => line.length > 0);
|
|
129
|
+
}
|
|
112
130
|
}
|
|
@@ -1,63 +1,61 @@
|
|
|
1
|
-
import { describe,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
describe(
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
expect(result).toHaveLength(0);
|
|
62
|
-
});
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import type { EntryPointConfig } from "../config/types.js";
|
|
3
|
+
import { EntryPointExpander } from "./entry-point.js";
|
|
4
|
+
|
|
5
|
+
describe("EntryPointExpander", () => {
|
|
6
|
+
const expander = new EntryPointExpander();
|
|
7
|
+
|
|
8
|
+
it("should include root entry point if there are any changes", async () => {
|
|
9
|
+
const entryPoints: EntryPointConfig[] = [{ path: "." }];
|
|
10
|
+
const changes = ["some/file.ts"];
|
|
11
|
+
|
|
12
|
+
const result = await expander.expand(entryPoints, changes);
|
|
13
|
+
|
|
14
|
+
expect(result).toHaveLength(1);
|
|
15
|
+
expect(result[0].path).toBe(".");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should match fixed directory entry points", async () => {
|
|
19
|
+
const entryPoints: EntryPointConfig[] = [
|
|
20
|
+
{ path: "apps/api" },
|
|
21
|
+
{ path: "apps/web" },
|
|
22
|
+
];
|
|
23
|
+
const changes = ["apps/api/src/index.ts"];
|
|
24
|
+
|
|
25
|
+
const result = await expander.expand(entryPoints, changes);
|
|
26
|
+
|
|
27
|
+
// Result should have root (implicit or explicit fallback in code) + matched
|
|
28
|
+
// Looking at code: "if (changedFiles.length > 0) ... results.push({ path: '.', ... })"
|
|
29
|
+
// Wait, the code creates a default root config if one isn't provided in the list?
|
|
30
|
+
// Code: "const rootConfig = rootEntryPoint ?? { path: '.' }; results.push({ path: '.', config: rootConfig });"
|
|
31
|
+
// Yes, it always pushes root if changes > 0.
|
|
32
|
+
|
|
33
|
+
expect(result.some((r) => r.path === "apps/api")).toBe(true);
|
|
34
|
+
expect(result.some((r) => r.path === "apps/web")).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should match wildcard entry points", async () => {
|
|
38
|
+
const entryPoints: EntryPointConfig[] = [{ path: "packages/*" }];
|
|
39
|
+
const changes = [
|
|
40
|
+
"packages/ui/button.ts",
|
|
41
|
+
"packages/utils/helper.ts",
|
|
42
|
+
"other/file.ts",
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const result = await expander.expand(entryPoints, changes);
|
|
46
|
+
|
|
47
|
+
const paths = result.map((r) => r.path);
|
|
48
|
+
expect(paths).toContain("packages/ui");
|
|
49
|
+
expect(paths).toContain("packages/utils");
|
|
50
|
+
expect(paths).not.toContain("packages/other");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should handle no changes", async () => {
|
|
54
|
+
const entryPoints: EntryPointConfig[] = [{ path: "." }];
|
|
55
|
+
const changes: string[] = [];
|
|
56
|
+
|
|
57
|
+
const result = await expander.expand(entryPoints, changes);
|
|
58
|
+
|
|
59
|
+
expect(result).toHaveLength(0);
|
|
60
|
+
});
|
|
63
61
|
});
|
package/src/core/entry-point.ts
CHANGED
|
@@ -1,80 +1,126 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { EntryPointConfig } from "../config/types.js";
|
|
3
4
|
|
|
4
5
|
export interface ExpandedEntryPoint {
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
path: string; // The specific directory (e.g., "engines/billing")
|
|
7
|
+
config: EntryPointConfig; // The config that generated this (e.g., "engines/*")
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
export class EntryPointExpander {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
11
|
+
async expand(
|
|
12
|
+
entryPoints: EntryPointConfig[],
|
|
13
|
+
changedFiles: string[],
|
|
14
|
+
): Promise<ExpandedEntryPoint[]> {
|
|
15
|
+
const results: ExpandedEntryPoint[] = [];
|
|
16
|
+
const rootEntryPoint = entryPoints.find((ep) => ep.path === ".");
|
|
17
|
+
|
|
18
|
+
// Always include root entry point if configured and there are ANY changes
|
|
19
|
+
// Or should it only run if files match root patterns?
|
|
20
|
+
// Spec says: "A root entry point always exists and applies to repository-wide gates."
|
|
21
|
+
// Usually root gates run on any change or specific files in root.
|
|
22
|
+
// For simplicity, if root is configured, we'll include it if there are any changed files.
|
|
23
|
+
if (changedFiles.length > 0) {
|
|
24
|
+
const rootConfig = rootEntryPoint ?? { path: "." };
|
|
25
|
+
results.push({ path: ".", config: rootConfig });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (const ep of entryPoints) {
|
|
29
|
+
if (ep.path === ".") continue; // Handled above
|
|
30
|
+
|
|
31
|
+
if (ep.path.endsWith("*")) {
|
|
32
|
+
// Wildcard directory (e.g., "engines/*")
|
|
33
|
+
const parentDir = ep.path.slice(0, -2); // "engines"
|
|
34
|
+
const expandedPaths = await this.expandWildcard(
|
|
35
|
+
parentDir,
|
|
36
|
+
changedFiles,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
for (const subDir of expandedPaths) {
|
|
40
|
+
results.push({
|
|
41
|
+
path: subDir,
|
|
42
|
+
config: ep,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
// Fixed directory (e.g., "apps/api")
|
|
47
|
+
if (this.hasChangesInDir(ep.path, changedFiles)) {
|
|
48
|
+
results.push({
|
|
49
|
+
path: ep.path,
|
|
50
|
+
config: ep,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return results;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async expandAll(
|
|
60
|
+
entryPoints: EntryPointConfig[],
|
|
61
|
+
): Promise<ExpandedEntryPoint[]> {
|
|
62
|
+
const results: ExpandedEntryPoint[] = [];
|
|
63
|
+
|
|
64
|
+
for (const ep of entryPoints) {
|
|
65
|
+
if (ep.path === ".") {
|
|
66
|
+
results.push({ path: ".", config: ep });
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (ep.path.endsWith("*")) {
|
|
71
|
+
const parentDir = ep.path.slice(0, -2);
|
|
72
|
+
const subDirs = await this.listSubDirectories(parentDir);
|
|
73
|
+
for (const subDir of subDirs) {
|
|
74
|
+
results.push({ path: subDir, config: ep });
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
results.push({ path: ep.path, config: ep });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return results;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private async expandWildcard(
|
|
85
|
+
parentDir: string,
|
|
86
|
+
changedFiles: string[],
|
|
87
|
+
): Promise<string[]> {
|
|
88
|
+
const affectedSubDirs = new Set<string>();
|
|
89
|
+
|
|
90
|
+
// Filter changes that are inside this parent directory
|
|
91
|
+
const relevantChanges = changedFiles.filter((f) =>
|
|
92
|
+
f.startsWith(`${parentDir}/`),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
for (const file of relevantChanges) {
|
|
96
|
+
// file: "engines/billing/src/foo.ts", parentDir: "engines"
|
|
97
|
+
// relPath: "billing/src/foo.ts"
|
|
98
|
+
const relPath = file.slice(parentDir.length + 1);
|
|
99
|
+
const subDirName = relPath.split("/")[0];
|
|
100
|
+
|
|
101
|
+
if (subDirName) {
|
|
102
|
+
affectedSubDirs.add(path.join(parentDir, subDirName));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return Array.from(affectedSubDirs);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private async listSubDirectories(parentDir: string): Promise<string[]> {
|
|
110
|
+
try {
|
|
111
|
+
const dirents = await fs.readdir(parentDir, { withFileTypes: true });
|
|
112
|
+
return dirents
|
|
113
|
+
.filter((d) => d.isDirectory())
|
|
114
|
+
.map((d) => path.join(parentDir, d.name));
|
|
115
|
+
} catch {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private hasChangesInDir(dirPath: string, changedFiles: string[]): boolean {
|
|
121
|
+
// Check if any changed file starts with the dirPath
|
|
122
|
+
// Need to ensure exact match or subdirectory (e.g. "app" should not match "apple")
|
|
123
|
+
const dirPrefix = dirPath.endsWith("/") ? dirPath : `${dirPath}/`;
|
|
124
|
+
return changedFiles.some((f) => f === dirPath || f.startsWith(dirPrefix));
|
|
125
|
+
}
|
|
80
126
|
}
|