@technomoron/agent-run 0.99.24 → 0.99.25
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/CHANGES +10 -0
- package/README.md +56 -30
- package/dist/agent-run.js +13 -3213
- package/dist/checks.js +110 -0
- package/dist/cli.js +369 -0
- package/dist/commands.js +291 -0
- package/dist/config-tree.js +170 -0
- package/dist/constants.js +62 -0
- package/dist/defaults.js +408 -0
- package/dist/guards.js +163 -0
- package/dist/manifest.js +190 -0
- package/dist/model.js +2 -0
- package/dist/process.js +208 -0
- package/dist/project.js +316 -0
- package/dist/renderer.js +441 -0
- package/dist/templates.js +203 -0
- package/dist/tools.js +93 -0
- package/dist/utils.js +117 -0
- package/dist/walk-tree.js +24 -0
- package/examples/basic-config/README.md +3 -3
- package/examples/basic-config/agent-config/agent-run.defaults.jsonc +38 -0
- package/examples/basic-config/agent-config/global/tool-templates/AGENTS.md.njk +1 -1
- package/examples/basic-config/agent-config/starter/basic-project/agent-run.jsonc +1 -21
- package/package.json +1 -1
package/dist/agent-run.js
CHANGED
|
@@ -1,3219 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.
|
|
5
|
-
exports.parseInvocation = parseInvocation;
|
|
6
|
-
exports.resolveAgentDir = resolveAgentDir;
|
|
7
|
-
exports.findProjectRoot = findProjectRoot;
|
|
8
|
-
exports.resolveProfile = resolveProfile;
|
|
9
|
-
exports.defaultConfigRoot = defaultConfigRoot;
|
|
10
|
-
exports.defaultConfigRootSearchCandidates = defaultConfigRootSearchCandidates;
|
|
11
|
-
exports.parseEditorCommand = parseEditorCommand;
|
|
12
|
-
const childProcess = require("child_process");
|
|
13
|
-
const fs = require("fs");
|
|
14
|
-
const os = require("os");
|
|
4
|
+
exports.parseEditorCommand = exports.resolveProfile = exports.resolveAgentDir = exports.findProjectRoot = exports.defaultConfigRootSearchCandidates = exports.defaultConfigRoot = exports.parseInvocation = void 0;
|
|
15
5
|
const path = require("path");
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
'CLAUDE.local.md',
|
|
28
|
-
'.mcp.json',
|
|
29
|
-
'codex.md'
|
|
30
|
-
]);
|
|
31
|
-
const LOCAL_AI_DIRECTORY_NAMES = new Set(['.agents', '.claude', '.codex']);
|
|
32
|
-
const CONFIG_ROOT_OVERRIDE_ENV = 'AGENT_RUN_CONFIG_ROOT_OVERRIDE';
|
|
33
|
-
const CONFIG_DIR_ENV = 'AGENT_CONFIG_DIR';
|
|
34
|
-
const VERBOSE_ENV = 'AGENT_RUN_VERBOSE';
|
|
35
|
-
const MANIFEST_FILE_NAME = 'agent-run.jsonc';
|
|
36
|
-
const LOCAL_TEMPLATE_FILE_NAME = 'local.md.njk';
|
|
37
|
-
const LIVE_DIR_NAME = 'live';
|
|
38
|
-
const PACKAGE_VERSION = '0.99.24';
|
|
39
|
-
const UNEXPANDED_TEMPLATE_RE = /\{\{[^}]+\}\}|\{%[^%]+%\}/;
|
|
40
|
-
const agentRunEnvCache = new Map();
|
|
41
|
-
const GENERATED_GITIGNORE_ENTRIES = [
|
|
42
|
-
'# Generated agent-run live profiles',
|
|
43
|
-
'**/live/',
|
|
44
|
-
'',
|
|
45
|
-
'# Optional generated caches',
|
|
46
|
-
'**/.agent-run-cache/',
|
|
47
|
-
'',
|
|
48
|
-
'# Legacy Codex runtime state',
|
|
49
|
-
'**/auth.json',
|
|
50
|
-
'**/history.jsonl',
|
|
51
|
-
'**/sessions/',
|
|
52
|
-
'**/archived_sessions/',
|
|
53
|
-
'**/log/',
|
|
54
|
-
'**/logs/',
|
|
55
|
-
'**/shell_snapshots/',
|
|
56
|
-
'**/*.sqlite*',
|
|
57
|
-
'**/models_cache.json',
|
|
58
|
-
'**/cache/',
|
|
59
|
-
'**/.tmp/',
|
|
60
|
-
'**/installation_id',
|
|
61
|
-
'**/version.json',
|
|
62
|
-
'**/.personality_migration',
|
|
63
|
-
'**/skills/.system/'
|
|
64
|
-
];
|
|
65
|
-
const REQUIRED_GLOBAL_TEMPLATES = [
|
|
66
|
-
'global/agents/code.md.njk',
|
|
67
|
-
'global/agents/writing.md.njk',
|
|
68
|
-
'global/snippets/git-rules.md.njk',
|
|
69
|
-
'global/snippets/no-ai-files.md.njk',
|
|
70
|
-
'global/snippets/verification.md.njk',
|
|
71
|
-
'global/tool-templates/codex-config.toml.njk',
|
|
72
|
-
'global/tool-templates/claude-settings.json.njk',
|
|
73
|
-
'global/skills/commit-workflow/SKILL.md.njk',
|
|
74
|
-
'global/skills/github-release/SKILL.md.njk',
|
|
75
|
-
'global/skills/release-package-check/SKILL.md.njk',
|
|
76
|
-
'global/skills/code-review-organizer/SKILL.md.njk'
|
|
77
|
-
];
|
|
78
|
-
function main(invokedTool, argv) {
|
|
79
|
-
const parsed = parseInvocation(invokedTool, argv);
|
|
80
|
-
if (parsed.command === 'check') {
|
|
81
|
-
runCheck(parsed);
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
if (parsed.command === 'init') {
|
|
85
|
-
runInit(parsed);
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
if (parsed.command === 'init-config') {
|
|
89
|
-
runInitConfig(parsed);
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
if (parsed.command === 'edit') {
|
|
93
|
-
runEdit(parsed);
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
if (parsed.command === 'update') {
|
|
97
|
-
runUpdate(parsed);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
if (parsed.command === 'migrate-config') {
|
|
101
|
-
runMigrateConfig(parsed);
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
runTool(parsed);
|
|
105
|
-
}
|
|
106
|
-
function parseInvocation(invokedTool, argv) {
|
|
107
|
-
let command = normalizeCommandName(invokedTool);
|
|
108
|
-
const extracted = extractGlobalOptions(argv);
|
|
109
|
-
const inputArgs = extracted.args;
|
|
110
|
-
if (extracted.verbose) {
|
|
111
|
-
process.env[VERBOSE_ENV] = '1';
|
|
112
|
-
}
|
|
113
|
-
if (extracted.configRootOverride !== null) {
|
|
114
|
-
process.env[CONFIG_ROOT_OVERRIDE_ENV] = extracted.configRootOverride;
|
|
115
|
-
}
|
|
116
|
-
if (extracted.initConfig) {
|
|
117
|
-
if (inputArgs.length > 0) {
|
|
118
|
-
fail('--init cannot be combined with a command');
|
|
119
|
-
}
|
|
120
|
-
return {
|
|
121
|
-
command: 'init-config',
|
|
122
|
-
targetPath: path.resolve(extracted.initConfigTargetPath ?? defaultConfigRoot())
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
if (command === null) {
|
|
126
|
-
if (inputArgs.length === 0) {
|
|
127
|
-
fail('usage: agent-run <codex|claude|check|init|edit|update> [options] (run with --help for details)');
|
|
128
|
-
}
|
|
129
|
-
const firstArg = inputArgs[0];
|
|
130
|
-
if (isHelpFlag(firstArg)) {
|
|
131
|
-
printHelp('general');
|
|
132
|
-
}
|
|
133
|
-
if (isVersionFlag(firstArg)) {
|
|
134
|
-
printVersion();
|
|
135
|
-
}
|
|
136
|
-
const commandIndex = findCommandArgIndex(inputArgs);
|
|
137
|
-
if (commandIndex === -1) {
|
|
138
|
-
fail(`unknown command: ${firstArg ?? ''} (run with --help for usage)`);
|
|
139
|
-
}
|
|
140
|
-
const parsedCommand = normalizeCommandName(inputArgs[commandIndex] ?? '');
|
|
141
|
-
if (parsedCommand === null) {
|
|
142
|
-
fail(`unknown command: ${firstArg ?? ''} (run with --help for usage)`);
|
|
143
|
-
}
|
|
144
|
-
command = parsedCommand;
|
|
145
|
-
inputArgs.splice(commandIndex, 1);
|
|
146
|
-
}
|
|
147
|
-
if (command === 'check') {
|
|
148
|
-
return parseCheckCommand(inputArgs);
|
|
149
|
-
}
|
|
150
|
-
if (command === 'init') {
|
|
151
|
-
return parseInitCommand(inputArgs);
|
|
152
|
-
}
|
|
153
|
-
if (command === 'edit') {
|
|
154
|
-
return parseEditCommand(inputArgs);
|
|
155
|
-
}
|
|
156
|
-
if (command === 'update') {
|
|
157
|
-
return parseUpdateCommand(inputArgs);
|
|
158
|
-
}
|
|
159
|
-
if (command === 'migrate-config') {
|
|
160
|
-
return parseMigrateConfigCommand(inputArgs);
|
|
161
|
-
}
|
|
162
|
-
return parseRunCommand(command, inputArgs);
|
|
163
|
-
}
|
|
164
|
-
function findCommandArgIndex(args) {
|
|
165
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
166
|
-
const arg = args[index];
|
|
167
|
-
if (arg === '--') {
|
|
168
|
-
return -1;
|
|
169
|
-
}
|
|
170
|
-
if (normalizeCommandName(arg ?? '') !== null) {
|
|
171
|
-
return index;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
return -1;
|
|
175
|
-
}
|
|
176
|
-
function extractGlobalOptions(argv) {
|
|
177
|
-
const args = [];
|
|
178
|
-
let configRootOverride = null;
|
|
179
|
-
let initConfig = false;
|
|
180
|
-
let initConfigTargetPath = null;
|
|
181
|
-
let verbose = false;
|
|
182
|
-
let passthrough = false;
|
|
183
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
184
|
-
const arg = argv[index];
|
|
185
|
-
if (passthrough || arg === undefined) {
|
|
186
|
-
if (arg !== undefined) {
|
|
187
|
-
args.push(arg);
|
|
188
|
-
}
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
if (arg === '--') {
|
|
192
|
-
passthrough = true;
|
|
193
|
-
args.push(arg);
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
if (arg === '-v' || arg === '--verbose') {
|
|
197
|
-
verbose = true;
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
if (arg === '--init') {
|
|
201
|
-
initConfig = true;
|
|
202
|
-
const nextArg = argv[index + 1];
|
|
203
|
-
if (nextArg !== undefined && !nextArg.startsWith('-')) {
|
|
204
|
-
initConfigTargetPath = nextArg;
|
|
205
|
-
index += 1;
|
|
206
|
-
}
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
if (arg === '--config-root') {
|
|
210
|
-
const nextArg = argv[index + 1];
|
|
211
|
-
if (nextArg === undefined) {
|
|
212
|
-
fail('missing value for --config-root');
|
|
213
|
-
}
|
|
214
|
-
configRootOverride = path.resolve(nextArg);
|
|
215
|
-
index += 1;
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
if (arg.startsWith('--config-root=')) {
|
|
219
|
-
const rootValue = arg.slice('--config-root='.length);
|
|
220
|
-
if (!rootValue) {
|
|
221
|
-
fail('missing value for --config-root');
|
|
222
|
-
}
|
|
223
|
-
configRootOverride = path.resolve(rootValue);
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
args.push(arg);
|
|
227
|
-
}
|
|
228
|
-
return { args, configRootOverride, initConfig, initConfigTargetPath, verbose };
|
|
229
|
-
}
|
|
230
|
-
function parseRunCommand(command, inputArgs) {
|
|
231
|
-
const wrapperArgs = {
|
|
232
|
-
none: false,
|
|
233
|
-
create: false,
|
|
234
|
-
local: false,
|
|
235
|
-
show: false,
|
|
236
|
-
generate: false,
|
|
237
|
-
codexSandboxMode: null,
|
|
238
|
-
codexNetwork: false
|
|
239
|
-
};
|
|
240
|
-
const args = [];
|
|
241
|
-
let passthrough = false;
|
|
242
|
-
for (const arg of inputArgs) {
|
|
243
|
-
if (passthrough) {
|
|
244
|
-
args.push(arg);
|
|
245
|
-
continue;
|
|
246
|
-
}
|
|
247
|
-
if (arg === '--') {
|
|
248
|
-
passthrough = true;
|
|
249
|
-
args.push(arg);
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
252
|
-
if (arg === '--none') {
|
|
253
|
-
wrapperArgs.none = true;
|
|
254
|
-
continue;
|
|
255
|
-
}
|
|
256
|
-
if (arg === '--create') {
|
|
257
|
-
wrapperArgs.create = true;
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
260
|
-
if (arg === '--local') {
|
|
261
|
-
wrapperArgs.local = true;
|
|
262
|
-
continue;
|
|
263
|
-
}
|
|
264
|
-
if (arg === '--show') {
|
|
265
|
-
wrapperArgs.show = true;
|
|
266
|
-
continue;
|
|
267
|
-
}
|
|
268
|
-
if (arg === '--generate') {
|
|
269
|
-
wrapperArgs.generate = true;
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
if (arg === '--danger') {
|
|
273
|
-
if (command !== 'codex') {
|
|
274
|
-
fail('--danger is only supported for agent-run codex');
|
|
275
|
-
}
|
|
276
|
-
if (wrapperArgs.codexSandboxMode === 'sandboxed') {
|
|
277
|
-
fail('cannot combine --danger and --sandboxed');
|
|
278
|
-
}
|
|
279
|
-
wrapperArgs.codexSandboxMode = 'danger';
|
|
280
|
-
continue;
|
|
281
|
-
}
|
|
282
|
-
if (arg === '--sandboxed') {
|
|
283
|
-
if (command !== 'codex') {
|
|
284
|
-
fail('--sandboxed is only supported for agent-run codex');
|
|
285
|
-
}
|
|
286
|
-
if (wrapperArgs.codexSandboxMode === 'danger') {
|
|
287
|
-
fail('cannot combine --danger and --sandboxed');
|
|
288
|
-
}
|
|
289
|
-
wrapperArgs.codexSandboxMode = 'sandboxed';
|
|
290
|
-
continue;
|
|
291
|
-
}
|
|
292
|
-
if (arg === '--network') {
|
|
293
|
-
if (command !== 'codex') {
|
|
294
|
-
fail('--network is only supported for agent-run codex');
|
|
295
|
-
}
|
|
296
|
-
wrapperArgs.codexNetwork = true;
|
|
297
|
-
continue;
|
|
298
|
-
}
|
|
299
|
-
args.push(arg);
|
|
300
|
-
}
|
|
301
|
-
if (wrapperArgs.codexNetwork && wrapperArgs.codexSandboxMode === 'danger') {
|
|
302
|
-
fail('--network cannot be combined with agent-run codex --danger');
|
|
303
|
-
}
|
|
304
|
-
return { args, command, wrapperArgs };
|
|
305
|
-
}
|
|
306
|
-
function parseCheckCommand(inputArgs) {
|
|
307
|
-
let all = false;
|
|
308
|
-
let targetPath = process.cwd();
|
|
309
|
-
for (const arg of inputArgs) {
|
|
310
|
-
if (isHelpFlag(arg)) {
|
|
311
|
-
printHelp('check');
|
|
312
|
-
}
|
|
313
|
-
if (arg === '--all') {
|
|
314
|
-
all = true;
|
|
315
|
-
continue;
|
|
316
|
-
}
|
|
317
|
-
if (arg.startsWith('--')) {
|
|
318
|
-
fail(`unknown check option: ${arg}`);
|
|
319
|
-
}
|
|
320
|
-
targetPath = arg;
|
|
321
|
-
}
|
|
322
|
-
return { all, command: 'check', targetPath: path.resolve(targetPath) };
|
|
323
|
-
}
|
|
324
|
-
function parseInitCommand(inputArgs) {
|
|
325
|
-
let targetPath = process.cwd();
|
|
326
|
-
for (const arg of inputArgs) {
|
|
327
|
-
if (isHelpFlag(arg)) {
|
|
328
|
-
printHelp('init');
|
|
329
|
-
}
|
|
330
|
-
if (arg.startsWith('--')) {
|
|
331
|
-
fail(`unknown init option: ${arg}`);
|
|
332
|
-
}
|
|
333
|
-
targetPath = arg;
|
|
334
|
-
}
|
|
335
|
-
return { command: 'init', targetPath: path.resolve(targetPath) };
|
|
336
|
-
}
|
|
337
|
-
function parseEditCommand(inputArgs) {
|
|
338
|
-
let targetPath = process.cwd();
|
|
339
|
-
for (const arg of inputArgs) {
|
|
340
|
-
if (isHelpFlag(arg)) {
|
|
341
|
-
printHelp('edit');
|
|
342
|
-
}
|
|
343
|
-
if (arg.startsWith('--')) {
|
|
344
|
-
fail(`unknown edit option: ${arg}`);
|
|
345
|
-
}
|
|
346
|
-
targetPath = arg;
|
|
347
|
-
}
|
|
348
|
-
return { command: 'edit', targetPath: path.resolve(targetPath) };
|
|
349
|
-
}
|
|
350
|
-
function parseUpdateCommand(inputArgs) {
|
|
351
|
-
let all = false;
|
|
352
|
-
let targetPath = process.cwd();
|
|
353
|
-
for (const arg of inputArgs) {
|
|
354
|
-
if (isHelpFlag(arg)) {
|
|
355
|
-
printHelp('update');
|
|
356
|
-
}
|
|
357
|
-
if (arg === '--all') {
|
|
358
|
-
all = true;
|
|
359
|
-
continue;
|
|
360
|
-
}
|
|
361
|
-
if (arg.startsWith('--')) {
|
|
362
|
-
fail(`unknown update option: ${arg}`);
|
|
363
|
-
}
|
|
364
|
-
targetPath = arg;
|
|
365
|
-
}
|
|
366
|
-
return { all, command: 'update', targetPath: path.resolve(targetPath) };
|
|
367
|
-
}
|
|
368
|
-
function parseMigrateConfigCommand(inputArgs) {
|
|
369
|
-
let configRoot = defaultConfigRoot();
|
|
370
|
-
for (const arg of inputArgs) {
|
|
371
|
-
if (isHelpFlag(arg)) {
|
|
372
|
-
printHelp('migrate-config');
|
|
373
|
-
}
|
|
374
|
-
if (arg.startsWith('--')) {
|
|
375
|
-
fail(`unknown migrate-config option: ${arg}`);
|
|
376
|
-
}
|
|
377
|
-
configRoot = arg;
|
|
378
|
-
}
|
|
379
|
-
return { command: 'migrate-config', configRoot: path.resolve(configRoot) };
|
|
380
|
-
}
|
|
381
|
-
function isHelpFlag(value) {
|
|
382
|
-
return value === '-h' || value === '--help';
|
|
383
|
-
}
|
|
384
|
-
function isVersionFlag(value) {
|
|
385
|
-
return value === '-V' || value === '--version';
|
|
386
|
-
}
|
|
387
|
-
function printVersion() {
|
|
388
|
-
process.stdout.write(`agent-run ${agentRunVersion()}\n`);
|
|
389
|
-
process.exit(0);
|
|
390
|
-
}
|
|
391
|
-
function printHelp(topic) {
|
|
392
|
-
process.stdout.write(renderHelp(topic));
|
|
393
|
-
process.exit(0);
|
|
394
|
-
}
|
|
395
|
-
function renderHelp(topic) {
|
|
396
|
-
switch (topic) {
|
|
397
|
-
case 'check':
|
|
398
|
-
return [
|
|
399
|
-
'Usage:',
|
|
400
|
-
' agent-run check [--all] [path]',
|
|
401
|
-
'',
|
|
402
|
-
'Check generated profile files, native skills, guard shims, and local AI-file leaks.',
|
|
403
|
-
'',
|
|
404
|
-
'Options:',
|
|
405
|
-
' -h, --help Show this help text',
|
|
406
|
-
' --all Check every repo under path',
|
|
407
|
-
''
|
|
408
|
-
].join('\n');
|
|
409
|
-
case 'init':
|
|
410
|
-
return [
|
|
411
|
-
'Usage:',
|
|
412
|
-
' agent-run init [path]',
|
|
413
|
-
'',
|
|
414
|
-
'Create the mapped profile manifest/templates and render generated files.',
|
|
415
|
-
''
|
|
416
|
-
].join('\n');
|
|
417
|
-
case 'edit':
|
|
418
|
-
return [
|
|
419
|
-
'Usage:',
|
|
420
|
-
' agent-run edit [path]',
|
|
421
|
-
'',
|
|
422
|
-
'Open the editable local profile template.',
|
|
423
|
-
''
|
|
424
|
-
].join('\n');
|
|
425
|
-
case 'update':
|
|
426
|
-
return [
|
|
427
|
-
'Usage:',
|
|
428
|
-
' agent-run update [--all] [path]',
|
|
429
|
-
'',
|
|
430
|
-
'Render generated files, native skills, config, and guard shims for the repo at path.',
|
|
431
|
-
'With --all, render every profile found under the config root without requiring code checkouts.',
|
|
432
|
-
''
|
|
433
|
-
].join('\n');
|
|
434
|
-
case 'migrate-config':
|
|
435
|
-
return [
|
|
436
|
-
'Usage:',
|
|
437
|
-
' agent-run migrate-config [config-root]',
|
|
438
|
-
'',
|
|
439
|
-
'Convert an existing agent config tree to manifest/local.md.njk/global layout.',
|
|
440
|
-
''
|
|
441
|
-
].join('\n');
|
|
442
|
-
case 'general':
|
|
443
|
-
default:
|
|
444
|
-
return [
|
|
445
|
-
`agent-run ${agentRunVersion()}`,
|
|
446
|
-
'',
|
|
447
|
-
'Usage:',
|
|
448
|
-
' agent-run <codex|claude|check|init|edit|update> [options]',
|
|
449
|
-
' agent-run --init [config-root]',
|
|
450
|
-
'',
|
|
451
|
-
'Commands:',
|
|
452
|
-
' codex [--none] [--create] [--local] [--show] [--generate] [--danger|--sandboxed] [--network] [args...]',
|
|
453
|
-
' Run codex with generated private config',
|
|
454
|
-
' claude [--none] [--create] [--local] [--show] [--generate] [args...]',
|
|
455
|
-
' Run claude with generated private config',
|
|
456
|
-
' check [--all] [path] Validate generated profile output',
|
|
457
|
-
' init [path] Create profile source files and render output',
|
|
458
|
-
' edit [path] Open local.md.njk or legacy AGENTS-MODS.md',
|
|
459
|
-
' update [--all] [path] Regenerate profile output',
|
|
460
|
-
' migrate-config [config-root] Convert existing config tree layout',
|
|
461
|
-
'',
|
|
462
|
-
'Global options:',
|
|
463
|
-
' -h, --help Show this help text',
|
|
464
|
-
' -V, --version Show the agent-run version',
|
|
465
|
-
' -v, --verbose Print path resolution and wrapper actions',
|
|
466
|
-
' --config-root DIR Override the agent-config root',
|
|
467
|
-
' --init [DIR] Copy the packaged starter config root to DIR (default ~/.agent-config)',
|
|
468
|
-
'',
|
|
469
|
-
'Run wrapper options:',
|
|
470
|
-
' --local Warn about local AI files instead of failing',
|
|
471
|
-
' --show Show read/include and generated files without running the tool',
|
|
472
|
-
' --generate Generate files without running the tool',
|
|
473
|
-
'',
|
|
474
|
-
'Codex wrapper options:',
|
|
475
|
-
' --danger Run Codex with no sandbox: -a never -s danger-full-access',
|
|
476
|
-
' --sandboxed Run Codex with workspace-write sandbox (default)',
|
|
477
|
-
' --network Enable network for the workspace-write sandbox',
|
|
478
|
-
''
|
|
479
|
-
].join('\n');
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
function agentRunVersion() {
|
|
483
|
-
const packagePath = path.resolve(__dirname, '..', 'package.json');
|
|
484
|
-
try {
|
|
485
|
-
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
486
|
-
if (typeof pkg.version === 'string' && pkg.version.length > 0) {
|
|
487
|
-
return pkg.version;
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
catch {
|
|
491
|
-
return PACKAGE_VERSION;
|
|
492
|
-
}
|
|
493
|
-
return PACKAGE_VERSION;
|
|
494
|
-
}
|
|
495
|
-
function normalizeCommandName(value) {
|
|
496
|
-
if (!value) {
|
|
497
|
-
return null;
|
|
498
|
-
}
|
|
499
|
-
const base = path.basename(value).toLowerCase();
|
|
500
|
-
if (base === 'agent-run' || base === 'agent-run.js' || base === 'agent-run.cmd') {
|
|
501
|
-
return null;
|
|
502
|
-
}
|
|
503
|
-
if (base === 'codex' || base === 'codex.cmd' || base === 'codex.exe' || base === 'codex.bat') {
|
|
504
|
-
return 'codex';
|
|
505
|
-
}
|
|
506
|
-
if (base === 'claude' || base === 'claude.cmd' || base === 'claude.exe' || base === 'claude.bat') {
|
|
507
|
-
return 'claude';
|
|
508
|
-
}
|
|
509
|
-
if (base === 'check') {
|
|
510
|
-
return 'check';
|
|
511
|
-
}
|
|
512
|
-
if (base === 'init') {
|
|
513
|
-
return 'init';
|
|
514
|
-
}
|
|
515
|
-
if (base === 'edit') {
|
|
516
|
-
return 'edit';
|
|
517
|
-
}
|
|
518
|
-
if (base === 'update') {
|
|
519
|
-
return 'update';
|
|
520
|
-
}
|
|
521
|
-
if (base === 'migrate-config' || base === 'migrate-config.cmd') {
|
|
522
|
-
return 'migrate-config';
|
|
523
|
-
}
|
|
524
|
-
return null;
|
|
525
|
-
}
|
|
526
|
-
function runTool(parsed) {
|
|
527
|
-
const { args, command, wrapperArgs } = parsed;
|
|
528
|
-
const projectRoot = findProjectRoot(process.cwd());
|
|
529
|
-
verbose(`run ${command}: cwd=${process.cwd()} projectRoot=${projectRoot}`);
|
|
530
|
-
if (wrapperArgs.show && wrapperArgs.none) {
|
|
531
|
-
fail('--show cannot be combined with --none');
|
|
532
|
-
}
|
|
533
|
-
if (wrapperArgs.generate && wrapperArgs.none) {
|
|
534
|
-
fail('--generate cannot be combined with --none');
|
|
535
|
-
}
|
|
536
|
-
if (wrapperArgs.generate && wrapperArgs.show) {
|
|
537
|
-
fail('--generate cannot be combined with --show');
|
|
538
|
-
}
|
|
539
|
-
if (wrapperArgs.show && isIgnoredDir(projectRoot)) {
|
|
540
|
-
fail(`cannot show generated ${command} files for ignored project: ${projectRoot}`);
|
|
541
|
-
}
|
|
542
|
-
if (wrapperArgs.generate && isIgnoredDir(projectRoot)) {
|
|
543
|
-
fail(`cannot generate ${command} files for ignored project: ${projectRoot}`);
|
|
544
|
-
}
|
|
545
|
-
if (wrapperArgs.none || isIgnoredDir(projectRoot)) {
|
|
546
|
-
const realBinary = findRealBinary(command);
|
|
547
|
-
const permissionArgs = getPermissionArgs(command);
|
|
548
|
-
verbose(`wrapper bypassed for ${command}${wrapperArgs.none ? ' via --none' : ' because project is ignored'}`);
|
|
549
|
-
execTool(realBinary, [...permissionArgs, ...args]);
|
|
550
|
-
return;
|
|
551
|
-
}
|
|
552
|
-
const profileResult = resolveProfileResult(projectRoot);
|
|
553
|
-
if (profileResult.profile === null) {
|
|
554
|
-
fail(profileResult.reason);
|
|
555
|
-
}
|
|
556
|
-
const profile = profileResult.profile;
|
|
557
|
-
if (wrapperArgs.show) {
|
|
558
|
-
const configRoot = defaultConfigRoot(projectRoot);
|
|
559
|
-
const agentDir = path.join(configRoot, profile);
|
|
560
|
-
verbose(`profile=${profile} configRoot=${configRoot} agentPath=${agentDir}`);
|
|
561
|
-
showToolProfile(command, projectRoot, agentDir);
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
564
|
-
if (wrapperArgs.create) {
|
|
565
|
-
runInit({ command: 'init', targetPath: projectRoot });
|
|
566
|
-
}
|
|
567
|
-
const configRoot = defaultConfigRoot(projectRoot);
|
|
568
|
-
const agentDir = path.join(configRoot, profile);
|
|
569
|
-
verbose(`profile=${profile} configRoot=${configRoot} agentPath=${agentDir}`);
|
|
570
|
-
ensureRunnableProfile(configRoot, agentDir, profile);
|
|
571
|
-
const preview = renderProfile(projectRoot, agentDir, true, command);
|
|
572
|
-
ensureToolEnabled(preview.context, command);
|
|
573
|
-
if (wrapperArgs.generate) {
|
|
574
|
-
const rendered = syncAgentProfile(projectRoot, agentDir);
|
|
575
|
-
printUpdateSummary(rendered);
|
|
576
|
-
return;
|
|
577
|
-
}
|
|
578
|
-
const localAiFiles = findLocalAiFiles(projectRoot, agentDir);
|
|
579
|
-
if (preview.context.guardrails.forbidRepoAiFiles && localAiFiles.length > 0) {
|
|
580
|
-
if (!wrapperArgs.local) {
|
|
581
|
-
failForLocalAiFiles(command, projectRoot, localAiFiles);
|
|
582
|
-
}
|
|
583
|
-
warnForLocalAiFiles(command, projectRoot, localAiFiles);
|
|
584
|
-
}
|
|
585
|
-
const realBinary = findRealBinary(command);
|
|
586
|
-
const permissionArgs = getPermissionArgs(command);
|
|
587
|
-
const rendered = syncAgentProfile(projectRoot, agentDir);
|
|
588
|
-
if (command === 'codex') {
|
|
589
|
-
runCodex(realBinary, permissionArgs, rendered.context, args, wrapperArgs);
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
runClaude(realBinary, permissionArgs, rendered.context, args, wrapperArgs);
|
|
593
|
-
}
|
|
594
|
-
function showToolProfile(command, projectRoot, agentDir) {
|
|
595
|
-
const trace = { sourceFiles: new Set() };
|
|
596
|
-
const rendered = renderProfile(projectRoot, agentDir, true, command, trace);
|
|
597
|
-
ensureToolEnabled(rendered.context, command);
|
|
598
|
-
const generatedFiles = uniqueSorted(rendered.files.map((file) => file.path));
|
|
599
|
-
const sourceFiles = uniqueSorted([...trace.sourceFiles]);
|
|
600
|
-
process.stdout.write([
|
|
601
|
-
`Agent: ${command}`,
|
|
602
|
-
`Profile: ${rendered.profile}`,
|
|
603
|
-
`Project root: ${projectRoot}`,
|
|
604
|
-
`Profile dir: ${agentDir}`,
|
|
605
|
-
'',
|
|
606
|
-
'Reads/includes:',
|
|
607
|
-
...formatPathList(sourceFiles),
|
|
608
|
-
'',
|
|
609
|
-
'Generates:',
|
|
610
|
-
...formatPathList(generatedFiles),
|
|
611
|
-
''
|
|
612
|
-
].join('\n'));
|
|
613
|
-
}
|
|
614
|
-
function ensureToolEnabled(context, command) {
|
|
615
|
-
if (!context.tools[command]) {
|
|
616
|
-
fail(`${command} is disabled for agent-run profile ${context.profile}`);
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
function uniqueSorted(values) {
|
|
620
|
-
return [...new Set(values.map((value) => path.resolve(value)))].sort((a, b) => a.localeCompare(b));
|
|
621
|
-
}
|
|
622
|
-
function formatPathList(paths) {
|
|
623
|
-
return paths.length === 0 ? [' (none)'] : paths.map((entry) => ` ${entry}`);
|
|
624
|
-
}
|
|
625
|
-
function runInit(parsed) {
|
|
626
|
-
const projectRoot = findProjectRoot(parsed.targetPath);
|
|
627
|
-
verbose(`init target=${parsed.targetPath} projectRoot=${projectRoot}`);
|
|
628
|
-
if (isIgnoredDir(projectRoot)) {
|
|
629
|
-
process.stdout.write(`SKIP ignored ${projectRoot}\n`);
|
|
630
|
-
return;
|
|
631
|
-
}
|
|
632
|
-
const profile = resolveProfile(projectRoot);
|
|
633
|
-
const configRoot = defaultConfigRoot(projectRoot, { preferProjectRoot: true });
|
|
634
|
-
const agentDir = path.join(configRoot, profile);
|
|
635
|
-
verbose(`profile=${profile} configRoot=${configRoot} agentPath=${agentDir}`);
|
|
636
|
-
ensureConfigRootLayout(configRoot);
|
|
637
|
-
ensureConfigRootGitignore(configRoot);
|
|
638
|
-
ensureDefaultGlobalTemplates(configRoot);
|
|
639
|
-
fs.mkdirSync(agentDir, { recursive: true });
|
|
640
|
-
ensureProfileOverridesDir(agentDir);
|
|
641
|
-
convertLegacyProfileIfNeeded(configRoot, agentDir, profile);
|
|
642
|
-
createDefaultManifestFile(agentDir, profile);
|
|
643
|
-
createDefaultLocalFile(agentDir, profile);
|
|
644
|
-
const rendered = syncAgentProfile(projectRoot, agentDir);
|
|
645
|
-
printUpdateSummary(rendered);
|
|
646
|
-
}
|
|
647
|
-
function runInitConfig(parsed) {
|
|
648
|
-
const targetPath = path.resolve(parsed.targetPath);
|
|
649
|
-
const sourcePath = starterConfigRootPath();
|
|
650
|
-
if (!fs.existsSync(sourcePath)) {
|
|
651
|
-
fail(`starter config skeleton not found: ${sourcePath}`);
|
|
652
|
-
}
|
|
653
|
-
copySkeletonTree(sourcePath, targetPath);
|
|
654
|
-
process.stdout.write(`OK copied starter config to ${targetPath}\n`);
|
|
655
|
-
}
|
|
656
|
-
function ensureRunnableProfile(configRoot, agentDir, profile) {
|
|
657
|
-
ensureConfigRootLayout(configRoot);
|
|
658
|
-
ensureConfigRootGitignore(configRoot);
|
|
659
|
-
ensureDefaultGlobalTemplates(configRoot);
|
|
660
|
-
fs.mkdirSync(agentDir, { recursive: true });
|
|
661
|
-
ensureProfileOverridesDir(agentDir);
|
|
662
|
-
convertLegacyProfileIfNeeded(configRoot, agentDir, profile);
|
|
663
|
-
createDefaultManifestFile(agentDir, profile);
|
|
664
|
-
createDefaultLocalFile(agentDir, profile);
|
|
665
|
-
}
|
|
666
|
-
function runEdit(parsed) {
|
|
667
|
-
const projectRoot = findProjectRoot(parsed.targetPath);
|
|
668
|
-
verbose(`edit target=${parsed.targetPath} projectRoot=${projectRoot}`);
|
|
669
|
-
if (isIgnoredDir(projectRoot)) {
|
|
670
|
-
process.stdout.write(`SKIP ignored ${projectRoot}\n`);
|
|
671
|
-
process.exit(0);
|
|
672
|
-
}
|
|
673
|
-
const agentDir = resolveAgentDir(projectRoot);
|
|
674
|
-
const localPath = path.join(agentDir, LOCAL_TEMPLATE_FILE_NAME);
|
|
675
|
-
const legacyPath = path.join(agentDir, 'AGENTS-MODS.md');
|
|
676
|
-
if (!fs.existsSync(localPath) && !fs.existsSync(legacyPath)) {
|
|
677
|
-
runInit({ command: 'init', targetPath: projectRoot });
|
|
678
|
-
}
|
|
679
|
-
const editPath = fs.existsSync(localPath) ? localPath : legacyPath;
|
|
680
|
-
process.stdout.write(`Edit: ${editPath}\n`);
|
|
681
|
-
openEditor(editPath);
|
|
682
|
-
}
|
|
683
|
-
function runUpdate(parsed) {
|
|
684
|
-
if (parsed.all) {
|
|
685
|
-
runUpdateAll(parsed.targetPath);
|
|
686
|
-
return;
|
|
687
|
-
}
|
|
688
|
-
const projectRoot = findProjectRoot(parsed.targetPath);
|
|
689
|
-
verbose(`update target=${parsed.targetPath} projectRoot=${projectRoot}`);
|
|
690
|
-
if (isIgnoredDir(projectRoot)) {
|
|
691
|
-
process.stdout.write(`SKIP ignored ${projectRoot}\n`);
|
|
692
|
-
return;
|
|
693
|
-
}
|
|
694
|
-
const profile = resolveProfile(projectRoot);
|
|
695
|
-
const configRoot = defaultConfigRoot(projectRoot);
|
|
696
|
-
const agentDir = resolveAgentDir(projectRoot);
|
|
697
|
-
ensureConfigRootLayout(configRoot);
|
|
698
|
-
ensureConfigRootGitignore(configRoot);
|
|
699
|
-
ensureDefaultGlobalTemplates(configRoot);
|
|
700
|
-
fs.mkdirSync(agentDir, { recursive: true });
|
|
701
|
-
ensureProfileOverridesDir(agentDir);
|
|
702
|
-
convertLegacyProfileIfNeeded(configRoot, agentDir, profile);
|
|
703
|
-
createDefaultManifestFile(agentDir, profile);
|
|
704
|
-
createDefaultLocalFile(agentDir, profile);
|
|
705
|
-
const rendered = syncAgentProfile(projectRoot, agentDir);
|
|
706
|
-
printUpdateSummary(rendered);
|
|
707
|
-
}
|
|
708
|
-
function runUpdateAll(targetPath) {
|
|
709
|
-
const configRoot = targetPath === path.resolve(process.cwd()) ? defaultConfigRoot() : path.resolve(targetPath);
|
|
710
|
-
verbose(`update --all configRoot=${configRoot}`);
|
|
711
|
-
if (!fs.existsSync(configRoot)) {
|
|
712
|
-
fail(`missing config root: ${configRoot}`);
|
|
713
|
-
}
|
|
714
|
-
ensureConfigRootLayout(configRoot);
|
|
715
|
-
ensureConfigRootGitignore(configRoot);
|
|
716
|
-
ensureDefaultGlobalTemplates(configRoot);
|
|
717
|
-
const profileDirs = findProfileDirs(configRoot);
|
|
718
|
-
let updated = 0;
|
|
719
|
-
for (const agentDir of profileDirs) {
|
|
720
|
-
const profile = path.relative(configRoot, agentDir).replace(/\\/g, '/');
|
|
721
|
-
const projectRoot = projectRootForProfile(profile, configRoot);
|
|
722
|
-
verbose(`update --all profile=${profile} syntheticProjectRoot=${projectRoot}`);
|
|
723
|
-
ensureProfileOverridesDir(agentDir);
|
|
724
|
-
convertLegacyProfileIfNeeded(configRoot, agentDir, profile);
|
|
725
|
-
createDefaultManifestFile(agentDir, profile);
|
|
726
|
-
createDefaultLocalFile(agentDir, profile);
|
|
727
|
-
syncAgentProfile(projectRoot, agentDir, { configRoot, profile });
|
|
728
|
-
process.stdout.write(`OK ${profile}\n`);
|
|
729
|
-
updated += 1;
|
|
730
|
-
}
|
|
731
|
-
process.stdout.write(`Updated profiles: ${updated}\n`);
|
|
732
|
-
}
|
|
733
|
-
function runMigrateConfig(parsed) {
|
|
734
|
-
const configRoot = parsed.configRoot;
|
|
735
|
-
verbose(`migrate config root=${configRoot}`);
|
|
736
|
-
if (!fs.existsSync(configRoot)) {
|
|
737
|
-
fail(`missing config root: ${configRoot}`);
|
|
738
|
-
}
|
|
739
|
-
ensureConfigRootLayout(configRoot);
|
|
740
|
-
ensureConfigRootGitignore(configRoot);
|
|
741
|
-
migrateOldTemplates(configRoot);
|
|
742
|
-
ensureDefaultGlobalTemplates(configRoot);
|
|
743
|
-
const profileDirs = findLegacyProfileDirs(configRoot);
|
|
744
|
-
let createdManifestCount = 0;
|
|
745
|
-
let createdLocalCount = 0;
|
|
746
|
-
let createdOverridesCount = 0;
|
|
747
|
-
let movedRuntimeCount = 0;
|
|
748
|
-
let movedReviewCount = 0;
|
|
749
|
-
let movedMemoryCount = 0;
|
|
750
|
-
for (const agentDir of profileDirs) {
|
|
751
|
-
const profile = path.relative(configRoot, agentDir).replace(/\\/g, '/');
|
|
752
|
-
const localPath = path.join(agentDir, LOCAL_TEMPLATE_FILE_NAME);
|
|
753
|
-
if (!fs.existsSync(localPath)) {
|
|
754
|
-
const legacyPath = path.join(agentDir, 'AGENTS-MODS.md');
|
|
755
|
-
fs.writeFileSync(localPath, convertLegacyTemplateVars(fs.readFileSync(legacyPath, 'utf8')), 'utf8');
|
|
756
|
-
createdLocalCount += 1;
|
|
757
|
-
}
|
|
758
|
-
const manifestPath = path.join(agentDir, MANIFEST_FILE_NAME);
|
|
759
|
-
if (!fs.existsSync(manifestPath)) {
|
|
760
|
-
createDefaultManifestFile(agentDir, profile);
|
|
761
|
-
createdManifestCount += 1;
|
|
762
|
-
}
|
|
763
|
-
const overridesDir = path.join(agentDir, 'overrides');
|
|
764
|
-
if (!fs.existsSync(overridesDir)) {
|
|
765
|
-
fs.mkdirSync(overridesDir, { recursive: true });
|
|
766
|
-
createdOverridesCount += 1;
|
|
767
|
-
}
|
|
768
|
-
movedRuntimeCount += migrateCodexRuntimeFiles(agentDir);
|
|
769
|
-
movedReviewCount += migrateLooseFiles(agentDir, /^REVIEW(?:-.+)?\.md$/, 'reviews');
|
|
770
|
-
movedMemoryCount += migrateLooseFiles(agentDir, /^memory.*\.md$/i, 'memories');
|
|
771
|
-
}
|
|
772
|
-
process.stdout.write(`OK migrated ${configRoot}\n`);
|
|
773
|
-
process.stdout.write(`Profiles converted: ${profileDirs.length}\n`);
|
|
774
|
-
process.stdout.write(`Created local.md.njk: ${createdLocalCount}\n`);
|
|
775
|
-
process.stdout.write(`Created agent-run.jsonc: ${createdManifestCount}\n`);
|
|
776
|
-
process.stdout.write(`Created overrides dirs: ${createdOverridesCount}\n`);
|
|
777
|
-
process.stdout.write(`Moved Codex runtime entries: ${movedRuntimeCount}\n`);
|
|
778
|
-
process.stdout.write(`Moved review files: ${movedReviewCount}\n`);
|
|
779
|
-
process.stdout.write(`Moved memory files: ${movedMemoryCount}\n`);
|
|
780
|
-
}
|
|
781
|
-
function runCheck(parsed) {
|
|
782
|
-
if (parsed.all) {
|
|
783
|
-
verbose(`check --all root=${parsed.targetPath}`);
|
|
784
|
-
const report = checkSourceTree(parsed.targetPath);
|
|
785
|
-
printBatchReport(report.root, report.entries);
|
|
786
|
-
process.exit(report.hasErrors ? 1 : 0);
|
|
787
|
-
}
|
|
788
|
-
const projectRoot = findProjectRoot(parsed.targetPath);
|
|
789
|
-
verbose(`check target=${parsed.targetPath} projectRoot=${projectRoot}`);
|
|
790
|
-
if (isIgnoredDir(projectRoot)) {
|
|
791
|
-
process.stdout.write(`Check: ${projectRoot}\n`);
|
|
792
|
-
process.stdout.write('SKIP ignored by .agent-run-ignore\n');
|
|
793
|
-
process.exit(0);
|
|
794
|
-
}
|
|
795
|
-
const findings = checkProject(projectRoot);
|
|
796
|
-
printProjectReport(projectRoot, findings);
|
|
797
|
-
process.exit(hasErrors(findings) ? 1 : 0);
|
|
798
|
-
}
|
|
799
|
-
function printUpdateSummary(rendered) {
|
|
800
|
-
process.stdout.write(`OK profile ${rendered.profile}\n`);
|
|
801
|
-
process.stdout.write(`Profile dir: ${rendered.agentDir}\n`);
|
|
802
|
-
process.stdout.write(`Live dir: ${rendered.context.paths.liveDir}\n`);
|
|
803
|
-
process.stdout.write(`Generated files: ${rendered.files.length}\n`);
|
|
804
|
-
process.stdout.write(`Installed skills: ${rendered.skills.map((skill) => skill.name).join(', ') || '(none)'}\n`);
|
|
805
|
-
}
|
|
806
|
-
function ensureConfigRootLayout(configRoot) {
|
|
807
|
-
fs.mkdirSync(configRoot, { recursive: true });
|
|
808
|
-
}
|
|
809
|
-
function ensureConfigRootGitignore(configRoot) {
|
|
810
|
-
fs.mkdirSync(configRoot, { recursive: true });
|
|
811
|
-
const gitignorePath = path.join(configRoot, '.gitignore');
|
|
812
|
-
const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf8').replace(/\r\n/g, '\n') : '';
|
|
813
|
-
const lines = existing.length > 0 ? existing.replace(/\n+$/, '').split('\n') : [];
|
|
814
|
-
const present = new Set(lines);
|
|
815
|
-
let changed = !fs.existsSync(gitignorePath);
|
|
816
|
-
for (const entry of GENERATED_GITIGNORE_ENTRIES) {
|
|
817
|
-
if (entry === '' || present.has(entry)) {
|
|
818
|
-
continue;
|
|
819
|
-
}
|
|
820
|
-
lines.push(entry);
|
|
821
|
-
present.add(entry);
|
|
822
|
-
changed = true;
|
|
823
|
-
}
|
|
824
|
-
if (!changed) {
|
|
825
|
-
return;
|
|
826
|
-
}
|
|
827
|
-
fs.writeFileSync(gitignorePath, `${lines.join('\n')}\n`, 'utf8');
|
|
828
|
-
}
|
|
829
|
-
function ensureDefaultGlobalTemplates(configRoot) {
|
|
830
|
-
const defaults = new Map([
|
|
831
|
-
['global/agents/code.md.njk', defaultCodeAgentTemplate()],
|
|
832
|
-
['global/agents/writing.md.njk', defaultWritingAgentTemplate()],
|
|
833
|
-
['global/snippets/git-rules.md.njk', defaultGitRulesSnippet()],
|
|
834
|
-
['global/snippets/no-ai-files.md.njk', defaultNoAiFilesSnippet()],
|
|
835
|
-
['global/snippets/verification.md.njk', defaultVerificationSnippet()],
|
|
836
|
-
['global/tool-templates/codex-config.toml.njk', defaultCodexConfigTemplate()],
|
|
837
|
-
['global/tool-templates/claude-settings.json.njk', defaultClaudeSettingsTemplate()],
|
|
838
|
-
['global/skills/commit-workflow/SKILL.md.njk', defaultCommitWorkflowSkill()],
|
|
839
|
-
['global/skills/github-release/SKILL.md.njk', defaultGithubReleaseSkill()],
|
|
840
|
-
['global/skills/release-package-check/SKILL.md.njk', defaultReleasePackageCheckSkill()],
|
|
841
|
-
['global/skills/code-review-organizer/SKILL.md.njk', defaultCodeReviewOrganizerSkill()]
|
|
842
|
-
]);
|
|
843
|
-
for (const [relativePath, content] of defaults) {
|
|
844
|
-
const filePath = path.join(configRoot, relativePath);
|
|
845
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
846
|
-
if (!fs.existsSync(filePath)) {
|
|
847
|
-
fs.writeFileSync(filePath, content, 'utf8');
|
|
848
|
-
verbose(`created ${filePath}`);
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
function migrateOldTemplates(configRoot) {
|
|
853
|
-
const oldCodeTemplate = path.join(configRoot, 'templates', 'AGENTS-CODE.md');
|
|
854
|
-
const newCodeTemplate = path.join(configRoot, 'global', 'agents', 'code.md.njk');
|
|
855
|
-
if (fs.existsSync(oldCodeTemplate) && !fs.existsSync(newCodeTemplate)) {
|
|
856
|
-
fs.mkdirSync(path.dirname(newCodeTemplate), { recursive: true });
|
|
857
|
-
fs.writeFileSync(newCodeTemplate, convertLegacyTemplateVars(fs.readFileSync(oldCodeTemplate, 'utf8')), 'utf8');
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
function findLegacyProfileDirs(configRoot) {
|
|
861
|
-
const dirs = [];
|
|
862
|
-
walkConfigTree(configRoot, 0, (fullPath, entry) => {
|
|
863
|
-
if (!entry.isDirectory()) {
|
|
864
|
-
return undefined;
|
|
865
|
-
}
|
|
866
|
-
if (shouldPruneConfigEntry(entry)) {
|
|
867
|
-
return 'skip';
|
|
868
|
-
}
|
|
869
|
-
if (fs.existsSync(path.join(fullPath, 'AGENTS-MODS.md'))) {
|
|
870
|
-
dirs.push(fullPath);
|
|
871
|
-
return 'skip';
|
|
872
|
-
}
|
|
873
|
-
return undefined;
|
|
874
|
-
});
|
|
875
|
-
return dirs.sort();
|
|
876
|
-
}
|
|
877
|
-
function findProfileDirs(configRoot) {
|
|
878
|
-
const dirs = [];
|
|
879
|
-
walkConfigTree(configRoot, 0, (fullPath, entry) => {
|
|
880
|
-
if (!entry.isDirectory()) {
|
|
881
|
-
return undefined;
|
|
882
|
-
}
|
|
883
|
-
if (shouldPruneConfigEntry(entry)) {
|
|
884
|
-
return 'skip';
|
|
885
|
-
}
|
|
886
|
-
if (fs.existsSync(path.join(fullPath, MANIFEST_FILE_NAME)) || fs.existsSync(path.join(fullPath, LOCAL_TEMPLATE_FILE_NAME))) {
|
|
887
|
-
dirs.push(fullPath);
|
|
888
|
-
return 'skip';
|
|
889
|
-
}
|
|
890
|
-
return undefined;
|
|
891
|
-
});
|
|
892
|
-
return dirs.sort();
|
|
893
|
-
}
|
|
894
|
-
function walkConfigTree(dir, depth, visitor) {
|
|
895
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
896
|
-
for (const entry of entries) {
|
|
897
|
-
const fullPath = path.join(dir, entry.name);
|
|
898
|
-
const result = visitor(fullPath, entry, depth + 1);
|
|
899
|
-
if (entry.isDirectory() && result !== 'skip') {
|
|
900
|
-
walkConfigTree(fullPath, depth + 1, visitor);
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
function shouldPruneConfigEntry(entry) {
|
|
905
|
-
return (entry.isDirectory() &&
|
|
906
|
-
(entry.name === '.git' ||
|
|
907
|
-
entry.name === 'global' ||
|
|
908
|
-
entry.name === 'templates' ||
|
|
909
|
-
entry.name === '.agents' ||
|
|
910
|
-
entry.name === '.claude' ||
|
|
911
|
-
entry.name === '.codex' ||
|
|
912
|
-
entry.name === 'bin' ||
|
|
913
|
-
entry.name === 'reviews' ||
|
|
914
|
-
entry.name === 'memories' ||
|
|
915
|
-
entry.name === 'cache' ||
|
|
916
|
-
entry.name === 'log' ||
|
|
917
|
-
entry.name === 'sessions' ||
|
|
918
|
-
entry.name === 'shell_snapshots' ||
|
|
919
|
-
entry.name === 'skills' ||
|
|
920
|
-
entry.name === 'tmp' ||
|
|
921
|
-
entry.name === '.tmp'));
|
|
922
|
-
}
|
|
923
|
-
function convertLegacyTemplateVars(content) {
|
|
924
|
-
const replacements = new Map([
|
|
925
|
-
['{{AGENT_DIR}}', '{{ agentDir }}'],
|
|
926
|
-
['{{AGENTS_MODS_PATH}}', '{{ agentDir }}/AGENTS-MODS.md'],
|
|
927
|
-
['{{AGENTS_PATH}}', '{{ agentDir }}/AGENTS.md'],
|
|
928
|
-
['{{CLAUDE_PATH}}', '{{ agentDir }}/CLAUDE.md'],
|
|
929
|
-
['{{CONFIG_ROOT}}', '{{ configRoot }}'],
|
|
930
|
-
['{{PROFILE}}', '{{ profile }}']
|
|
931
|
-
]);
|
|
932
|
-
let converted = content;
|
|
933
|
-
for (const [from, to] of replacements) {
|
|
934
|
-
converted = converted.split(from).join(to);
|
|
935
|
-
}
|
|
936
|
-
return converted
|
|
937
|
-
.replace(/^@[^\n]*templates\/AGENTS-CODE\.md[^\n]*\n?/gm, '')
|
|
938
|
-
.replace(/^\n{2,}/, '\n');
|
|
939
|
-
}
|
|
940
|
-
function migrateCodexRuntimeFiles(agentDir) {
|
|
941
|
-
const runtimeNames = fs
|
|
942
|
-
.readdirSync(agentDir)
|
|
943
|
-
.filter((name) => /^(?:\.personality_migration|\.tmp|auth\.json|cache|history\.jsonl|installation_id|log|logs|models_cache\.json|sessions|archived_sessions|shell_snapshots|skills|tmp|version\.json)$/.test(name) || /^(?:logs|state)_.*\.sqlite(?:-.+)?$/.test(name));
|
|
944
|
-
const codexHome = path.join(agentDir, LIVE_DIR_NAME, 'memories', 'codex-home');
|
|
945
|
-
let moved = 0;
|
|
946
|
-
for (const name of runtimeNames) {
|
|
947
|
-
const source = path.join(agentDir, name);
|
|
948
|
-
if (!fs.existsSync(source) && !isSymlink(source)) {
|
|
949
|
-
continue;
|
|
950
|
-
}
|
|
951
|
-
let target = path.join(codexHome, name);
|
|
952
|
-
if (fs.existsSync(target) || isSymlink(target)) {
|
|
953
|
-
target = nextBackupPath(target);
|
|
954
|
-
}
|
|
955
|
-
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
956
|
-
fs.renameSync(source, target);
|
|
957
|
-
moved += 1;
|
|
958
|
-
}
|
|
959
|
-
return moved;
|
|
960
|
-
}
|
|
961
|
-
function migrateLooseFiles(agentDir, pattern, targetDirName) {
|
|
962
|
-
const entries = fs.readdirSync(agentDir, { withFileTypes: true });
|
|
963
|
-
let moved = 0;
|
|
964
|
-
for (const entry of entries) {
|
|
965
|
-
if (!entry.isFile() || !pattern.test(entry.name)) {
|
|
966
|
-
continue;
|
|
967
|
-
}
|
|
968
|
-
const source = path.join(agentDir, entry.name);
|
|
969
|
-
const targetDir = path.join(agentDir, targetDirName);
|
|
970
|
-
const target = path.join(targetDir, entry.name);
|
|
971
|
-
if (fs.existsSync(target)) {
|
|
972
|
-
continue;
|
|
973
|
-
}
|
|
974
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
975
|
-
fs.renameSync(source, target);
|
|
976
|
-
moved += 1;
|
|
977
|
-
}
|
|
978
|
-
return moved;
|
|
979
|
-
}
|
|
980
|
-
function convertLegacyProfileIfNeeded(_configRoot, agentDir, _profile) {
|
|
981
|
-
const legacyPath = path.join(agentDir, 'AGENTS-MODS.md');
|
|
982
|
-
const localPath = path.join(agentDir, LOCAL_TEMPLATE_FILE_NAME);
|
|
983
|
-
if (fs.existsSync(legacyPath) && !fs.existsSync(localPath)) {
|
|
984
|
-
fs.mkdirSync(agentDir, { recursive: true });
|
|
985
|
-
fs.writeFileSync(localPath, fs.readFileSync(legacyPath, 'utf8'), 'utf8');
|
|
986
|
-
verbose(`converted ${legacyPath} -> ${localPath}`);
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
function createDefaultManifestFile(agentDir, profile) {
|
|
990
|
-
const manifestPath = path.join(agentDir, MANIFEST_FILE_NAME);
|
|
991
|
-
if (fs.existsSync(manifestPath)) {
|
|
992
|
-
return;
|
|
993
|
-
}
|
|
994
|
-
fs.mkdirSync(agentDir, { recursive: true });
|
|
995
|
-
fs.writeFileSync(manifestPath, stringifyDefaultManifest(profile), 'utf8');
|
|
996
|
-
verbose(`created ${manifestPath}`);
|
|
997
|
-
}
|
|
998
|
-
function createDefaultLocalFile(agentDir, profile) {
|
|
999
|
-
const localPath = path.join(agentDir, LOCAL_TEMPLATE_FILE_NAME);
|
|
1000
|
-
if (fs.existsSync(localPath)) {
|
|
1001
|
-
return;
|
|
1002
|
-
}
|
|
1003
|
-
const legacyPath = path.join(agentDir, 'AGENTS-MODS.md');
|
|
1004
|
-
const content = fs.existsSync(legacyPath) ? fs.readFileSync(legacyPath, 'utf8') : defaultLocalTemplate(profile);
|
|
1005
|
-
fs.mkdirSync(agentDir, { recursive: true });
|
|
1006
|
-
fs.writeFileSync(localPath, content, 'utf8');
|
|
1007
|
-
verbose(`created ${localPath}`);
|
|
1008
|
-
}
|
|
1009
|
-
function ensureProfileOverridesDir(agentDir) {
|
|
1010
|
-
fs.mkdirSync(path.join(agentDir, 'overrides'), { recursive: true });
|
|
1011
|
-
}
|
|
1012
|
-
function loadManifest(_configRoot, agentDir, profile, trace) {
|
|
1013
|
-
const manifestPath = path.join(agentDir, MANIFEST_FILE_NAME);
|
|
1014
|
-
if (!fs.existsSync(manifestPath)) {
|
|
1015
|
-
const manifest = defaultManifest(profile);
|
|
1016
|
-
const localPath = path.join(agentDir, LOCAL_TEMPLATE_FILE_NAME);
|
|
1017
|
-
const legacyPath = path.join(agentDir, 'AGENTS-MODS.md');
|
|
1018
|
-
if (!fs.existsSync(localPath) && fs.existsSync(legacyPath)) {
|
|
1019
|
-
manifest.agent = {
|
|
1020
|
-
...(manifest.agent ?? {}),
|
|
1021
|
-
includes: ['{{ profile }}/AGENTS-MODS.md']
|
|
1022
|
-
};
|
|
1023
|
-
}
|
|
1024
|
-
if (!fs.existsSync(localPath) && !fs.existsSync(legacyPath)) {
|
|
1025
|
-
manifest.agent = {
|
|
1026
|
-
...(manifest.agent ?? {}),
|
|
1027
|
-
includes: []
|
|
1028
|
-
};
|
|
1029
|
-
}
|
|
1030
|
-
return manifest;
|
|
1031
|
-
}
|
|
1032
|
-
trace?.sourceFiles.add(manifestPath);
|
|
1033
|
-
const text = fs.readFileSync(manifestPath, 'utf8');
|
|
1034
|
-
const errors = [];
|
|
1035
|
-
const parsed = (0, jsonc_parser_1.parse)(text, errors, { allowTrailingComma: true });
|
|
1036
|
-
if (errors.length > 0) {
|
|
1037
|
-
const first = errors[0];
|
|
1038
|
-
const detail = first ? `${(0, jsonc_parser_1.printParseErrorCode)(first.error)} at offset ${first.offset}` : 'unknown JSONC parse error';
|
|
1039
|
-
throw new Error(`invalid JSONC in ${manifestPath}: ${detail}`);
|
|
1040
|
-
}
|
|
1041
|
-
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
1042
|
-
throw new Error(`manifest must be an object: ${manifestPath}`);
|
|
1043
|
-
}
|
|
1044
|
-
return parsed;
|
|
1045
|
-
}
|
|
1046
|
-
function defaultManifest(profile) {
|
|
1047
|
-
return {
|
|
1048
|
-
profile,
|
|
1049
|
-
kind: 'code',
|
|
1050
|
-
agent: {
|
|
1051
|
-
base: 'global/agents/code.md.njk',
|
|
1052
|
-
includes: [`{{ profile }}/${LOCAL_TEMPLATE_FILE_NAME}`]
|
|
1053
|
-
},
|
|
1054
|
-
skills: {
|
|
1055
|
-
install: ['commit-workflow', 'github-release', 'release-package-check', 'code-review-organizer']
|
|
1056
|
-
},
|
|
1057
|
-
tools: {
|
|
1058
|
-
codex: true,
|
|
1059
|
-
claude: true
|
|
1060
|
-
},
|
|
1061
|
-
checks: ['agent-run check .', 'pnpm run cleanbuild'],
|
|
1062
|
-
guardrails: {
|
|
1063
|
-
blockGitWrite: true,
|
|
1064
|
-
blockPublish: true,
|
|
1065
|
-
blockGithubRelease: true,
|
|
1066
|
-
forbidRepoAiFiles: true
|
|
1067
|
-
},
|
|
1068
|
-
paths: {
|
|
1069
|
-
changesFile: '{{ projectRoot }}/CHANGES',
|
|
1070
|
-
reviewDir: '{{ profileDir }}/reviews',
|
|
1071
|
-
reviewFile: '{{ profileDir }}/reviews/REVIEW-{{ date }}.md',
|
|
1072
|
-
memoriesDir: '{{ agentDir }}/memories'
|
|
1073
|
-
}
|
|
1074
|
-
};
|
|
1075
|
-
}
|
|
1076
|
-
function normalizeManifest(manifest, profile) {
|
|
1077
|
-
const baseManifest = defaultManifest(profile);
|
|
1078
|
-
const installedSkills = Array.isArray(manifest.skills)
|
|
1079
|
-
? manifest.skills
|
|
1080
|
-
: manifest.skills?.install ?? asSkillObject(baseManifest.skills).install;
|
|
1081
|
-
const overrides = Array.isArray(manifest.skills) ? {} : manifest.skills?.overrides ?? {};
|
|
1082
|
-
const reviewDir = normalizeLegacyDefaultReviewPath(manifest.paths?.reviewDir ?? baseManifest.paths?.reviewDir ?? '{{ profileDir }}/reviews');
|
|
1083
|
-
const reviewFile = normalizeLegacyDefaultReviewPath(manifest.paths?.reviewFile ?? baseManifest.paths?.reviewFile ?? '{{ profileDir }}/reviews/REVIEW-{{ date }}.md');
|
|
1084
|
-
return {
|
|
1085
|
-
profile: manifest.profile ?? profile,
|
|
1086
|
-
kind: manifest.kind ?? baseManifest.kind ?? 'code',
|
|
1087
|
-
agent: {
|
|
1088
|
-
base: manifest.agent?.base ?? baseManifest.agent?.base ?? 'global/agents/code.md.njk',
|
|
1089
|
-
includes: manifest.agent?.includes ?? defaultAgentIncludesForProfile(profile)
|
|
1090
|
-
},
|
|
1091
|
-
skills: {
|
|
1092
|
-
install: normalizeInstalledSkills(installedSkills),
|
|
1093
|
-
overrides
|
|
1094
|
-
},
|
|
1095
|
-
tools: {
|
|
1096
|
-
codex: manifest.tools?.codex ?? true,
|
|
1097
|
-
claude: manifest.tools?.claude ?? true
|
|
1098
|
-
},
|
|
1099
|
-
checks: manifest.checks ?? baseManifest.checks ?? [],
|
|
1100
|
-
guardrails: {
|
|
1101
|
-
blockGitWrite: manifest.guardrails?.blockGitWrite ?? true,
|
|
1102
|
-
blockPublish: manifest.guardrails?.blockPublish ?? true,
|
|
1103
|
-
blockGithubRelease: manifest.guardrails?.blockGithubRelease ?? true,
|
|
1104
|
-
forbidRepoAiFiles: manifest.guardrails?.forbidRepoAiFiles ?? true
|
|
1105
|
-
},
|
|
1106
|
-
paths: {
|
|
1107
|
-
changesFile: manifest.paths?.changesFile ?? baseManifest.paths?.changesFile ?? '{{ projectRoot }}/CHANGES',
|
|
1108
|
-
reviewDir,
|
|
1109
|
-
reviewFile,
|
|
1110
|
-
reviewConsolidatedFile: manifest.paths?.reviewConsolidatedFile ?? baseManifest.paths?.reviewConsolidatedFile,
|
|
1111
|
-
memoriesDir: manifest.paths?.memoriesDir ?? baseManifest.paths?.memoriesDir ?? '{{ agentDir }}/memories'
|
|
1112
|
-
}
|
|
1113
|
-
};
|
|
1114
|
-
}
|
|
1115
|
-
function normalizeSkillName(name) {
|
|
1116
|
-
const normalized = name === 'code-review' ? 'code-review-organizer' : name;
|
|
1117
|
-
if (!/^[A-Za-z0-9._-]+$/.test(normalized)) {
|
|
1118
|
-
throw new Error(`invalid skill name: ${name}`);
|
|
1119
|
-
}
|
|
1120
|
-
return normalized;
|
|
1121
|
-
}
|
|
1122
|
-
function normalizeInstalledSkills(names) {
|
|
1123
|
-
return [...new Set(names.map(normalizeSkillName))];
|
|
1124
|
-
}
|
|
1125
|
-
function normalizeLegacyDefaultReviewPath(value) {
|
|
1126
|
-
if (value === '{{ agentDir }}/reviews') {
|
|
1127
|
-
return '{{ profileDir }}/reviews';
|
|
1128
|
-
}
|
|
1129
|
-
if (value === '{{ agentDir }}/reviews/REVIEW-{{ date }}.md') {
|
|
1130
|
-
return '{{ profileDir }}/reviews/REVIEW-{{ date }}.md';
|
|
1131
|
-
}
|
|
1132
|
-
return value;
|
|
1133
|
-
}
|
|
1134
|
-
function asSkillObject(skills) {
|
|
1135
|
-
if (Array.isArray(skills)) {
|
|
1136
|
-
return { install: skills, overrides: {} };
|
|
1137
|
-
}
|
|
1138
|
-
return {
|
|
1139
|
-
install: skills?.install ?? [],
|
|
1140
|
-
overrides: skills?.overrides ?? {}
|
|
1141
|
-
};
|
|
1142
|
-
}
|
|
1143
|
-
function defaultAgentIncludesForProfile(profile) {
|
|
1144
|
-
return [`{{ profile }}/${LOCAL_TEMPLATE_FILE_NAME}`, `{{ profile }}/AGENTS-MODS.md`];
|
|
1145
|
-
}
|
|
1146
|
-
function createNunjucksEnv(configRoot) {
|
|
1147
|
-
const env = new nunjucks.Environment(new nunjucks.FileSystemLoader(configRoot, { noCache: true }), {
|
|
1148
|
-
autoescape: false,
|
|
1149
|
-
trimBlocks: true,
|
|
1150
|
-
lstripBlocks: true,
|
|
1151
|
-
throwOnUndefined: true
|
|
1152
|
-
});
|
|
1153
|
-
return env;
|
|
1154
|
-
}
|
|
1155
|
-
function buildRenderContext(projectRoot, profileDir, configRoot, manifest, env) {
|
|
1156
|
-
const date = localDateString();
|
|
1157
|
-
const liveDir = path.join(profileDir, LIVE_DIR_NAME);
|
|
1158
|
-
const baseContext = {
|
|
1159
|
-
profile: manifest.profile,
|
|
1160
|
-
kind: manifest.kind,
|
|
1161
|
-
projectRoot,
|
|
1162
|
-
agentDir: liveDir,
|
|
1163
|
-
profileDir,
|
|
1164
|
-
configRoot,
|
|
1165
|
-
date,
|
|
1166
|
-
checks: manifest.checks,
|
|
1167
|
-
tools: manifest.tools,
|
|
1168
|
-
guardrails: manifest.guardrails
|
|
1169
|
-
};
|
|
1170
|
-
const reviewDir = resolveRuntimePath(configRoot, manifest.paths.reviewDir, env, baseContext);
|
|
1171
|
-
const memoriesDir = resolveRuntimePath(configRoot, manifest.paths.memoriesDir, env, baseContext);
|
|
1172
|
-
const codexHomeDir = path.join(memoriesDir, 'codex-home');
|
|
1173
|
-
const paths = {
|
|
1174
|
-
profileDir,
|
|
1175
|
-
liveDir,
|
|
1176
|
-
changesFile: resolveRuntimePath(configRoot, manifest.paths.changesFile, env, baseContext),
|
|
1177
|
-
reviewDir,
|
|
1178
|
-
reviewFile: resolveRuntimePath(configRoot, manifest.paths.reviewFile, env, baseContext),
|
|
1179
|
-
reviewConsolidatedFile: manifest.paths.reviewConsolidatedFile
|
|
1180
|
-
? resolveRuntimePath(configRoot, manifest.paths.reviewConsolidatedFile, env, baseContext)
|
|
1181
|
-
: path.join(reviewDir, 'REVIEW.md'),
|
|
1182
|
-
memoriesDir,
|
|
1183
|
-
globalMemoryDir: globalMemoryDir(configRoot),
|
|
1184
|
-
codexHomeDir,
|
|
1185
|
-
overridesDir: path.join(profileDir, 'overrides'),
|
|
1186
|
-
codexSkillsDir: path.join(codexHomeDir, 'skills'),
|
|
1187
|
-
claudeSkillsDir: path.join(liveDir, '.claude', 'skills'),
|
|
1188
|
-
binDir: path.join(liveDir, 'bin')
|
|
1189
|
-
};
|
|
1190
|
-
return {
|
|
1191
|
-
...baseContext,
|
|
1192
|
-
paths,
|
|
1193
|
-
permissionsAllow: buildPermissionsAllow(manifest),
|
|
1194
|
-
skills: [],
|
|
1195
|
-
renderedAgentSections: []
|
|
1196
|
-
};
|
|
1197
|
-
}
|
|
1198
|
-
function buildPermissionsAllow(manifest) {
|
|
1199
|
-
const allow = new Set();
|
|
1200
|
-
for (const check of manifest.checks) {
|
|
1201
|
-
const normalizedCheck = check.trim();
|
|
1202
|
-
if (normalizedCheck) {
|
|
1203
|
-
allow.add(`Bash(${normalizedCheck})`);
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
return [...allow];
|
|
1207
|
-
}
|
|
1208
|
-
function resolveConfigPath(configRoot, relativePath, context) {
|
|
1209
|
-
const env = createNunjucksEnv(configRoot);
|
|
1210
|
-
const rendered = renderInlineTemplate(env, relativePath, context);
|
|
1211
|
-
const resolved = path.resolve(configRoot, rendered);
|
|
1212
|
-
if (!isSamePathOrDescendant(resolved, path.resolve(configRoot))) {
|
|
1213
|
-
throw new Error(`config path escapes config root: ${relativePath}`);
|
|
1214
|
-
}
|
|
1215
|
-
return resolved;
|
|
1216
|
-
}
|
|
1217
|
-
function resolveRuntimePath(configRoot, pathTemplate, env, context) {
|
|
1218
|
-
const rendered = renderInlineTemplate(env, pathTemplate, context);
|
|
1219
|
-
return path.isAbsolute(rendered) ? path.resolve(rendered) : path.resolve(configRoot, rendered);
|
|
1220
|
-
}
|
|
1221
|
-
function renderTemplateFile(env, configRoot, templatePath, context, trace) {
|
|
1222
|
-
const resolvedPath = resolveConfigPath(configRoot, templatePath, context);
|
|
1223
|
-
if (!fs.existsSync(resolvedPath)) {
|
|
1224
|
-
throw new Error(`missing template: ${resolvedPath}`);
|
|
1225
|
-
}
|
|
1226
|
-
const relativePath = path.relative(configRoot, resolvedPath).replace(/\\/g, '/');
|
|
1227
|
-
let content = fs.readFileSync(resolvedPath, 'utf8');
|
|
1228
|
-
traceTemplateSource(configRoot, resolvedPath, content, context, trace);
|
|
1229
|
-
if (path.basename(resolvedPath) === 'AGENTS-MODS.md') {
|
|
1230
|
-
content = renderLegacyAgentsMods(resolvedPath, [], trace);
|
|
1231
|
-
return renderInlineTemplate(env, content, context);
|
|
1232
|
-
}
|
|
1233
|
-
return env.render(relativePath, context);
|
|
1234
|
-
}
|
|
1235
|
-
function traceTemplateSource(configRoot, sourcePath, content, context, trace, stack = []) {
|
|
1236
|
-
if (trace === undefined) {
|
|
1237
|
-
return;
|
|
1238
|
-
}
|
|
1239
|
-
const resolvedSource = path.resolve(sourcePath);
|
|
1240
|
-
trace.sourceFiles.add(resolvedSource);
|
|
1241
|
-
if (stack.includes(resolvedSource)) {
|
|
1242
|
-
return;
|
|
1243
|
-
}
|
|
1244
|
-
const nextStack = [...stack, resolvedSource];
|
|
1245
|
-
const includeRe = /{%\s*(?:include|extends|import)\s+["']([^"']+)["']|{%\s*from\s+["']([^"']+)["']/g;
|
|
1246
|
-
for (;;) {
|
|
1247
|
-
const match = includeRe.exec(content);
|
|
1248
|
-
if (match === null) {
|
|
1249
|
-
break;
|
|
1250
|
-
}
|
|
1251
|
-
const includePath = match[1] ?? match[2];
|
|
1252
|
-
if (!includePath) {
|
|
1253
|
-
continue;
|
|
1254
|
-
}
|
|
1255
|
-
const includedPath = resolveConfigPath(configRoot, includePath, context);
|
|
1256
|
-
if (!fs.existsSync(includedPath)) {
|
|
1257
|
-
continue;
|
|
1258
|
-
}
|
|
1259
|
-
const includedContent = fs.readFileSync(includedPath, 'utf8');
|
|
1260
|
-
traceTemplateSource(configRoot, includedPath, includedContent, context, trace, nextStack);
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
|
-
function renderInlineTemplate(env, source, context) {
|
|
1264
|
-
return env.renderString(source, context);
|
|
1265
|
-
}
|
|
1266
|
-
function assertNoUnexpandedTemplateVars(label, content) {
|
|
1267
|
-
if (UNEXPANDED_TEMPLATE_RE.test(content)) {
|
|
1268
|
-
throw new Error(`unexpanded template syntax remains in ${label}`);
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
function writeGeneratedFile(filePath, content, executable = false) {
|
|
1272
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
1273
|
-
fs.writeFileSync(filePath, content, 'utf8');
|
|
1274
|
-
if (executable && !IS_WINDOWS) {
|
|
1275
|
-
fs.chmodSync(filePath, 0o755);
|
|
1276
|
-
}
|
|
1277
|
-
verbose(`write ${filePath}`);
|
|
1278
|
-
}
|
|
1279
|
-
function starterConfigRootPath() {
|
|
1280
|
-
return path.resolve(__dirname, '..', 'examples', 'basic-config', 'agent-config');
|
|
1281
|
-
}
|
|
1282
|
-
function copySkeletonTree(sourceDir, targetDir) {
|
|
1283
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
1284
|
-
const entries = fs.readdirSync(sourceDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
1285
|
-
for (const entry of entries) {
|
|
1286
|
-
const sourcePath = path.join(sourceDir, entry.name);
|
|
1287
|
-
const targetName = entry.name === 'gitignore' ? '.gitignore' : entry.name;
|
|
1288
|
-
const targetPath = path.join(targetDir, targetName);
|
|
1289
|
-
if (entry.isDirectory()) {
|
|
1290
|
-
copySkeletonTree(sourcePath, targetPath);
|
|
1291
|
-
continue;
|
|
1292
|
-
}
|
|
1293
|
-
if (!entry.isFile()) {
|
|
1294
|
-
continue;
|
|
1295
|
-
}
|
|
1296
|
-
if (fs.existsSync(targetPath)) {
|
|
1297
|
-
continue;
|
|
1298
|
-
}
|
|
1299
|
-
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
1300
|
-
fs.copyFileSync(sourcePath, targetPath);
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
function renderProfile(projectRoot, agentDir, checkOnly = false, targetTool = null, trace, options) {
|
|
1304
|
-
const configRoot = options?.configRoot ?? defaultConfigRoot(projectRoot);
|
|
1305
|
-
const profile = options?.profile ?? resolveProfile(projectRoot);
|
|
1306
|
-
const env = createNunjucksEnv(configRoot);
|
|
1307
|
-
const manifest = normalizeManifest(loadManifest(configRoot, agentDir, profile, trace), profile);
|
|
1308
|
-
if (manifest.profile !== profile) {
|
|
1309
|
-
const parsed = parseProfile(manifest.profile, `${path.join(agentDir, MANIFEST_FILE_NAME)} profile`);
|
|
1310
|
-
if (parsed.profile === null) {
|
|
1311
|
-
throw new Error(parsed.reason);
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1314
|
-
if (!checkOnly) {
|
|
1315
|
-
ensureConfigRootGitignore(configRoot);
|
|
1316
|
-
}
|
|
1317
|
-
const context = buildRenderContext(projectRoot, agentDir, configRoot, manifest, env);
|
|
1318
|
-
const sections = [renderTemplateFile(env, configRoot, manifest.agent.base, context, trace)];
|
|
1319
|
-
for (const include of manifest.agent.includes) {
|
|
1320
|
-
const includePath = resolveConfigPath(configRoot, include, context);
|
|
1321
|
-
if (!fs.existsSync(includePath)) {
|
|
1322
|
-
if (include.includes('AGENTS-MODS.md') || include.includes(LOCAL_TEMPLATE_FILE_NAME)) {
|
|
1323
|
-
continue;
|
|
1324
|
-
}
|
|
1325
|
-
throw new Error(`missing template: ${includePath}`);
|
|
1326
|
-
}
|
|
1327
|
-
sections.push(renderTemplateFile(env, configRoot, include, context, trace));
|
|
1328
|
-
}
|
|
1329
|
-
context.renderedAgentSections = sections.map((section, index) => {
|
|
1330
|
-
const label = index === 0 ? manifest.agent.base : manifest.agent.includes[index - 1] ?? `section ${index}`;
|
|
1331
|
-
assertNoUnexpandedTemplateVars(label, section);
|
|
1332
|
-
return section.trimEnd();
|
|
1333
|
-
});
|
|
1334
|
-
context.skills = renderSkills(env, configRoot, manifest, context, trace);
|
|
1335
|
-
const files = [];
|
|
1336
|
-
if (context.tools.codex && (targetTool === null || targetTool === 'codex')) {
|
|
1337
|
-
const agentsContent = renderToolInstructions('AGENTS.md', env, configRoot, context, false, trace);
|
|
1338
|
-
files.push({ path: path.join(context.paths.codexHomeDir, 'AGENTS.md'), content: agentsContent });
|
|
1339
|
-
files.push({ path: path.join(context.paths.codexHomeDir, 'config.toml'), content: renderCodexConfig(env, configRoot, context, trace) });
|
|
1340
|
-
files.push(...renderNativeSkillFiles(context, 'codex'));
|
|
1341
|
-
}
|
|
1342
|
-
if (context.tools.claude && (targetTool === null || targetTool === 'claude')) {
|
|
1343
|
-
const claudeContent = renderToolInstructions('CLAUDE.md', env, configRoot, context, true, trace);
|
|
1344
|
-
files.push({ path: path.join(context.paths.liveDir, 'CLAUDE.md'), content: claudeContent });
|
|
1345
|
-
files.push({
|
|
1346
|
-
path: path.join(context.paths.liveDir, '.claude', 'agent-run-settings.json'),
|
|
1347
|
-
content: renderClaudeSettings(env, configRoot, context, trace)
|
|
1348
|
-
});
|
|
1349
|
-
files.push({
|
|
1350
|
-
path: path.join(context.paths.liveDir, '.claude', '.claude-plugin', 'plugin.json'),
|
|
1351
|
-
content: claudePluginManifestContent(context)
|
|
1352
|
-
});
|
|
1353
|
-
files.push(...renderNativeSkillFiles(context, 'claude'));
|
|
1354
|
-
}
|
|
1355
|
-
files.push(...renderGuardShims(context));
|
|
1356
|
-
return {
|
|
1357
|
-
agentDir,
|
|
1358
|
-
configRoot,
|
|
1359
|
-
profile,
|
|
1360
|
-
context,
|
|
1361
|
-
files,
|
|
1362
|
-
skills: context.skills
|
|
1363
|
-
};
|
|
1364
|
-
}
|
|
1365
|
-
function syncAgentProfile(projectRoot, agentDir, options) {
|
|
1366
|
-
const rendered = renderProfile(projectRoot, agentDir, false, null, undefined, options);
|
|
1367
|
-
syncRuntimeDirs(rendered.context);
|
|
1368
|
-
removeStaleGeneratedEntries(rendered);
|
|
1369
|
-
for (const file of rendered.files) {
|
|
1370
|
-
writeGeneratedFile(file.path, file.content, file.executable ?? false);
|
|
1371
|
-
}
|
|
1372
|
-
removeLegacyGeneratedCodexFiles(rendered.context);
|
|
1373
|
-
removeLegacyGeneratedClaudeSettings(rendered.context);
|
|
1374
|
-
return rendered;
|
|
1375
|
-
}
|
|
1376
|
-
function removeStaleGeneratedEntries(rendered) {
|
|
1377
|
-
const expectedFiles = new Set(rendered.files.map((file) => path.resolve(file.path)));
|
|
1378
|
-
for (const filePath of [
|
|
1379
|
-
path.join(rendered.context.paths.codexHomeDir, 'AGENTS.md'),
|
|
1380
|
-
path.join(rendered.context.paths.codexHomeDir, 'config.toml'),
|
|
1381
|
-
path.join(rendered.context.paths.liveDir, 'CLAUDE.md'),
|
|
1382
|
-
path.join(rendered.context.paths.liveDir, '.claude', 'CLAUDE.md'),
|
|
1383
|
-
path.join(rendered.context.paths.liveDir, '.claude', 'agent-run-settings.json'),
|
|
1384
|
-
path.join(rendered.context.paths.liveDir, '.claude', '.claude-plugin', 'plugin.json')
|
|
1385
|
-
]) {
|
|
1386
|
-
if (!expectedFiles.has(path.resolve(filePath))) {
|
|
1387
|
-
fs.rmSync(filePath, { force: true });
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
for (const name of ['git', 'git.cmd', 'npm', 'npm.cmd', 'pnpm', 'pnpm.cmd', 'gh', 'gh.cmd']) {
|
|
1391
|
-
const filePath = path.join(rendered.context.paths.binDir, name);
|
|
1392
|
-
if (!expectedFiles.has(path.resolve(filePath))) {
|
|
1393
|
-
fs.rmSync(filePath, { force: true });
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
const skillNames = new Set(rendered.skills.map((skill) => skill.name));
|
|
1397
|
-
removeStaleGeneratedSkills(rendered.context.paths.codexSkillsDir, rendered.context.tools.codex ? skillNames : new Set(), new Set(['.system']));
|
|
1398
|
-
removeStaleGeneratedSkills(rendered.context.paths.claudeSkillsDir, rendered.context.tools.claude ? skillNames : new Set());
|
|
1399
|
-
}
|
|
1400
|
-
function removeStaleGeneratedSkills(dir, expectedNames, preservedNames = new Set()) {
|
|
1401
|
-
if (!fs.existsSync(dir)) {
|
|
1402
|
-
return;
|
|
1403
|
-
}
|
|
1404
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
1405
|
-
if (expectedNames.has(entry.name) || preservedNames.has(entry.name)) {
|
|
1406
|
-
continue;
|
|
1407
|
-
}
|
|
1408
|
-
fs.rmSync(path.join(dir, entry.name), { recursive: true, force: true });
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
function removeLegacyGeneratedCodexFiles(context) {
|
|
1412
|
-
for (const legacyPath of [
|
|
1413
|
-
path.join(context.paths.liveDir, 'AGENTS.md'),
|
|
1414
|
-
path.join(context.paths.liveDir, 'config.toml')
|
|
1415
|
-
]) {
|
|
1416
|
-
if (!fs.existsSync(legacyPath)) {
|
|
1417
|
-
continue;
|
|
1418
|
-
}
|
|
1419
|
-
fs.rmSync(legacyPath);
|
|
1420
|
-
verbose(`removed legacy generated Codex file ${legacyPath}`);
|
|
1421
|
-
}
|
|
1422
|
-
}
|
|
1423
|
-
function removeLegacyGeneratedClaudeSettings(context) {
|
|
1424
|
-
for (const legacyPath of [
|
|
1425
|
-
path.join(context.paths.liveDir, '.claude', 'settings.json'),
|
|
1426
|
-
path.join(context.profileDir, '.claude', 'settings.json')
|
|
1427
|
-
]) {
|
|
1428
|
-
if (!fs.existsSync(legacyPath)) {
|
|
1429
|
-
continue;
|
|
1430
|
-
}
|
|
1431
|
-
const actual = fs.readFileSync(legacyPath, 'utf8').replace(/\r\n/g, '\n');
|
|
1432
|
-
const legacyGeneratedContents = [
|
|
1433
|
-
legacyDefaultClaudeSettingsContent(context, context.agentDir),
|
|
1434
|
-
legacyDefaultClaudeSettingsContent(context, context.profileDir)
|
|
1435
|
-
];
|
|
1436
|
-
if (legacyGeneratedContents.includes(actual)) {
|
|
1437
|
-
fs.rmSync(legacyPath);
|
|
1438
|
-
verbose(`removed legacy generated Claude settings ${legacyPath}`);
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
function checkRenderedProfile(projectRoot, agentDir) {
|
|
1443
|
-
const findings = [];
|
|
1444
|
-
const configRoot = defaultConfigRoot(projectRoot);
|
|
1445
|
-
try {
|
|
1446
|
-
const rendered = renderProfile(projectRoot, agentDir, true);
|
|
1447
|
-
for (const file of rendered.files) {
|
|
1448
|
-
if (!fs.existsSync(file.path)) {
|
|
1449
|
-
findings.push({ message: `missing generated file: ${file.path}`, severity: 'ERROR' });
|
|
1450
|
-
continue;
|
|
1451
|
-
}
|
|
1452
|
-
const actual = fs.readFileSync(file.path, 'utf8').replace(/\r\n/g, '\n');
|
|
1453
|
-
if (actual !== file.content) {
|
|
1454
|
-
findings.push({ message: `generated file is out of date: ${file.path}`, severity: 'ERROR' });
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
for (const dir of [
|
|
1458
|
-
rendered.context.paths.reviewDir,
|
|
1459
|
-
rendered.context.paths.memoriesDir,
|
|
1460
|
-
rendered.context.paths.codexHomeDir,
|
|
1461
|
-
rendered.context.paths.liveDir,
|
|
1462
|
-
rendered.context.paths.overridesDir,
|
|
1463
|
-
rendered.context.paths.codexSkillsDir,
|
|
1464
|
-
rendered.context.paths.claudeSkillsDir,
|
|
1465
|
-
rendered.context.paths.binDir
|
|
1466
|
-
]) {
|
|
1467
|
-
if (!fs.existsSync(dir)) {
|
|
1468
|
-
findings.push({ message: `missing generated runtime directory: ${dir}`, severity: 'ERROR' });
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1471
|
-
for (const skill of rendered.skills) {
|
|
1472
|
-
validateRenderedSkill(skill.renderedContent, skill.sourcePath);
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
|
-
catch (error) {
|
|
1476
|
-
findings.push({
|
|
1477
|
-
message: `failed to render profile ${agentDir}: ${formatError(error)}`,
|
|
1478
|
-
severity: 'ERROR'
|
|
1479
|
-
});
|
|
1480
|
-
}
|
|
1481
|
-
const gitignorePath = path.join(configRoot, '.gitignore');
|
|
1482
|
-
if (!fs.existsSync(gitignorePath)) {
|
|
1483
|
-
findings.push({ message: `missing config root .gitignore: ${gitignorePath}`, severity: 'ERROR' });
|
|
1484
|
-
}
|
|
1485
|
-
else {
|
|
1486
|
-
const gitignore = fs.readFileSync(gitignorePath, 'utf8');
|
|
1487
|
-
for (const entry of GENERATED_GITIGNORE_ENTRIES) {
|
|
1488
|
-
if (entry && entry.startsWith('#') === false && !gitignore.includes(entry)) {
|
|
1489
|
-
findings.push({ message: `config root .gitignore missing entry: ${entry}`, severity: 'ERROR' });
|
|
1490
|
-
}
|
|
1491
|
-
}
|
|
1492
|
-
}
|
|
1493
|
-
return findings;
|
|
1494
|
-
}
|
|
1495
|
-
function renderSkills(env, configRoot, manifest, context, trace) {
|
|
1496
|
-
const skills = [];
|
|
1497
|
-
for (const name of manifest.skills.install) {
|
|
1498
|
-
const sourceTemplate = manifest.skills.overrides[name] ?? `global/skills/${name}/SKILL.md.njk`;
|
|
1499
|
-
const sourcePath = resolveConfigPath(configRoot, sourceTemplate, context);
|
|
1500
|
-
if (!fs.existsSync(sourcePath)) {
|
|
1501
|
-
throw new Error(`missing skill template for ${name}: ${sourcePath}`);
|
|
1502
|
-
}
|
|
1503
|
-
const renderedContent = renderTemplateFile(env, configRoot, sourceTemplate, context, trace);
|
|
1504
|
-
assertNoUnexpandedTemplateVars(`skill ${name}`, renderedContent);
|
|
1505
|
-
validateRenderedSkill(renderedContent, sourcePath);
|
|
1506
|
-
const description = extractSkillDescription(renderedContent);
|
|
1507
|
-
skills.push({ name, sourcePath, renderedContent, description });
|
|
1508
|
-
}
|
|
1509
|
-
return skills;
|
|
1510
|
-
}
|
|
1511
|
-
function renderNativeSkillFiles(context, targetTool = null) {
|
|
1512
|
-
const files = [];
|
|
1513
|
-
for (const skill of context.skills) {
|
|
1514
|
-
if (targetTool === null || targetTool === 'codex') {
|
|
1515
|
-
files.push({
|
|
1516
|
-
path: path.join(context.paths.codexSkillsDir, skill.name, 'SKILL.md'),
|
|
1517
|
-
content: skill.renderedContent
|
|
1518
|
-
});
|
|
1519
|
-
}
|
|
1520
|
-
if (targetTool === null || targetTool === 'claude') {
|
|
1521
|
-
files.push({
|
|
1522
|
-
path: path.join(context.paths.claudeSkillsDir, skill.name, 'SKILL.md'),
|
|
1523
|
-
content: skill.renderedContent
|
|
1524
|
-
});
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
return files;
|
|
1528
|
-
}
|
|
1529
|
-
function renderToolInstructions(templateName, env, configRoot, context, isClaude, trace) {
|
|
1530
|
-
const templatePath = `global/tool-templates/${templateName}.njk`;
|
|
1531
|
-
const absoluteTemplate = path.join(configRoot, templatePath);
|
|
1532
|
-
const content = fs.existsSync(absoluteTemplate)
|
|
1533
|
-
? renderTemplateFile(env, configRoot, templatePath, context, trace)
|
|
1534
|
-
: renderInlineTemplate(env, defaultToolInstructionsTemplate(isClaude), context);
|
|
1535
|
-
assertNoUnexpandedTemplateVars(templateName, content);
|
|
1536
|
-
return content.replace(/\n*$/, '\n');
|
|
1537
|
-
}
|
|
1538
|
-
function renderCodexConfig(env, configRoot, context, trace) {
|
|
1539
|
-
const templatePath = resolveProfileOverrideTemplate(configRoot, context, 'codex-config.toml.njk', 'global/tool-templates/codex-config.toml.njk');
|
|
1540
|
-
const content = templatePath !== null
|
|
1541
|
-
? renderTemplateFile(env, configRoot, templatePath, context, trace)
|
|
1542
|
-
: defaultCodexConfigContent(context);
|
|
1543
|
-
assertNoUnexpandedTemplateVars('config.toml', content);
|
|
1544
|
-
return content.replace(/\n*$/, '\n');
|
|
1545
|
-
}
|
|
1546
|
-
function renderClaudeSettings(env, configRoot, context, trace) {
|
|
1547
|
-
const templatePath = resolveProfileOverrideTemplate(configRoot, context, 'claude-settings.json.njk', 'global/tool-templates/claude-settings.json.njk');
|
|
1548
|
-
const content = templatePath !== null
|
|
1549
|
-
? renderTemplateFile(env, configRoot, templatePath, context, trace)
|
|
1550
|
-
: defaultClaudeSettingsContent(context);
|
|
1551
|
-
assertNoUnexpandedTemplateVars('claude settings.json', content);
|
|
1552
|
-
try {
|
|
1553
|
-
JSON.parse(content);
|
|
1554
|
-
}
|
|
1555
|
-
catch (error) {
|
|
1556
|
-
throw new Error(`invalid generated Claude settings JSON: ${formatError(error)}`);
|
|
1557
|
-
}
|
|
1558
|
-
return content.replace(/\n*$/, '\n');
|
|
1559
|
-
}
|
|
1560
|
-
function claudePluginManifestContent(context) {
|
|
1561
|
-
return `${JSON.stringify({
|
|
1562
|
-
name: 'agent-run-profile',
|
|
1563
|
-
description: `Generated skills for agent-run profile ${context.profile}`,
|
|
1564
|
-
version: '1.0.0',
|
|
1565
|
-
author: {
|
|
1566
|
-
name: 'Technomoron'
|
|
1567
|
-
}
|
|
1568
|
-
}, null, 2)}\n`;
|
|
1569
|
-
}
|
|
1570
|
-
function resolveProfileOverrideTemplate(configRoot, context, overrideFileName, globalTemplatePath) {
|
|
1571
|
-
const overridePath = `${context.profile}/overrides/${overrideFileName}`;
|
|
1572
|
-
if (fs.existsSync(path.join(configRoot, overridePath))) {
|
|
1573
|
-
return overridePath;
|
|
1574
|
-
}
|
|
1575
|
-
if (fs.existsSync(path.join(configRoot, globalTemplatePath))) {
|
|
1576
|
-
return globalTemplatePath;
|
|
1577
|
-
}
|
|
1578
|
-
return null;
|
|
1579
|
-
}
|
|
1580
|
-
function renderGuardShims(context) {
|
|
1581
|
-
const names = [];
|
|
1582
|
-
if (context.guardrails.blockGitWrite) {
|
|
1583
|
-
names.push('git');
|
|
1584
|
-
}
|
|
1585
|
-
if (context.guardrails.blockPublish) {
|
|
1586
|
-
names.push('npm', 'pnpm');
|
|
1587
|
-
}
|
|
1588
|
-
if (context.guardrails.blockGithubRelease) {
|
|
1589
|
-
names.push('gh');
|
|
1590
|
-
}
|
|
1591
|
-
if (IS_WINDOWS) {
|
|
1592
|
-
return names.map((name) => ({
|
|
1593
|
-
path: path.join(context.paths.binDir, `${name}.cmd`),
|
|
1594
|
-
content: windowsShim(name)
|
|
1595
|
-
}));
|
|
1596
|
-
}
|
|
1597
|
-
return names.map((name) => ({
|
|
1598
|
-
path: path.join(context.paths.binDir, name),
|
|
1599
|
-
content: name === 'git'
|
|
1600
|
-
? posixGitShim()
|
|
1601
|
-
: name === 'gh'
|
|
1602
|
-
? posixGhShim()
|
|
1603
|
-
: posixPublishShim(name),
|
|
1604
|
-
executable: true
|
|
1605
|
-
}));
|
|
1606
|
-
}
|
|
1607
|
-
function syncRuntimeDirs(context) {
|
|
1608
|
-
for (const dir of [
|
|
1609
|
-
context.paths.reviewDir,
|
|
1610
|
-
context.paths.memoriesDir,
|
|
1611
|
-
context.paths.codexHomeDir,
|
|
1612
|
-
context.paths.overridesDir,
|
|
1613
|
-
context.paths.codexSkillsDir,
|
|
1614
|
-
context.paths.claudeSkillsDir,
|
|
1615
|
-
context.paths.binDir,
|
|
1616
|
-
path.join(context.paths.liveDir, '.claude')
|
|
1617
|
-
]) {
|
|
1618
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
1619
|
-
}
|
|
1620
|
-
migrateLiveReviewFiles(context);
|
|
1621
|
-
removeLegacyCodexSkillDirs(context);
|
|
1622
|
-
removeLegacyCodeReviewSkillDirs(context);
|
|
1623
|
-
ensureSharedCodexAuth(context.paths.codexHomeDir);
|
|
1624
|
-
}
|
|
1625
|
-
function ensureSharedCodexAuth(codexHomeDir) {
|
|
1626
|
-
const sharedAuthPath = path.join(os.homedir(), '.codex', 'auth.json');
|
|
1627
|
-
if (!fs.existsSync(sharedAuthPath)) {
|
|
1628
|
-
return;
|
|
1629
|
-
}
|
|
1630
|
-
const profileAuthPath = path.join(codexHomeDir, 'auth.json');
|
|
1631
|
-
if (path.resolve(profileAuthPath) === path.resolve(sharedAuthPath)) {
|
|
1632
|
-
return;
|
|
1633
|
-
}
|
|
1634
|
-
if (fs.existsSync(profileAuthPath) || isSymlink(profileAuthPath)) {
|
|
1635
|
-
if (isSymlinkTo(profileAuthPath, sharedAuthPath)) {
|
|
1636
|
-
return;
|
|
1637
|
-
}
|
|
1638
|
-
const backupPath = nextBackupPath(profileAuthPath);
|
|
1639
|
-
fs.renameSync(profileAuthPath, backupPath);
|
|
1640
|
-
verbose(`backed up profile Codex auth ${profileAuthPath} -> ${backupPath}`);
|
|
1641
|
-
}
|
|
1642
|
-
fs.mkdirSync(path.dirname(profileAuthPath), { recursive: true });
|
|
1643
|
-
try {
|
|
1644
|
-
fs.symlinkSync(sharedAuthPath, profileAuthPath);
|
|
1645
|
-
verbose(`linked profile Codex auth ${profileAuthPath} -> ${sharedAuthPath}`);
|
|
1646
|
-
}
|
|
1647
|
-
catch (error) {
|
|
1648
|
-
if (IS_WINDOWS) {
|
|
1649
|
-
fs.copyFileSync(sharedAuthPath, profileAuthPath);
|
|
1650
|
-
verbose(`copied shared Codex auth ${sharedAuthPath} -> ${profileAuthPath}`);
|
|
1651
|
-
return;
|
|
1652
|
-
}
|
|
1653
|
-
throw error;
|
|
1654
|
-
}
|
|
1655
|
-
}
|
|
1656
|
-
function isSymlink(filePath) {
|
|
1657
|
-
try {
|
|
1658
|
-
return fs.lstatSync(filePath).isSymbolicLink();
|
|
1659
|
-
}
|
|
1660
|
-
catch {
|
|
1661
|
-
return false;
|
|
1662
|
-
}
|
|
1663
|
-
}
|
|
1664
|
-
function isSymlinkTo(filePath, targetPath) {
|
|
1665
|
-
try {
|
|
1666
|
-
if (!fs.lstatSync(filePath).isSymbolicLink()) {
|
|
1667
|
-
return false;
|
|
1668
|
-
}
|
|
1669
|
-
const linkTarget = fs.readlinkSync(filePath);
|
|
1670
|
-
const resolvedTarget = path.resolve(path.dirname(filePath), linkTarget);
|
|
1671
|
-
return path.resolve(resolvedTarget) === path.resolve(targetPath);
|
|
1672
|
-
}
|
|
1673
|
-
catch {
|
|
1674
|
-
return false;
|
|
1675
|
-
}
|
|
1676
|
-
}
|
|
1677
|
-
function nextBackupPath(filePath) {
|
|
1678
|
-
const timestamp = new Date().toISOString().replace(/[-:TZ.]/g, '').slice(0, 14);
|
|
1679
|
-
let candidate = `${filePath}.bak.${timestamp}`;
|
|
1680
|
-
let index = 1;
|
|
1681
|
-
while (fs.existsSync(candidate) || isSymlink(candidate)) {
|
|
1682
|
-
candidate = `${filePath}.bak.${timestamp}.${index}`;
|
|
1683
|
-
index += 1;
|
|
1684
|
-
}
|
|
1685
|
-
return candidate;
|
|
1686
|
-
}
|
|
1687
|
-
function removeLegacyCodexSkillDirs(context) {
|
|
1688
|
-
const legacyCodexDir = path.join(context.paths.liveDir, '.agents');
|
|
1689
|
-
if (isSamePathOrDescendant(context.paths.codexSkillsDir, legacyCodexDir)) {
|
|
1690
|
-
return;
|
|
1691
|
-
}
|
|
1692
|
-
fs.rmSync(legacyCodexDir, { recursive: true, force: true });
|
|
1693
|
-
}
|
|
1694
|
-
function removeLegacyCodeReviewSkillDirs(context) {
|
|
1695
|
-
if (!context.skills.some((skill) => skill.name === 'code-review-organizer')) {
|
|
1696
|
-
return;
|
|
1697
|
-
}
|
|
1698
|
-
for (const dir of [
|
|
1699
|
-
path.join(context.paths.liveDir, '.agents', 'skills', 'code-review'),
|
|
1700
|
-
path.join(context.paths.codexSkillsDir, 'code-review'),
|
|
1701
|
-
path.join(context.paths.claudeSkillsDir, 'code-review')
|
|
1702
|
-
]) {
|
|
1703
|
-
if (fs.existsSync(dir)) {
|
|
1704
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1707
|
-
}
|
|
1708
|
-
function migrateLiveReviewFiles(context) {
|
|
1709
|
-
const legacyReviewDir = path.join(context.paths.liveDir, 'reviews');
|
|
1710
|
-
if (path.resolve(legacyReviewDir) === path.resolve(context.paths.reviewDir) || !fs.existsSync(legacyReviewDir)) {
|
|
1711
|
-
return;
|
|
1712
|
-
}
|
|
1713
|
-
fs.mkdirSync(context.paths.reviewDir, { recursive: true });
|
|
1714
|
-
for (const entry of fs.readdirSync(legacyReviewDir, { withFileTypes: true })) {
|
|
1715
|
-
if (!entry.isFile()) {
|
|
1716
|
-
continue;
|
|
1717
|
-
}
|
|
1718
|
-
const sourcePath = path.join(legacyReviewDir, entry.name);
|
|
1719
|
-
let targetPath = path.join(context.paths.reviewDir, entry.name);
|
|
1720
|
-
if (fs.existsSync(targetPath)) {
|
|
1721
|
-
if (filesHaveSameContent(sourcePath, targetPath)) {
|
|
1722
|
-
fs.rmSync(sourcePath);
|
|
1723
|
-
continue;
|
|
1724
|
-
}
|
|
1725
|
-
targetPath = nextAvailablePath(context.paths.reviewDir, entry.name);
|
|
1726
|
-
}
|
|
1727
|
-
moveFile(sourcePath, targetPath);
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
|
-
function filesHaveSameContent(leftPath, rightPath) {
|
|
1731
|
-
const left = fs.readFileSync(leftPath);
|
|
1732
|
-
const right = fs.readFileSync(rightPath);
|
|
1733
|
-
return left.length === right.length && left.equals(right);
|
|
1734
|
-
}
|
|
1735
|
-
function nextAvailablePath(dir, filename) {
|
|
1736
|
-
const parsed = path.parse(filename);
|
|
1737
|
-
for (let sequence = 1;; sequence += 1) {
|
|
1738
|
-
const candidate = path.join(dir, `${parsed.name}.${sequence}${parsed.ext}`);
|
|
1739
|
-
if (!fs.existsSync(candidate)) {
|
|
1740
|
-
return candidate;
|
|
1741
|
-
}
|
|
1742
|
-
}
|
|
1743
|
-
}
|
|
1744
|
-
function moveFile(sourcePath, targetPath) {
|
|
1745
|
-
try {
|
|
1746
|
-
fs.renameSync(sourcePath, targetPath);
|
|
1747
|
-
}
|
|
1748
|
-
catch (error) {
|
|
1749
|
-
const code = error.code;
|
|
1750
|
-
if (code !== 'EXDEV') {
|
|
1751
|
-
throw error;
|
|
1752
|
-
}
|
|
1753
|
-
fs.copyFileSync(sourcePath, targetPath);
|
|
1754
|
-
fs.rmSync(sourcePath);
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
function validateRenderedSkill(content, sourcePath) {
|
|
1758
|
-
const frontMatter = /^---\n([\s\S]*?)\n---\n/.exec(content.replace(/\r\n/g, '\n'));
|
|
1759
|
-
if (frontMatter === null) {
|
|
1760
|
-
throw new Error(`generated skill missing YAML front matter: ${sourcePath}`);
|
|
1761
|
-
}
|
|
1762
|
-
const yaml = frontMatter[1] ?? '';
|
|
1763
|
-
if (!/^name:\s*\S+/m.test(yaml)) {
|
|
1764
|
-
throw new Error(`generated skill missing name: ${sourcePath}`);
|
|
1765
|
-
}
|
|
1766
|
-
if (!/^description:\s*(?:\S|>\s*$)/m.test(yaml)) {
|
|
1767
|
-
throw new Error(`generated skill missing description: ${sourcePath}`);
|
|
1768
|
-
}
|
|
1769
|
-
}
|
|
1770
|
-
function extractSkillDescription(content) {
|
|
1771
|
-
const normalized = content.replace(/\r\n/g, '\n');
|
|
1772
|
-
const simple = /^description:\s*['"]?(.+?)['"]?\s*$/m.exec(normalized);
|
|
1773
|
-
if (simple?.[1]) {
|
|
1774
|
-
return simple[1].trim();
|
|
1775
|
-
}
|
|
1776
|
-
const folded = /^description:\s*>\s*\n((?:[ \t]+.+\n?)+)/m.exec(normalized);
|
|
1777
|
-
if (folded?.[1]) {
|
|
1778
|
-
return folded[1]
|
|
1779
|
-
.split('\n')
|
|
1780
|
-
.map((line) => line.trim())
|
|
1781
|
-
.filter(Boolean)
|
|
1782
|
-
.join(' ');
|
|
1783
|
-
}
|
|
1784
|
-
return '';
|
|
1785
|
-
}
|
|
1786
|
-
function renderLegacyAgentsMods(sourceFile, stack = [], trace) {
|
|
1787
|
-
const resolvedSource = path.resolve(sourceFile);
|
|
1788
|
-
trace?.sourceFiles.add(resolvedSource);
|
|
1789
|
-
if (stack.includes(resolvedSource)) {
|
|
1790
|
-
throw new Error(`Include cycle detected: ${[...stack, resolvedSource].join(' -> ')}`);
|
|
1791
|
-
}
|
|
1792
|
-
const lines = fs.readFileSync(resolvedSource, 'utf8').replace(/\r\n/g, '\n').split('\n');
|
|
1793
|
-
const output = [];
|
|
1794
|
-
const nextStack = [...stack, resolvedSource];
|
|
1795
|
-
let sawLeadingInclude = false;
|
|
1796
|
-
let insertedOverrideNote = false;
|
|
1797
|
-
let contentStarted = false;
|
|
1798
|
-
let fence = null;
|
|
1799
|
-
for (const line of lines) {
|
|
1800
|
-
const trimmed = line.trim();
|
|
1801
|
-
const isIndentedCode = /^(?: {4}|\t)/.test(line);
|
|
1802
|
-
const fenceMatch = isIndentedCode ? null : /^(`{3,}|~{3,})/.exec(trimmed);
|
|
1803
|
-
if (fence !== null) {
|
|
1804
|
-
output.push(line);
|
|
1805
|
-
contentStarted = true;
|
|
1806
|
-
const marker = fenceMatch?.[1];
|
|
1807
|
-
if (marker !== undefined &&
|
|
1808
|
-
marker[0] === fence.character &&
|
|
1809
|
-
marker.length >= fence.length &&
|
|
1810
|
-
trimmed.slice(marker.length).trim() === '') {
|
|
1811
|
-
fence = null;
|
|
1812
|
-
}
|
|
1813
|
-
continue;
|
|
1814
|
-
}
|
|
1815
|
-
if (!contentStarted && trimmed === '') {
|
|
1816
|
-
continue;
|
|
1817
|
-
}
|
|
1818
|
-
if (fenceMatch?.[1]) {
|
|
1819
|
-
fence = { character: fenceMatch[1][0] ?? '`', length: fenceMatch[1].length };
|
|
1820
|
-
}
|
|
1821
|
-
if (fence === null && !isIndentedCode && trimmed.startsWith('@')) {
|
|
1822
|
-
const includePath = trimmed.slice(1).trim();
|
|
1823
|
-
if (!includePath) {
|
|
1824
|
-
continue;
|
|
1825
|
-
}
|
|
1826
|
-
const resolvedInclude = resolveIncludePath(resolvedSource, includePath);
|
|
1827
|
-
trace?.sourceFiles.add(resolvedInclude);
|
|
1828
|
-
output.push(renderLegacyAgentsMods(resolvedInclude, nextStack, trace));
|
|
1829
|
-
if (!contentStarted) {
|
|
1830
|
-
sawLeadingInclude = true;
|
|
1831
|
-
}
|
|
1832
|
-
continue;
|
|
1833
|
-
}
|
|
1834
|
-
if (sawLeadingInclude && !insertedOverrideNote) {
|
|
1835
|
-
output.push('');
|
|
1836
|
-
output.push('If anything below this point conflicts with anything included above,');
|
|
1837
|
-
output.push('the later instructions below take precedence.');
|
|
1838
|
-
output.push('');
|
|
1839
|
-
insertedOverrideNote = true;
|
|
1840
|
-
}
|
|
1841
|
-
output.push(line);
|
|
1842
|
-
contentStarted = true;
|
|
1843
|
-
}
|
|
1844
|
-
return output.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
|
|
1845
|
-
}
|
|
1846
|
-
function resolveIncludePath(sourceFile, includePath) {
|
|
1847
|
-
if (path.isAbsolute(includePath)) {
|
|
1848
|
-
return includePath;
|
|
1849
|
-
}
|
|
1850
|
-
const sourceRelativePath = path.resolve(path.dirname(sourceFile), includePath);
|
|
1851
|
-
if (fs.existsSync(sourceRelativePath)) {
|
|
1852
|
-
return sourceRelativePath;
|
|
1853
|
-
}
|
|
1854
|
-
const includeRoot = findIncludeRoot(sourceFile);
|
|
1855
|
-
return path.resolve(includeRoot, includePath.replace(/^(\.\.\/)+/, ''));
|
|
1856
|
-
}
|
|
1857
|
-
function findIncludeRoot(sourceFile) {
|
|
1858
|
-
let dir = path.dirname(sourceFile);
|
|
1859
|
-
for (;;) {
|
|
1860
|
-
if (fs.existsSync(path.join(dir, 'global')) || fs.existsSync(path.join(dir, 'templates'))) {
|
|
1861
|
-
return dir;
|
|
1862
|
-
}
|
|
1863
|
-
const parent = path.dirname(dir);
|
|
1864
|
-
if (parent === dir) {
|
|
1865
|
-
return path.dirname(sourceFile);
|
|
1866
|
-
}
|
|
1867
|
-
dir = parent;
|
|
1868
|
-
}
|
|
1869
|
-
}
|
|
1870
|
-
function checkProject(projectRoot) {
|
|
1871
|
-
const findings = [];
|
|
1872
|
-
verbose(`checking project ${projectRoot}`);
|
|
1873
|
-
const profileResult = resolveProfileResult(projectRoot);
|
|
1874
|
-
if (profileResult.profile === null) {
|
|
1875
|
-
for (const file of findLocalAiFiles(projectRoot, null)) {
|
|
1876
|
-
findings.push({ message: `local AI file in project: ${file}`, severity: 'ERROR' });
|
|
1877
|
-
}
|
|
1878
|
-
findings.push({ message: profileResult.reason, severity: 'ERROR' });
|
|
1879
|
-
return findings;
|
|
1880
|
-
}
|
|
1881
|
-
const agentDir = resolveAgentDir(projectRoot);
|
|
1882
|
-
let forbidRepoAiFiles = true;
|
|
1883
|
-
try {
|
|
1884
|
-
const manifest = normalizeManifest(loadManifest(defaultConfigRoot(projectRoot), agentDir, profileResult.profile), profileResult.profile);
|
|
1885
|
-
forbidRepoAiFiles = manifest.guardrails.forbidRepoAiFiles;
|
|
1886
|
-
}
|
|
1887
|
-
catch {
|
|
1888
|
-
// The profile check below reports malformed or missing source details.
|
|
1889
|
-
}
|
|
1890
|
-
if (forbidRepoAiFiles) {
|
|
1891
|
-
for (const file of findLocalAiFiles(projectRoot, agentDir)) {
|
|
1892
|
-
findings.push({ message: `local AI file in project: ${file}`, severity: 'ERROR' });
|
|
1893
|
-
}
|
|
1894
|
-
}
|
|
1895
|
-
findings.push(...checkAgentDirectory(projectRoot, agentDir));
|
|
1896
|
-
return findings;
|
|
1897
|
-
}
|
|
1898
|
-
function checkAgentDirectory(projectRoot, agentDir) {
|
|
1899
|
-
const findings = [];
|
|
1900
|
-
const configRoot = defaultConfigRoot(projectRoot);
|
|
1901
|
-
const manifestPath = path.join(agentDir, MANIFEST_FILE_NAME);
|
|
1902
|
-
const legacyPath = path.join(agentDir, 'AGENTS-MODS.md');
|
|
1903
|
-
const localPath = path.join(agentDir, LOCAL_TEMPLATE_FILE_NAME);
|
|
1904
|
-
if (!fs.existsSync(manifestPath) && !fs.existsSync(legacyPath) && !fs.existsSync(localPath)) {
|
|
1905
|
-
findings.push({ message: `missing manifest or legacy/local source file in ${agentDir}`, severity: 'ERROR' });
|
|
1906
|
-
return findings;
|
|
1907
|
-
}
|
|
1908
|
-
for (const relativeTemplate of REQUIRED_GLOBAL_TEMPLATES) {
|
|
1909
|
-
if (!fs.existsSync(path.join(configRoot, relativeTemplate))) {
|
|
1910
|
-
findings.push({
|
|
1911
|
-
message: `missing required global template: ${path.join(configRoot, relativeTemplate)}`,
|
|
1912
|
-
severity: 'ERROR'
|
|
1913
|
-
});
|
|
1914
|
-
}
|
|
1915
|
-
}
|
|
1916
|
-
findings.push(...checkRenderedProfile(projectRoot, agentDir));
|
|
1917
|
-
return findings;
|
|
1918
|
-
}
|
|
1919
|
-
function checkSourceTree(rootPath) {
|
|
1920
|
-
const sourceRepos = findSourceRepos(rootPath);
|
|
1921
|
-
const entries = sourceRepos.map((projectRoot) => ({
|
|
1922
|
-
findings: checkProject(projectRoot),
|
|
1923
|
-
label: projectRoot
|
|
1924
|
-
}));
|
|
1925
|
-
return { entries, hasErrors: entries.length === 0 || entries.some((entry) => hasErrors(entry.findings)), root: rootPath };
|
|
1926
|
-
}
|
|
1927
|
-
function findSourceRepos(rootPath) {
|
|
1928
|
-
const resolvedRoot = path.resolve(rootPath);
|
|
1929
|
-
const repos = new Set();
|
|
1930
|
-
if (isIgnoredDir(resolvedRoot)) {
|
|
1931
|
-
return [];
|
|
1932
|
-
}
|
|
1933
|
-
if (looksLikeRepoRoot(resolvedRoot)) {
|
|
1934
|
-
return [resolvedRoot];
|
|
1935
|
-
}
|
|
1936
|
-
walkSourceTree(resolvedRoot, 0, (fullPath, entry) => {
|
|
1937
|
-
if (!entry.isDirectory()) {
|
|
1938
|
-
return undefined;
|
|
1939
|
-
}
|
|
1940
|
-
if (shouldPruneSourceTreeEntry(entry) || isIgnoredDir(fullPath)) {
|
|
1941
|
-
return 'skip';
|
|
1942
|
-
}
|
|
1943
|
-
if (fs.existsSync(path.join(fullPath, '.git'))) {
|
|
1944
|
-
repos.add(fullPath);
|
|
1945
|
-
return 'skip';
|
|
1946
|
-
}
|
|
1947
|
-
return undefined;
|
|
1948
|
-
});
|
|
1949
|
-
return [...repos].sort();
|
|
1950
|
-
}
|
|
1951
|
-
function looksLikeRepoRoot(dir) {
|
|
1952
|
-
return fs.existsSync(path.join(dir, '.git')) || fs.existsSync(path.join(dir, 'package.json'));
|
|
1953
|
-
}
|
|
1954
|
-
function shouldPruneSourceTreeEntry(entry) {
|
|
1955
|
-
return (entry.name === 'node_modules' ||
|
|
1956
|
-
entry.name === '.pnpm-store' ||
|
|
1957
|
-
entry.name === 'dist' ||
|
|
1958
|
-
entry.name === 'build');
|
|
1959
|
-
}
|
|
1960
|
-
function printProjectReport(projectRoot, findings) {
|
|
1961
|
-
process.stdout.write(`Check: ${projectRoot}\n`);
|
|
1962
|
-
if (findings.length === 0) {
|
|
1963
|
-
process.stdout.write('OK no issues found\n');
|
|
1964
|
-
return;
|
|
1965
|
-
}
|
|
1966
|
-
for (const finding of findings) {
|
|
1967
|
-
process.stdout.write(`${finding.severity} ${finding.message}\n`);
|
|
1968
|
-
}
|
|
1969
|
-
process.stdout.write(`Summary: ${countErrors(findings)} error(s), ${countWarnings(findings)} warning(s)\n`);
|
|
1970
|
-
}
|
|
1971
|
-
function printBatchReport(rootPath, entries) {
|
|
1972
|
-
process.stdout.write(`Check all: ${rootPath}\n`);
|
|
1973
|
-
if (entries.length === 0) {
|
|
1974
|
-
process.stdout.write('ERROR no source repos found\n');
|
|
1975
|
-
return;
|
|
1976
|
-
}
|
|
1977
|
-
let totalErrors = 0;
|
|
1978
|
-
let totalWarnings = 0;
|
|
1979
|
-
for (const entry of entries) {
|
|
1980
|
-
if (entry.findings.length === 0) {
|
|
1981
|
-
process.stdout.write(`OK ${entry.label}\n`);
|
|
1982
|
-
continue;
|
|
1983
|
-
}
|
|
1984
|
-
process.stdout.write(`FAIL ${entry.label}\n`);
|
|
1985
|
-
for (const finding of entry.findings) {
|
|
1986
|
-
process.stdout.write(` ${finding.severity} ${finding.message}\n`);
|
|
1987
|
-
}
|
|
1988
|
-
totalErrors += countErrors(entry.findings);
|
|
1989
|
-
totalWarnings += countWarnings(entry.findings);
|
|
1990
|
-
}
|
|
1991
|
-
process.stdout.write(`Summary: ${totalErrors} error(s), ${totalWarnings} warning(s)\n`);
|
|
1992
|
-
}
|
|
1993
|
-
function hasErrors(findings) {
|
|
1994
|
-
return findings.some((finding) => finding.severity === 'ERROR');
|
|
1995
|
-
}
|
|
1996
|
-
function countErrors(findings) {
|
|
1997
|
-
return findings.filter((finding) => finding.severity === 'ERROR').length;
|
|
1998
|
-
}
|
|
1999
|
-
function countWarnings(findings) {
|
|
2000
|
-
return findings.filter((finding) => finding.severity === 'WARN').length;
|
|
2001
|
-
}
|
|
2002
|
-
function formatError(error) {
|
|
2003
|
-
return error instanceof Error ? error.message : String(error);
|
|
2004
|
-
}
|
|
2005
|
-
function runCodex(realBinary, permissionArgs, context, args, wrapperArgs) {
|
|
2006
|
-
const liveDir = context.paths.liveDir;
|
|
2007
|
-
const codexHomeDir = context.paths.codexHomeDir;
|
|
2008
|
-
const projectRoot = context.projectRoot;
|
|
2009
|
-
const agentsPath = path.join(codexHomeDir, 'AGENTS.md');
|
|
2010
|
-
if (!fs.existsSync(agentsPath)) {
|
|
2011
|
-
failMissingConfig('codex', context.profileDir);
|
|
2012
|
-
}
|
|
2013
|
-
const guardBin = path.join(liveDir, 'bin');
|
|
2014
|
-
const realPath = process.env.AGENT_RUN_REAL_PATH ?? process.env.PATH ?? '';
|
|
2015
|
-
fs.mkdirSync(codexHomeDir, { recursive: true });
|
|
2016
|
-
const env = {
|
|
2017
|
-
...process.env,
|
|
2018
|
-
CODEX_HOME: codexHomeDir,
|
|
2019
|
-
AGENT_DIR: liveDir,
|
|
2020
|
-
AGENT_PROFILE_DIR: context.profileDir,
|
|
2021
|
-
AGENT_RUN_PROJECT_ROOT: projectRoot,
|
|
2022
|
-
AGENT_RUN_REAL_PATH: realPath,
|
|
2023
|
-
PATH: `${guardBin}${path.delimiter}${realPath}`
|
|
2024
|
-
};
|
|
2025
|
-
const codexRuntimeArgs = buildCodexRuntimeArgs(wrapperArgs);
|
|
2026
|
-
execCommand(realBinary, [
|
|
2027
|
-
...permissionArgs,
|
|
2028
|
-
...codexRuntimeArgs,
|
|
2029
|
-
'-C',
|
|
2030
|
-
projectRoot,
|
|
2031
|
-
...(wrapperArgs.codexSandboxMode !== 'danger' && wrapperArgs.codexNetwork
|
|
2032
|
-
? ['--config', 'sandbox_workspace_write.network_access=true']
|
|
2033
|
-
: []),
|
|
2034
|
-
...args
|
|
2035
|
-
], env, codexHomeDir, (code) => postflightProjectCheck(projectRoot, context.profileDir, context.guardrails.forbidRepoAiFiles, wrapperArgs.local, code));
|
|
2036
|
-
}
|
|
2037
|
-
function buildCodexRuntimeArgs(wrapperArgs) {
|
|
2038
|
-
if (wrapperArgs.codexSandboxMode === 'danger') {
|
|
2039
|
-
return ['-a', 'never', '-s', 'danger-full-access'];
|
|
2040
|
-
}
|
|
2041
|
-
return ['-a', 'on-request', '-s', 'workspace-write'];
|
|
2042
|
-
}
|
|
2043
|
-
function runClaude(realBinary, permissionArgs, context, args, wrapperArgs) {
|
|
2044
|
-
const liveDir = context.paths.liveDir;
|
|
2045
|
-
const projectRoot = context.projectRoot;
|
|
2046
|
-
const claudePath = path.join(liveDir, 'CLAUDE.md');
|
|
2047
|
-
if (!fs.existsSync(claudePath)) {
|
|
2048
|
-
failMissingConfig('claude', context.profileDir);
|
|
2049
|
-
}
|
|
2050
|
-
const guardBin = path.join(liveDir, 'bin');
|
|
2051
|
-
const claudeConfigDir = path.join(liveDir, '.claude');
|
|
2052
|
-
const realPath = process.env.AGENT_RUN_REAL_PATH ?? process.env.PATH ?? '';
|
|
2053
|
-
const env = {
|
|
2054
|
-
...process.env,
|
|
2055
|
-
AGENT_DIR: liveDir,
|
|
2056
|
-
AGENT_PROFILE_DIR: context.profileDir,
|
|
2057
|
-
AGENT_RUN_PROJECT_ROOT: projectRoot,
|
|
2058
|
-
AGENT_RUN_REAL_PATH: realPath,
|
|
2059
|
-
PATH: `${guardBin}${path.delimiter}${realPath}`
|
|
2060
|
-
};
|
|
2061
|
-
execCommand(realBinary, [
|
|
2062
|
-
...permissionArgs,
|
|
2063
|
-
'--append-system-prompt-file',
|
|
2064
|
-
claudePath,
|
|
2065
|
-
'--settings',
|
|
2066
|
-
path.join(claudeConfigDir, 'agent-run-settings.json'),
|
|
2067
|
-
'--plugin-dir',
|
|
2068
|
-
claudeConfigDir,
|
|
2069
|
-
...args
|
|
2070
|
-
], env, projectRoot, (code) => postflightProjectCheck(projectRoot, context.profileDir, context.guardrails.forbidRepoAiFiles, wrapperArgs.local, code));
|
|
2071
|
-
}
|
|
2072
|
-
function globalMemoryDir(configRoot) {
|
|
2073
|
-
return path.join(configRoot, 'notes', 'memory');
|
|
2074
|
-
}
|
|
2075
|
-
function postflightProjectCheck(projectRoot, agentDir, forbidRepoAiFiles, allowLocal, code) {
|
|
2076
|
-
if (!forbidRepoAiFiles) {
|
|
2077
|
-
return code;
|
|
2078
|
-
}
|
|
2079
|
-
const localAiFiles = findLocalAiFiles(projectRoot, agentDir);
|
|
2080
|
-
if (localAiFiles.length > 0) {
|
|
2081
|
-
warnForLocalAiFiles(null, projectRoot, localAiFiles);
|
|
2082
|
-
if (!allowLocal) {
|
|
2083
|
-
return 1;
|
|
2084
|
-
}
|
|
2085
|
-
}
|
|
2086
|
-
return code;
|
|
2087
|
-
}
|
|
2088
|
-
function failMissingConfig(tool, agentDir) {
|
|
2089
|
-
process.stderr.write(`agent-run: no ${tool} config found for this project: ${agentDir}\n`);
|
|
2090
|
-
process.stderr.write(`agent-run: run \`agent-run ${tool} --none\` to bypass agent setup, or \`agent-run ${tool} --create\` to create profile files.\n`);
|
|
2091
|
-
process.exit(1);
|
|
2092
|
-
}
|
|
2093
|
-
function getPermissionArgs(tool) {
|
|
2094
|
-
if (process.env.AGENT_WRAPPER_FORCE_PERMISSIVE !== '1') {
|
|
2095
|
-
return [];
|
|
2096
|
-
}
|
|
2097
|
-
if (tool === 'claude' && typeof process.getuid === 'function' && process.getuid() !== 0) {
|
|
2098
|
-
return ['--permission-mode', 'bypassPermissions'];
|
|
2099
|
-
}
|
|
2100
|
-
return [];
|
|
2101
|
-
}
|
|
2102
|
-
function resolveAgentDir(projectRoot) {
|
|
2103
|
-
const profile = resolveProfile(projectRoot);
|
|
2104
|
-
const configRoot = defaultConfigRoot(projectRoot);
|
|
2105
|
-
const agentDir = path.join(configRoot, profile);
|
|
2106
|
-
verbose(`profile=${profile} configRoot=${configRoot} agentPath=${agentDir}`);
|
|
2107
|
-
return agentDir;
|
|
2108
|
-
}
|
|
2109
|
-
function findLocalAiFiles(projectRoot, excludedDir = null) {
|
|
2110
|
-
const matches = [];
|
|
2111
|
-
if (isIgnoredDir(projectRoot)) {
|
|
2112
|
-
return matches;
|
|
2113
|
-
}
|
|
2114
|
-
const resolvedExcludedDir = excludedDir ? path.resolve(excludedDir) : null;
|
|
2115
|
-
walk(projectRoot, (fullPath, entry) => {
|
|
2116
|
-
if (resolvedExcludedDir !== null && isSamePathOrDescendant(fullPath, resolvedExcludedDir)) {
|
|
2117
|
-
return entry.isDirectory() ? 'skip' : undefined;
|
|
2118
|
-
}
|
|
2119
|
-
if (isLocalAiDirectory(entry)) {
|
|
2120
|
-
matches.push(fullPath);
|
|
2121
|
-
return 'skip';
|
|
2122
|
-
}
|
|
2123
|
-
if (isLocalAiFile(entry)) {
|
|
2124
|
-
matches.push(fullPath);
|
|
2125
|
-
}
|
|
2126
|
-
return undefined;
|
|
2127
|
-
});
|
|
2128
|
-
return matches.sort();
|
|
2129
|
-
}
|
|
2130
|
-
function walkSourceTree(dir, depth, visitor) {
|
|
2131
|
-
if (isIgnoredDir(dir)) {
|
|
2132
|
-
return;
|
|
2133
|
-
}
|
|
2134
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
2135
|
-
for (const entry of entries) {
|
|
2136
|
-
const fullPath = path.join(dir, entry.name);
|
|
2137
|
-
const result = visitor(fullPath, entry, depth + 1);
|
|
2138
|
-
if (entry.isDirectory() && result !== 'skip') {
|
|
2139
|
-
walkSourceTree(fullPath, depth + 1, visitor);
|
|
2140
|
-
}
|
|
2141
|
-
}
|
|
2142
|
-
}
|
|
2143
|
-
function walk(dir, visitor) {
|
|
2144
|
-
if (isIgnoredDir(dir)) {
|
|
2145
|
-
return;
|
|
2146
|
-
}
|
|
2147
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
2148
|
-
for (const entry of entries) {
|
|
2149
|
-
if (shouldPruneProjectEntry(entry)) {
|
|
2150
|
-
continue;
|
|
2151
|
-
}
|
|
2152
|
-
const fullPath = path.join(dir, entry.name);
|
|
2153
|
-
const result = visitor(fullPath, entry);
|
|
2154
|
-
if (entry.isDirectory() && result !== 'skip') {
|
|
2155
|
-
walk(fullPath, visitor);
|
|
2156
|
-
}
|
|
2157
|
-
}
|
|
2158
|
-
}
|
|
2159
|
-
function isSamePathOrDescendant(candidatePath, parentPath) {
|
|
2160
|
-
const relative = path.relative(parentPath, candidatePath);
|
|
2161
|
-
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
2162
|
-
}
|
|
2163
|
-
function shouldPruneProjectEntry(entry) {
|
|
2164
|
-
return (entry.isDirectory() &&
|
|
2165
|
-
(entry.name === '.git' ||
|
|
2166
|
-
entry.name === 'node_modules' ||
|
|
2167
|
-
entry.name === '.pnpm-store' ||
|
|
2168
|
-
entry.name === 'dist' ||
|
|
2169
|
-
entry.name === 'build'));
|
|
2170
|
-
}
|
|
2171
|
-
function isLocalAiDirectory(entry) {
|
|
2172
|
-
return LOCAL_AI_DIRECTORY_NAMES.has(entry.name);
|
|
2173
|
-
}
|
|
2174
|
-
function isLocalAiFile(entry) {
|
|
2175
|
-
return LOCAL_AI_FILE_NAMES.has(entry.name);
|
|
2176
|
-
}
|
|
2177
|
-
function isIgnoredDir(dir) {
|
|
2178
|
-
if (fs.existsSync(path.join(dir, IGNORE_FILE_NAME))) {
|
|
2179
|
-
return true;
|
|
2180
|
-
}
|
|
2181
|
-
const env = readAgentRunEnv(dir);
|
|
2182
|
-
return parseBooleanEnv(env.AGENT_RUN_IGNORE);
|
|
2183
|
-
}
|
|
2184
|
-
function warnForLocalAiFiles(tool, projectRoot, localAiFiles) {
|
|
2185
|
-
process.stderr.write(`WARNING: local AI files found in project root ${projectRoot}\n`);
|
|
2186
|
-
for (const file of localAiFiles) {
|
|
2187
|
-
process.stderr.write(`WARNING: ${path.relative(projectRoot, file)}\n`);
|
|
2188
|
-
}
|
|
2189
|
-
if (tool !== null) {
|
|
2190
|
-
process.stderr.write(`WARNING: consider moving these files out of the project, or run \`agent-run ${tool} --none\` to bypass the wrapper.\n`);
|
|
2191
|
-
}
|
|
2192
|
-
}
|
|
2193
|
-
function failForLocalAiFiles(tool, projectRoot, localAiFiles) {
|
|
2194
|
-
process.stderr.write(`agent-run: found local AI files in project root ${projectRoot}\n`);
|
|
2195
|
-
for (const file of localAiFiles) {
|
|
2196
|
-
process.stderr.write(`agent-run: ${file}\n`);
|
|
2197
|
-
}
|
|
2198
|
-
process.stderr.write(`agent-run: move these files manually out of the project, run \`agent-run ${tool} --local\` to warn and continue, or run \`agent-run ${tool} --none\` to bypass the wrapper for this invocation.\n`);
|
|
2199
|
-
process.exit(1);
|
|
2200
|
-
}
|
|
2201
|
-
function findProjectRoot(cwd) {
|
|
2202
|
-
let dir = path.resolve(cwd);
|
|
2203
|
-
let nearestPackageRoot = '';
|
|
2204
|
-
let workspaceRoot = '';
|
|
2205
|
-
let gitRoot = '';
|
|
2206
|
-
for (;;) {
|
|
2207
|
-
const packagePath = path.join(dir, 'package.json');
|
|
2208
|
-
if (isWorkspaceRoot(dir)) {
|
|
2209
|
-
workspaceRoot = dir;
|
|
2210
|
-
}
|
|
2211
|
-
if (fs.existsSync(packagePath)) {
|
|
2212
|
-
if (!nearestPackageRoot) {
|
|
2213
|
-
nearestPackageRoot = dir;
|
|
2214
|
-
}
|
|
2215
|
-
}
|
|
2216
|
-
if (fs.existsSync(path.join(dir, '.git'))) {
|
|
2217
|
-
gitRoot = dir;
|
|
2218
|
-
break;
|
|
2219
|
-
}
|
|
2220
|
-
const parent = path.dirname(dir);
|
|
2221
|
-
if (parent === dir) {
|
|
2222
|
-
break;
|
|
2223
|
-
}
|
|
2224
|
-
dir = parent;
|
|
2225
|
-
}
|
|
2226
|
-
const resolvedRoot = workspaceRoot || nearestPackageRoot || gitRoot || path.resolve(cwd);
|
|
2227
|
-
verbose(`findProjectRoot cwd=${path.resolve(cwd)} workspaceRoot=${workspaceRoot || '-'} nearestPackageRoot=${nearestPackageRoot || '-'} gitRoot=${gitRoot || '-'} resolved=${resolvedRoot}`);
|
|
2228
|
-
return resolvedRoot;
|
|
2229
|
-
}
|
|
2230
|
-
function isWorkspaceRoot(dir) {
|
|
2231
|
-
if (fs.existsSync(path.join(dir, 'pnpm-workspace.yaml'))) {
|
|
2232
|
-
return true;
|
|
2233
|
-
}
|
|
2234
|
-
const packagePath = path.join(dir, 'package.json');
|
|
2235
|
-
if (!fs.existsSync(packagePath)) {
|
|
2236
|
-
return false;
|
|
2237
|
-
}
|
|
2238
|
-
try {
|
|
2239
|
-
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
2240
|
-
return Array.isArray(pkg.workspaces) || (pkg.workspaces !== null && typeof pkg.workspaces === 'object');
|
|
2241
|
-
}
|
|
2242
|
-
catch {
|
|
2243
|
-
return false;
|
|
2244
|
-
}
|
|
2245
|
-
}
|
|
2246
|
-
function resolveProfile(projectRoot) {
|
|
2247
|
-
const result = resolveProfileResult(projectRoot);
|
|
2248
|
-
if (result.profile === null) {
|
|
2249
|
-
fail(result.reason);
|
|
2250
|
-
}
|
|
2251
|
-
return result.profile;
|
|
2252
|
-
}
|
|
2253
|
-
function resolveProfileResult(projectRoot) {
|
|
2254
|
-
const env = readAgentRunEnv(projectRoot);
|
|
2255
|
-
const envProfile = env.AGENT_RUN_PROFILE?.trim();
|
|
2256
|
-
if (envProfile) {
|
|
2257
|
-
verbose(`using ${ENV_FILE_NAME} AGENT_RUN_PROFILE=${envProfile}`);
|
|
2258
|
-
return parseProfile(envProfile, `${ENV_FILE_NAME} AGENT_RUN_PROFILE`);
|
|
2259
|
-
}
|
|
2260
|
-
const githubProfile = resolveGitHubProfile(projectRoot);
|
|
2261
|
-
if (githubProfile !== null) {
|
|
2262
|
-
verbose(`using GitHub origin profile=${githubProfile}`);
|
|
2263
|
-
return parseProfile(githubProfile, 'GitHub remote origin');
|
|
2264
|
-
}
|
|
2265
|
-
const packagePath = path.join(projectRoot, 'package.json');
|
|
2266
|
-
if (fs.existsSync(packagePath)) {
|
|
2267
|
-
try {
|
|
2268
|
-
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
2269
|
-
if (typeof pkg.name === 'string' && pkg.name.length > 0) {
|
|
2270
|
-
verbose(`using package.json name=${pkg.name}`);
|
|
2271
|
-
return parseProfile(pkg.name.startsWith('@') ? pkg.name.slice(1) : pkg.name, `${packagePath} name`);
|
|
2272
|
-
}
|
|
2273
|
-
}
|
|
2274
|
-
catch {
|
|
2275
|
-
return { profile: null, reason: `cannot parse package.json: ${packagePath}` };
|
|
2276
|
-
}
|
|
2277
|
-
}
|
|
2278
|
-
return {
|
|
2279
|
-
profile: null,
|
|
2280
|
-
reason: `cannot resolve agent profile for ${projectRoot}; add ${ENV_FILE_NAME} with AGENT_RUN_PROFILE=<org/project>, add a GitHub origin remote, or set package.json.name`
|
|
2281
|
-
};
|
|
2282
|
-
}
|
|
2283
|
-
function resolveGitHubProfile(projectRoot) {
|
|
2284
|
-
const remote = readGitOrigin(projectRoot);
|
|
2285
|
-
return remote === null ? null : parseGitHubRemoteProfile(remote);
|
|
2286
|
-
}
|
|
2287
|
-
function readGitOrigin(projectRoot) {
|
|
2288
|
-
const result = childProcess.spawnSync('git', ['-C', projectRoot, 'config', '--get', 'remote.origin.url'], {
|
|
2289
|
-
encoding: 'utf8',
|
|
2290
|
-
shell: false,
|
|
2291
|
-
stdio: ['ignore', 'pipe', 'ignore']
|
|
2292
|
-
});
|
|
2293
|
-
if (result.status !== 0) {
|
|
2294
|
-
return null;
|
|
2295
|
-
}
|
|
2296
|
-
const remote = result.stdout.trim();
|
|
2297
|
-
return remote || null;
|
|
2298
|
-
}
|
|
2299
|
-
function parseGitHubRemoteProfile(remote) {
|
|
2300
|
-
const patterns = [
|
|
2301
|
-
/^git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/,
|
|
2302
|
-
/^https:\/\/github\.com\/([^/]+)\/(.+?)(?:\.git)?$/,
|
|
2303
|
-
/^ssh:\/\/git@github\.com\/([^/]+)\/(.+?)(?:\.git)?$/
|
|
2304
|
-
];
|
|
2305
|
-
for (const pattern of patterns) {
|
|
2306
|
-
const match = pattern.exec(remote.trim());
|
|
2307
|
-
if (match?.[1] && match[2]) {
|
|
2308
|
-
return `${match[1]}/${match[2].replace(/\.git$/, '')}`;
|
|
2309
|
-
}
|
|
2310
|
-
}
|
|
2311
|
-
return null;
|
|
2312
|
-
}
|
|
2313
|
-
function parseProfile(profile, source) {
|
|
2314
|
-
const normalized = profile.trim().replace(/\\/g, '/');
|
|
2315
|
-
if (!normalized) {
|
|
2316
|
-
return { profile: null, reason: `${source} must be a non-empty path relative to the config root` };
|
|
2317
|
-
}
|
|
2318
|
-
if (normalized.startsWith('/') || normalized.startsWith('\\\\') || /^[A-Za-z]:\//.test(normalized)) {
|
|
2319
|
-
return { profile: null, reason: `${source} must be relative to the config root, not an absolute path` };
|
|
2320
|
-
}
|
|
2321
|
-
const segments = normalized.split('/').filter((segment) => segment.length > 0);
|
|
2322
|
-
if (segments.length === 0 ||
|
|
2323
|
-
segments.some((segment) => segment === '.' || segment === '..' || !/^[A-Za-z0-9._-]+$/.test(segment))) {
|
|
2324
|
-
return { profile: null, reason: `${source} must be a clean relative path like org/my-project` };
|
|
2325
|
-
}
|
|
2326
|
-
return { profile: segments.join('/'), reason: '' };
|
|
2327
|
-
}
|
|
2328
|
-
function projectRootForProfile(profile, configRoot) {
|
|
2329
|
-
if (configRoot && path.basename(configRoot) === 'agent-config') {
|
|
2330
|
-
return path.join(path.dirname(configRoot), ...profile.split('/'));
|
|
2331
|
-
}
|
|
2332
|
-
return path.join(os.homedir(), ...profile.split('/'));
|
|
2333
|
-
}
|
|
2334
|
-
function readAgentRunEnv(dir) {
|
|
2335
|
-
const resolvedDir = path.resolve(dir);
|
|
2336
|
-
const cached = agentRunEnvCache.get(resolvedDir);
|
|
2337
|
-
if (cached) {
|
|
2338
|
-
return cached;
|
|
2339
|
-
}
|
|
2340
|
-
const filePath = path.join(resolvedDir, ENV_FILE_NAME);
|
|
2341
|
-
if (!fs.existsSync(filePath)) {
|
|
2342
|
-
const emptyEnv = {};
|
|
2343
|
-
agentRunEnvCache.set(resolvedDir, emptyEnv);
|
|
2344
|
-
return emptyEnv;
|
|
2345
|
-
}
|
|
2346
|
-
const env = {};
|
|
2347
|
-
const lines = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n').split('\n');
|
|
2348
|
-
for (const rawLine of lines) {
|
|
2349
|
-
const line = rawLine.trim();
|
|
2350
|
-
if (!line || line.startsWith('#')) {
|
|
2351
|
-
continue;
|
|
2352
|
-
}
|
|
2353
|
-
const equalsIndex = line.indexOf('=');
|
|
2354
|
-
if (equalsIndex <= 0) {
|
|
2355
|
-
continue;
|
|
2356
|
-
}
|
|
2357
|
-
const key = line.slice(0, equalsIndex).trim();
|
|
2358
|
-
let value = line.slice(equalsIndex + 1).trim();
|
|
2359
|
-
if (value.length >= 2 && ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))) {
|
|
2360
|
-
value = value.slice(1, -1);
|
|
2361
|
-
}
|
|
2362
|
-
env[key] = value;
|
|
2363
|
-
}
|
|
2364
|
-
agentRunEnvCache.set(resolvedDir, env);
|
|
2365
|
-
return env;
|
|
2366
|
-
}
|
|
2367
|
-
function parseBooleanEnv(value) {
|
|
2368
|
-
if (value === undefined) {
|
|
2369
|
-
return false;
|
|
2370
|
-
}
|
|
2371
|
-
const normalized = value.trim().toLowerCase();
|
|
2372
|
-
return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';
|
|
2373
|
-
}
|
|
2374
|
-
function defaultConfigRoot(projectRoot, options = {}) {
|
|
2375
|
-
const overrideRoot = process.env[CONFIG_ROOT_OVERRIDE_ENV];
|
|
2376
|
-
if (overrideRoot) {
|
|
2377
|
-
return path.resolve(overrideRoot);
|
|
2378
|
-
}
|
|
2379
|
-
if (process.env[CONFIG_DIR_ENV]) {
|
|
2380
|
-
return path.resolve(process.env[CONFIG_DIR_ENV]);
|
|
2381
|
-
}
|
|
2382
|
-
if (process.env.AGENT_CONFIG_ROOT) {
|
|
2383
|
-
return path.resolve(process.env.AGENT_CONFIG_ROOT);
|
|
2384
|
-
}
|
|
2385
|
-
if (projectRoot) {
|
|
2386
|
-
const env = readAgentRunEnv(projectRoot);
|
|
2387
|
-
const localConfigDir = env[CONFIG_DIR_ENV]?.trim();
|
|
2388
|
-
if (localConfigDir) {
|
|
2389
|
-
return path.resolve(projectRoot, localConfigDir);
|
|
2390
|
-
}
|
|
2391
|
-
const localConfigRoot = env.AGENT_CONFIG_ROOT?.trim();
|
|
2392
|
-
if (localConfigRoot) {
|
|
2393
|
-
return path.resolve(projectRoot, localConfigRoot);
|
|
2394
|
-
}
|
|
2395
|
-
}
|
|
2396
|
-
if (projectRoot) {
|
|
2397
|
-
const projectConfigRoot = defaultCodeConfigRoot(projectRoot);
|
|
2398
|
-
if (options.preferProjectRoot || fs.existsSync(projectConfigRoot)) {
|
|
2399
|
-
return projectConfigRoot;
|
|
2400
|
-
}
|
|
2401
|
-
const discoveredConfigRoot = findExistingConfigRoot(defaultConfigRootSearchCandidates());
|
|
2402
|
-
return discoveredConfigRoot ?? projectConfigRoot;
|
|
2403
|
-
}
|
|
2404
|
-
const discoveredConfigRoot = findExistingConfigRoot(defaultConfigRootSearchCandidates());
|
|
2405
|
-
return discoveredConfigRoot ?? path.join(os.homedir(), '.agent-config');
|
|
2406
|
-
}
|
|
2407
|
-
function defaultCodeConfigRoot(projectRoot) {
|
|
2408
|
-
const resolvedProjectRoot = path.resolve(projectRoot);
|
|
2409
|
-
const ownerDir = path.dirname(resolvedProjectRoot);
|
|
2410
|
-
const codeRoot = path.dirname(ownerDir);
|
|
2411
|
-
if (ownerDir === resolvedProjectRoot || codeRoot === ownerDir) {
|
|
2412
|
-
return path.join(os.homedir(), '.agent-config');
|
|
2413
|
-
}
|
|
2414
|
-
return path.join(codeRoot, 'agent-config');
|
|
2415
|
-
}
|
|
2416
|
-
function findExistingConfigRoot(candidates) {
|
|
2417
|
-
for (const candidate of candidates) {
|
|
2418
|
-
if (fs.existsSync(candidate)) {
|
|
2419
|
-
return candidate;
|
|
2420
|
-
}
|
|
2421
|
-
}
|
|
2422
|
-
return null;
|
|
2423
|
-
}
|
|
2424
|
-
function defaultConfigRootSearchCandidates(_projectRoot, options = {}) {
|
|
2425
|
-
const platform = options.platform ?? process.platform;
|
|
2426
|
-
const pathApi = platform === 'win32' ? path.win32 : path.posix;
|
|
2427
|
-
const homeDir = options.homeDir ?? os.homedir();
|
|
2428
|
-
const configNames = ['agent-config', 'agent-configs'];
|
|
2429
|
-
const candidates = [];
|
|
2430
|
-
if (platform === 'win32') {
|
|
2431
|
-
for (const codeRoot of [pathApi.join(homeDir, 'Documents', 'code'), pathApi.join(homeDir, 'Desktop', 'code'), 'C:\\code']) {
|
|
2432
|
-
for (const configName of configNames) {
|
|
2433
|
-
candidates.push(pathApi.join(codeRoot, configName));
|
|
2434
|
-
}
|
|
2435
|
-
}
|
|
2436
|
-
return candidates;
|
|
2437
|
-
}
|
|
2438
|
-
for (const configName of configNames) {
|
|
2439
|
-
candidates.push(pathApi.join(homeDir, 'code', configName));
|
|
2440
|
-
}
|
|
2441
|
-
for (const configName of configNames) {
|
|
2442
|
-
candidates.push(pathApi.join(homeDir, configName));
|
|
2443
|
-
}
|
|
2444
|
-
for (const configName of configNames) {
|
|
2445
|
-
candidates.push(pathApi.join(homeDir, `.${configName}`));
|
|
2446
|
-
}
|
|
2447
|
-
return candidates;
|
|
2448
|
-
}
|
|
2449
|
-
function findRealBinary(tool) {
|
|
2450
|
-
const pathValue = process.env.PATH || '';
|
|
2451
|
-
const pathDirs = pathValue.split(path.delimiter).filter((entry) => entry.length > 0);
|
|
2452
|
-
const currentScriptArg = process.argv[1];
|
|
2453
|
-
const currentScript = currentScriptArg ? fs.realpathSync(currentScriptArg) : '';
|
|
2454
|
-
const wrapperCandidates = new Set([
|
|
2455
|
-
path.join(__dirname, tool),
|
|
2456
|
-
path.join(__dirname, `${tool}.js`),
|
|
2457
|
-
path.join(__dirname, `${tool}.cmd`),
|
|
2458
|
-
path.join(__dirname, `${tool}.bat`),
|
|
2459
|
-
path.join(__dirname, `${tool}.exe`),
|
|
2460
|
-
path.join(__dirname, 'agent-run'),
|
|
2461
|
-
path.join(__dirname, 'agent-run.js'),
|
|
2462
|
-
path.join(__dirname, 'agent-run.cmd'),
|
|
2463
|
-
path.join(__dirname, 'agent-run.bat'),
|
|
2464
|
-
path.join(__dirname, 'agent-run.exe')
|
|
2465
|
-
]
|
|
2466
|
-
.filter((candidate) => fs.existsSync(candidate))
|
|
2467
|
-
.map((candidate) => {
|
|
2468
|
-
try {
|
|
2469
|
-
return fs.realpathSync(candidate);
|
|
2470
|
-
}
|
|
2471
|
-
catch {
|
|
2472
|
-
return candidate;
|
|
2473
|
-
}
|
|
2474
|
-
}));
|
|
2475
|
-
const extensions = getExecutableExtensions(tool);
|
|
2476
|
-
for (const dir of pathDirs) {
|
|
2477
|
-
for (const extension of extensions) {
|
|
2478
|
-
const candidate = path.join(dir, `${tool}${extension}`);
|
|
2479
|
-
if (!fs.existsSync(candidate) || !isExecutable(candidate)) {
|
|
2480
|
-
continue;
|
|
2481
|
-
}
|
|
2482
|
-
let resolvedCandidate = candidate;
|
|
2483
|
-
try {
|
|
2484
|
-
resolvedCandidate = fs.realpathSync(candidate);
|
|
2485
|
-
}
|
|
2486
|
-
catch {
|
|
2487
|
-
resolvedCandidate = candidate;
|
|
2488
|
-
}
|
|
2489
|
-
if (resolvedCandidate === currentScript || wrapperCandidates.has(resolvedCandidate)) {
|
|
2490
|
-
continue;
|
|
2491
|
-
}
|
|
2492
|
-
if (isAgentRunRedirectShim(candidate)) {
|
|
2493
|
-
verbose(`skip ${tool} redirect shim: ${candidate}`);
|
|
2494
|
-
continue;
|
|
2495
|
-
}
|
|
2496
|
-
return candidate;
|
|
2497
|
-
}
|
|
2498
|
-
}
|
|
2499
|
-
fail(`no ${tool} binary found in PATH`);
|
|
2500
|
-
}
|
|
2501
|
-
function isAgentRunRedirectShim(filePath) {
|
|
2502
|
-
try {
|
|
2503
|
-
const handle = fs.openSync(filePath, 'r');
|
|
2504
|
-
try {
|
|
2505
|
-
const buffer = Buffer.alloc(4096);
|
|
2506
|
-
const bytesRead = fs.readSync(handle, buffer, 0, buffer.length, 0);
|
|
2507
|
-
const content = buffer.subarray(0, bytesRead).toString('utf8');
|
|
2508
|
-
return content.includes('Run agent-run instead');
|
|
2509
|
-
}
|
|
2510
|
-
finally {
|
|
2511
|
-
fs.closeSync(handle);
|
|
2512
|
-
}
|
|
2513
|
-
}
|
|
2514
|
-
catch {
|
|
2515
|
-
return false;
|
|
2516
|
-
}
|
|
2517
|
-
}
|
|
2518
|
-
function getExecutableExtensions(tool) {
|
|
2519
|
-
if (!IS_WINDOWS || path.extname(tool)) {
|
|
2520
|
-
return [''];
|
|
2521
|
-
}
|
|
2522
|
-
const pathExt = (process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM')
|
|
2523
|
-
.split(';')
|
|
2524
|
-
.filter((entry) => entry.length > 0)
|
|
2525
|
-
.map((entry) => entry.toLowerCase());
|
|
2526
|
-
return pathExt;
|
|
2527
|
-
}
|
|
2528
|
-
function isExecutable(filePath) {
|
|
2529
|
-
try {
|
|
2530
|
-
if (IS_WINDOWS) {
|
|
2531
|
-
return fs.statSync(filePath).isFile();
|
|
2532
|
-
}
|
|
2533
|
-
fs.accessSync(filePath, fs.constants.X_OK);
|
|
2534
|
-
return true;
|
|
2535
|
-
}
|
|
2536
|
-
catch {
|
|
2537
|
-
return false;
|
|
2538
|
-
}
|
|
2539
|
-
}
|
|
2540
|
-
function execTool(command, args) {
|
|
2541
|
-
verbose(`exec tool: ${formatCommand(command, args)}`);
|
|
2542
|
-
execCommand(command, args);
|
|
2543
|
-
}
|
|
2544
|
-
function execCommand(command, args, env, cwd, onExit) {
|
|
2545
|
-
verbose(`spawn: ${formatCommand(command, args)} cwd=${cwd ?? process.cwd()}`);
|
|
2546
|
-
const child = crossSpawn(command, args, { cwd, env, stdio: 'inherit' });
|
|
2547
|
-
child.on('exit', (code, signal) => {
|
|
2548
|
-
if (signal) {
|
|
2549
|
-
process.kill(process.pid, signal);
|
|
2550
|
-
return;
|
|
2551
|
-
}
|
|
2552
|
-
const originalCode = code === null ? 1 : code;
|
|
2553
|
-
process.exit(onExit ? onExit(originalCode) : originalCode);
|
|
2554
|
-
});
|
|
2555
|
-
child.on('error', (error) => {
|
|
2556
|
-
fail(error.message);
|
|
2557
|
-
});
|
|
2558
|
-
}
|
|
2559
|
-
function openEditor(filePath) {
|
|
2560
|
-
const visual = process.env.VISUAL?.trim();
|
|
2561
|
-
if (visual) {
|
|
2562
|
-
runConfiguredEditor(visual, filePath);
|
|
2563
|
-
return;
|
|
2564
|
-
}
|
|
2565
|
-
const editor = process.env.EDITOR?.trim();
|
|
2566
|
-
if (editor) {
|
|
2567
|
-
runConfiguredEditor(editor, filePath);
|
|
2568
|
-
return;
|
|
2569
|
-
}
|
|
2570
|
-
const vscodeCommand = findVsCodeEditorCommand();
|
|
2571
|
-
if (vscodeCommand !== null) {
|
|
2572
|
-
execCommand(vscodeCommand, ['--reuse-window', filePath]);
|
|
2573
|
-
return;
|
|
2574
|
-
}
|
|
2575
|
-
const fallbackEditor = findFallbackEditor();
|
|
2576
|
-
if (fallbackEditor !== null) {
|
|
2577
|
-
execCommand(fallbackEditor, [filePath]);
|
|
2578
|
-
return;
|
|
2579
|
-
}
|
|
2580
|
-
if (IS_WINDOWS) {
|
|
2581
|
-
execCommand('explorer.exe', [filePath]);
|
|
2582
|
-
return;
|
|
2583
|
-
}
|
|
2584
|
-
if (process.platform === 'darwin') {
|
|
2585
|
-
execCommand('open', [filePath]);
|
|
2586
|
-
return;
|
|
2587
|
-
}
|
|
2588
|
-
execCommand('xdg-open', [filePath]);
|
|
2589
|
-
}
|
|
2590
|
-
function runConfiguredEditor(commandLine, filePath) {
|
|
2591
|
-
let editorArgs;
|
|
2592
|
-
try {
|
|
2593
|
-
editorArgs = parseEditorCommand(commandLine);
|
|
2594
|
-
}
|
|
2595
|
-
catch (error) {
|
|
2596
|
-
fail(`invalid editor command: ${formatError(error)}`);
|
|
2597
|
-
}
|
|
2598
|
-
const [command, ...args] = editorArgs;
|
|
2599
|
-
if (!command) {
|
|
2600
|
-
fail('invalid editor command: command is empty');
|
|
2601
|
-
}
|
|
2602
|
-
execCommand(command, [...args, filePath]);
|
|
2603
|
-
}
|
|
2604
|
-
function parseEditorCommand(commandLine) {
|
|
2605
|
-
const args = [];
|
|
2606
|
-
let current = '';
|
|
2607
|
-
let quote = null;
|
|
2608
|
-
let tokenStarted = false;
|
|
2609
|
-
for (let index = 0; index < commandLine.length; index += 1) {
|
|
2610
|
-
const character = commandLine[index] ?? '';
|
|
2611
|
-
if (quote !== null) {
|
|
2612
|
-
if (character === quote) {
|
|
2613
|
-
quote = null;
|
|
2614
|
-
continue;
|
|
2615
|
-
}
|
|
2616
|
-
if (character === '\\' && quote === '"') {
|
|
2617
|
-
const next = commandLine[index + 1];
|
|
2618
|
-
if (next === '"' || next === '\\') {
|
|
2619
|
-
current += next;
|
|
2620
|
-
index += 1;
|
|
2621
|
-
continue;
|
|
2622
|
-
}
|
|
2623
|
-
}
|
|
2624
|
-
current += character;
|
|
2625
|
-
continue;
|
|
2626
|
-
}
|
|
2627
|
-
if (character === "'" || character === '"') {
|
|
2628
|
-
quote = character;
|
|
2629
|
-
tokenStarted = true;
|
|
2630
|
-
continue;
|
|
2631
|
-
}
|
|
2632
|
-
if (/\s/.test(character)) {
|
|
2633
|
-
if (tokenStarted) {
|
|
2634
|
-
args.push(current);
|
|
2635
|
-
current = '';
|
|
2636
|
-
tokenStarted = false;
|
|
2637
|
-
}
|
|
2638
|
-
continue;
|
|
2639
|
-
}
|
|
2640
|
-
if (character === '\\') {
|
|
2641
|
-
const next = commandLine[index + 1];
|
|
2642
|
-
if (next !== undefined && (/\s/.test(next) || next === "'" || next === '"' || next === '\\')) {
|
|
2643
|
-
current += next;
|
|
2644
|
-
index += 1;
|
|
2645
|
-
tokenStarted = true;
|
|
2646
|
-
continue;
|
|
2647
|
-
}
|
|
2648
|
-
}
|
|
2649
|
-
current += character;
|
|
2650
|
-
tokenStarted = true;
|
|
2651
|
-
}
|
|
2652
|
-
if (quote !== null) {
|
|
2653
|
-
throw new Error(`unterminated ${quote} quote`);
|
|
2654
|
-
}
|
|
2655
|
-
if (tokenStarted) {
|
|
2656
|
-
args.push(current);
|
|
2657
|
-
}
|
|
2658
|
-
if (args.length === 0) {
|
|
2659
|
-
throw new Error('command is empty');
|
|
2660
|
-
}
|
|
2661
|
-
return args;
|
|
2662
|
-
}
|
|
2663
|
-
function findVsCodeEditorCommand() {
|
|
2664
|
-
if (!isRunningInVsCodeTerminal()) {
|
|
2665
|
-
return null;
|
|
2666
|
-
}
|
|
2667
|
-
for (const candidate of ['code', 'codium']) {
|
|
2668
|
-
const resolved = findExecutable(candidate);
|
|
2669
|
-
if (resolved !== null) {
|
|
2670
|
-
return resolved;
|
|
2671
|
-
}
|
|
2672
|
-
}
|
|
2673
|
-
return null;
|
|
2674
|
-
}
|
|
2675
|
-
function isRunningInVsCodeTerminal() {
|
|
2676
|
-
const termProgram = process.env.TERM_PROGRAM?.trim().toLowerCase();
|
|
2677
|
-
return (termProgram === 'vscode' ||
|
|
2678
|
-
Boolean(process.env.VSCODE_GIT_IPC_HANDLE) ||
|
|
2679
|
-
Boolean(process.env.VSCODE_IPC_HOOK) ||
|
|
2680
|
-
Boolean(process.env.VSCODE_IPC_HOOK_CLI));
|
|
2681
|
-
}
|
|
2682
|
-
function findFallbackEditor() {
|
|
2683
|
-
const candidates = IS_WINDOWS ? ['notepad.exe'] : ['joe', 'sensible-editor', 'editor', 'nano', 'nvim', 'vim', 'vi'];
|
|
2684
|
-
for (const candidate of candidates) {
|
|
2685
|
-
const resolved = findExecutable(candidate);
|
|
2686
|
-
if (resolved !== null) {
|
|
2687
|
-
return resolved;
|
|
2688
|
-
}
|
|
2689
|
-
}
|
|
2690
|
-
return null;
|
|
2691
|
-
}
|
|
2692
|
-
function findExecutable(command) {
|
|
2693
|
-
const pathValue = process.env.PATH || '';
|
|
2694
|
-
const pathDirs = pathValue.split(path.delimiter).filter((entry) => entry.length > 0);
|
|
2695
|
-
const extensions = getExecutableExtensions(command);
|
|
2696
|
-
for (const dir of pathDirs) {
|
|
2697
|
-
for (const extension of extensions) {
|
|
2698
|
-
const candidate = path.join(dir, `${command}${extension}`);
|
|
2699
|
-
if (fs.existsSync(candidate) && isExecutable(candidate)) {
|
|
2700
|
-
return candidate;
|
|
2701
|
-
}
|
|
2702
|
-
}
|
|
2703
|
-
}
|
|
2704
|
-
return null;
|
|
2705
|
-
}
|
|
2706
|
-
function localDateString() {
|
|
2707
|
-
const now = new Date();
|
|
2708
|
-
const year = String(now.getFullYear()).padStart(4, '0');
|
|
2709
|
-
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
2710
|
-
const day = String(now.getDate()).padStart(2, '0');
|
|
2711
|
-
return `${year}-${month}-${day}`;
|
|
2712
|
-
}
|
|
2713
|
-
function stringifyDefaultManifest(profile) {
|
|
2714
|
-
const manifest = defaultManifest(profile);
|
|
2715
|
-
return `${JSON.stringify(manifest, null, 2)}\n`;
|
|
2716
|
-
}
|
|
2717
|
-
function defaultLocalTemplate(_profile) {
|
|
2718
|
-
return [
|
|
2719
|
-
'# {{ profile }} local agent instructions',
|
|
2720
|
-
'',
|
|
2721
|
-
'Project root:',
|
|
2722
|
-
'',
|
|
2723
|
-
'```text',
|
|
2724
|
-
'{{ projectRoot }}',
|
|
2725
|
-
'```',
|
|
2726
|
-
'',
|
|
2727
|
-
'Agent directory:',
|
|
2728
|
-
'',
|
|
2729
|
-
'```text',
|
|
2730
|
-
'{{ agentDir }}',
|
|
2731
|
-
'```',
|
|
2732
|
-
'',
|
|
2733
|
-
'Profile source directory:',
|
|
2734
|
-
'',
|
|
2735
|
-
'```text',
|
|
2736
|
-
'{{ profileDir }}',
|
|
2737
|
-
'```',
|
|
2738
|
-
'',
|
|
2739
|
-
'Add project-specific rules here.',
|
|
2740
|
-
''
|
|
2741
|
-
].join('\n');
|
|
2742
|
-
}
|
|
2743
|
-
function defaultCodeAgentTemplate() {
|
|
2744
|
-
return [
|
|
2745
|
-
'# Code Agent',
|
|
2746
|
-
'',
|
|
2747
|
-
'Work in the project root shown by agent-run. Keep changes scoped to the user request.',
|
|
2748
|
-
'',
|
|
2749
|
-
'{% include "global/snippets/git-rules.md.njk" %}',
|
|
2750
|
-
'',
|
|
2751
|
-
'{% include "global/snippets/no-ai-files.md.njk" %}',
|
|
2752
|
-
'',
|
|
2753
|
-
'{% include "global/snippets/verification.md.njk" %}',
|
|
2754
|
-
''
|
|
2755
|
-
].join('\n');
|
|
2756
|
-
}
|
|
2757
|
-
function defaultWritingAgentTemplate() {
|
|
2758
|
-
return [
|
|
2759
|
-
'# Writing Agent',
|
|
2760
|
-
'',
|
|
2761
|
-
'Write clearly and preserve the existing voice, structure, and facts in the project.',
|
|
2762
|
-
'',
|
|
2763
|
-
'{% include "global/snippets/no-ai-files.md.njk" %}',
|
|
2764
|
-
''
|
|
2765
|
-
].join('\n');
|
|
2766
|
-
}
|
|
2767
|
-
function defaultGitRulesSnippet() {
|
|
2768
|
-
return [
|
|
2769
|
-
'## Git Rules',
|
|
2770
|
-
'',
|
|
2771
|
-
'Do not commit unless the user explicitly asks. Before committing, show changed files and the exact commit message.',
|
|
2772
|
-
'Use human commit messages unless the user asks for conventional commits. Do not push unless explicitly asked.',
|
|
2773
|
-
''
|
|
2774
|
-
].join('\n');
|
|
2775
|
-
}
|
|
2776
|
-
function defaultNoAiFilesSnippet() {
|
|
2777
|
-
return [
|
|
2778
|
-
'## Agent File Storage',
|
|
2779
|
-
'',
|
|
2780
|
-
'Do not create AGENTS.md, CLAUDE.md, .agents, .claude, .codex, or other agent runtime files inside the project repository.',
|
|
2781
|
-
'Generated agent-only files belong under {{ paths.liveDir }}.',
|
|
2782
|
-
'Source config files belong under {{ profileDir }}.',
|
|
2783
|
-
''
|
|
2784
|
-
].join('\n');
|
|
2785
|
-
}
|
|
2786
|
-
function defaultVerificationSnippet() {
|
|
2787
|
-
return [
|
|
2788
|
-
'## Verification',
|
|
2789
|
-
'',
|
|
2790
|
-
'Run relevant checks before reporting completion. Configured checks:',
|
|
2791
|
-
'',
|
|
2792
|
-
'{% for check in checks %}',
|
|
2793
|
-
'- `{{ check }}`',
|
|
2794
|
-
'{% endfor %}',
|
|
2795
|
-
''
|
|
2796
|
-
].join('\n');
|
|
2797
|
-
}
|
|
2798
|
-
function defaultCommitWorkflowSkill() {
|
|
2799
|
-
return [
|
|
2800
|
-
'---',
|
|
2801
|
-
'name: commit-workflow',
|
|
2802
|
-
'description: Use for preparing or creating commits with explicit user approval.',
|
|
2803
|
-
'---',
|
|
2804
|
-
'',
|
|
2805
|
-
'# Commit Workflow',
|
|
2806
|
-
'',
|
|
2807
|
-
'Never commit unless explicitly asked. Before committing, show changed files and the exact commit message.',
|
|
2808
|
-
'Use human commit messages unless conventional commits are explicitly requested. Do not push.',
|
|
2809
|
-
'Run configured checks before committing:',
|
|
2810
|
-
'',
|
|
2811
|
-
'{% for check in checks %}',
|
|
2812
|
-
'- `{{ check }}`',
|
|
2813
|
-
'{% endfor %}',
|
|
2814
|
-
''
|
|
2815
|
-
].join('\n');
|
|
2816
|
-
}
|
|
2817
|
-
function defaultGithubReleaseSkill() {
|
|
2818
|
-
return [
|
|
2819
|
-
'---',
|
|
2820
|
-
'name: github-release',
|
|
2821
|
-
'description: Use for releases, version bumps, tags, publishing, GitHub Releases, and CHANGES updates.',
|
|
2822
|
-
'---',
|
|
2823
|
-
'',
|
|
2824
|
-
'# GitHub Release',
|
|
2825
|
-
'',
|
|
2826
|
-
'Use {{ paths.changesFile }} for change notes.',
|
|
2827
|
-
'Require confirmation before writing CHANGES, bumping versions, committing, tagging, pushing, publishing, or creating a GitHub Release.',
|
|
2828
|
-
'Follow `$commit-workflow` for release commits.',
|
|
2829
|
-
''
|
|
2830
|
-
].join('\n');
|
|
2831
|
-
}
|
|
2832
|
-
function defaultReleasePackageCheckSkill() {
|
|
2833
|
-
return [
|
|
2834
|
-
'---',
|
|
2835
|
-
'name: release-package-check',
|
|
2836
|
-
'description: Use before package releases and publish-ready changes to verify package metadata, release notes, lockfiles, workflows, git state, and agent-file hygiene.',
|
|
2837
|
-
'---',
|
|
2838
|
-
'',
|
|
2839
|
-
'# Release Package Check',
|
|
2840
|
-
'',
|
|
2841
|
-
'Use this skill instead of an external `repo-check` command.',
|
|
2842
|
-
'For package release or publish-ready work, verify the relevant package directory before finalizing:',
|
|
2843
|
-
'',
|
|
2844
|
-
'- The directory contains `package.json` and is inside a git repository.',
|
|
2845
|
-
'- The repository path follows `<org>/<repo>`, and `package.json.name` is `@<org>/<package-dir-name>`.',
|
|
2846
|
-
'- `package.json.version`, `package.json.license`, and `package.json.copyright` are present and non-empty.',
|
|
2847
|
-
'- A package-local `LICENSE` file exists and contains the exact copyright string from `package.json.copyright`.',
|
|
2848
|
-
'- A package-local `CHANGES` file exists.',
|
|
2849
|
-
'- The first `Version ...` line in `CHANGES` matches `Version <package.json.version> (<YYYY-MM-DD>)`.',
|
|
2850
|
-
'- The year in the top `CHANGES` version line appears in `package.json.copyright`.',
|
|
2851
|
-
'- The git `origin` remote matches `<org>/<repo>`.',
|
|
2852
|
-
'- The current branch has an upstream configured and is in sync with it after fetching.',
|
|
2853
|
-
'- The repository does not contain agent files or directories such as `AGENTS.md`, `CLAUDE.md`, `codex.md`, `.claude`, or `.codex`.',
|
|
2854
|
-
'- The repository root has a lockfile: `pnpm-lock.yaml`, `package-lock.json`, `yarn.lock`, `bun.lockb`, or `bun.lock`.',
|
|
2855
|
-
'- Every GitHub Actions `uses:` reference is pinned to a full commit SHA.',
|
|
2856
|
-
'- The pinned `actions/setup-node` release is v6-compatible and workflows use `node-version: 24`.',
|
|
2857
|
-
'- GitHub workflows do not reference `node20` or `node22`.',
|
|
2858
|
-
'- The git working tree is clean for files under the checked package directory.',
|
|
2859
|
-
'- If files beyond `CHANGES` changed in the checked package directory, that package `CHANGES` file is also changed.',
|
|
2860
|
-
'',
|
|
2861
|
-
'Run the configured checks after this checklist:',
|
|
2862
|
-
'',
|
|
2863
|
-
'{% for check in checks %}',
|
|
2864
|
-
'- `{{ check }}`',
|
|
2865
|
-
'{% endfor %}',
|
|
2866
|
-
''
|
|
2867
|
-
].join('\n');
|
|
2868
|
-
}
|
|
2869
|
-
function defaultCodeReviewOrganizerSkill() {
|
|
2870
|
-
return [
|
|
2871
|
-
'---',
|
|
2872
|
-
'name: code-review-organizer',
|
|
2873
|
-
'description: Use for organizing code review findings, review files, and review follow-up state.',
|
|
2874
|
-
'---',
|
|
2875
|
-
'',
|
|
2876
|
-
'# Code Review Organizer',
|
|
2877
|
-
'',
|
|
2878
|
-
'On review startup, move any files from {{ paths.liveDir }}/reviews into {{ paths.reviewDir }} so review history persists outside the ephemeral live directory.',
|
|
2879
|
-
'Before doing any code review, read the generated AGENTS.md file and the current consolidated review file: {{ paths.reviewConsolidatedFile }}.',
|
|
2880
|
-
'Save each active review to {{ paths.reviewDir }}/yyyy-mm-dd-[sequence].md, where the sequence increments for multiple reviews on the same date.',
|
|
2881
|
-
'Whenever a saved review is created or changed, update {{ paths.reviewConsolidatedFile }} with the consolidated current review findings.',
|
|
2882
|
-
'In {{ paths.reviewConsolidatedFile }}, include each active finding with its review date and source review filename.',
|
|
2883
|
-
'When a finding is fixed, remove it from {{ paths.reviewConsolidatedFile }} and move it to {{ paths.reviewDir }}/DONE.md with the time it was fixed.',
|
|
2884
|
-
'When a finding is intentionally not fixed, remove it from {{ paths.reviewConsolidatedFile }} and move it to {{ paths.reviewDir }}/DONE.md with the time it was marked intentional and a note that it was intentionally left as-is.',
|
|
2885
|
-
'When a dated review file has no remaining active findings, delete it.',
|
|
2886
|
-
'Also include active findings in the final user-facing reply.',
|
|
2887
|
-
'Do not save review files inside the project repo.',
|
|
2888
|
-
'Before the final reply for review work, verify that no REVIEW*.md files exist in {{ projectRoot }}, active dated review files exist only when they still contain active findings, and the consolidated REVIEW.md file was updated.',
|
|
2889
|
-
''
|
|
2890
|
-
].join('\n');
|
|
2891
|
-
}
|
|
2892
|
-
function defaultToolInstructionsTemplate(isClaude) {
|
|
2893
|
-
return [
|
|
2894
|
-
`# Generated ${isClaude ? 'Claude' : 'agent'} instructions for {{ profile }}`,
|
|
2895
|
-
'',
|
|
2896
|
-
'Do not edit this file directly. Edit:',
|
|
2897
|
-
'',
|
|
2898
|
-
'```text',
|
|
2899
|
-
'{{ profileDir }}/agent-run.jsonc',
|
|
2900
|
-
'{{ profileDir }}/local.md.njk',
|
|
2901
|
-
'```',
|
|
2902
|
-
'',
|
|
2903
|
-
'{% for section in renderedAgentSections %}',
|
|
2904
|
-
'{{ section }}',
|
|
2905
|
-
'',
|
|
2906
|
-
'{% endfor %}',
|
|
2907
|
-
'## Absolute Paths',
|
|
2908
|
-
'',
|
|
2909
|
-
'Project root:',
|
|
2910
|
-
'',
|
|
2911
|
-
'```text',
|
|
2912
|
-
'{{ projectRoot }}',
|
|
2913
|
-
'```',
|
|
2914
|
-
'',
|
|
2915
|
-
'Agent directory:',
|
|
2916
|
-
'',
|
|
2917
|
-
'```text',
|
|
2918
|
-
'{{ agentDir }}',
|
|
2919
|
-
'```',
|
|
2920
|
-
'',
|
|
2921
|
-
'Profile source directory:',
|
|
2922
|
-
'',
|
|
2923
|
-
'```text',
|
|
2924
|
-
'{{ profileDir }}',
|
|
2925
|
-
'```',
|
|
2926
|
-
'',
|
|
2927
|
-
'Review directory:',
|
|
2928
|
-
'',
|
|
2929
|
-
'```text',
|
|
2930
|
-
'{{ paths.reviewDir }}',
|
|
2931
|
-
'```',
|
|
2932
|
-
'',
|
|
2933
|
-
"Today's review file:",
|
|
2934
|
-
'',
|
|
2935
|
-
'```text',
|
|
2936
|
-
'{{ paths.reviewFile }}',
|
|
2937
|
-
'```',
|
|
2938
|
-
'',
|
|
2939
|
-
'Consolidated review file:',
|
|
2940
|
-
'',
|
|
2941
|
-
'```text',
|
|
2942
|
-
'{{ paths.reviewConsolidatedFile }}',
|
|
2943
|
-
'```',
|
|
2944
|
-
'',
|
|
2945
|
-
'Memories directory:',
|
|
2946
|
-
'',
|
|
2947
|
-
'```text',
|
|
2948
|
-
'{{ paths.memoriesDir }}',
|
|
2949
|
-
'```',
|
|
2950
|
-
'',
|
|
2951
|
-
'Global memory directory:',
|
|
2952
|
-
'',
|
|
2953
|
-
'```text',
|
|
2954
|
-
'{{ paths.globalMemoryDir }}',
|
|
2955
|
-
'```',
|
|
2956
|
-
'',
|
|
2957
|
-
'Changes file:',
|
|
2958
|
-
'',
|
|
2959
|
-
'```text',
|
|
2960
|
-
'{{ paths.changesFile }}',
|
|
2961
|
-
'```',
|
|
2962
|
-
'',
|
|
2963
|
-
'## Available Skills',
|
|
2964
|
-
'',
|
|
2965
|
-
'{% for skill in skills %}',
|
|
2966
|
-
'- `${{ skill.name }}`{% if skill.description %}: {{ skill.description }}{% endif %}',
|
|
2967
|
-
'{% endfor %}',
|
|
2968
|
-
'',
|
|
2969
|
-
'## Tool Note',
|
|
2970
|
-
'',
|
|
2971
|
-
isClaude
|
|
2972
|
-
? 'Claude receives {{ agentDir }}/CLAUDE.md through `--append-system-prompt-file` and loads generated skills as a local plugin.'
|
|
2973
|
-
: 'Codex loads AGENTS.md, config.toml, and skills natively from {{ paths.codexHomeDir }}.',
|
|
2974
|
-
'',
|
|
2975
|
-
'## Mandatory Path Rule',
|
|
2976
|
-
'',
|
|
2977
|
-
'Do not create AGENTS.md, CLAUDE.md, .agents, .claude, .codex, or AI-related files inside the project repository.',
|
|
2978
|
-
'Generated agent-only files must be stored under the agent directory shown above.',
|
|
2979
|
-
'Source config files must be stored under the profile source directory shown above.',
|
|
2980
|
-
''
|
|
2981
|
-
].join('\n');
|
|
2982
|
-
}
|
|
2983
|
-
function defaultCodexConfigTemplate() {
|
|
2984
|
-
return [
|
|
2985
|
-
'# Generated by agent-run. Do not edit directly.',
|
|
2986
|
-
'',
|
|
2987
|
-
'project_doc_max_bytes = 65536',
|
|
2988
|
-
'approval_policy = "on-request"',
|
|
2989
|
-
'sandbox_mode = "workspace-write"',
|
|
2990
|
-
'',
|
|
2991
|
-
'[sandbox_workspace_write]',
|
|
2992
|
-
'writable_roots = [',
|
|
2993
|
-
' {{ projectRoot | dump }},',
|
|
2994
|
-
' {{ paths.reviewDir | dump }},',
|
|
2995
|
-
' {{ paths.memoriesDir | dump }}',
|
|
2996
|
-
']',
|
|
2997
|
-
'network_access = false',
|
|
2998
|
-
''
|
|
2999
|
-
].join('\n');
|
|
3000
|
-
}
|
|
3001
|
-
function defaultClaudeSettingsTemplate() {
|
|
3002
|
-
const envBlock = [
|
|
3003
|
-
' "env": {',
|
|
3004
|
-
' "AGENT_DIR": {{ agentDir | dump }},',
|
|
3005
|
-
' "AGENT_PROFILE_DIR": {{ profileDir | dump }},',
|
|
3006
|
-
' "AGENT_RUN_PROJECT_ROOT": {{ projectRoot | dump }},',
|
|
3007
|
-
' "AGENT_GLOBAL_MEMORY_DIR": {{ paths.globalMemoryDir | dump }}',
|
|
3008
|
-
' }'
|
|
3009
|
-
].join('\n');
|
|
3010
|
-
return [
|
|
3011
|
-
'{% if permissionsAllow.length %}{',
|
|
3012
|
-
envBlock + ',',
|
|
3013
|
-
' "permissions": {',
|
|
3014
|
-
' "allow": {{ permissionsAllow | dump }}',
|
|
3015
|
-
' }',
|
|
3016
|
-
'}',
|
|
3017
|
-
'{% else %}{',
|
|
3018
|
-
envBlock,
|
|
3019
|
-
'}',
|
|
3020
|
-
'{% endif %}'
|
|
3021
|
-
].join('\n');
|
|
3022
|
-
}
|
|
3023
|
-
function defaultCodexConfigContent(context) {
|
|
3024
|
-
return [
|
|
3025
|
-
'# Generated by agent-run. Do not edit directly.',
|
|
3026
|
-
'',
|
|
3027
|
-
'project_doc_max_bytes = 65536',
|
|
3028
|
-
'approval_policy = "on-request"',
|
|
3029
|
-
'sandbox_mode = "workspace-write"',
|
|
3030
|
-
'',
|
|
3031
|
-
'[sandbox_workspace_write]',
|
|
3032
|
-
'writable_roots = [',
|
|
3033
|
-
` ${jsonString(context.projectRoot)},`,
|
|
3034
|
-
` ${jsonString(context.paths.reviewDir)},`,
|
|
3035
|
-
` ${jsonString(context.paths.memoriesDir)}`,
|
|
3036
|
-
']',
|
|
3037
|
-
'network_access = false',
|
|
3038
|
-
''
|
|
3039
|
-
].join('\n');
|
|
3040
|
-
}
|
|
3041
|
-
function defaultClaudeSettingsContent(context) {
|
|
3042
|
-
const settings = {
|
|
3043
|
-
env: {
|
|
3044
|
-
AGENT_DIR: context.agentDir,
|
|
3045
|
-
AGENT_PROFILE_DIR: context.profileDir,
|
|
3046
|
-
AGENT_RUN_PROJECT_ROOT: context.projectRoot,
|
|
3047
|
-
AGENT_GLOBAL_MEMORY_DIR: context.paths.globalMemoryDir
|
|
3048
|
-
}
|
|
3049
|
-
};
|
|
3050
|
-
if (context.permissionsAllow.length > 0) {
|
|
3051
|
-
settings.permissions = { allow: context.permissionsAllow };
|
|
3052
|
-
}
|
|
3053
|
-
return `${JSON.stringify(settings, null, 2)}\n`;
|
|
3054
|
-
}
|
|
3055
|
-
function legacyDefaultClaudeSettingsContent(context, agentDir) {
|
|
3056
|
-
return `${JSON.stringify({
|
|
3057
|
-
env: {
|
|
3058
|
-
AGENT_DIR: agentDir,
|
|
3059
|
-
AGENT_RUN_PROJECT_ROOT: context.projectRoot,
|
|
3060
|
-
AGENT_GLOBAL_MEMORY_DIR: context.paths.globalMemoryDir
|
|
3061
|
-
}
|
|
3062
|
-
}, null, 2)}\n`;
|
|
3063
|
-
}
|
|
3064
|
-
function jsonString(value) {
|
|
3065
|
-
return JSON.stringify(value);
|
|
3066
|
-
}
|
|
3067
|
-
function posixGitShim() {
|
|
3068
|
-
return [
|
|
3069
|
-
'#!/usr/bin/env bash',
|
|
3070
|
-
'set -euo pipefail',
|
|
3071
|
-
'for arg in "$@"; do',
|
|
3072
|
-
' case "$arg" in',
|
|
3073
|
-
' commit|tag|push)',
|
|
3074
|
-
' if [ "${AGENT_RUN_ALLOW_GIT_WRITE:-}" != "1" ]; then',
|
|
3075
|
-
' echo "agent-run: blocked git $arg. Review and run it manually, or set AGENT_RUN_ALLOW_GIT_WRITE=1 for this invocation." >&2',
|
|
3076
|
-
' exit 42',
|
|
3077
|
-
' fi',
|
|
3078
|
-
' ;;',
|
|
3079
|
-
' esac',
|
|
3080
|
-
'done',
|
|
3081
|
-
...posixRunRealCommand('git'),
|
|
3082
|
-
''
|
|
3083
|
-
].join('\n');
|
|
3084
|
-
}
|
|
3085
|
-
function posixPublishShim(tool) {
|
|
3086
|
-
return [
|
|
3087
|
-
'#!/usr/bin/env bash',
|
|
3088
|
-
'set -euo pipefail',
|
|
3089
|
-
'for arg in "$@"; do',
|
|
3090
|
-
' if [ "$arg" = "publish" ] && [ "${AGENT_RUN_ALLOW_PUBLISH:-}" != "1" ]; then',
|
|
3091
|
-
` echo "agent-run: blocked ${tool} publish. Review and run it manually, or set AGENT_RUN_ALLOW_PUBLISH=1 for this invocation." >&2`,
|
|
3092
|
-
' exit 42',
|
|
3093
|
-
' fi',
|
|
3094
|
-
'done',
|
|
3095
|
-
...posixRunRealCommand(tool),
|
|
3096
|
-
''
|
|
3097
|
-
].join('\n');
|
|
3098
|
-
}
|
|
3099
|
-
function posixGhShim() {
|
|
3100
|
-
return [
|
|
3101
|
-
'#!/usr/bin/env bash',
|
|
3102
|
-
'set -euo pipefail',
|
|
3103
|
-
'saw_release=0',
|
|
3104
|
-
'for arg in "$@"; do',
|
|
3105
|
-
' if [ "$arg" = "release" ]; then',
|
|
3106
|
-
' saw_release=1',
|
|
3107
|
-
' elif [ "$saw_release" = "1" ] && [ "$arg" = "create" ] && [ "${AGENT_RUN_ALLOW_GITHUB_RELEASE:-}" != "1" ]; then',
|
|
3108
|
-
' echo "agent-run: blocked gh release create. Review and run it manually, or set AGENT_RUN_ALLOW_GITHUB_RELEASE=1 for this invocation." >&2',
|
|
3109
|
-
' exit 42',
|
|
3110
|
-
' fi',
|
|
3111
|
-
'done',
|
|
3112
|
-
...posixRunRealCommand('gh'),
|
|
3113
|
-
''
|
|
3114
|
-
].join('\n');
|
|
3115
|
-
}
|
|
3116
|
-
function posixRunRealCommand(tool) {
|
|
3117
|
-
return [
|
|
3118
|
-
'shim_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"',
|
|
3119
|
-
'real_path="${AGENT_RUN_REAL_PATH:-}"',
|
|
3120
|
-
'if [ -z "$real_path" ]; then',
|
|
3121
|
-
' case "${PATH:-}" in',
|
|
3122
|
-
' "$shim_dir") real_path="" ;;',
|
|
3123
|
-
' "$shim_dir":*) real_path="${PATH#*:}" ;;',
|
|
3124
|
-
' *) real_path="${PATH:-}" ;;',
|
|
3125
|
-
' esac',
|
|
3126
|
-
'fi',
|
|
3127
|
-
`real_command="$(PATH="$real_path" command -v ${tool} || true)"`,
|
|
3128
|
-
'if [ -z "$real_command" ]; then',
|
|
3129
|
-
` echo "agent-run: cannot find the real ${tool} command outside the guard directory." >&2`,
|
|
3130
|
-
' exit 127',
|
|
3131
|
-
'fi',
|
|
3132
|
-
`if [ "$real_command" = "$shim_dir/${tool}" ]; then`,
|
|
3133
|
-
` echo "agent-run: refused recursive ${tool} guard resolution." >&2`,
|
|
3134
|
-
' exit 127',
|
|
3135
|
-
'fi',
|
|
3136
|
-
'PATH="$real_path" exec "$real_command" "$@"'
|
|
3137
|
-
];
|
|
3138
|
-
}
|
|
3139
|
-
function windowsShim(name) {
|
|
3140
|
-
const executable = name === 'npm' || name === 'pnpm' ? `${name}.cmd` : `${name}.exe`;
|
|
3141
|
-
const guard = name === 'git'
|
|
3142
|
-
? [
|
|
3143
|
-
':scan',
|
|
3144
|
-
'if "%~1"=="" goto run',
|
|
3145
|
-
'if /I "%~1"=="commit" goto block_git',
|
|
3146
|
-
'if /I "%~1"=="tag" goto block_git',
|
|
3147
|
-
'if /I "%~1"=="push" goto block_git',
|
|
3148
|
-
'shift',
|
|
3149
|
-
'goto scan',
|
|
3150
|
-
':block_git',
|
|
3151
|
-
'if "%AGENT_RUN_ALLOW_GIT_WRITE%"=="1" goto run',
|
|
3152
|
-
'echo agent-run: blocked git write. Review and run it manually, or set AGENT_RUN_ALLOW_GIT_WRITE=1 for this invocation. 1>&2',
|
|
3153
|
-
'exit /b 42'
|
|
3154
|
-
]
|
|
3155
|
-
: name === 'gh'
|
|
3156
|
-
? [
|
|
3157
|
-
'set "SAW_RELEASE=0"',
|
|
3158
|
-
':scan',
|
|
3159
|
-
'if "%~1"=="" goto run',
|
|
3160
|
-
'if /I "%~1"=="release" set "SAW_RELEASE=1"',
|
|
3161
|
-
'if "%SAW_RELEASE%"=="1" if /I "%~1"=="create" goto block_release',
|
|
3162
|
-
'shift',
|
|
3163
|
-
'goto scan',
|
|
3164
|
-
':block_release',
|
|
3165
|
-
'if "%AGENT_RUN_ALLOW_GITHUB_RELEASE%"=="1" goto run',
|
|
3166
|
-
'echo agent-run: blocked gh release create. Review and run it manually, or set AGENT_RUN_ALLOW_GITHUB_RELEASE=1 for this invocation. 1>&2',
|
|
3167
|
-
'exit /b 42'
|
|
3168
|
-
]
|
|
3169
|
-
: [
|
|
3170
|
-
':scan',
|
|
3171
|
-
'if "%~1"=="" goto run',
|
|
3172
|
-
'if /I "%~1"=="publish" goto block_publish',
|
|
3173
|
-
'shift',
|
|
3174
|
-
'goto scan',
|
|
3175
|
-
':block_publish',
|
|
3176
|
-
'if "%AGENT_RUN_ALLOW_PUBLISH%"=="1" goto run',
|
|
3177
|
-
`echo agent-run: blocked ${name} publish. Review and run it manually, or set AGENT_RUN_ALLOW_PUBLISH=1 for this invocation. 1>&2`,
|
|
3178
|
-
'exit /b 42'
|
|
3179
|
-
];
|
|
3180
|
-
return [
|
|
3181
|
-
'@echo off',
|
|
3182
|
-
...guard,
|
|
3183
|
-
':run',
|
|
3184
|
-
'if defined AGENT_RUN_REAL_PATH (',
|
|
3185
|
-
' set "PATH=%AGENT_RUN_REAL_PATH%"',
|
|
3186
|
-
') else (',
|
|
3187
|
-
' for /f "tokens=1,* delims=;" %%A in ("%PATH%") do set "PATH=%%B"',
|
|
3188
|
-
')',
|
|
3189
|
-
`${executable} %*`,
|
|
3190
|
-
''
|
|
3191
|
-
].join('\n');
|
|
3192
|
-
}
|
|
3193
|
-
function isVerbose() {
|
|
3194
|
-
return parseBooleanEnv(process.env[VERBOSE_ENV]);
|
|
3195
|
-
}
|
|
3196
|
-
function verbose(message) {
|
|
3197
|
-
if (isVerbose()) {
|
|
3198
|
-
process.stderr.write(`agent-run: ${message}\n`);
|
|
3199
|
-
}
|
|
3200
|
-
}
|
|
3201
|
-
function formatCommand(command, args) {
|
|
3202
|
-
return [command, ...args].map(quoteArg).join(' ');
|
|
3203
|
-
}
|
|
3204
|
-
function quoteArg(value) {
|
|
3205
|
-
if (value === '') {
|
|
3206
|
-
return '""';
|
|
3207
|
-
}
|
|
3208
|
-
if (/^[A-Za-z0-9_./:=+-]+$/.test(value)) {
|
|
3209
|
-
return value;
|
|
3210
|
-
}
|
|
3211
|
-
return JSON.stringify(value);
|
|
3212
|
-
}
|
|
3213
|
-
function fail(message) {
|
|
3214
|
-
process.stderr.write(`agent-run: ${message}\n`);
|
|
3215
|
-
process.exit(1);
|
|
3216
|
-
}
|
|
6
|
+
const commands_1 = require("./commands");
|
|
7
|
+
var cli_1 = require("./cli");
|
|
8
|
+
Object.defineProperty(exports, "parseInvocation", { enumerable: true, get: function () { return cli_1.parseInvocation; } });
|
|
9
|
+
var project_1 = require("./project");
|
|
10
|
+
Object.defineProperty(exports, "defaultConfigRoot", { enumerable: true, get: function () { return project_1.defaultConfigRoot; } });
|
|
11
|
+
Object.defineProperty(exports, "defaultConfigRootSearchCandidates", { enumerable: true, get: function () { return project_1.defaultConfigRootSearchCandidates; } });
|
|
12
|
+
Object.defineProperty(exports, "findProjectRoot", { enumerable: true, get: function () { return project_1.findProjectRoot; } });
|
|
13
|
+
Object.defineProperty(exports, "resolveAgentDir", { enumerable: true, get: function () { return project_1.resolveAgentDir; } });
|
|
14
|
+
Object.defineProperty(exports, "resolveProfile", { enumerable: true, get: function () { return project_1.resolveProfile; } });
|
|
15
|
+
var process_1 = require("./process");
|
|
16
|
+
Object.defineProperty(exports, "parseEditorCommand", { enumerable: true, get: function () { return process_1.parseEditorCommand; } });
|
|
3217
17
|
if (require.main === module) {
|
|
3218
|
-
main(path.basename(process.argv[1] ?? 'agent-run'), process.argv.slice(2));
|
|
18
|
+
(0, commands_1.main)(path.basename(process.argv[1] ?? 'agent-run'), process.argv.slice(2));
|
|
3219
19
|
}
|