agile-context-engineering 0.5.0 → 0.5.1
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/.claude-plugin/marketplace.json +18 -0
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +7 -1
- package/README.md +16 -12
- package/agents/ace-code-discovery-analyst.md +245 -245
- package/agents/ace-code-integration-analyst.md +248 -248
- package/agents/ace-code-reviewer.md +375 -375
- package/agents/ace-product-owner.md +365 -361
- package/agents/ace-project-researcher.md +606 -606
- package/agents/ace-technical-application-architect.md +315 -315
- package/bin/install.js +587 -173
- package/hooks/ace-check-update.js +15 -14
- package/hooks/ace-statusline.js +30 -12
- package/hooks/hooks.json +14 -0
- package/package.json +3 -2
- package/shared/lib/ace-core.js +53 -0
- package/shared/lib/ace-core.test.js +308 -308
- package/shared/lib/ace-story.test.js +250 -250
- package/skills/execute-story/SKILL.md +116 -110
- package/skills/execute-story/script.js +13 -27
- package/skills/execute-story/script.test.js +261 -261
- package/skills/execute-story/story-template.xml +451 -451
- package/skills/execute-story/workflow.xml +3 -1
- package/skills/help/SKILL.md +71 -69
- package/skills/help/script.js +32 -35
- package/skills/help/script.test.js +183 -183
- package/skills/help/workflow.xml +14 -3
- package/skills/init-coding-standards/SKILL.md +91 -72
- package/skills/init-coding-standards/coding-standards-template.xml +531 -531
- package/skills/init-coding-standards/script.js +50 -59
- package/skills/init-coding-standards/script.test.js +70 -70
- package/skills/init-coding-standards/workflow.xml +1 -1
- package/skills/map-cross-cutting/SKILL.md +126 -89
- package/skills/map-cross-cutting/workflow.xml +1 -1
- package/skills/map-guide/SKILL.md +126 -89
- package/skills/map-guide/workflow.xml +1 -1
- package/skills/map-pattern/SKILL.md +125 -89
- package/skills/map-pattern/workflow.xml +1 -1
- package/skills/map-story/SKILL.md +180 -127
- package/skills/map-story/templates/tech-debt-index.xml +125 -125
- package/skills/map-story/workflow.xml +2 -2
- package/skills/map-subsystem/SKILL.md +155 -111
- package/skills/map-subsystem/script.js +51 -60
- package/skills/map-subsystem/script.test.js +68 -68
- package/skills/map-subsystem/templates/subsystem-architecture.xml +343 -343
- package/skills/map-subsystem/templates/subsystem-structure.xml +234 -234
- package/skills/map-subsystem/workflow.xml +1173 -1173
- package/skills/map-sys-doc/SKILL.md +125 -90
- package/skills/map-sys-doc/workflow.xml +1 -1
- package/skills/map-system/SKILL.md +103 -85
- package/skills/map-system/script.js +75 -84
- package/skills/map-system/script.test.js +73 -73
- package/skills/map-system/templates/system-structure.xml +177 -177
- package/skills/map-system/templates/testing-framework.xml +283 -283
- package/skills/map-system/workflow.xml +667 -667
- package/skills/map-walkthrough/SKILL.md +140 -92
- package/skills/map-walkthrough/workflow.xml +457 -457
- package/skills/plan-backlog/SKILL.md +93 -75
- package/skills/plan-backlog/script.js +121 -136
- package/skills/plan-backlog/script.test.js +83 -83
- package/skills/plan-backlog/workflow.xml +1348 -1348
- package/skills/plan-feature/SKILL.md +99 -76
- package/skills/plan-feature/feature-template.xml +361 -361
- package/skills/plan-feature/script.js +131 -148
- package/skills/plan-feature/script.test.js +80 -80
- package/skills/plan-feature/workflow.xml +1 -1
- package/skills/plan-product-vision/SKILL.md +91 -75
- package/skills/plan-product-vision/product-vision-template.xml +227 -227
- package/skills/plan-product-vision/script.js +51 -60
- package/skills/plan-product-vision/script.test.js +69 -69
- package/skills/plan-product-vision/workflow.xml +337 -337
- package/skills/plan-story/SKILL.md +125 -102
- package/skills/plan-story/script.js +18 -49
- package/skills/plan-story/story-template.xml +8 -1
- package/skills/plan-story/workflow.xml +17 -1
- package/skills/research-external-solution/SKILL.md +120 -107
- package/skills/research-external-solution/external-solution-template.xml +832 -832
- package/skills/research-external-solution/script.js +229 -238
- package/skills/research-external-solution/script.test.js +134 -134
- package/skills/research-external-solution/workflow.xml +657 -657
- package/skills/research-integration-solution/SKILL.md +121 -98
- package/skills/research-integration-solution/integration-solution-template.xml +1015 -1015
- package/skills/research-integration-solution/script.js +223 -231
- package/skills/research-integration-solution/script.test.js +134 -134
- package/skills/research-integration-solution/workflow.xml +711 -711
- package/skills/research-story-wiki/SKILL.md +101 -92
- package/skills/research-story-wiki/script.js +223 -231
- package/skills/research-story-wiki/script.test.js +138 -138
- package/skills/research-story-wiki/story-wiki-template.xml +194 -194
- package/skills/research-story-wiki/workflow.xml +473 -473
- package/skills/research-technical-solution/SKILL.md +131 -103
- package/skills/research-technical-solution/script.js +223 -231
- package/skills/research-technical-solution/script.test.js +134 -134
- package/skills/research-technical-solution/technical-solution-template.xml +1025 -1025
- package/skills/research-technical-solution/workflow.xml +761 -761
- package/skills/review-story/SKILL.md +99 -100
- package/skills/review-story/script.js +8 -16
- package/skills/review-story/script.test.js +169 -169
- package/skills/review-story/story-template.xml +451 -451
- package/skills/review-story/workflow.xml +1 -1
- package/skills/update/SKILL.md +65 -53
- package/skills/update/workflow.xml +21 -5
package/bin/install.js
CHANGED
|
@@ -4,6 +4,7 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const readline = require('readline');
|
|
6
6
|
const os = require('os');
|
|
7
|
+
const { execSync } = require('child_process');
|
|
7
8
|
|
|
8
9
|
const VERSION = require('../package.json').version;
|
|
9
10
|
|
|
@@ -25,22 +26,38 @@ const RUNTIMES = {
|
|
|
25
26
|
name: 'Claude Code',
|
|
26
27
|
description: "Anthropic's Claude Code CLI",
|
|
27
28
|
globalDir: '.claude',
|
|
28
|
-
|
|
29
|
+
supportsPlugin: true,
|
|
30
|
+
},
|
|
31
|
+
codex: {
|
|
32
|
+
name: 'Codex',
|
|
33
|
+
description: "OpenAI's Codex CLI",
|
|
34
|
+
globalDir: '.codex',
|
|
29
35
|
agentsDir: 'agents',
|
|
30
|
-
|
|
36
|
+
supportsPlugin: false,
|
|
31
37
|
},
|
|
32
38
|
opencode: {
|
|
33
39
|
name: 'Crush',
|
|
34
40
|
description: 'Crush AI coding assistant (formerly OpenCode)',
|
|
35
41
|
globalDir: '.opencode',
|
|
36
|
-
commandsDir: 'commands',
|
|
37
42
|
agentsDir: 'agents',
|
|
38
|
-
|
|
43
|
+
supportsPlugin: false,
|
|
39
44
|
},
|
|
40
45
|
};
|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
const
|
|
47
|
+
const MARKETPLACE_NAME = 'ace-marketplace';
|
|
48
|
+
const CODEX_CONFIG_BEGIN = '# ACE Agent Configuration - managed by agile-context-engineering installer';
|
|
49
|
+
const CODEX_CONFIG_END = '# End ACE Agent Configuration';
|
|
50
|
+
|
|
51
|
+
const CODEX_AGENT_SANDBOX = {
|
|
52
|
+
'ace-code-reviewer': 'read-only',
|
|
53
|
+
'ace-code-discovery-analyst': 'read-only',
|
|
54
|
+
'ace-code-integration-analyst': 'read-only',
|
|
55
|
+
'ace-project-researcher': 'read-only',
|
|
56
|
+
'ace-research-synthesizer': 'workspace-write',
|
|
57
|
+
'ace-product-owner': 'workspace-write',
|
|
58
|
+
'ace-technical-application-architect': 'workspace-write',
|
|
59
|
+
'ace-wiki-mapper': 'workspace-write',
|
|
60
|
+
};
|
|
44
61
|
|
|
45
62
|
function log(message, color = '') {
|
|
46
63
|
console.log(`${color}${message}${colors.reset}`);
|
|
@@ -63,6 +80,7 @@ function parseArgs() {
|
|
|
63
80
|
const args = process.argv.slice(2);
|
|
64
81
|
const flags = {
|
|
65
82
|
claude: args.includes('--claude'),
|
|
83
|
+
codex: args.includes('--codex'),
|
|
66
84
|
opencode: args.includes('--opencode'),
|
|
67
85
|
all: args.includes('--all'),
|
|
68
86
|
global: args.includes('--global'),
|
|
@@ -86,22 +104,17 @@ function readSettings(settingsPath) {
|
|
|
86
104
|
return {};
|
|
87
105
|
}
|
|
88
106
|
|
|
89
|
-
// Build hook command with proper quoting for the target directory
|
|
90
|
-
function buildHookCommand(targetDir, hookFile) {
|
|
91
|
-
const hookPath = path.join(targetDir, 'hooks', hookFile);
|
|
92
|
-
return `node "${hookPath.replace(/\\/g, '/')}"`;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
107
|
function showHelp() {
|
|
96
108
|
log(`
|
|
97
109
|
Usage: npx agile-context-engineering [options]
|
|
98
110
|
|
|
99
111
|
Options:
|
|
100
112
|
--claude Install for Claude Code only
|
|
113
|
+
--codex Install for Codex only
|
|
101
114
|
--opencode Install for Crush (formerly OpenCode)
|
|
102
115
|
--all Install for all supported runtimes
|
|
103
|
-
--global Install globally (~/.claude, ~/.opencode)
|
|
104
|
-
--local Install locally (.claude, .opencode)
|
|
116
|
+
--global Install globally (~/.claude, ~/.codex, ~/.opencode)
|
|
117
|
+
--local Install locally (.claude, .codex, .opencode)
|
|
105
118
|
--force-statusline Replace existing statusline configuration
|
|
106
119
|
-h, --help Show this help message
|
|
107
120
|
-v, --version Show version number
|
|
@@ -109,6 +122,7 @@ Options:
|
|
|
109
122
|
Examples:
|
|
110
123
|
npx agile-context-engineering # Interactive installation
|
|
111
124
|
npx agile-context-engineering --claude --local # Claude Code, local install
|
|
125
|
+
npx agile-context-engineering --codex --global # Codex, global install
|
|
112
126
|
npx agile-context-engineering --opencode --global # Crush (formerly OpenCode), global install
|
|
113
127
|
npx agile-context-engineering --all --global # All runtimes, global install
|
|
114
128
|
`);
|
|
@@ -178,6 +192,10 @@ function getBasePath(runtime, scope) {
|
|
|
178
192
|
const cwd = process.cwd();
|
|
179
193
|
const config = RUNTIMES[runtime];
|
|
180
194
|
|
|
195
|
+
if (runtime === 'codex' && scope === 'global' && process.env.CODEX_HOME) {
|
|
196
|
+
return path.resolve(process.env.CODEX_HOME);
|
|
197
|
+
}
|
|
198
|
+
|
|
181
199
|
return scope === 'global'
|
|
182
200
|
? path.join(home, config.globalDir)
|
|
183
201
|
: path.join(cwd, config.globalDir);
|
|
@@ -190,8 +208,9 @@ const TRANSFORMABLE_EXTENSIONS = new Set(['.md', '.xml', '.js']);
|
|
|
190
208
|
function transformForRuntime(content, runtime) {
|
|
191
209
|
if (runtime === 'claude') return content; // Source files already use .claude paths
|
|
192
210
|
const targetDir = RUNTIMES[runtime].globalDir; // e.g. '.opencode'
|
|
193
|
-
|
|
194
|
-
|
|
211
|
+
return content
|
|
212
|
+
.replace(/\.claude\//g, `${targetDir}/`)
|
|
213
|
+
.replace(/\.claudeignore\b/g, `${targetDir}ignore`);
|
|
195
214
|
}
|
|
196
215
|
|
|
197
216
|
// Copy directory recursively, optionally transforming text file content for the target runtime
|
|
@@ -211,7 +230,6 @@ function copyDir(src, dest, runtime) {
|
|
|
211
230
|
} else {
|
|
212
231
|
const ext = path.extname(entry.name).toLowerCase();
|
|
213
232
|
if (runtime !== 'claude' && TRANSFORMABLE_EXTENSIONS.has(ext)) {
|
|
214
|
-
// Transform path references for non-Claude runtimes
|
|
215
233
|
const content = fs.readFileSync(srcPath, 'utf-8');
|
|
216
234
|
fs.writeFileSync(destPath, transformForRuntime(content, runtime), 'utf-8');
|
|
217
235
|
} else {
|
|
@@ -221,106 +239,566 @@ function copyDir(src, dest, runtime) {
|
|
|
221
239
|
}
|
|
222
240
|
}
|
|
223
241
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const basePath = getBasePath(runtime, scope);
|
|
228
|
-
const agentsPath = path.join(basePath, config.agentsDir);
|
|
242
|
+
function toPosixPath(filePath) {
|
|
243
|
+
return filePath.replace(/\\/g, '/');
|
|
244
|
+
}
|
|
229
245
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const srcAgents = path.join(packageDir, 'agents');
|
|
246
|
+
function replaceAceInvocations(content, codexStyle) {
|
|
247
|
+
if (!codexStyle) return content;
|
|
248
|
+
return content.replace(/\/ace:([a-z0-9-]+)/g, '$ace-$1');
|
|
249
|
+
}
|
|
235
250
|
|
|
236
|
-
|
|
237
|
-
|
|
251
|
+
function transformCodexSkillContent(content, skillName, skillDir) {
|
|
252
|
+
let transformed = content
|
|
253
|
+
.replace(/\.claude\//g, '.codex/')
|
|
254
|
+
.replace(/\.claudeignore\b/g, '.codexignore')
|
|
255
|
+
.replace(/\$\{CLAUDE_SKILL_DIR\}/g, toPosixPath(skillDir))
|
|
256
|
+
.replace(/"\$CLAUDE_SKILL_DIR\//g, `"${toPosixPath(skillDir)}/`)
|
|
257
|
+
.replace(/\$CLAUDE_SKILL_DIR\//g, `${toPosixPath(skillDir)}/`)
|
|
258
|
+
.replace(/CLAUDE_SKILL_DIR/g, 'CODEX_SKILL_DIR');
|
|
259
|
+
|
|
260
|
+
transformed = replaceAceInvocations(transformed, true);
|
|
238
261
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if (fs.existsSync(aceCommandsPath)) {
|
|
242
|
-
fs.rmSync(aceCommandsPath, { recursive: true });
|
|
262
|
+
if (path.basename(skillDir) === skillName && content.includes('---')) {
|
|
263
|
+
transformed = transformed.replace(/^name:\s*.+$/m, `name: ace-${skillName}`);
|
|
243
264
|
}
|
|
265
|
+
|
|
266
|
+
return transformed;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function codexSkillAdapter(skillName, skillDir) {
|
|
270
|
+
const posixSkillDir = toPosixPath(skillDir);
|
|
271
|
+
return `<codex_skill_adapter>
|
|
272
|
+
## Codex Invocation
|
|
273
|
+
- Invoke this skill by mentioning \`$ace-${skillName}\`.
|
|
274
|
+
- Treat any user text after \`$ace-${skillName}\` as the command arguments.
|
|
275
|
+
- In examples copied from Claude Code, \`/ace:${skillName}\` means \`$ace-${skillName}\`.
|
|
276
|
+
|
|
277
|
+
## Runtime Compatibility
|
|
278
|
+
- This repository is authored as a Claude Code plugin. In Codex, the installer copies it to \`${posixSkillDir}\`.
|
|
279
|
+
- Claude's \`!\` resource expansion does not run in Codex. Before following the workflow, manually read the supporting files referenced near the top of this SKILL.md.
|
|
280
|
+
- When a workflow says \`AskUserQuestion\`, use Codex \`request_user_input\` if available; otherwise ask the user directly and continue with a reasonable default only when the choice is low risk.
|
|
281
|
+
- When a workflow says \`Task(..., subagent_type="X")\`, use \`spawn_agent(agent_type="X", message="...")\` only when the user explicitly requested sub-agents. Otherwise complete the work inline in the current agent.
|
|
282
|
+
- Prefer commands that use the absolute skill path above. It keeps Windows and Linux shells from depending on a runtime-specific skill directory environment variable.
|
|
283
|
+
</codex_skill_adapter>
|
|
284
|
+
|
|
285
|
+
`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function copySkillsForCodex(srcSkills, destSkills) {
|
|
289
|
+
if (!fs.existsSync(srcSkills)) return 0;
|
|
290
|
+
fs.mkdirSync(destSkills, { recursive: true });
|
|
291
|
+
|
|
292
|
+
for (const entry of fs.readdirSync(destSkills, { withFileTypes: true })) {
|
|
293
|
+
if (entry.isDirectory() && entry.name.startsWith('ace-')) {
|
|
294
|
+
fs.rmSync(path.join(destSkills, entry.name), { recursive: true });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
let count = 0;
|
|
299
|
+
for (const entry of fs.readdirSync(srcSkills, { withFileTypes: true })) {
|
|
300
|
+
if (!entry.isDirectory()) continue;
|
|
301
|
+
const skillName = entry.name;
|
|
302
|
+
const srcDir = path.join(srcSkills, skillName);
|
|
303
|
+
const destDir = path.join(destSkills, `ace-${skillName}`);
|
|
304
|
+
copyCodexSkillDir(srcDir, destDir, skillName);
|
|
305
|
+
count += 1;
|
|
306
|
+
}
|
|
307
|
+
return count;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function copyCodexSkillDir(src, dest, skillName) {
|
|
311
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
312
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
313
|
+
const srcPath = path.join(src, entry.name);
|
|
314
|
+
const destPath = path.join(dest, entry.name);
|
|
315
|
+
if (entry.isDirectory()) {
|
|
316
|
+
copyCodexSkillDir(srcPath, destPath, skillName);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
321
|
+
if (TRANSFORMABLE_EXTENSIONS.has(ext)) {
|
|
322
|
+
let content = fs.readFileSync(srcPath, 'utf-8');
|
|
323
|
+
content = transformCodexSkillContent(content, skillName, dest);
|
|
324
|
+
if (entry.name === 'SKILL.md') {
|
|
325
|
+
content = content.replace(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/, (match, body) => {
|
|
326
|
+
const lines = body.split(/\r?\n/);
|
|
327
|
+
const filtered = lines.filter(line =>
|
|
328
|
+
!/^(allowed-tools|argument-hint|disable-model-invocation|model|effort):/.test(line.trim())
|
|
329
|
+
);
|
|
330
|
+
const nameIndex = filtered.findIndex(line => /^name:/.test(line));
|
|
331
|
+
if (nameIndex >= 0) {
|
|
332
|
+
filtered[nameIndex] = `name: ace-${skillName}`;
|
|
333
|
+
} else {
|
|
334
|
+
filtered.unshift(`name: ace-${skillName}`);
|
|
335
|
+
}
|
|
336
|
+
return `---\n${filtered.join('\n')}\n---\n\n`;
|
|
337
|
+
});
|
|
338
|
+
content = content.replace(/^(---\r?\n[\s\S]*?\r?\n---\r?\n)/, `$1\n${codexSkillAdapter(skillName, dest)}`);
|
|
339
|
+
}
|
|
340
|
+
fs.writeFileSync(destPath, content, 'utf-8');
|
|
341
|
+
} else {
|
|
342
|
+
fs.copyFileSync(srcPath, destPath);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function extractFrontmatter(content) {
|
|
348
|
+
const match = content.match(/^\s*(?:<!--[\s\S]*?-->\s*)*---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
349
|
+
if (!match) return { frontmatter: '', body: content };
|
|
350
|
+
return { frontmatter: match[1], body: content.slice(match[0].length) };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function extractFrontmatterField(frontmatter, field) {
|
|
354
|
+
const re = new RegExp(`^${field}:\\s*(.*)$`, 'm');
|
|
355
|
+
const match = frontmatter.match(re);
|
|
356
|
+
return match ? match[1].trim().replace(/^["']|["']$/g, '') : '';
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function generateCodexAgentToml(agentName, sourceContent) {
|
|
360
|
+
const content = replaceAceInvocations(
|
|
361
|
+
sourceContent
|
|
362
|
+
.replace(/\.claude\//g, '.codex/')
|
|
363
|
+
.replace(/\.claudeignore\b/g, '.codexignore')
|
|
364
|
+
.replace(/CLAUDE_SKILL_DIR/g, 'CODEX_SKILL_DIR'),
|
|
365
|
+
true
|
|
366
|
+
);
|
|
367
|
+
const { frontmatter, body } = extractFrontmatter(content);
|
|
368
|
+
const name = extractFrontmatterField(frontmatter, 'name') || agentName;
|
|
369
|
+
const description = extractFrontmatterField(frontmatter, 'description') || `ACE agent ${name}`;
|
|
370
|
+
const sandbox = CODEX_AGENT_SANDBOX[name] || 'workspace-write';
|
|
371
|
+
return [
|
|
372
|
+
`name = ${JSON.stringify(name)}`,
|
|
373
|
+
`description = ${JSON.stringify(description)}`,
|
|
374
|
+
`sandbox_mode = ${JSON.stringify(sandbox)}`,
|
|
375
|
+
`developer_instructions = '''`,
|
|
376
|
+
body.trim(),
|
|
377
|
+
`'''`,
|
|
378
|
+
'',
|
|
379
|
+
].join('\n');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function installCodexAgents(configDir, srcAgents) {
|
|
383
|
+
if (!fs.existsSync(srcAgents)) return 0;
|
|
384
|
+
const agentsDir = path.join(configDir, 'agents');
|
|
385
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
386
|
+
|
|
387
|
+
for (const file of fs.readdirSync(agentsDir)) {
|
|
388
|
+
if (file.startsWith('ace-') && file.endsWith('.toml')) {
|
|
389
|
+
fs.rmSync(path.join(agentsDir, file), { force: true });
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const agents = [];
|
|
394
|
+
for (const file of fs.readdirSync(srcAgents)) {
|
|
395
|
+
if (!file.startsWith('ace-') || !file.endsWith('.md')) continue;
|
|
396
|
+
const agentName = file.slice(0, -3);
|
|
397
|
+
const content = fs.readFileSync(path.join(srcAgents, file), 'utf-8');
|
|
398
|
+
const { frontmatter } = extractFrontmatter(content);
|
|
399
|
+
const description = extractFrontmatterField(frontmatter, 'description') || `ACE agent ${agentName}`;
|
|
400
|
+
fs.writeFileSync(path.join(agentsDir, `${agentName}.toml`), generateCodexAgentToml(agentName, content), 'utf-8');
|
|
401
|
+
agents.push({ name: agentName, description });
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
mergeCodexAgentConfig(path.join(configDir, 'config.toml'), agents, agentsDir);
|
|
405
|
+
return agents.length;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function mergeCodexAgentConfig(configPath, agents, agentsDir) {
|
|
409
|
+
const eol = os.EOL;
|
|
410
|
+
let existing = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf-8') : '';
|
|
411
|
+
const blockPattern = new RegExp(`${escapeRegex(CODEX_CONFIG_BEGIN)}[\\s\\S]*?${escapeRegex(CODEX_CONFIG_END)}\\r?\\n?`, 'm');
|
|
412
|
+
existing = existing.replace(blockPattern, '').trimEnd();
|
|
413
|
+
|
|
414
|
+
const lines = [CODEX_CONFIG_BEGIN, ''];
|
|
415
|
+
for (const agent of agents) {
|
|
416
|
+
lines.push(`[agents.${agent.name}]`);
|
|
417
|
+
lines.push(`description = ${JSON.stringify(agent.description)}`);
|
|
418
|
+
lines.push(`config_file = ${JSON.stringify(`${toPosixPath(agentsDir)}/${agent.name}.toml`)}`);
|
|
419
|
+
lines.push('');
|
|
420
|
+
}
|
|
421
|
+
lines.push(CODEX_CONFIG_END);
|
|
422
|
+
|
|
423
|
+
const prefix = existing ? `${existing}${eol}${eol}` : '';
|
|
424
|
+
fs.writeFileSync(configPath, `${prefix}${lines.join(eol)}${eol}`, 'utf-8');
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function escapeRegex(value) {
|
|
428
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Check if claude CLI is available
|
|
432
|
+
function hasClaudeCli() {
|
|
433
|
+
try {
|
|
434
|
+
execSync('claude --version', { encoding: 'utf8', stdio: 'pipe', windowsHide: true });
|
|
435
|
+
return true;
|
|
436
|
+
} catch {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Run a claude CLI command, return { success, output }
|
|
442
|
+
function runClaude(args) {
|
|
443
|
+
try {
|
|
444
|
+
const output = execSync(`claude ${args}`, { encoding: 'utf8', stdio: 'pipe', windowsHide: true, timeout: 30000 });
|
|
445
|
+
return { success: true, output: output.trim() };
|
|
446
|
+
} catch (e) {
|
|
447
|
+
return { success: false, output: (e.stderr || e.message || '').trim() };
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Clean up old standalone ACE installation from ~/.claude/
|
|
452
|
+
function cleanLegacyInstall(basePath) {
|
|
453
|
+
const dirsToClean = [
|
|
454
|
+
path.join(basePath, 'skills'),
|
|
455
|
+
path.join(basePath, 'shared'),
|
|
456
|
+
path.join(basePath, '.claude-plugin'),
|
|
457
|
+
path.join(basePath, 'agile-context-engineering'), // pre-plugin legacy
|
|
458
|
+
];
|
|
459
|
+
const commandsAce = path.join(basePath, 'commands', 'ace');
|
|
460
|
+
if (fs.existsSync(commandsAce)) {
|
|
461
|
+
dirsToClean.push(commandsAce);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
let cleaned = false;
|
|
465
|
+
for (const dir of dirsToClean) {
|
|
466
|
+
if (fs.existsSync(dir)) {
|
|
467
|
+
fs.rmSync(dir, { recursive: true });
|
|
468
|
+
cleaned = true;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Clean ace-* agents
|
|
473
|
+
const agentsPath = path.join(basePath, 'agents');
|
|
244
474
|
if (fs.existsSync(agentsPath)) {
|
|
245
475
|
for (const f of fs.readdirSync(agentsPath)) {
|
|
246
476
|
if (f.startsWith('ace-')) {
|
|
247
477
|
fs.rmSync(path.join(agentsPath, f), { recursive: true });
|
|
478
|
+
cleaned = true;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Clean legacy hooks from settings.json
|
|
484
|
+
const settingsPath = path.join(basePath, 'settings.json');
|
|
485
|
+
if (fs.existsSync(settingsPath)) {
|
|
486
|
+
try {
|
|
487
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
488
|
+
let modified = false;
|
|
489
|
+
|
|
490
|
+
// Remove ACE SessionStart hooks (now handled by plugin hooks.json)
|
|
491
|
+
if (settings.hooks?.SessionStart) {
|
|
492
|
+
const before = settings.hooks.SessionStart.length;
|
|
493
|
+
settings.hooks.SessionStart = settings.hooks.SessionStart.filter(entry =>
|
|
494
|
+
!(entry.hooks && entry.hooks.some(h => h.command && h.command.includes('ace-')))
|
|
495
|
+
);
|
|
496
|
+
if (settings.hooks.SessionStart.length === 0) {
|
|
497
|
+
delete settings.hooks.SessionStart;
|
|
498
|
+
}
|
|
499
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
500
|
+
delete settings.hooks;
|
|
501
|
+
}
|
|
502
|
+
if (settings.hooks?.SessionStart?.length !== before) {
|
|
503
|
+
modified = true;
|
|
504
|
+
}
|
|
248
505
|
}
|
|
506
|
+
|
|
507
|
+
if (modified) {
|
|
508
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
509
|
+
}
|
|
510
|
+
} catch {}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return cleaned;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Install ACE for Claude Code using the plugin marketplace system
|
|
517
|
+
function installForClaude(scope, packageDir, flags) {
|
|
518
|
+
const basePath = getBasePath('claude', scope);
|
|
519
|
+
|
|
520
|
+
log(`\nInstalling ACE for Claude Code (plugin marketplace)...`, colors.cyan);
|
|
521
|
+
log(` Scope: ${scope}`, colors.dim);
|
|
522
|
+
|
|
523
|
+
// Step 1: Clean old standalone install
|
|
524
|
+
const hadLegacy = cleanLegacyInstall(basePath);
|
|
525
|
+
if (hadLegacy) {
|
|
526
|
+
log(` ✓ Cleaned legacy standalone installation`, colors.green);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Step 2: Clean old cached plugin versions
|
|
530
|
+
const aceCacheDir = path.join(basePath, 'plugins', 'cache', MARKETPLACE_NAME, 'ace');
|
|
531
|
+
if (fs.existsSync(aceCacheDir)) {
|
|
532
|
+
const cachedVersions = fs.readdirSync(aceCacheDir);
|
|
533
|
+
if (cachedVersions.length > 0) {
|
|
534
|
+
for (const ver of cachedVersions) {
|
|
535
|
+
fs.rmSync(path.join(aceCacheDir, ver), { recursive: true });
|
|
536
|
+
}
|
|
537
|
+
log(` ✓ Cleaned ${cachedVersions.length} old cached version(s)`, colors.green);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Step 3: Check for claude CLI
|
|
542
|
+
if (!hasClaudeCli()) {
|
|
543
|
+
log(` ✗ Claude CLI not found in PATH`, colors.red);
|
|
544
|
+
log(` Install Claude Code first: https://code.claude.com/docs/en/quickstart`, colors.dim);
|
|
545
|
+
return { success: false, path: basePath };
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Step 4: Add/update marketplace from this package directory
|
|
549
|
+
// First check if marketplace already exists
|
|
550
|
+
const listResult = runClaude('plugin marketplace list --json');
|
|
551
|
+
let marketplaceExists = false;
|
|
552
|
+
if (listResult.success) {
|
|
553
|
+
try {
|
|
554
|
+
// Check output for our marketplace name
|
|
555
|
+
marketplaceExists = listResult.output.includes(MARKETPLACE_NAME);
|
|
556
|
+
} catch {}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const packageDirUnix = packageDir.replace(/\\/g, '/');
|
|
560
|
+
|
|
561
|
+
if (marketplaceExists) {
|
|
562
|
+
// Update existing marketplace to pick up changes
|
|
563
|
+
const updateResult = runClaude(`plugin marketplace update ${MARKETPLACE_NAME}`);
|
|
564
|
+
if (updateResult.success) {
|
|
565
|
+
log(` ✓ Updated ACE marketplace`, colors.green);
|
|
566
|
+
} else {
|
|
567
|
+
// If update fails, remove and re-add
|
|
568
|
+
runClaude(`plugin marketplace remove ${MARKETPLACE_NAME}`);
|
|
569
|
+
const addResult = runClaude(`plugin marketplace add "${packageDirUnix}"`);
|
|
570
|
+
if (addResult.success) {
|
|
571
|
+
log(` ✓ Re-added ACE marketplace`, colors.green);
|
|
572
|
+
} else {
|
|
573
|
+
log(` ✗ Failed to add marketplace: ${addResult.output}`, colors.red);
|
|
574
|
+
return { success: false, path: basePath };
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
} else {
|
|
578
|
+
const addResult = runClaude(`plugin marketplace add "${packageDirUnix}"`);
|
|
579
|
+
if (addResult.success) {
|
|
580
|
+
log(` ✓ Added ACE marketplace`, colors.green);
|
|
581
|
+
} else {
|
|
582
|
+
log(` ✗ Failed to add marketplace: ${addResult.output}`, colors.red);
|
|
583
|
+
return { success: false, path: basePath };
|
|
249
584
|
}
|
|
250
585
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
586
|
+
|
|
587
|
+
// Step 5: Install or update the ACE plugin
|
|
588
|
+
const pluginId = `ace@${MARKETPLACE_NAME}`;
|
|
589
|
+
const scopeFlag = scope === 'global' ? '--scope user' : `--scope ${scope}`;
|
|
590
|
+
|
|
591
|
+
// Try install first; if already installed, try update
|
|
592
|
+
const installResult = runClaude(`plugin install ${pluginId} ${scopeFlag}`);
|
|
593
|
+
if (installResult.success) {
|
|
594
|
+
log(` ✓ ACE plugin installed`, colors.green);
|
|
595
|
+
} else if (installResult.output.includes('already installed') || installResult.output.includes('already enabled')) {
|
|
596
|
+
const updateResult = runClaude(`plugin update ${pluginId} ${scopeFlag}`);
|
|
597
|
+
if (updateResult.success) {
|
|
598
|
+
log(` ✓ ACE plugin updated`, colors.green);
|
|
599
|
+
} else {
|
|
600
|
+
log(` ⚠ Plugin update note: ${updateResult.output}`, colors.yellow);
|
|
601
|
+
}
|
|
602
|
+
} else {
|
|
603
|
+
log(` ✗ Failed to install plugin: ${installResult.output}`, colors.red);
|
|
604
|
+
return { success: false, path: basePath };
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Step 6: Configure statusline (not part of plugin hooks — goes in settings.json)
|
|
608
|
+
configureStatusline(basePath, flags);
|
|
609
|
+
|
|
610
|
+
return { success: true, path: basePath };
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Configure the ACE statusline in settings.json
|
|
614
|
+
function configureStatusline(basePath, flags) {
|
|
615
|
+
const settingsPath = path.join(basePath, 'settings.json');
|
|
616
|
+
const settings = readSettings(settingsPath);
|
|
617
|
+
|
|
618
|
+
// Statusline is a settings.json config, not a plugin hook, so CLAUDE_PLUGIN_ROOT
|
|
619
|
+
// is not available. We write a thin wrapper to ~/.claude/hooks/ that finds the
|
|
620
|
+
// installed plugin's statusline script at runtime.
|
|
621
|
+
const wrapperPath = path.join(basePath, 'hooks', 'ace-statusline-wrapper.js');
|
|
622
|
+
const wrapperDir = path.join(basePath, 'hooks');
|
|
623
|
+
if (!fs.existsSync(wrapperDir)) {
|
|
624
|
+
fs.mkdirSync(wrapperDir, { recursive: true });
|
|
255
625
|
}
|
|
256
|
-
|
|
626
|
+
|
|
627
|
+
// Write a thin wrapper that finds the ace plugin statusline script
|
|
628
|
+
fs.writeFileSync(wrapperPath, `#!/usr/bin/env node
|
|
629
|
+
// ACE statusline wrapper — finds the installed plugin's statusline script
|
|
630
|
+
const fs = require('fs');
|
|
631
|
+
const path = require('path');
|
|
632
|
+
const home = require('os').homedir();
|
|
633
|
+
|
|
634
|
+
// Search plugin cache for ace plugin's statusline
|
|
635
|
+
const cacheDir = path.join(home, '.claude', 'plugins', 'cache');
|
|
636
|
+
let scriptPath = null;
|
|
637
|
+
|
|
638
|
+
function findScript(dir, depth) {
|
|
639
|
+
if (depth > 5 || !fs.existsSync(dir)) return false;
|
|
640
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
641
|
+
const full = path.join(dir, entry.name);
|
|
642
|
+
if (entry.isDirectory()) {
|
|
643
|
+
if (findScript(full, depth + 1)) return true;
|
|
644
|
+
} else if (entry.name === 'ace-statusline.js' && dir.includes('ace')) {
|
|
645
|
+
scriptPath = full;
|
|
646
|
+
return true;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (fs.existsSync(cacheDir)) {
|
|
653
|
+
findScript(cacheDir, 0);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (scriptPath) {
|
|
657
|
+
// Pipe stdin through to the actual script
|
|
658
|
+
const { spawn } = require('child_process');
|
|
659
|
+
const child = spawn(process.execPath, [scriptPath], {
|
|
660
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
661
|
+
windowsHide: true
|
|
662
|
+
});
|
|
663
|
+
process.stdin.pipe(child.stdin);
|
|
664
|
+
child.on('exit', (code) => process.exit(code || 0));
|
|
665
|
+
} else {
|
|
666
|
+
// Fallback: basic statusline
|
|
667
|
+
let input = '';
|
|
668
|
+
process.stdin.setEncoding('utf8');
|
|
669
|
+
process.stdin.on('data', chunk => input += chunk);
|
|
670
|
+
process.stdin.on('end', () => {
|
|
671
|
+
try {
|
|
672
|
+
const data = JSON.parse(input);
|
|
673
|
+
const model = data.model?.display_name || 'Claude';
|
|
674
|
+
const dir = path.basename(data.workspace?.current_dir || process.cwd());
|
|
675
|
+
process.stdout.write(model + ' | ' + dir);
|
|
676
|
+
} catch {}
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
`, 'utf-8');
|
|
680
|
+
|
|
681
|
+
const statuslineCmd = `node "${wrapperPath.replace(/\\/g, '/')}"`;
|
|
682
|
+
|
|
683
|
+
const hasExisting = settings.statusLine != null;
|
|
684
|
+
const isAceStatusline = hasExisting && settings.statusLine.command &&
|
|
685
|
+
(settings.statusLine.command.includes('ace-statusline') || settings.statusLine.command.includes('ace-'));
|
|
686
|
+
|
|
687
|
+
if (!hasExisting || flags.forceStatusline) {
|
|
688
|
+
settings.statusLine = { type: 'command', command: statuslineCmd };
|
|
689
|
+
log(` ✓ Configured statusline`, colors.green);
|
|
690
|
+
} else if (isAceStatusline) {
|
|
691
|
+
settings.statusLine = { type: 'command', command: statuslineCmd };
|
|
692
|
+
log(` ✓ Updated statusline`, colors.green);
|
|
693
|
+
} else {
|
|
694
|
+
log(` ⚠ Skipping statusline (already configured, use --force-statusline to replace)`, colors.yellow);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Write settings
|
|
698
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Install ACE for Crush (legacy copy approach — no plugin system)
|
|
702
|
+
function installForCrush(scope, packageDir) {
|
|
703
|
+
const config = RUNTIMES.opencode;
|
|
704
|
+
const basePath = getBasePath('opencode', scope);
|
|
705
|
+
const agentsPath = path.join(basePath, config.agentsDir);
|
|
706
|
+
|
|
707
|
+
const srcSkills = path.join(packageDir, 'skills');
|
|
708
|
+
const srcShared = path.join(packageDir, 'shared');
|
|
709
|
+
const srcPlugin = path.join(packageDir, '.claude-plugin');
|
|
710
|
+
const srcAgents = path.join(packageDir, 'agents');
|
|
711
|
+
|
|
712
|
+
log(`\nInstalling ACE for ${config.name} (legacy copy)...`, colors.cyan);
|
|
713
|
+
log(` Target: ${basePath}`, colors.dim);
|
|
714
|
+
|
|
715
|
+
// Clean previous installation
|
|
257
716
|
const skillsPath = path.join(basePath, 'skills');
|
|
258
717
|
const sharedPath = path.join(basePath, 'shared');
|
|
259
718
|
const pluginPath = path.join(basePath, '.claude-plugin');
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
fs.rmSync(sharedPath, { recursive: true });
|
|
719
|
+
const legacyPath = path.join(basePath, 'agile-context-engineering');
|
|
720
|
+
|
|
721
|
+
for (const p of [skillsPath, sharedPath, pluginPath, legacyPath]) {
|
|
722
|
+
if (fs.existsSync(p)) fs.rmSync(p, { recursive: true });
|
|
265
723
|
}
|
|
266
|
-
if (fs.existsSync(
|
|
267
|
-
fs.
|
|
724
|
+
if (fs.existsSync(agentsPath)) {
|
|
725
|
+
for (const f of fs.readdirSync(agentsPath)) {
|
|
726
|
+
if (f.startsWith('ace-')) fs.rmSync(path.join(agentsPath, f), { recursive: true });
|
|
727
|
+
}
|
|
268
728
|
}
|
|
269
729
|
|
|
270
|
-
// Create directories
|
|
271
730
|
fs.mkdirSync(agentsPath, { recursive: true });
|
|
272
731
|
|
|
273
|
-
// Copy skills (plugin skill directories)
|
|
274
732
|
if (fs.existsSync(srcSkills)) {
|
|
275
|
-
copyDir(srcSkills, skillsPath,
|
|
733
|
+
copyDir(srcSkills, skillsPath, 'opencode');
|
|
276
734
|
log(` ✓ Skills installed`, colors.green);
|
|
277
735
|
}
|
|
278
|
-
|
|
279
|
-
// Copy shared libs (ace-core.js, ace-story.js, ace-github.js, utils)
|
|
280
736
|
if (fs.existsSync(srcShared)) {
|
|
281
|
-
copyDir(srcShared, sharedPath,
|
|
737
|
+
copyDir(srcShared, sharedPath, 'opencode');
|
|
282
738
|
log(` ✓ Shared libs installed`, colors.green);
|
|
283
739
|
}
|
|
284
|
-
|
|
285
|
-
// Copy plugin manifest
|
|
286
740
|
if (fs.existsSync(srcPlugin)) {
|
|
287
|
-
copyDir(srcPlugin, pluginPath,
|
|
741
|
+
copyDir(srcPlugin, pluginPath, 'opencode');
|
|
288
742
|
log(` ✓ Plugin manifest installed`, colors.green);
|
|
289
743
|
}
|
|
290
|
-
|
|
291
|
-
// Copy agents (transform paths for target runtime)
|
|
292
744
|
if (fs.existsSync(srcAgents)) {
|
|
293
|
-
copyDir(srcAgents, agentsPath,
|
|
745
|
+
copyDir(srcAgents, agentsPath, 'opencode');
|
|
294
746
|
log(` ✓ Agents installed`, colors.green);
|
|
295
747
|
}
|
|
296
748
|
|
|
297
|
-
//
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
749
|
+
// Write VERSION file
|
|
750
|
+
const versionFile = path.join(sharedPath, 'VERSION');
|
|
751
|
+
if (!fs.existsSync(sharedPath)) fs.mkdirSync(sharedPath, { recursive: true });
|
|
752
|
+
fs.writeFileSync(versionFile, VERSION, 'utf-8');
|
|
753
|
+
|
|
754
|
+
// Copy CHANGELOG.md
|
|
755
|
+
const changelogSrc = path.join(packageDir, 'CHANGELOG.md');
|
|
756
|
+
const changelogDest = path.join(sharedPath, 'CHANGELOG.md');
|
|
757
|
+
if (fs.existsSync(changelogSrc)) {
|
|
758
|
+
fs.copyFileSync(changelogSrc, changelogDest);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
return basePath;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Install ACE for Codex as native Codex skills + agent TOML config
|
|
765
|
+
function installForCodex(scope, packageDir) {
|
|
766
|
+
const config = RUNTIMES.codex;
|
|
767
|
+
const basePath = getBasePath('codex', scope);
|
|
768
|
+
const skillsPath = path.join(basePath, 'skills');
|
|
769
|
+
const sharedPath = path.join(basePath, 'shared');
|
|
770
|
+
|
|
771
|
+
const srcSkills = path.join(packageDir, 'skills');
|
|
772
|
+
const srcShared = path.join(packageDir, 'shared');
|
|
773
|
+
const srcAgents = path.join(packageDir, 'agents');
|
|
774
|
+
|
|
775
|
+
log(`\nInstalling ACE for ${config.name} (native skills)...`, colors.cyan);
|
|
776
|
+
log(` Target: ${basePath}`, colors.dim);
|
|
777
|
+
|
|
778
|
+
fs.mkdirSync(basePath, { recursive: true });
|
|
779
|
+
|
|
780
|
+
const skillCount = copySkillsForCodex(srcSkills, skillsPath);
|
|
781
|
+
log(` ✓ ${skillCount} skills installed`, colors.green);
|
|
782
|
+
|
|
783
|
+
if (fs.existsSync(sharedPath)) {
|
|
784
|
+
fs.rmSync(sharedPath, { recursive: true });
|
|
785
|
+
}
|
|
786
|
+
if (fs.existsSync(srcShared)) {
|
|
787
|
+
copyDir(srcShared, sharedPath, 'codex');
|
|
788
|
+
log(` ✓ Shared libs installed`, colors.green);
|
|
311
789
|
}
|
|
312
790
|
|
|
313
|
-
|
|
791
|
+
const agentCount = installCodexAgents(basePath, srcAgents);
|
|
792
|
+
log(` ✓ ${agentCount} agents configured`, colors.green);
|
|
793
|
+
|
|
314
794
|
const versionFile = path.join(sharedPath, 'VERSION');
|
|
315
795
|
if (!fs.existsSync(sharedPath)) fs.mkdirSync(sharedPath, { recursive: true });
|
|
316
796
|
fs.writeFileSync(versionFile, VERSION, 'utf-8');
|
|
317
797
|
|
|
318
|
-
// Copy CHANGELOG.md
|
|
319
798
|
const changelogSrc = path.join(packageDir, 'CHANGELOG.md');
|
|
320
799
|
const changelogDest = path.join(sharedPath, 'CHANGELOG.md');
|
|
321
800
|
if (fs.existsSync(changelogSrc)) {
|
|
322
801
|
fs.copyFileSync(changelogSrc, changelogDest);
|
|
323
|
-
log(` ✓ CHANGELOG.md installed`, colors.green);
|
|
324
802
|
}
|
|
325
803
|
|
|
326
804
|
return basePath;
|
|
@@ -343,44 +821,41 @@ async function main() {
|
|
|
343
821
|
|
|
344
822
|
banner();
|
|
345
823
|
|
|
346
|
-
// Determine package directory (where this script is located)
|
|
347
824
|
const packageDir = path.join(__dirname, '..');
|
|
348
825
|
|
|
349
826
|
let runtimes = [];
|
|
350
827
|
let scope = null;
|
|
351
828
|
|
|
352
|
-
|
|
353
|
-
const hasRuntimeFlag = flags.claude || flags.opencode || flags.all;
|
|
829
|
+
const hasRuntimeFlag = flags.claude || flags.codex || flags.opencode || flags.all;
|
|
354
830
|
const hasScopeFlag = flags.global || flags.local;
|
|
355
831
|
const isInteractive = !hasRuntimeFlag && !hasScopeFlag;
|
|
356
832
|
|
|
357
833
|
if (isInteractive) {
|
|
358
834
|
const rl = createPrompt();
|
|
359
835
|
|
|
360
|
-
// Ask for runtime selection (multiple choice)
|
|
361
836
|
runtimes = await askMultiple(rl, '\nWhich runtime(s) do you want to install ACE for?', [
|
|
362
837
|
{ label: 'Claude Code', value: 'claude', description: "Anthropic's Claude Code CLI" },
|
|
838
|
+
{ label: 'Codex', value: 'codex', description: "OpenAI's Codex CLI" },
|
|
363
839
|
{ label: 'Crush', value: 'opencode', description: 'Crush AI coding assistant (formerly OpenCode)' },
|
|
364
840
|
]);
|
|
365
841
|
|
|
366
|
-
// Ask for scope
|
|
367
842
|
scope = await ask(rl, '\nWhere should ACE be installed?', [
|
|
368
|
-
{ label: 'Global', value: 'global', description: 'Install in home directory (~/.claude, ~/.opencode)' },
|
|
369
|
-
{ label: 'Local', value: 'local', description: 'Install in current project (.claude, .opencode)' },
|
|
843
|
+
{ label: 'Global', value: 'global', description: 'Install in home directory (~/.claude, ~/.codex, ~/.opencode)' },
|
|
844
|
+
{ label: 'Local', value: 'local', description: 'Install in current project (.claude, .codex, .opencode)' },
|
|
370
845
|
]);
|
|
371
846
|
|
|
372
847
|
rl.close();
|
|
373
848
|
} else {
|
|
374
|
-
// Non-interactive mode
|
|
375
849
|
if (flags.all) {
|
|
376
|
-
runtimes = ['claude', 'opencode'];
|
|
850
|
+
runtimes = ['claude', 'codex', 'opencode'];
|
|
377
851
|
} else {
|
|
378
852
|
if (flags.claude) runtimes.push('claude');
|
|
853
|
+
if (flags.codex) runtimes.push('codex');
|
|
379
854
|
if (flags.opencode) runtimes.push('opencode');
|
|
380
855
|
}
|
|
381
856
|
|
|
382
857
|
if (runtimes.length === 0) {
|
|
383
|
-
log('Error: No runtime specified. Use --claude, --opencode (Crush), or --all', colors.red);
|
|
858
|
+
log('Error: No runtime specified. Use --claude, --codex, --opencode (Crush), or --all', colors.red);
|
|
384
859
|
process.exit(1);
|
|
385
860
|
}
|
|
386
861
|
|
|
@@ -396,98 +871,31 @@ async function main() {
|
|
|
396
871
|
const installedPaths = [];
|
|
397
872
|
|
|
398
873
|
for (const runtime of runtimes) {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
if (runtime !== 'claude') continue;
|
|
406
|
-
|
|
407
|
-
const settingsPath = path.join(basePath, 'settings.json');
|
|
408
|
-
const settings = readSettings(settingsPath);
|
|
409
|
-
|
|
410
|
-
const statuslineCommand = buildHookCommand(basePath, 'ace-statusline.js');
|
|
411
|
-
const updateCheckCommand = buildHookCommand(basePath, 'ace-check-update.js');
|
|
412
|
-
|
|
413
|
-
// Register SessionStart hook for background update checking
|
|
414
|
-
if (!settings.hooks) settings.hooks = {};
|
|
415
|
-
if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
|
|
416
|
-
|
|
417
|
-
const hasAceUpdateHook = settings.hooks.SessionStart.some(entry =>
|
|
418
|
-
entry.hooks && entry.hooks.some(h => h.command && h.command.includes('ace-check-update'))
|
|
419
|
-
);
|
|
420
|
-
if (!hasAceUpdateHook) {
|
|
421
|
-
settings.hooks.SessionStart.push({
|
|
422
|
-
hooks: [{ type: 'command', command: updateCheckCommand }]
|
|
423
|
-
});
|
|
424
|
-
log(` ✓ Configured update check hook`, colors.green);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// Handle statusline configuration
|
|
428
|
-
const hasExisting = settings.statusLine != null;
|
|
429
|
-
const isAceStatusline = hasExisting && settings.statusLine.command &&
|
|
430
|
-
settings.statusLine.command.includes('ace-statusline');
|
|
431
|
-
|
|
432
|
-
if (!hasExisting || flags.forceStatusline) {
|
|
433
|
-
// No existing statusline or force flag — install ACE statusline
|
|
434
|
-
settings.statusLine = { type: 'command', command: statuslineCommand };
|
|
435
|
-
log(` ✓ Configured statusline`, colors.green);
|
|
436
|
-
} else if (isAceStatusline) {
|
|
437
|
-
// Already ACE statusline — update path
|
|
438
|
-
settings.statusLine = { type: 'command', command: statuslineCommand };
|
|
439
|
-
log(` ✓ Updated statusline`, colors.green);
|
|
440
|
-
} else if (isInteractive) {
|
|
441
|
-
// Existing non-ACE statusline in interactive mode — ask user
|
|
442
|
-
const existingCmd = settings.statusLine.command || settings.statusLine.url || '(custom)';
|
|
443
|
-
log(`\n ⚠ Existing statusline detected`, colors.yellow);
|
|
444
|
-
log(` Current: ${existingCmd}`, colors.dim);
|
|
445
|
-
log(`\n ACE statusline shows:`, colors.cyan);
|
|
446
|
-
log(` • Model name`, colors.dim);
|
|
447
|
-
log(` • Current task (from todo list)`, colors.dim);
|
|
448
|
-
log(` • Context window usage (color-coded)`, colors.dim);
|
|
449
|
-
log(` • Update notifications`, colors.dim);
|
|
450
|
-
|
|
451
|
-
const rl = createPrompt();
|
|
452
|
-
const choice = await ask(rl, '\n What would you like to do?', [
|
|
453
|
-
{ label: 'Keep existing statusline', value: 'keep' },
|
|
454
|
-
{ label: 'Replace with ACE statusline', value: 'replace' },
|
|
455
|
-
]);
|
|
456
|
-
rl.close();
|
|
457
|
-
|
|
458
|
-
if (choice === 'replace') {
|
|
459
|
-
settings.statusLine = { type: 'command', command: statuslineCommand };
|
|
460
|
-
log(` ✓ Configured statusline`, colors.green);
|
|
461
|
-
} else {
|
|
462
|
-
log(` ⚠ Skipping statusline (kept existing)`, colors.yellow);
|
|
463
|
-
}
|
|
874
|
+
if (runtime === 'claude') {
|
|
875
|
+
const result = installForClaude(scope, packageDir, flags);
|
|
876
|
+
installedPaths.push({ runtime, name: 'Claude Code', ...result });
|
|
877
|
+
} else if (runtime === 'codex') {
|
|
878
|
+
const p = installForCodex(scope, packageDir);
|
|
879
|
+
installedPaths.push({ runtime, name: RUNTIMES[runtime].name, path: p, success: true });
|
|
464
880
|
} else {
|
|
465
|
-
|
|
466
|
-
|
|
881
|
+
const p = installForCrush(scope, packageDir);
|
|
882
|
+
installedPaths.push({ runtime, name: RUNTIMES[runtime].name, path: p, success: true });
|
|
467
883
|
}
|
|
468
|
-
|
|
469
|
-
// Write settings
|
|
470
|
-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
471
884
|
}
|
|
472
885
|
|
|
473
|
-
|
|
474
|
-
log(`\n${'═'.repeat(50)}`, colors.green);
|
|
475
|
-
log(` ACE installed successfully!`, colors.green + colors.bright);
|
|
476
|
-
log(`${'═'.repeat(50)}`, colors.green);
|
|
886
|
+
const anyFailed = installedPaths.some(p => !p.success);
|
|
477
887
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
}
|
|
888
|
+
// Show success message
|
|
889
|
+
log(`\n${'═'.repeat(50)}`, anyFailed ? colors.yellow : colors.green);
|
|
890
|
+
log(` ACE installation ${anyFailed ? 'completed with warnings' : 'complete'}!`, (anyFailed ? colors.yellow : colors.green) + colors.bright);
|
|
891
|
+
log(`${'═'.repeat(50)}`, anyFailed ? colors.yellow : colors.green);
|
|
482
892
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
log(` hooks/ Statusline & update hooks`, colors.dim);
|
|
490
|
-
log(` .claude-plugin/ Plugin manifest`, colors.dim);
|
|
893
|
+
for (const { name: runtimeName, success } of installedPaths) {
|
|
894
|
+
if (success) {
|
|
895
|
+
log(` ✓ ${runtimeName}: installed`, colors.green);
|
|
896
|
+
} else {
|
|
897
|
+
log(` ✗ ${runtimeName}: failed (see errors above)`, colors.red);
|
|
898
|
+
}
|
|
491
899
|
}
|
|
492
900
|
|
|
493
901
|
log(`\nAvailable skills:`, colors.cyan);
|
|
@@ -500,11 +908,17 @@ async function main() {
|
|
|
500
908
|
log(` /ace:map-system Map system-wide architecture`, colors.dim);
|
|
501
909
|
log(` /ace:map-subsystem Map a subsystem's internals`, colors.dim);
|
|
502
910
|
log(` /ace:init-coding-standards Generate coding standards`, colors.dim);
|
|
911
|
+
if (runtimes.includes('codex')) {
|
|
912
|
+
log(`\nCodex skill names use $ace-* instead of /ace:*:`, colors.cyan);
|
|
913
|
+
log(` $ace-help Check project status and next steps`, colors.dim);
|
|
914
|
+
log(` $ace-plan-story Plan a story specification`, colors.dim);
|
|
915
|
+
log(` $ace-execute-story Execute a planned story`, colors.dim);
|
|
916
|
+
}
|
|
503
917
|
|
|
504
918
|
log(`\nGet started:`, colors.cyan);
|
|
505
|
-
log(` 1.
|
|
506
|
-
log(` 2. Run /ace:help
|
|
507
|
-
log(` 3. Run /ace:plan-product-vision to define your product\n`, colors.dim);
|
|
919
|
+
log(` 1. Restart your AI coding assistant`, colors.dim);
|
|
920
|
+
log(` 2. Run /ace:help in Claude/Crush or $ace-help in Codex`, colors.dim);
|
|
921
|
+
log(` 3. Run /ace:plan-product-vision or $ace-plan-product-vision to define your product\n`, colors.dim);
|
|
508
922
|
}
|
|
509
923
|
|
|
510
924
|
main().catch((err) => {
|