agent-gauntlet 0.1.9 → 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.
- package/README.md +1 -1
- package/package.json +4 -2
- 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 +65 -59
- package/src/commands/detect.test.ts +38 -32
- package/src/commands/detect.ts +74 -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 +30 -22
- package/src/commands/index.ts +9 -9
- package/src/commands/init.test.ts +118 -107
- package/src/commands/init.ts +515 -417
- package/src/commands/list.test.ts +87 -70
- package/src/commands/list.ts +28 -24
- package/src/commands/rerun.ts +142 -119
- package/src/commands/review.test.ts +26 -20
- package/src/commands/review.ts +65 -59
- package/src/commands/run.test.ts +22 -20
- package/src/commands/run.ts +64 -58
- package/src/commands/shared.ts +44 -35
- package/src/config/loader.test.ts +112 -90
- package/src/config/loader.ts +132 -123
- package/src/config/schema.ts +49 -47
- package/src/config/types.ts +15 -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 +76 -67
- package/src/core/job.ts +69 -59
- package/src/core/runner.ts +261 -221
- package/src/gates/check.ts +78 -69
- package/src/gates/result.ts +7 -6
- package/src/gates/review.test.ts +188 -0
- package/src/gates/review.ts +717 -506
- package/src/index.ts +16 -15
- package/src/output/console.ts +253 -198
- package/src/output/logger.ts +65 -51
- package/src/templates/run_gauntlet.template.md +18 -0
- package/src/utils/diff-parser.ts +64 -62
- package/src/utils/log-parser.ts +227 -206
- package/src/utils/sanitizer.ts +1 -1
package/src/commands/init.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import readline from "node:readline";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import type { Command } from "commander";
|
|
6
|
+
import { type CLIAdapter, getAllAdapters } from "../cli-adapters/index.js";
|
|
7
|
+
import { exists } from "./shared.js";
|
|
8
8
|
|
|
9
9
|
const MAX_PROMPT_ATTEMPTS = 10;
|
|
10
10
|
|
|
@@ -25,105 +25,119 @@ Execute the autonomous verification suite.
|
|
|
25
25
|
- All gates pass
|
|
26
26
|
- You disagree with remaining failures (ask the human how to proceed)
|
|
27
27
|
- Still failing after 3 rerun attempts
|
|
28
|
+
8. Once all gates pass, do NOT commit or push your changes—await the human's review and explicit instruction to commit.
|
|
28
29
|
`;
|
|
29
30
|
|
|
30
|
-
type InstallLevel =
|
|
31
|
+
type InstallLevel = "none" | "project" | "user";
|
|
31
32
|
|
|
32
33
|
interface InitOptions {
|
|
33
|
-
|
|
34
|
+
yes?: boolean;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
interface InitConfig {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
sourceDir: string;
|
|
39
|
+
lintCmd: string | null; // null means not selected, empty string means selected but blank (TODO)
|
|
40
|
+
testCmd: string | null; // null means not selected, empty string means selected but blank (TODO)
|
|
41
|
+
selectedAdapters: CLIAdapter[];
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
export function registerInitCommand(program: Command): void {
|
|
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
|
-
|
|
45
|
+
program
|
|
46
|
+
.command("init")
|
|
47
|
+
.description("Initialize .gauntlet configuration")
|
|
48
|
+
.option(
|
|
49
|
+
"-y, --yes",
|
|
50
|
+
"Skip prompts and use defaults (all available CLIs, source: ., no extra checks)",
|
|
51
|
+
)
|
|
52
|
+
.action(async (options: InitOptions) => {
|
|
53
|
+
const projectRoot = process.cwd();
|
|
54
|
+
const targetDir = path.join(projectRoot, ".gauntlet");
|
|
55
|
+
|
|
56
|
+
if (await exists(targetDir)) {
|
|
57
|
+
console.log(chalk.yellow(".gauntlet directory already exists."));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 1. CLI Detection
|
|
62
|
+
console.log("Detecting available CLI agents...");
|
|
63
|
+
const availableAdapters = await detectAvailableCLIs();
|
|
64
|
+
|
|
65
|
+
if (availableAdapters.length === 0) {
|
|
66
|
+
console.log();
|
|
67
|
+
console.log(
|
|
68
|
+
chalk.red("Error: No CLI agents found. Install at least one:"),
|
|
69
|
+
);
|
|
70
|
+
console.log(
|
|
71
|
+
" - Claude: https://docs.anthropic.com/en/docs/claude-code",
|
|
72
|
+
);
|
|
73
|
+
console.log(" - Gemini: https://github.com/google-gemini/gemini-cli");
|
|
74
|
+
console.log(" - Codex: https://github.com/openai/codex");
|
|
75
|
+
console.log();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let config: InitConfig;
|
|
80
|
+
|
|
81
|
+
if (options.yes) {
|
|
82
|
+
config = {
|
|
83
|
+
sourceDir: ".",
|
|
84
|
+
lintCmd: null,
|
|
85
|
+
testCmd: null,
|
|
86
|
+
selectedAdapters: availableAdapters,
|
|
87
|
+
};
|
|
88
|
+
} else {
|
|
89
|
+
config = await promptForConfig(availableAdapters);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Create base config structure
|
|
93
|
+
await fs.mkdir(targetDir);
|
|
94
|
+
await fs.mkdir(path.join(targetDir, "checks"));
|
|
95
|
+
await fs.mkdir(path.join(targetDir, "reviews"));
|
|
96
|
+
|
|
97
|
+
// 4. Commented Config Templates
|
|
98
|
+
// Generate config.yml
|
|
99
|
+
const configContent = generateConfigYml(config);
|
|
100
|
+
await fs.writeFile(path.join(targetDir, "config.yml"), configContent);
|
|
101
|
+
console.log(chalk.green("Created .gauntlet/config.yml"));
|
|
102
|
+
|
|
103
|
+
// Generate check files if selected
|
|
104
|
+
if (config.lintCmd !== null) {
|
|
105
|
+
const lintContent = `name: lint
|
|
106
|
+
command: ${config.lintCmd || "# command: TODO - add your lint command (e.g., npm run lint)"}
|
|
99
107
|
# parallel: false
|
|
100
108
|
# run_in_ci: true
|
|
101
109
|
# run_locally: true
|
|
102
110
|
# timeout: 300
|
|
103
111
|
`;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
112
|
+
await fs.writeFile(
|
|
113
|
+
path.join(targetDir, "checks", "lint.yml"),
|
|
114
|
+
lintContent,
|
|
115
|
+
);
|
|
116
|
+
console.log(chalk.green("Created .gauntlet/checks/lint.yml"));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (config.testCmd !== null) {
|
|
120
|
+
const testContent = `name: unit-tests
|
|
121
|
+
command: ${config.testCmd || "# command: TODO - add your test command (e.g., npm test)"}
|
|
111
122
|
# parallel: false
|
|
112
123
|
# run_in_ci: true
|
|
113
124
|
# run_locally: true
|
|
114
125
|
# timeout: 300
|
|
115
126
|
`;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
127
|
+
await fs.writeFile(
|
|
128
|
+
path.join(targetDir, "checks", "unit-tests.yml"),
|
|
129
|
+
testContent,
|
|
130
|
+
);
|
|
131
|
+
console.log(chalk.green("Created .gauntlet/checks/unit-tests.yml"));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 5. Improved Default Code Review Prompt
|
|
135
|
+
const reviewContent = `---
|
|
122
136
|
num_reviews: 1
|
|
123
137
|
# parallel: true
|
|
124
138
|
# timeout: 300
|
|
125
139
|
# cli_preference:
|
|
126
|
-
# - ${config.selectedAdapters[0]?.name ||
|
|
140
|
+
# - ${config.selectedAdapters[0]?.name || "claude"}
|
|
127
141
|
---
|
|
128
142
|
|
|
129
143
|
# Code Review
|
|
@@ -137,160 +151,191 @@ Review the diff for quality issues:
|
|
|
137
151
|
|
|
138
152
|
For each issue: cite file:line, explain the problem, suggest a fix.
|
|
139
153
|
`;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
154
|
+
await fs.writeFile(
|
|
155
|
+
path.join(targetDir, "reviews", "code-quality.md"),
|
|
156
|
+
reviewContent,
|
|
157
|
+
);
|
|
158
|
+
console.log(chalk.green("Created .gauntlet/reviews/code-quality.md"));
|
|
159
|
+
|
|
160
|
+
// Write the canonical gauntlet command file
|
|
161
|
+
const canonicalCommandPath = path.join(targetDir, "run_gauntlet.md");
|
|
162
|
+
await fs.writeFile(canonicalCommandPath, GAUNTLET_COMMAND_CONTENT);
|
|
163
|
+
console.log(chalk.green("Created .gauntlet/run_gauntlet.md"));
|
|
164
|
+
|
|
165
|
+
// Handle command installation
|
|
166
|
+
if (options.yes) {
|
|
167
|
+
// Default: install at project level for all selected agents (if they support it)
|
|
168
|
+
const adaptersToInstall = config.selectedAdapters.filter(
|
|
169
|
+
(a) => a.getProjectCommandDir() !== null,
|
|
170
|
+
);
|
|
171
|
+
if (adaptersToInstall.length > 0) {
|
|
172
|
+
await installCommands(
|
|
173
|
+
"project",
|
|
174
|
+
adaptersToInstall.map((a) => a.name),
|
|
175
|
+
projectRoot,
|
|
176
|
+
canonicalCommandPath,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
// Interactive prompts - passing available adapters to avoid re-checking or offering unavailable ones
|
|
181
|
+
await promptAndInstallCommands(
|
|
182
|
+
projectRoot,
|
|
183
|
+
canonicalCommandPath,
|
|
184
|
+
availableAdapters,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
160
188
|
}
|
|
161
189
|
|
|
162
190
|
async function detectAvailableCLIs(): Promise<CLIAdapter[]> {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
191
|
+
const allAdapters = getAllAdapters();
|
|
192
|
+
const available: CLIAdapter[] = [];
|
|
193
|
+
|
|
194
|
+
for (const adapter of allAdapters) {
|
|
195
|
+
const isAvailable = await adapter.isAvailable();
|
|
196
|
+
if (isAvailable) {
|
|
197
|
+
console.log(chalk.green(` ✓ ${adapter.name}`));
|
|
198
|
+
available.push(adapter);
|
|
199
|
+
} else {
|
|
200
|
+
console.log(chalk.dim(` ✗ ${adapter.name} (not installed)`));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return available;
|
|
176
204
|
}
|
|
177
205
|
|
|
178
|
-
async function promptForConfig(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
206
|
+
async function promptForConfig(
|
|
207
|
+
availableAdapters: CLIAdapter[],
|
|
208
|
+
): Promise<InitConfig> {
|
|
209
|
+
const rl = readline.createInterface({
|
|
210
|
+
input: process.stdin,
|
|
211
|
+
output: process.stdout,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const question = (prompt: string): Promise<string> => {
|
|
215
|
+
return new Promise((resolve) => {
|
|
216
|
+
rl.question(prompt, (answer) => {
|
|
217
|
+
resolve(answer?.trim() ?? "");
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
// CLI Selection
|
|
224
|
+
console.log();
|
|
225
|
+
console.log("Which CLIs would you like to use?");
|
|
226
|
+
availableAdapters.forEach((adapter, i) => {
|
|
227
|
+
console.log(` ${i + 1}) ${adapter.name}`);
|
|
228
|
+
});
|
|
229
|
+
console.log(` ${availableAdapters.length + 1}) All`);
|
|
230
|
+
|
|
231
|
+
let selectedAdapters: CLIAdapter[] = [];
|
|
232
|
+
let attempts = 0;
|
|
233
|
+
while (true) {
|
|
234
|
+
attempts++;
|
|
235
|
+
if (attempts > MAX_PROMPT_ATTEMPTS)
|
|
236
|
+
throw new Error("Too many invalid attempts");
|
|
237
|
+
const answer = await question(`(comma-separated, e.g., 1,2): `);
|
|
238
|
+
const selections = answer
|
|
239
|
+
.split(",")
|
|
240
|
+
.map((s) => s.trim())
|
|
241
|
+
.filter((s) => s);
|
|
242
|
+
|
|
243
|
+
if (selections.length === 0) {
|
|
244
|
+
// Default to all if empty? Or force selection? Plan says "Which CLIs...".
|
|
245
|
+
// Let's assume user must pick or we default to all if they just hit enter?
|
|
246
|
+
// Actually, usually enter means default. Let's make All the default if just Enter.
|
|
247
|
+
selectedAdapters = availableAdapters;
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let valid = true;
|
|
252
|
+
const chosen: CLIAdapter[] = [];
|
|
253
|
+
|
|
254
|
+
for (const sel of selections) {
|
|
255
|
+
const num = parseInt(sel, 10);
|
|
256
|
+
if (
|
|
257
|
+
Number.isNaN(num) ||
|
|
258
|
+
num < 1 ||
|
|
259
|
+
num > availableAdapters.length + 1
|
|
260
|
+
) {
|
|
261
|
+
console.log(chalk.yellow(`Invalid selection: ${sel}`));
|
|
262
|
+
valid = false;
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
if (num === availableAdapters.length + 1) {
|
|
266
|
+
chosen.push(...availableAdapters);
|
|
267
|
+
} else {
|
|
268
|
+
chosen.push(availableAdapters[num - 1]);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (valid) {
|
|
273
|
+
selectedAdapters = [...new Set(chosen)];
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Source Directory
|
|
279
|
+
console.log();
|
|
280
|
+
const sourceDirInput = await question(
|
|
281
|
+
"Enter your source directory (e.g., src, lib, .) [default: .]: ",
|
|
282
|
+
);
|
|
283
|
+
const sourceDir = sourceDirInput || ".";
|
|
284
|
+
|
|
285
|
+
// Lint Check
|
|
286
|
+
console.log();
|
|
287
|
+
const addLint = await question(
|
|
288
|
+
"Would you like to add a linting check? [y/N]: ",
|
|
289
|
+
);
|
|
290
|
+
let lintCmd: string | null = null;
|
|
291
|
+
if (addLint.toLowerCase().startsWith("y")) {
|
|
292
|
+
lintCmd = await question("Enter lint command (blank to fill later): ");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Unit Test Check
|
|
296
|
+
console.log();
|
|
297
|
+
const addTest = await question(
|
|
298
|
+
"Would you like to add a unit test check? [y/N]: ",
|
|
299
|
+
);
|
|
300
|
+
let testCmd: string | null = null;
|
|
301
|
+
if (addTest.toLowerCase().startsWith("y")) {
|
|
302
|
+
testCmd = await question("Enter test command (blank to fill later): ");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
rl.close();
|
|
306
|
+
return {
|
|
307
|
+
sourceDir,
|
|
308
|
+
lintCmd,
|
|
309
|
+
testCmd,
|
|
310
|
+
selectedAdapters,
|
|
311
|
+
};
|
|
312
|
+
} catch (error) {
|
|
313
|
+
rl.close();
|
|
314
|
+
throw error;
|
|
315
|
+
}
|
|
273
316
|
}
|
|
274
317
|
|
|
275
318
|
function generateConfigYml(config: InitConfig): string {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
319
|
+
const cliList = config.selectedAdapters
|
|
320
|
+
.map((a) => ` - ${a.name}`)
|
|
321
|
+
.join("\n");
|
|
322
|
+
|
|
323
|
+
let entryPoints = "";
|
|
324
|
+
|
|
325
|
+
// If we have checks, we need a source directory entry point
|
|
326
|
+
if (config.lintCmd !== null || config.testCmd !== null) {
|
|
327
|
+
entryPoints += ` - path: "${config.sourceDir}"
|
|
283
328
|
checks:\n`;
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
329
|
+
if (config.lintCmd !== null) entryPoints += ` - lint\n`;
|
|
330
|
+
if (config.testCmd !== null) entryPoints += ` - unit-tests\n`;
|
|
331
|
+
}
|
|
287
332
|
|
|
288
|
-
|
|
289
|
-
|
|
333
|
+
// Always include root entry point for reviews
|
|
334
|
+
entryPoints += ` - path: "."
|
|
290
335
|
reviews:
|
|
291
336
|
- code-quality`;
|
|
292
337
|
|
|
293
|
-
|
|
338
|
+
return `base_branch: origin/main
|
|
294
339
|
log_dir: .gauntlet_logs
|
|
295
340
|
|
|
296
341
|
# Run gates in parallel when possible (default: true)
|
|
@@ -307,202 +352,255 @@ ${entryPoints}
|
|
|
307
352
|
`;
|
|
308
353
|
}
|
|
309
354
|
|
|
310
|
-
async function promptAndInstallCommands(
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
355
|
+
async function promptAndInstallCommands(
|
|
356
|
+
projectRoot: string,
|
|
357
|
+
canonicalCommandPath: string,
|
|
358
|
+
availableAdapters: CLIAdapter[],
|
|
359
|
+
): Promise<void> {
|
|
360
|
+
// Only proceed if we have available adapters
|
|
361
|
+
if (availableAdapters.length === 0) return;
|
|
362
|
+
|
|
363
|
+
const rl = readline.createInterface({
|
|
364
|
+
input: process.stdin,
|
|
365
|
+
output: process.stdout,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const question = (prompt: string): Promise<string> => {
|
|
369
|
+
return new Promise((resolve) => {
|
|
370
|
+
rl.question(prompt, (answer) => {
|
|
371
|
+
resolve(answer?.trim() ?? "");
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
console.log();
|
|
378
|
+
console.log(chalk.bold("CLI Agent Command Setup"));
|
|
379
|
+
console.log(
|
|
380
|
+
chalk.dim(
|
|
381
|
+
"The gauntlet command can be installed for CLI agents so you can run /gauntlet directly.",
|
|
382
|
+
),
|
|
383
|
+
);
|
|
384
|
+
console.log();
|
|
385
|
+
|
|
386
|
+
// Question 1: Install level
|
|
387
|
+
console.log("Where would you like to install the /gauntlet command?");
|
|
388
|
+
console.log(" 1) Don't install commands");
|
|
389
|
+
console.log(
|
|
390
|
+
" 2) Project level (in this repo's .claude/commands, .gemini/commands, etc.)",
|
|
391
|
+
);
|
|
392
|
+
console.log(
|
|
393
|
+
" 3) User level (in ~/.claude/commands, ~/.gemini/commands, etc.)",
|
|
394
|
+
);
|
|
395
|
+
console.log();
|
|
396
|
+
|
|
397
|
+
let installLevel: InstallLevel = "none";
|
|
398
|
+
let answer = await question("Select option [1-3]: ");
|
|
399
|
+
let installLevelAttempts = 0;
|
|
400
|
+
|
|
401
|
+
while (true) {
|
|
402
|
+
installLevelAttempts++;
|
|
403
|
+
if (installLevelAttempts > MAX_PROMPT_ATTEMPTS)
|
|
404
|
+
throw new Error("Too many invalid attempts");
|
|
405
|
+
|
|
406
|
+
if (answer === "1") {
|
|
407
|
+
installLevel = "none";
|
|
408
|
+
break;
|
|
409
|
+
} else if (answer === "2") {
|
|
410
|
+
installLevel = "project";
|
|
411
|
+
break;
|
|
412
|
+
} else if (answer === "3") {
|
|
413
|
+
installLevel = "user";
|
|
414
|
+
break;
|
|
415
|
+
} else {
|
|
416
|
+
console.log(chalk.yellow("Please enter 1, 2, or 3"));
|
|
417
|
+
answer = await question("Select option [1-3]: ");
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (installLevel === "none") {
|
|
422
|
+
console.log(chalk.dim("\nSkipping command installation."));
|
|
423
|
+
rl.close();
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Filter available adapters based on install level support
|
|
428
|
+
const installableAdapters =
|
|
429
|
+
installLevel === "project"
|
|
430
|
+
? availableAdapters.filter((a) => a.getProjectCommandDir() !== null)
|
|
431
|
+
: availableAdapters.filter((a) => a.getUserCommandDir() !== null);
|
|
432
|
+
|
|
433
|
+
if (installableAdapters.length === 0) {
|
|
434
|
+
console.log(
|
|
435
|
+
chalk.yellow(
|
|
436
|
+
`No available agents support ${installLevel}-level commands.`,
|
|
437
|
+
),
|
|
438
|
+
);
|
|
439
|
+
rl.close();
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
console.log();
|
|
444
|
+
console.log("Which CLI agents would you like to install the command for?");
|
|
445
|
+
installableAdapters.forEach((adapter, i) => {
|
|
446
|
+
console.log(` ${i + 1}) ${adapter.name}`);
|
|
447
|
+
});
|
|
448
|
+
console.log(` ${installableAdapters.length + 1}) All of the above`);
|
|
449
|
+
console.log();
|
|
450
|
+
|
|
451
|
+
let selectedAgents: string[] = [];
|
|
452
|
+
answer = await question(
|
|
453
|
+
`Select options (comma-separated, e.g., 1,2 or ${installableAdapters.length + 1} for all): `,
|
|
454
|
+
);
|
|
455
|
+
let agentSelectionAttempts = 0;
|
|
456
|
+
|
|
457
|
+
while (true) {
|
|
458
|
+
agentSelectionAttempts++;
|
|
459
|
+
if (agentSelectionAttempts > MAX_PROMPT_ATTEMPTS)
|
|
460
|
+
throw new Error("Too many invalid attempts");
|
|
461
|
+
|
|
462
|
+
const selections = answer
|
|
463
|
+
.split(",")
|
|
464
|
+
.map((s) => s.trim())
|
|
465
|
+
.filter((s) => s);
|
|
466
|
+
|
|
467
|
+
if (selections.length === 0) {
|
|
468
|
+
console.log(chalk.yellow("Please select at least one option"));
|
|
469
|
+
answer = await question(
|
|
470
|
+
`Select options (comma-separated, e.g., 1,2 or ${installableAdapters.length + 1} for all): `,
|
|
471
|
+
);
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
let valid = true;
|
|
476
|
+
const agents: string[] = [];
|
|
477
|
+
|
|
478
|
+
for (const sel of selections) {
|
|
479
|
+
const num = parseInt(sel, 10);
|
|
480
|
+
if (
|
|
481
|
+
Number.isNaN(num) ||
|
|
482
|
+
num < 1 ||
|
|
483
|
+
num > installableAdapters.length + 1
|
|
484
|
+
) {
|
|
485
|
+
console.log(chalk.yellow(`Invalid selection: ${sel}`));
|
|
486
|
+
valid = false;
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
if (num === installableAdapters.length + 1) {
|
|
490
|
+
agents.push(...installableAdapters.map((a) => a.name));
|
|
491
|
+
} else {
|
|
492
|
+
agents.push(installableAdapters[num - 1].name);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (valid) {
|
|
497
|
+
selectedAgents = [...new Set(agents)]; // Dedupe
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
answer = await question(
|
|
501
|
+
`Select options (comma-separated, e.g., 1,2 or ${installableAdapters.length + 1} for all): `,
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
rl.close();
|
|
506
|
+
|
|
507
|
+
// Install commands
|
|
508
|
+
await installCommands(
|
|
509
|
+
installLevel,
|
|
510
|
+
selectedAgents,
|
|
511
|
+
projectRoot,
|
|
512
|
+
canonicalCommandPath,
|
|
513
|
+
);
|
|
514
|
+
} catch (error: unknown) {
|
|
515
|
+
rl.close();
|
|
516
|
+
throw error;
|
|
517
|
+
}
|
|
437
518
|
}
|
|
438
519
|
|
|
439
520
|
async function installCommands(
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
521
|
+
level: InstallLevel,
|
|
522
|
+
agentNames: string[],
|
|
523
|
+
projectRoot: string,
|
|
524
|
+
canonicalCommandPath: string,
|
|
444
525
|
): Promise<void> {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
526
|
+
if (level === "none" || agentNames.length === 0) {
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
console.log();
|
|
531
|
+
const allAdapters = getAllAdapters();
|
|
532
|
+
|
|
533
|
+
for (const agentName of agentNames) {
|
|
534
|
+
const adapter = allAdapters.find((a) => a.name === agentName);
|
|
535
|
+
if (!adapter) continue;
|
|
536
|
+
|
|
537
|
+
let commandDir: string | null;
|
|
538
|
+
let isUserLevel: boolean;
|
|
539
|
+
|
|
540
|
+
if (level === "project") {
|
|
541
|
+
commandDir = adapter.getProjectCommandDir();
|
|
542
|
+
isUserLevel = false;
|
|
543
|
+
if (commandDir) {
|
|
544
|
+
commandDir = path.join(projectRoot, commandDir);
|
|
545
|
+
}
|
|
546
|
+
} else {
|
|
547
|
+
commandDir = adapter.getUserCommandDir();
|
|
548
|
+
isUserLevel = true;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (!commandDir) {
|
|
552
|
+
// This shouldn't happen if we filtered correctly, but good safety check
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const commandFileName = `gauntlet${adapter.getCommandExtension()}`;
|
|
557
|
+
const commandFilePath = path.join(commandDir, commandFileName);
|
|
558
|
+
|
|
559
|
+
try {
|
|
560
|
+
// Ensure command directory exists
|
|
561
|
+
await fs.mkdir(commandDir, { recursive: true });
|
|
562
|
+
|
|
563
|
+
// Check if file already exists
|
|
564
|
+
if (await exists(commandFilePath)) {
|
|
565
|
+
const relPath = isUserLevel
|
|
566
|
+
? commandFilePath
|
|
567
|
+
: path.relative(projectRoot, commandFilePath);
|
|
568
|
+
console.log(
|
|
569
|
+
chalk.dim(` ${adapter.name}: ${relPath} already exists, skipping`),
|
|
570
|
+
);
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// For project-level with symlink support, create symlink
|
|
575
|
+
// For user-level or adapters that need transformation, write the file
|
|
576
|
+
if (!isUserLevel && adapter.canUseSymlink()) {
|
|
577
|
+
// Calculate relative path from command dir to canonical file
|
|
578
|
+
const relativePath = path.relative(commandDir, canonicalCommandPath);
|
|
579
|
+
await fs.symlink(relativePath, commandFilePath);
|
|
580
|
+
const relPath = path.relative(projectRoot, commandFilePath);
|
|
581
|
+
console.log(
|
|
582
|
+
chalk.green(
|
|
583
|
+
`Created ${relPath} (symlink to .gauntlet/run_gauntlet.md)`,
|
|
584
|
+
),
|
|
585
|
+
);
|
|
586
|
+
} else {
|
|
587
|
+
// Transform and write the command file
|
|
588
|
+
const transformedContent = adapter.transformCommand(
|
|
589
|
+
GAUNTLET_COMMAND_CONTENT,
|
|
590
|
+
);
|
|
591
|
+
await fs.writeFile(commandFilePath, transformedContent);
|
|
592
|
+
const relPath = isUserLevel
|
|
593
|
+
? commandFilePath
|
|
594
|
+
: path.relative(projectRoot, commandFilePath);
|
|
595
|
+
console.log(chalk.green(`Created ${relPath}`));
|
|
596
|
+
}
|
|
597
|
+
} catch (error: unknown) {
|
|
598
|
+
const err = error as { message?: string };
|
|
599
|
+
console.log(
|
|
600
|
+
chalk.yellow(
|
|
601
|
+
` ${adapter.name}: Could not create command - ${err.message}`,
|
|
602
|
+
),
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
508
606
|
}
|