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
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
|
|
|
@@ -28,103 +28,116 @@ Execute the autonomous verification suite.
|
|
|
28
28
|
8. Once all gates pass, do NOT commit or push your changes—await the human's review and explicit instruction to commit.
|
|
29
29
|
`;
|
|
30
30
|
|
|
31
|
-
type InstallLevel =
|
|
31
|
+
type InstallLevel = "none" | "project" | "user";
|
|
32
32
|
|
|
33
33
|
interface InitOptions {
|
|
34
|
-
|
|
34
|
+
yes?: boolean;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
interface InitConfig {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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[];
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
export function registerInitCommand(program: Command): void {
|
|
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
|
-
|
|
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)"}
|
|
100
107
|
# parallel: false
|
|
101
108
|
# run_in_ci: true
|
|
102
109
|
# run_locally: true
|
|
103
110
|
# timeout: 300
|
|
104
111
|
`;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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)"}
|
|
112
122
|
# parallel: false
|
|
113
123
|
# run_in_ci: true
|
|
114
124
|
# run_locally: true
|
|
115
125
|
# timeout: 300
|
|
116
126
|
`;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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 = `---
|
|
123
136
|
num_reviews: 1
|
|
124
137
|
# parallel: true
|
|
125
138
|
# timeout: 300
|
|
126
139
|
# cli_preference:
|
|
127
|
-
# - ${config.selectedAdapters[0]?.name ||
|
|
140
|
+
# - ${config.selectedAdapters[0]?.name || "claude"}
|
|
128
141
|
---
|
|
129
142
|
|
|
130
143
|
# Code Review
|
|
@@ -138,160 +151,191 @@ Review the diff for quality issues:
|
|
|
138
151
|
|
|
139
152
|
For each issue: cite file:line, explain the problem, suggest a fix.
|
|
140
153
|
`;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
+
});
|
|
161
188
|
}
|
|
162
189
|
|
|
163
190
|
async function detectAvailableCLIs(): Promise<CLIAdapter[]> {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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;
|
|
177
204
|
}
|
|
178
205
|
|
|
179
|
-
async function promptForConfig(
|
|
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
|
-
|
|
273
|
-
|
|
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
|
+
}
|
|
274
316
|
}
|
|
275
317
|
|
|
276
318
|
function generateConfigYml(config: InitConfig): string {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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}"
|
|
284
328
|
checks:\n`;
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
329
|
+
if (config.lintCmd !== null) entryPoints += ` - lint\n`;
|
|
330
|
+
if (config.testCmd !== null) entryPoints += ` - unit-tests\n`;
|
|
331
|
+
}
|
|
288
332
|
|
|
289
|
-
|
|
290
|
-
|
|
333
|
+
// Always include root entry point for reviews
|
|
334
|
+
entryPoints += ` - path: "."
|
|
291
335
|
reviews:
|
|
292
336
|
- code-quality`;
|
|
293
337
|
|
|
294
|
-
|
|
338
|
+
return `base_branch: origin/main
|
|
295
339
|
log_dir: .gauntlet_logs
|
|
296
340
|
|
|
297
341
|
# Run gates in parallel when possible (default: true)
|
|
@@ -308,202 +352,255 @@ ${entryPoints}
|
|
|
308
352
|
`;
|
|
309
353
|
}
|
|
310
354
|
|
|
311
|
-
async function promptAndInstallCommands(
|
|
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
|
-
|
|
437
|
-
|
|
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
|
+
}
|
|
438
518
|
}
|
|
439
519
|
|
|
440
520
|
async function installCommands(
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
521
|
+
level: InstallLevel,
|
|
522
|
+
agentNames: string[],
|
|
523
|
+
projectRoot: string,
|
|
524
|
+
canonicalCommandPath: string,
|
|
445
525
|
): Promise<void> {
|
|
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
|
-
|
|
508
|
-
|
|
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
|
+
}
|
|
509
606
|
}
|