@marcomigozzi/trailhead 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.it.md +359 -0
- package/README.md +35 -22
- package/bin/lib/codex-projection.js +421 -0
- package/bin/lib/codex-projection.test.js +266 -0
- package/bin/lib/host-descriptor.js +404 -0
- package/bin/lib/host-descriptor.test.js +172 -0
- package/bin/trailhead.js +518 -67
- package/bin/trailhead.test.js +399 -0
- package/package.json +1 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +8 -3
- package/plugins/trailhead/commands/adopt.md +1 -1
- package/plugins/trailhead/commands/bug.md +1 -1
- package/plugins/trailhead/commands/config.md +1 -1
- package/plugins/trailhead/commands/dashboard.md +1 -1
- package/plugins/trailhead/commands/grill.md +1 -1
- package/plugins/trailhead/commands/idea.md +1 -1
- package/plugins/trailhead/commands/inbox.md +1 -1
- package/plugins/trailhead/commands/map.md +1 -1
- package/plugins/trailhead/commands/new.md +1 -1
- package/plugins/trailhead/commands/note.md +1 -1
- package/plugins/trailhead/commands/pause.md +1 -1
- package/plugins/trailhead/commands/quick.md +3 -1
- package/plugins/trailhead/commands/resume.md +1 -1
- package/plugins/trailhead/commands/seed.md +1 -1
- package/plugins/trailhead/commands/split.md +1 -1
- package/plugins/trailhead/commands/ticket.md +1 -1
- package/plugins/trailhead/commands/todo.md +1 -1
- package/plugins/trailhead/commands/update.md +1 -1
- package/plugins/trailhead/commands/whiteboard.md +1 -1
- package/plugins/trailhead/commands/work.md +1 -1
- package/plugins/trailhead/hooks/lib/commit-message-check.js +74 -0
- package/plugins/trailhead/hooks/lib/commit-message-check.test.js +83 -0
- package/plugins/trailhead/hooks/trailhead-check-update.js +54 -24
- package/plugins/trailhead/hooks/trailhead-commit-guard.js +7 -10
- package/plugins/trailhead/hooks/trailhead-secret-guard.js +19 -4
- package/plugins/trailhead/hooks/trailhead-secret-guard.test.js +41 -1
- package/plugins/trailhead/skills/{trailhead/references/configuration.md → _shared/configuration-reference.md} +15 -3
- package/plugins/trailhead/skills/_shared/configuration.md +8 -0
- package/plugins/trailhead/skills/_shared/multi-map.md +27 -0
- package/plugins/trailhead/skills/_shared/out-of-scope.md +11 -0
- package/plugins/trailhead/skills/_shared/principles.md +8 -0
- package/plugins/trailhead/skills/_shared/session-handoff.md +22 -0
- package/plugins/trailhead/skills/_shared/substrate-commands.md +159 -0
- package/plugins/trailhead/skills/_shared/substrate.md +35 -0
- package/plugins/trailhead/skills/{trailhead/references → _shared}/teamwork.md +8 -5
- package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/acceptance-testing.md +3 -2
- package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/code-review.md +5 -1
- package/plugins/trailhead/skills/_shared/techniques/plan-review.md +21 -0
- package/plugins/trailhead/skills/_shared/techniques/prototype.md +36 -0
- package/plugins/trailhead/skills/_shared/techniques.md +20 -0
- package/plugins/trailhead/skills/_shared/ticket-anatomy.md +32 -0
- package/plugins/trailhead/skills/_shared/ticket-language.md +3 -0
- package/plugins/trailhead/skills/trailhead/SKILL.md +25 -450
- package/plugins/trailhead/skills/trailhead-capture/SKILL.md +28 -0
- package/plugins/trailhead/skills/{trailhead → trailhead-capture}/references/capture.md +5 -3
- package/plugins/trailhead/skills/trailhead-chart/SKILL.md +51 -0
- package/plugins/trailhead/skills/trailhead-chart/references/charting.md +24 -0
- package/plugins/trailhead/skills/{trailhead → trailhead-chart}/references/inbox.md +1 -1
- package/plugins/trailhead/skills/trailhead-manage/SKILL.md +29 -0
- package/plugins/trailhead/skills/trailhead-view/SKILL.md +112 -0
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +65 -0
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +52 -0
- package/plugins/trailhead/templates/statusline-trailhead.sh +40 -8
- package/plugins/trailhead/templates/trailhead-commit-msg +57 -0
- package/plugins/trailhead/skills/trailhead/references/techniques/plan-review.md +0 -24
- package/plugins/trailhead/skills/trailhead/references/techniques/prototype.md +0 -18
- /package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/codebase-map.md +0 -0
- /package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/debug.md +0 -0
- /package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/domain-vocabulary.md +0 -0
- /package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/grilling.md +0 -0
- /package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/research.md +0 -0
- /package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/tdd.md +0 -0
- /package/plugins/trailhead/skills/{trailhead → trailhead-manage}/references/updating.md +0 -0
package/bin/trailhead.js
CHANGED
|
@@ -3,58 +3,219 @@
|
|
|
3
3
|
// trailhead installer: places the skill, commands, hooks, and templates into
|
|
4
4
|
// an agent's config dir, and registers the hooks in settings.json.
|
|
5
5
|
// No dependencies. Idempotent. Usage:
|
|
6
|
-
// npx @marcomigozzi/trailhead
|
|
7
|
-
// npx @marcomigozzi/trailhead --
|
|
6
|
+
// npx @marcomigozzi/trailhead install (auto-detects the CLI on your PATH; asks if it can't tell)
|
|
7
|
+
// npx @marcomigozzi/trailhead --claude force install for Claude Code (~/.claude or $CLAUDE_CONFIG_DIR)
|
|
8
|
+
// npx @marcomigozzi/trailhead --codex force install for Codex ($CODEX_HOME or ~/.codex)
|
|
9
|
+
// npx @marcomigozzi/trailhead --symlink dev install (symlink to the package, live edits; Claude only)
|
|
8
10
|
// npx @marcomigozzi/trailhead --uninstall
|
|
9
11
|
// npx @marcomigozzi/trailhead --dir=/path/to/configdir
|
|
10
12
|
//
|
|
11
|
-
// Multi-
|
|
12
|
-
//
|
|
13
|
+
// Multi-host: host layout/behaviour comes from bin/lib/host-descriptor.js
|
|
14
|
+
// (the descriptor table for claude/codex) and, for Codex, the artifact
|
|
15
|
+
// projection in bin/lib/codex-projection.js (engine text rewrites, prompt
|
|
16
|
+
// generation). With no --codex/--claude flag the host is auto-detected from
|
|
17
|
+
// the CLIs on $PATH; the interactive prompt only fires when that is ambiguous.
|
|
13
18
|
|
|
14
19
|
const fs = require('fs');
|
|
15
20
|
const os = require('os');
|
|
16
21
|
const path = require('path');
|
|
22
|
+
const readline = require('readline');
|
|
23
|
+
const { spawnSync } = require('child_process');
|
|
24
|
+
|
|
25
|
+
const { getHost, configDirFor, codexVersionGate } = require('./lib/host-descriptor.js');
|
|
26
|
+
const { codexLayout, convertToCodex, injectCodexAdapterHeader, codexAgentsYaml, codexClusterAgentsYaml, codexHookEntries, enableCodexHooksFeature, codexAgentTomlPlan, codexVerbSkillPlan, enableCodexMultiAgentV2Feature } = require('./lib/codex-projection.js');
|
|
17
27
|
|
|
18
28
|
const PKG = path.resolve(__dirname, '..');
|
|
19
29
|
const SRC = path.join(PKG, 'plugins', 'trailhead');
|
|
30
|
+
|
|
31
|
+
// Enumerate the engine skill dirs shipped under plugins/trailhead/skills: the
|
|
32
|
+
// dispatcher, the cluster skills, and the shared core. A subdir qualifies when
|
|
33
|
+
// it is `_shared` (the shared core, no SKILL.md) or contains a SKILL.md, so a
|
|
34
|
+
// stray empty dir is skipped and a new cluster is picked up automatically.
|
|
35
|
+
function engineSkillDirs() {
|
|
36
|
+
const skillsRoot = path.join(SRC, 'skills');
|
|
37
|
+
if (!fs.existsSync(skillsRoot)) return [];
|
|
38
|
+
return fs.readdirSync(skillsRoot, { withFileTypes: true })
|
|
39
|
+
.filter((e) => e.isDirectory())
|
|
40
|
+
.map((e) => e.name)
|
|
41
|
+
.filter((name) => name === '_shared' || fs.existsSync(path.join(skillsRoot, name, 'SKILL.md')))
|
|
42
|
+
.sort();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Sweep every trailhead-owned skill dir under a skills root, by name: the
|
|
46
|
+
// dispatcher (`trailhead`), the shared core (`_shared`), and every
|
|
47
|
+
// `trailhead-<cluster|verb>` sibling. Clears a prior layout's stale dirs (an
|
|
48
|
+
// old monolith fallback, a removed cluster, a renamed verb skill) the current
|
|
49
|
+
// install no longer ships, so a re-install or /trailhead:update never leaves a
|
|
50
|
+
// dangling skill. The skills root is shared with other plugins, so only
|
|
51
|
+
// trailhead's own names are touched, never the whole dir.
|
|
52
|
+
function sweepTrailheadSkills(skillsRoot) {
|
|
53
|
+
rmrf(path.join(skillsRoot, 'trailhead'));
|
|
54
|
+
rmrf(path.join(skillsRoot, '_shared'));
|
|
55
|
+
if (!fs.existsSync(skillsRoot)) return;
|
|
56
|
+
for (const f of fs.readdirSync(skillsRoot)) {
|
|
57
|
+
if (f.startsWith('trailhead-')) rmrf(path.join(skillsRoot, f));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
20
61
|
const args = process.argv.slice(2);
|
|
21
62
|
const has = (f) => args.includes(f);
|
|
22
63
|
const useSymlink = has('--symlink');
|
|
23
64
|
const uninstall = has('--uninstall');
|
|
24
65
|
const dirArg = (args.find((a) => a.startsWith('--dir=')) || '').split('=')[1];
|
|
25
66
|
|
|
26
|
-
// --- ADAPTERS: one config layout per target agent. Extend this map for multi-CLI. ---
|
|
27
|
-
const ADAPTERS = {
|
|
28
|
-
claude: {
|
|
29
|
-
label: 'Claude Code',
|
|
30
|
-
configDir: process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude'),
|
|
31
|
-
},
|
|
32
|
-
// codex: { label: 'Codex', configDir: process.env.CODEX_HOME || path.join(os.homedir(), '.codex') },
|
|
33
|
-
// gemini: { label: 'Gemini', configDir: process.env.GEMINI_CONFIG_DIR || path.join(os.homedir(), '.gemini') },
|
|
34
|
-
};
|
|
35
|
-
const adapter = ADAPTERS.claude; // only Claude for now
|
|
36
|
-
const configDir = dirArg || adapter.configDir;
|
|
37
|
-
|
|
38
|
-
const P = {
|
|
39
|
-
skill: path.join(configDir, 'skills', 'trailhead'),
|
|
40
|
-
commands: path.join(configDir, 'commands', 'trailhead'),
|
|
41
|
-
hooksDir: path.join(configDir, 'hooks'),
|
|
42
|
-
templates: path.join(configDir, 'trailhead', 'templates'),
|
|
43
|
-
settings: path.join(configDir, 'settings.json'),
|
|
44
|
-
};
|
|
45
|
-
const HOOK_FILES = ['trailhead-commit-guard.js', 'trailhead-issue-injection-scanner.js', 'trailhead-secret-guard.js', 'trailhead-check-update.js'];
|
|
46
|
-
const hookCmd = (name) => `node "${path.join(P.hooksDir, name)}"`;
|
|
47
|
-
|
|
48
67
|
const rmrf = (p) => fs.rmSync(p, { recursive: true, force: true });
|
|
49
68
|
const ensure = (p) => fs.mkdirSync(p, { recursive: true });
|
|
50
|
-
|
|
69
|
+
const readJSON = (p) => { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return {}; } };
|
|
70
|
+
const writeJSON = (p, o) => { ensure(path.dirname(p)); fs.writeFileSync(p, JSON.stringify(o, null, 2) + '\n'); };
|
|
71
|
+
|
|
72
|
+
// --- readCodexModels ---------------------------------------------------------
|
|
73
|
+
// Resolve the merged models.codex.* config for the Codex agent-TOML projection:
|
|
74
|
+
// the project-local .trailhead/config.json (found by walking up from cwd, capped
|
|
75
|
+
// at 8 levels) overlaid on the Codex-side global at <codexHome>/trailhead/config.json,
|
|
76
|
+
// project winning per key. Never throws: any missing/malformed file just
|
|
77
|
+
// contributes nothing.
|
|
78
|
+
function findProjectConfig(startDir) {
|
|
79
|
+
let dir = startDir;
|
|
80
|
+
for (let i = 0; i < 8; i++) {
|
|
81
|
+
const candidate = path.join(dir, '.trailhead', 'config.json');
|
|
82
|
+
if (fs.existsSync(candidate)) return readJSON(candidate);
|
|
83
|
+
const parent = path.dirname(dir);
|
|
84
|
+
if (parent === dir) break;
|
|
85
|
+
dir = parent;
|
|
86
|
+
}
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function readCodexModels(codexHome) {
|
|
91
|
+
try {
|
|
92
|
+
const projectCfg = findProjectConfig(process.cwd()) || {};
|
|
93
|
+
const globalCfg = readJSON(path.join(codexHome, 'trailhead', 'config.json')) || {};
|
|
94
|
+
const globalCodex = globalCfg.models && typeof globalCfg.models === 'object' && typeof globalCfg.models.codex === 'object' && globalCfg.models.codex ? globalCfg.models.codex : {};
|
|
95
|
+
const projectCodex = projectCfg.models && typeof projectCfg.models === 'object' && typeof projectCfg.models.codex === 'object' && projectCfg.models.codex ? projectCfg.models.codex : {};
|
|
96
|
+
return { ...globalCodex, ...projectCodex };
|
|
97
|
+
} catch {
|
|
98
|
+
return {};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// --- host resolution -------------------------------------------------------
|
|
103
|
+
// Explicit --codex / --claude win outright. With neither, the host is
|
|
104
|
+
// auto-detected from the CLIs on $PATH: exactly one found -> install for it,
|
|
105
|
+
// no question; ambiguous (none, or several) on an interactive TTY -> ask; an
|
|
106
|
+
// ambiguous non-interactive run (npx/CI, or --dir=) falls back to claude so
|
|
107
|
+
// scripted installs never hang on a prompt nobody can answer.
|
|
108
|
+
const KNOWN_HOSTS = ['claude', 'codex']; // preference order for the fallback; mirrors host-descriptor HOSTS
|
|
109
|
+
|
|
110
|
+
// True if an executable named `bin` sits in one of $PATH's directories. No
|
|
111
|
+
// child process: we just probe each PATH entry for an executable file.
|
|
112
|
+
function isOnPath(bin, env = process.env) {
|
|
113
|
+
for (const dir of (env.PATH || '').split(path.delimiter)) {
|
|
114
|
+
if (!dir) continue;
|
|
115
|
+
try {
|
|
116
|
+
fs.accessSync(path.join(dir, bin), fs.constants.X_OK);
|
|
117
|
+
return true;
|
|
118
|
+
} catch { /* not here, keep looking */ }
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function detectInstalledHosts(env = process.env) {
|
|
124
|
+
return KNOWN_HOSTS.filter((name) => isOnPath(name, env));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function promptForHost(detected = []) {
|
|
128
|
+
const mark = (name) => (detected.includes(name) ? ' (detected)' : '');
|
|
129
|
+
return new Promise((resolve) => {
|
|
130
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
131
|
+
rl.question(
|
|
132
|
+
'Install trailhead for which host?\n' +
|
|
133
|
+
` 1) Claude Code${mark('claude')} (default)\n` +
|
|
134
|
+
` 2) Codex${mark('codex')}\n> `,
|
|
135
|
+
(answer) => {
|
|
136
|
+
rl.close();
|
|
137
|
+
const choice = String(answer).trim().toLowerCase();
|
|
138
|
+
resolve(choice === '2' || choice === 'codex' ? 'codex' : 'claude');
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function resolveHostName() {
|
|
145
|
+
const wantCodex = has('--codex');
|
|
146
|
+
const wantClaude = has('--claude');
|
|
147
|
+
if (wantCodex && wantClaude) {
|
|
148
|
+
console.error('Pass only one of --codex or --claude.');
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
if (wantCodex) return 'codex';
|
|
152
|
+
if (wantClaude) return 'claude';
|
|
153
|
+
|
|
154
|
+
// No explicit flag: auto-detect from the CLIs on $PATH.
|
|
155
|
+
const detected = detectInstalledHosts();
|
|
156
|
+
if (detected.length === 1) {
|
|
157
|
+
console.log(`Detected ${getHost(detected[0]).label} on your PATH; installing for it.`);
|
|
158
|
+
return detected[0];
|
|
159
|
+
}
|
|
160
|
+
// --dir= alone signals scripted/automation usage (tests, CI harnesses), even
|
|
161
|
+
// when stdin happens to be a TTY, so it never triggers the prompt. Ambiguous
|
|
162
|
+
// (0 or 2+ CLIs) + interactive -> ask; otherwise fall back to claude.
|
|
163
|
+
if (process.stdin.isTTY && !dirArg) {
|
|
164
|
+
return promptForHost(detected);
|
|
165
|
+
}
|
|
166
|
+
return 'claude';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// --- Claude adapter (unchanged behaviour) -----------------------------------
|
|
170
|
+
function claudePaths(configDir) {
|
|
171
|
+
return {
|
|
172
|
+
commands: path.join(configDir, 'commands', 'trailhead'),
|
|
173
|
+
hooksDir: path.join(configDir, 'hooks'),
|
|
174
|
+
templates: path.join(configDir, 'trailhead', 'templates'),
|
|
175
|
+
settings: path.join(configDir, 'settings.json'),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const HOOK_FILES = ['trailhead-commit-guard.js', 'trailhead-issue-injection-scanner.js', 'trailhead-secret-guard.js', 'trailhead-check-update.js'];
|
|
179
|
+
// Runtime libs the hook scripts require (trailhead-commit-guard.js does
|
|
180
|
+
// require('./lib/commit-message-check.js')). Copied by name, never a recursive
|
|
181
|
+
// sweep: the Claude hooks/lib dir is shared with other plugins, so we must not
|
|
182
|
+
// clobber a co-tenant's lib nor ship our own *.test.js files.
|
|
183
|
+
const HOOK_LIB_FILES = ['commit-message-check.js'];
|
|
184
|
+
|
|
185
|
+
// Copy the hook scripts and their runtime libs into a destination hooks dir
|
|
186
|
+
// (Claude's ~/.claude/hooks or Codex's skills/trailhead/hooks). Shared by both
|
|
187
|
+
// install paths so the lib copy can never drift from the script copy again.
|
|
188
|
+
// Place the allow-listed hook scripts into destHooksDir. Normal install copies
|
|
189
|
+
// (+chmod so the exec bit is set regardless of the source mode); a --symlink dev
|
|
190
|
+
// install links each file per-file into the package source so hook edits are
|
|
191
|
+
// live too, matching skills/commands. Never symlink the whole hooks/ dir: it is
|
|
192
|
+
// shared with other plugins, so only the curated allowlist is touched. rmrf the
|
|
193
|
+
// dest first so a reinstall over an existing file or symlink never hits EEXIST.
|
|
194
|
+
function copyHookScripts(destHooksDir, { useSymlink = false } = {}) {
|
|
195
|
+
ensure(destHooksDir);
|
|
196
|
+
const put = (src, dest) => {
|
|
197
|
+
rmrf(dest);
|
|
198
|
+
if (useSymlink) {
|
|
199
|
+
fs.symlinkSync(src, dest, 'file');
|
|
200
|
+
} else {
|
|
201
|
+
fs.copyFileSync(src, dest);
|
|
202
|
+
fs.chmodSync(dest, 0o755);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
for (const f of HOOK_FILES) put(path.join(SRC, 'hooks', f), path.join(destHooksDir, f));
|
|
206
|
+
if (HOOK_LIB_FILES.length) {
|
|
207
|
+
const libDest = path.join(destHooksDir, 'lib');
|
|
208
|
+
ensure(libDest);
|
|
209
|
+
for (const f of HOOK_LIB_FILES) put(path.join(SRC, 'hooks', 'lib', f), path.join(libDest, f));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function place(src, dest, symlink) {
|
|
51
214
|
rmrf(dest);
|
|
52
215
|
ensure(path.dirname(dest));
|
|
53
|
-
if (
|
|
216
|
+
if (symlink) fs.symlinkSync(src, dest, 'dir');
|
|
54
217
|
else fs.cpSync(src, dest, { recursive: true });
|
|
55
218
|
}
|
|
56
|
-
const readJSON = (p) => { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return {}; } };
|
|
57
|
-
const writeJSON = (p, o) => { ensure(path.dirname(p)); fs.writeFileSync(p, JSON.stringify(o, null, 2) + '\n'); };
|
|
58
219
|
|
|
59
220
|
function addHook(s, event, matcher, command) {
|
|
60
221
|
s.hooks = s.hooks || {};
|
|
@@ -72,9 +233,16 @@ function stripHook(s, event, substr) {
|
|
|
72
233
|
s.hooks[event] = s.hooks[event].filter((g) => (g.hooks || []).length);
|
|
73
234
|
}
|
|
74
235
|
|
|
75
|
-
|
|
76
|
-
|
|
236
|
+
function uninstallClaude(configDir) {
|
|
237
|
+
const P = claudePaths(configDir);
|
|
238
|
+
// Sweep every trailhead-owned skill dir (current set + any stale dir a prior
|
|
239
|
+
// layout left that this package no longer ships), never the shared dir itself.
|
|
240
|
+
sweepTrailheadSkills(path.join(configDir, 'skills'));
|
|
241
|
+
[P.commands, path.join(configDir, 'trailhead')].forEach(rmrf);
|
|
77
242
|
HOOK_FILES.forEach((f) => rmrf(path.join(P.hooksDir, f)));
|
|
243
|
+
// Remove only trailhead's own lib files by name: the hooks/lib dir is shared
|
|
244
|
+
// with other plugins, so never rmrf the whole dir.
|
|
245
|
+
HOOK_LIB_FILES.forEach((f) => rmrf(path.join(P.hooksDir, 'lib', f)));
|
|
78
246
|
const s = readJSON(P.settings);
|
|
79
247
|
stripHook(s, 'PreToolUse', 'trailhead-commit-guard.js');
|
|
80
248
|
stripHook(s, 'PreToolUse', 'trailhead-secret-guard.js');
|
|
@@ -82,38 +250,321 @@ if (uninstall) {
|
|
|
82
250
|
stripHook(s, 'SessionStart', 'trailhead-check-update.js');
|
|
83
251
|
writeJSON(P.settings, s);
|
|
84
252
|
console.log(`trailhead uninstalled from ${configDir}`);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
//
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function installClaude(configDir, { useSymlink }) {
|
|
256
|
+
const P = claudePaths(configDir);
|
|
257
|
+
const hookCmd = (name) => `node "${path.join(P.hooksDir, name)}"`;
|
|
258
|
+
|
|
259
|
+
// Place each engine skill dir as a sibling under configDir/skills so the
|
|
260
|
+
// ../_shared/ and ../trailhead-<cluster>/ relative refs resolve. The skills
|
|
261
|
+
// dir is shared with other plugins: place by name, never touch the whole dir.
|
|
262
|
+
// Migration: sweep any stale trailhead-owned skill dirs from a prior layout
|
|
263
|
+
// (an old monolith, a removed cluster) before laying the current set down, so
|
|
264
|
+
// an update over an old install leaves no dangling skill. The skills dir is
|
|
265
|
+
// shared with other plugins: place by name, never touch the whole dir.
|
|
266
|
+
const skillDirs = engineSkillDirs();
|
|
267
|
+
sweepTrailheadSkills(path.join(configDir, 'skills'));
|
|
268
|
+
for (const name of skillDirs) {
|
|
269
|
+
place(path.join(SRC, 'skills', name), path.join(configDir, 'skills', name), useSymlink);
|
|
270
|
+
}
|
|
271
|
+
place(path.join(SRC, 'commands'), P.commands, useSymlink);
|
|
272
|
+
place(path.join(SRC, 'templates'), P.templates, useSymlink);
|
|
273
|
+
copyHookScripts(P.hooksDir, { useSymlink });
|
|
274
|
+
// version marker: serve al check-update hook per la versione installata (canale npm)
|
|
275
|
+
const pkgVersion = (readJSON(path.join(SRC, '.claude-plugin', 'plugin.json')).version || '').trim();
|
|
276
|
+
if (pkgVersion) {
|
|
277
|
+
ensure(path.join(configDir, 'trailhead'));
|
|
278
|
+
fs.writeFileSync(path.join(configDir, 'trailhead', 'VERSION'), pkgVersion + '\n');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const s = readJSON(P.settings);
|
|
282
|
+
const added = [
|
|
283
|
+
addHook(s, 'PreToolUse', 'Bash', hookCmd('trailhead-commit-guard.js')),
|
|
284
|
+
addHook(s, 'PreToolUse', 'Bash', hookCmd('trailhead-secret-guard.js')),
|
|
285
|
+
addHook(s, 'PostToolUse', 'Bash', hookCmd('trailhead-issue-injection-scanner.js')),
|
|
286
|
+
addHook(s, 'SessionStart', '', hookCmd('trailhead-check-update.js')),
|
|
287
|
+
].some(Boolean);
|
|
288
|
+
writeJSON(P.settings, s);
|
|
289
|
+
|
|
290
|
+
console.log(`✓ trailhead installed for Claude Code → ${configDir}`);
|
|
291
|
+
console.log(` skills → ${path.join(configDir, 'skills')}/ (${skillDirs.join(', ')})${useSymlink ? ' (symlink)' : ''}`);
|
|
292
|
+
console.log(` commands → ${P.commands} (/trailhead:*)`);
|
|
293
|
+
console.log(` hooks → ${P.hooksDir}/ (${added ? 'registered' : 'already present'} in settings.json)`);
|
|
294
|
+
console.log(` templates → ${P.templates}`);
|
|
295
|
+
console.log('\nRestart or reload your agent to pick up the commands, then run /trailhead to start.');
|
|
296
|
+
console.log('Note: the commit guard now runs on every git commit (conventional + no Co-Authored-By).');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// --- Codex adapter -----------------------------------------------------------
|
|
300
|
+
// Codex has a native subagent toolkit (the multi_agent tools, on by default at
|
|
301
|
+
// the floor). trailhead installs as one native Codex skill under
|
|
302
|
+
// skills/trailhead/: the engine text is projected with convertToCodex +
|
|
303
|
+
// codexSkillAdapterHeader, and agents/openai.yaml (codexAgentsYaml) registers
|
|
304
|
+
// it for explicit-only `$trailhead <verb>` invocation.
|
|
305
|
+
// Base multi_agent fan-out needs no manifest, but when models.codex.* is set
|
|
306
|
+
// per technique, the installer projects those pins into per-technique
|
|
307
|
+
// trailhead-<technique>.toml files under the real ~/.codex/agents/ dir
|
|
308
|
+
// (codexAgentTomlPlan) and enables features.multi_agent_v2 so Codex honours
|
|
309
|
+
// them; with no models.codex.* set, subagents still inherit the session model.
|
|
310
|
+
|
|
311
|
+
// Recursively copy plugins/trailhead/skills/trailhead into destDir, applying
|
|
312
|
+
// convertToCodex to every .md file (and prepending the adapter header to the
|
|
313
|
+
// top-level SKILL.md). Mirrors the source tree shape (references/, references/techniques/, ...).
|
|
314
|
+
function copySkillConverted(srcDir, destDir, isRoot) {
|
|
315
|
+
ensure(destDir);
|
|
316
|
+
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
317
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
318
|
+
const destPath = path.join(destDir, entry.name);
|
|
319
|
+
if (entry.isDirectory()) {
|
|
320
|
+
copySkillConverted(srcPath, destPath, false);
|
|
321
|
+
} else if (entry.name.endsWith('.md')) {
|
|
322
|
+
const raw = fs.readFileSync(srcPath, 'utf8');
|
|
323
|
+
let converted = convertToCodex(raw);
|
|
324
|
+
if (isRoot && entry.name === 'SKILL.md') {
|
|
325
|
+
// Inject the adapter header AFTER the YAML frontmatter so the
|
|
326
|
+
// frontmatter stays at the top and Codex registers the skill (#40).
|
|
327
|
+
converted = injectCodexAdapterHeader(converted);
|
|
328
|
+
}
|
|
329
|
+
fs.writeFileSync(destPath, converted);
|
|
330
|
+
} else {
|
|
331
|
+
fs.copyFileSync(srcPath, destPath);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Read the canonical command surface from plugins/trailhead/commands/*.md:
|
|
337
|
+
// each file's basename is the verb; its frontmatter description/argument-hint
|
|
338
|
+
// (when present) ride along as the Codex prompt shim's discoverability metadata.
|
|
339
|
+
// Single-sources the verb list so the Codex shims never drift from the commands.
|
|
340
|
+
function readCommandVerbs() {
|
|
341
|
+
const dir = path.join(SRC, 'commands');
|
|
342
|
+
if (!fs.existsSync(dir)) return [];
|
|
343
|
+
return fs.readdirSync(dir)
|
|
344
|
+
.filter((f) => f.endsWith('.md'))
|
|
345
|
+
.map((f) => {
|
|
346
|
+
const raw = fs.readFileSync(path.join(dir, f), 'utf8');
|
|
347
|
+
return { verb: f.slice(0, -3), description: fmValue(raw, 'description'), argumentHint: fmValue(raw, 'argument-hint') };
|
|
348
|
+
})
|
|
349
|
+
.sort((a, b) => a.verb.localeCompare(b.verb));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Extract one frontmatter scalar's raw value verbatim (an already-valid YAML
|
|
353
|
+
// scalar re-serialises unchanged). Looks only inside the leading ---...--- block.
|
|
354
|
+
// Returns undefined when the key is absent or its value is empty.
|
|
355
|
+
function fmValue(md, key) {
|
|
356
|
+
const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(md);
|
|
357
|
+
const block = fm ? fm[1] : '';
|
|
358
|
+
const m = new RegExp(`^${key}:[ \\t]*(.*)$`, 'm').exec(block);
|
|
359
|
+
if (!m) return undefined;
|
|
360
|
+
const v = m[1].trim();
|
|
361
|
+
return v === '' ? undefined : v;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Best-effort `codex --version` probe for the install-time floor gate. Returns
|
|
365
|
+
// the raw version string, or null when codex is not on PATH / errors out.
|
|
366
|
+
function detectCodexVersion() {
|
|
367
|
+
try {
|
|
368
|
+
const r = spawnSync('codex', ['--version'], { encoding: 'utf8', timeout: 5000 });
|
|
369
|
+
if (r.error || r.status !== 0) return null;
|
|
370
|
+
return String(r.stdout || r.stderr || '').trim() || null;
|
|
371
|
+
} catch {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function installCodex(configDir, { useSymlink }) {
|
|
377
|
+
// Floor gate (decision #27): refuse to project onto a Codex below the
|
|
378
|
+
// multi_agent floor; warn (but proceed) when the version can't be read.
|
|
379
|
+
const gate = codexVersionGate(detectCodexVersion());
|
|
380
|
+
if (!gate.proceed) {
|
|
381
|
+
console.error(`✗ ${gate.message}`);
|
|
382
|
+
process.exitCode = 1;
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (gate.undetermined) console.warn(`⚠ ${gate.message}`);
|
|
386
|
+
|
|
387
|
+
const L = codexLayout(configDir);
|
|
388
|
+
|
|
389
|
+
if (useSymlink) {
|
|
390
|
+
console.log('Note: on Codex, --symlink links the verbatim artifacts (hooks, templates) into the package source for live edits; the skill files are text-converted at install, so they are written (projected), not linked.');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Migration: sweep the pre-#25 layout AND the #42 prompt shims that never
|
|
394
|
+
// surfaced as Codex slash commands (#46): prompts/trailhead*.md + the
|
|
395
|
+
// trailhead/ engine dir. Removing them on every (re)install cleans up an
|
|
396
|
+
// upgrade from the shim layout.
|
|
397
|
+
rmrf(path.join(L.promptsDir, 'trailhead.md'));
|
|
398
|
+
for (const f of (fs.existsSync(L.promptsDir) ? fs.readdirSync(L.promptsDir) : [])) {
|
|
399
|
+
if (f.startsWith('trailhead-')) rmrf(path.join(L.promptsDir, f));
|
|
400
|
+
}
|
|
401
|
+
rmrf(L.legacyEngineDir);
|
|
402
|
+
|
|
403
|
+
// Sweep the whole engine + any prior per-verb skills, then project fresh.
|
|
404
|
+
sweepTrailheadSkills(L.skillsRoot);
|
|
405
|
+
|
|
406
|
+
// Project each engine skill dir (dispatcher + 5 clusters + _shared) as a
|
|
407
|
+
// sibling under skills/, converting text and injecting the adapter header into
|
|
408
|
+
// each top-level SKILL.md. The ../_shared/ relative refs survive conversion and
|
|
409
|
+
// resolve as siblings (decision #54). _shared has no SKILL.md, so no header.
|
|
410
|
+
const engineDirs = engineSkillDirs();
|
|
411
|
+
for (const name of engineDirs) {
|
|
412
|
+
copySkillConverted(path.join(SRC, 'skills', name), path.join(L.skillsRoot, name), true);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Templates (verbatim, never converted), bundled inside the skill dir so
|
|
416
|
+
// ${CLAUDE_PLUGIN_ROOT}/templates resolves to ~/.codex/skills/trailhead/templates.
|
|
417
|
+
place(path.join(SRC, 'templates'), L.templatesDir, useSymlink);
|
|
418
|
+
|
|
419
|
+
// agents/openai.yaml: UI metadata + explicit-only invocation (no auto-trigger).
|
|
420
|
+
ensure(L.agentsDir);
|
|
421
|
+
fs.writeFileSync(L.agentsYaml, codexAgentsYaml());
|
|
422
|
+
// Each cluster skill also gets explicit-only UI metadata (never auto-invoked).
|
|
423
|
+
for (const name of engineDirs) {
|
|
424
|
+
if (name === 'trailhead' || name === '_shared') continue;
|
|
425
|
+
const dir = path.join(L.skillsRoot, name, 'agents');
|
|
426
|
+
ensure(dir);
|
|
427
|
+
fs.writeFileSync(path.join(dir, 'openai.yaml'), codexClusterAgentsYaml(name));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Per-verb discoverability (#46): one thin Codex skill per verb
|
|
431
|
+
// (skills/trailhead-<verb>/, invocable as $trailhead-<verb>), each delegating
|
|
432
|
+
// to $trailhead <verb>. Codex custom prompts (~/.codex/prompts/) never surface
|
|
433
|
+
// as slash commands, so per-verb skills carry the surface, exactly as GSD does.
|
|
434
|
+
// Verbs come from the canonical commands/*.md so the surfaces never drift.
|
|
435
|
+
// Skip any verb whose skill dir would collide with a projected cluster skill
|
|
436
|
+
// (e.g. verb `work` vs the trailhead-work cluster): the cluster skill is its
|
|
437
|
+
// own $trailhead-<name> entry. Compute the skip-set from the projected dirs.
|
|
438
|
+
const clusterDirNames = new Set(engineDirs);
|
|
439
|
+
const verbList = readCommandVerbs().filter((c) => !clusterDirNames.has(`trailhead-${c.verb}`));
|
|
440
|
+
const verbSkillPlan = codexVerbSkillPlan(configDir, verbList);
|
|
441
|
+
for (const d of verbSkillPlan.dirs) ensure(path.join(d, 'agents'));
|
|
442
|
+
for (const w of verbSkillPlan.writes) fs.writeFileSync(w.path, w.content);
|
|
443
|
+
|
|
444
|
+
const pkgVersion = (readJSON(path.join(SRC, '.claude-plugin', 'plugin.json')).version || '').trim();
|
|
445
|
+
if (pkgVersion) fs.writeFileSync(L.versionFile, pkgVersion + '\n');
|
|
446
|
+
|
|
447
|
+
// Hooks: copy the 4 guard scripts into the skill dir and register them in
|
|
448
|
+
// ~/.codex/hooks.json (same shape as Claude's settings.json hooks block).
|
|
449
|
+
copyHookScripts(L.hooksScriptsDir, { useSymlink });
|
|
450
|
+
const h = readJSON(L.hooksJson);
|
|
451
|
+
for (const e of codexHookEntries(L.hooksScriptsDir)) addHook(h, e.event, e.matcher, e.command);
|
|
452
|
+
writeJSON(L.hooksJson, h);
|
|
453
|
+
|
|
454
|
+
// Feature flag: Codex gates the hook bus behind features.hooks = true.
|
|
455
|
+
let current = '';
|
|
456
|
+
try { current = fs.readFileSync(L.configToml, 'utf8'); } catch { current = ''; }
|
|
457
|
+
const updatedToml = enableCodexHooksFeature(current);
|
|
458
|
+
let featureManual = false;
|
|
459
|
+
if (updatedToml && typeof updatedToml === 'string') {
|
|
460
|
+
ensure(path.dirname(L.configToml));
|
|
461
|
+
fs.writeFileSync(L.configToml, updatedToml);
|
|
462
|
+
current = updatedToml;
|
|
463
|
+
} else if (updatedToml && updatedToml.unsafe) {
|
|
464
|
+
featureManual = true;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Per-technique model pins: project models.codex.* (merged project + global
|
|
468
|
+
// config) into trailhead-<technique>.toml under the real ~/.codex/agents/
|
|
469
|
+
// dir. Sweep only trailhead's own files first, so a stale pin from a prior
|
|
470
|
+
// install (or a removed technique) never lingers, without touching the
|
|
471
|
+
// user's other custom-agent TOMLs.
|
|
472
|
+
const codexModels = readCodexModels(configDir);
|
|
473
|
+
ensure(L.codexAgentsDir);
|
|
474
|
+
for (const f of fs.readdirSync(L.codexAgentsDir)) {
|
|
475
|
+
if (/^trailhead-.*\.toml$/.test(f)) rmrf(path.join(L.codexAgentsDir, f));
|
|
476
|
+
}
|
|
477
|
+
const plan = codexAgentTomlPlan(configDir, codexModels);
|
|
478
|
+
for (const w of plan.writes) fs.writeFileSync(w.path, w.content);
|
|
479
|
+
|
|
480
|
+
// Feature flag: Codex gates per-subagent model pinning behind
|
|
481
|
+
// features.multi_agent_v2 = true. Enable it ONLY when we actually projected
|
|
482
|
+
// pins: with no models.codex.* set, base multi_agent already fans out and the
|
|
483
|
+
// adapter's §D runtime detection expects no trailhead agent_type registry, so
|
|
484
|
+
// leaving v2 off keeps that contract sound. (A reinstall that drops all pins
|
|
485
|
+
// sweeps the TOMLs above but leaves any prior v2 flag in place; harmless, as
|
|
486
|
+
// no trailhead-* agents remain for the runtime to dispatch to.)
|
|
487
|
+
let featureManualV2 = false;
|
|
488
|
+
if (plan.writes.length > 0) {
|
|
489
|
+
const updatedTomlV2 = enableCodexMultiAgentV2Feature(current);
|
|
490
|
+
if (updatedTomlV2 && typeof updatedTomlV2 === 'string') {
|
|
491
|
+
ensure(path.dirname(L.configToml));
|
|
492
|
+
fs.writeFileSync(L.configToml, updatedTomlV2);
|
|
493
|
+
} else if (updatedTomlV2 && updatedTomlV2.unsafe) {
|
|
494
|
+
featureManualV2 = true;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
console.log(`✓ trailhead installed for Codex → ${configDir}`);
|
|
499
|
+
console.log(` skill → ${L.skillDir}/ (invoke $trailhead)`);
|
|
500
|
+
console.log(` templates → ${L.templatesDir}/`);
|
|
501
|
+
console.log(` verb skills → ${L.skillsRoot}/trailhead-<verb>/ (${verbSkillPlan.dirs.length} discoverability skills: invoke $trailhead-<verb>)`);
|
|
502
|
+
console.log(` hooks → ${L.hooksScriptsDir}/ (registered in hooks.json)`);
|
|
503
|
+
if (plan.writes.length > 0) {
|
|
504
|
+
console.log(` agents → ${L.codexAgentsDir}/ (${plan.writes.length} model pin(s): ${plan.writes.map((w) => w.name + '.toml').join(', ')})`);
|
|
505
|
+
} else {
|
|
506
|
+
console.log(' agents → no models.codex.* set, subagents inherit the session model');
|
|
507
|
+
}
|
|
508
|
+
console.log('\nRestart or reload Codex to pick up the skill, then run $trailhead to start.');
|
|
509
|
+
// The trust prompt only fires once the feature flag is on. When we could not
|
|
510
|
+
// edit config.toml, the hooks stay inert until the user sets it by hand, so
|
|
511
|
+
// don't reassure them about a trust review that won't happen yet.
|
|
512
|
+
if (featureManual) {
|
|
513
|
+
console.log(` ⚠ could not edit ${L.configToml} automatically; add \`features.hooks = true\` under [features] by hand, then Codex will ask you to trust trailhead's hooks on next start.`);
|
|
514
|
+
} else {
|
|
515
|
+
console.log('Note: Codex will ask you to trust trailhead\'s hooks on next start (feature `features.hooks`).');
|
|
516
|
+
}
|
|
517
|
+
if (featureManualV2) {
|
|
518
|
+
console.log(` ⚠ could not edit ${L.configToml} automatically, add \`features.multi_agent_v2 = true\` under [features] by hand to let Codex honour the projected model pins.`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function uninstallCodex(configDir) {
|
|
523
|
+
const L = codexLayout(configDir);
|
|
524
|
+
// Sweep the dispatcher, _shared, the clusters, and the per-verb skills.
|
|
525
|
+
sweepTrailheadSkills(L.skillsRoot);
|
|
526
|
+
// sweep any pre-#25 layout AND the #42 prompt shims from prompts/
|
|
527
|
+
rmrf(path.join(L.promptsDir, 'trailhead.md'));
|
|
528
|
+
for (const f of (fs.existsSync(L.promptsDir) ? fs.readdirSync(L.promptsDir) : [])) {
|
|
529
|
+
if (f.startsWith('trailhead-')) rmrf(path.join(L.promptsDir, f));
|
|
530
|
+
}
|
|
531
|
+
rmrf(L.legacyEngineDir);
|
|
532
|
+
// Sweep trailhead's own per-technique model-pin TOMLs, never the user's
|
|
533
|
+
// other custom-agent TOMLs in the same dir.
|
|
534
|
+
if (fs.existsSync(L.codexAgentsDir)) {
|
|
535
|
+
for (const f of fs.readdirSync(L.codexAgentsDir)) {
|
|
536
|
+
if (/^trailhead-.*\.toml$/.test(f)) rmrf(path.join(L.codexAgentsDir, f));
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
// Strip trailhead's entries from hooks.json, but only if it already exists
|
|
540
|
+
// (uninstall must never create it). Leave features.hooks and
|
|
541
|
+
// features.multi_agent_v2 in config.toml untouched: the user may rely on
|
|
542
|
+
// them for other hooks/agents, mirroring how uninstallClaude leaves
|
|
543
|
+
// settings.json flags alone.
|
|
544
|
+
if (fs.existsSync(L.hooksJson)) {
|
|
545
|
+
const h = readJSON(L.hooksJson);
|
|
546
|
+
stripHook(h, 'PreToolUse', 'trailhead-commit-guard.js');
|
|
547
|
+
stripHook(h, 'PreToolUse', 'trailhead-secret-guard.js');
|
|
548
|
+
stripHook(h, 'PostToolUse', 'trailhead-issue-injection-scanner.js');
|
|
549
|
+
stripHook(h, 'SessionStart', 'trailhead-check-update.js');
|
|
550
|
+
writeJSON(L.hooksJson, h);
|
|
551
|
+
}
|
|
552
|
+
console.log(`trailhead uninstalled from ${configDir}`);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// --- main --------------------------------------------------------------------
|
|
556
|
+
async function main() {
|
|
557
|
+
const host = await resolveHostName();
|
|
558
|
+
const configDir = dirArg || configDirFor(host);
|
|
559
|
+
|
|
560
|
+
if (uninstall) {
|
|
561
|
+
if (host === 'codex') uninstallCodex(configDir);
|
|
562
|
+
else uninstallClaude(configDir);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (host === 'codex') installCodex(configDir, { useSymlink });
|
|
567
|
+
else installClaude(configDir, { useSymlink });
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
main().catch((e) => { console.error(e.message); process.exit(1); });
|