@mobiman/vector 1.1.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/LICENSE +21 -0
- package/README.md +117 -0
- package/agents/vector-codebase-mapper.md +770 -0
- package/agents/vector-debugger.md +1338 -0
- package/agents/vector-executor.md +487 -0
- package/agents/vector-integration-checker.md +443 -0
- package/agents/vector-nyquist-auditor.md +176 -0
- package/agents/vector-phase-researcher.md +553 -0
- package/agents/vector-plan-checker.md +706 -0
- package/agents/vector-planner.md +1307 -0
- package/agents/vector-project-researcher.md +629 -0
- package/agents/vector-research-synthesizer.md +247 -0
- package/agents/vector-roadmapper.md +650 -0
- package/agents/vector-ui-auditor.md +439 -0
- package/agents/vector-ui-checker.md +300 -0
- package/agents/vector-ui-researcher.md +353 -0
- package/agents/vector-verifier.md +579 -0
- package/bin/install.cjs +2907 -0
- package/bin/install.cjs.map +1 -0
- package/bin/install.cts +3103 -0
- package/bin/install.d.cts +3 -0
- package/bin/install.d.cts.map +1 -0
- package/commands/vector/add-phase.md +43 -0
- package/commands/vector/add-tests.md +41 -0
- package/commands/vector/add-todo.md +47 -0
- package/commands/vector/audit-milestone.md +36 -0
- package/commands/vector/autonomous.md +41 -0
- package/commands/vector/check-todos.md +45 -0
- package/commands/vector/cleanup.md +18 -0
- package/commands/vector/complete-milestone.md +136 -0
- package/commands/vector/debug.md +168 -0
- package/commands/vector/discuss-phase.md +90 -0
- package/commands/vector/do.md +30 -0
- package/commands/vector/execute-phase.md +41 -0
- package/commands/vector/health.md +22 -0
- package/commands/vector/help.md +22 -0
- package/commands/vector/insert-phase.md +32 -0
- package/commands/vector/join-discord.md +18 -0
- package/commands/vector/list-phase-assumptions.md +46 -0
- package/commands/vector/map-codebase.md +71 -0
- package/commands/vector/new-milestone.md +44 -0
- package/commands/vector/new-project.md +42 -0
- package/commands/vector/pause-work.md +38 -0
- package/commands/vector/plan-milestone-gaps.md +34 -0
- package/commands/vector/plan-phase.md +45 -0
- package/commands/vector/progress.md +24 -0
- package/commands/vector/quick.md +47 -0
- package/commands/vector/reapply-patches.md +123 -0
- package/commands/vector/remove-phase.md +31 -0
- package/commands/vector/research-phase.md +190 -0
- package/commands/vector/resume-work.md +40 -0
- package/commands/vector/set-profile.md +12 -0
- package/commands/vector/settings.md +36 -0
- package/commands/vector/stats.md +18 -0
- package/commands/vector/ui-phase.md +34 -0
- package/commands/vector/ui-review.md +32 -0
- package/commands/vector/update.md +37 -0
- package/commands/vector/validate-phase.md +35 -0
- package/commands/vector/verify-work.md +38 -0
- package/core/bin/lib/commands.cjs +641 -0
- package/core/bin/lib/commands.cjs.map +1 -0
- package/core/bin/lib/commands.cts +712 -0
- package/core/bin/lib/commands.d.cts +23 -0
- package/core/bin/lib/commands.d.cts.map +1 -0
- package/core/bin/lib/config.cjs +281 -0
- package/core/bin/lib/config.cjs.map +1 -0
- package/core/bin/lib/config.cts +301 -0
- package/core/bin/lib/config.d.cts +50 -0
- package/core/bin/lib/config.d.cts.map +1 -0
- package/core/bin/lib/core.cjs +483 -0
- package/core/bin/lib/core.cjs.map +1 -0
- package/core/bin/lib/core.cts +544 -0
- package/core/bin/lib/core.d.cts +96 -0
- package/core/bin/lib/core.d.cts.map +1 -0
- package/core/bin/lib/frontmatter.cjs +341 -0
- package/core/bin/lib/frontmatter.cjs.map +1 -0
- package/core/bin/lib/frontmatter.cts +295 -0
- package/core/bin/lib/frontmatter.d.cts +18 -0
- package/core/bin/lib/frontmatter.d.cts.map +1 -0
- package/core/bin/lib/init.cjs +674 -0
- package/core/bin/lib/init.cjs.map +1 -0
- package/core/bin/lib/init.cts +775 -0
- package/core/bin/lib/init.d.cts +16 -0
- package/core/bin/lib/init.d.cts.map +1 -0
- package/core/bin/lib/milestone.cjs +210 -0
- package/core/bin/lib/milestone.cjs.map +1 -0
- package/core/bin/lib/milestone.cts +241 -0
- package/core/bin/lib/milestone.d.cts +11 -0
- package/core/bin/lib/milestone.d.cts.map +1 -0
- package/core/bin/lib/model-profiles.cjs +62 -0
- package/core/bin/lib/model-profiles.cjs.map +1 -0
- package/core/bin/lib/model-profiles.cts +66 -0
- package/core/bin/lib/model-profiles.d.cts +33 -0
- package/core/bin/lib/model-profiles.d.cts.map +1 -0
- package/core/bin/lib/phase.cjs +713 -0
- package/core/bin/lib/phase.cjs.map +1 -0
- package/core/bin/lib/phase.cts +914 -0
- package/core/bin/lib/phase.d.cts +21 -0
- package/core/bin/lib/phase.d.cts.map +1 -0
- package/core/bin/lib/roadmap.cjs +246 -0
- package/core/bin/lib/roadmap.cjs.map +1 -0
- package/core/bin/lib/roadmap.cts +311 -0
- package/core/bin/lib/roadmap.d.cts +7 -0
- package/core/bin/lib/roadmap.d.cts.map +1 -0
- package/core/bin/lib/state.cjs +709 -0
- package/core/bin/lib/state.cjs.map +1 -0
- package/core/bin/lib/state.cts +718 -0
- package/core/bin/lib/state.d.cts +47 -0
- package/core/bin/lib/state.d.cts.map +1 -0
- package/core/bin/lib/template.cjs +220 -0
- package/core/bin/lib/template.cjs.map +1 -0
- package/core/bin/lib/template.cts +229 -0
- package/core/bin/lib/template.d.cts +15 -0
- package/core/bin/lib/template.d.cts.map +1 -0
- package/core/bin/lib/verify.cjs +824 -0
- package/core/bin/lib/verify.cjs.map +1 -0
- package/core/bin/lib/verify.cts +829 -0
- package/core/bin/lib/verify.d.cts +17 -0
- package/core/bin/lib/verify.d.cts.map +1 -0
- package/core/bin/vector-tools.cjs +641 -0
- package/core/bin/vector-tools.cjs.map +1 -0
- package/core/bin/vector-tools.cts +603 -0
- package/core/bin/vector-tools.d.cts +128 -0
- package/core/bin/vector-tools.d.cts.map +1 -0
- package/core/references/checkpoints.md +776 -0
- package/core/references/continuation-format.md +249 -0
- package/core/references/decimal-phase-calculation.md +65 -0
- package/core/references/git-integration.md +248 -0
- package/core/references/git-planning-commit.md +38 -0
- package/core/references/model-profile-resolution.md +36 -0
- package/core/references/model-profiles.md +101 -0
- package/core/references/phase-argument-parsing.md +61 -0
- package/core/references/planning-config.md +200 -0
- package/core/references/questioning.md +162 -0
- package/core/references/tdd.md +263 -0
- package/core/references/ui-brand.md +160 -0
- package/core/references/verification-patterns.md +612 -0
- package/core/templates/DEBUG.md +164 -0
- package/core/templates/UAT.md +247 -0
- package/core/templates/UI-SPEC.md +100 -0
- package/core/templates/VALIDATION.md +76 -0
- package/core/templates/codebase/architecture.md +255 -0
- package/core/templates/codebase/concerns.md +310 -0
- package/core/templates/codebase/conventions.md +307 -0
- package/core/templates/codebase/integrations.md +280 -0
- package/core/templates/codebase/stack.md +186 -0
- package/core/templates/codebase/structure.md +285 -0
- package/core/templates/codebase/testing.md +480 -0
- package/core/templates/config.json +37 -0
- package/core/templates/context.md +352 -0
- package/core/templates/continue-here.md +78 -0
- package/core/templates/copilot-instructions.md +7 -0
- package/core/templates/debug-subagent-prompt.md +91 -0
- package/core/templates/discovery.md +146 -0
- package/core/templates/milestone-archive.md +123 -0
- package/core/templates/milestone.md +115 -0
- package/core/templates/phase-prompt.md +610 -0
- package/core/templates/planner-subagent-prompt.md +117 -0
- package/core/templates/project.md +184 -0
- package/core/templates/requirements.md +231 -0
- package/core/templates/research-project/ARCHITECTURE.md +204 -0
- package/core/templates/research-project/FEATURES.md +147 -0
- package/core/templates/research-project/PITFALLS.md +200 -0
- package/core/templates/research-project/STACK.md +120 -0
- package/core/templates/research-project/SUMMARY.md +170 -0
- package/core/templates/research.md +552 -0
- package/core/templates/retrospective.md +54 -0
- package/core/templates/roadmap.md +202 -0
- package/core/templates/state.md +176 -0
- package/core/templates/summary-complex.md +59 -0
- package/core/templates/summary-minimal.md +41 -0
- package/core/templates/summary-standard.md +48 -0
- package/core/templates/summary.md +248 -0
- package/core/templates/user-setup.md +311 -0
- package/core/templates/verification-report.md +322 -0
- package/core/workflows/add-phase.md +112 -0
- package/core/workflows/add-tests.md +351 -0
- package/core/workflows/add-todo.md +158 -0
- package/core/workflows/audit-milestone.md +332 -0
- package/core/workflows/autonomous.md +743 -0
- package/core/workflows/check-todos.md +177 -0
- package/core/workflows/cleanup.md +152 -0
- package/core/workflows/complete-milestone.md +766 -0
- package/core/workflows/diagnose-issues.md +219 -0
- package/core/workflows/discovery-phase.md +289 -0
- package/core/workflows/discuss-phase.md +762 -0
- package/core/workflows/do.md +104 -0
- package/core/workflows/execute-phase.md +468 -0
- package/core/workflows/execute-plan.md +483 -0
- package/core/workflows/health.md +159 -0
- package/core/workflows/help.md +513 -0
- package/core/workflows/insert-phase.md +130 -0
- package/core/workflows/list-phase-assumptions.md +178 -0
- package/core/workflows/map-codebase.md +316 -0
- package/core/workflows/new-milestone.md +386 -0
- package/core/workflows/new-project.md +1113 -0
- package/core/workflows/node-repair.md +92 -0
- package/core/workflows/pause-work.md +122 -0
- package/core/workflows/plan-milestone-gaps.md +274 -0
- package/core/workflows/plan-phase.md +666 -0
- package/core/workflows/progress.md +382 -0
- package/core/workflows/quick.md +717 -0
- package/core/workflows/remove-phase.md +155 -0
- package/core/workflows/research-phase.md +74 -0
- package/core/workflows/resume-project.md +307 -0
- package/core/workflows/settings.md +243 -0
- package/core/workflows/stats.md +60 -0
- package/core/workflows/transition.md +544 -0
- package/core/workflows/ui-phase.md +290 -0
- package/core/workflows/ui-review.md +157 -0
- package/core/workflows/update.md +320 -0
- package/core/workflows/validate-phase.md +167 -0
- package/core/workflows/verify-phase.md +243 -0
- package/core/workflows/verify-work.md +584 -0
- package/package.json +55 -0
- package/scripts/build-hooks.cjs +38 -0
- package/scripts/build-hooks.cjs.map +1 -0
- package/scripts/build-hooks.cts +41 -0
- package/scripts/build-hooks.d.cts +6 -0
- package/scripts/build-hooks.d.cts.map +1 -0
- package/scripts/run-tests.cjs +28 -0
- package/scripts/run-tests.cjs.map +1 -0
- package/scripts/run-tests.cts +28 -0
- package/scripts/run-tests.d.cts +3 -0
- package/scripts/run-tests.d.cts.map +1 -0
package/bin/install.cts
ADDED
|
@@ -0,0 +1,3103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import readline from 'readline';
|
|
7
|
+
import crypto from 'crypto';
|
|
8
|
+
import pkg from '../package.json';
|
|
9
|
+
|
|
10
|
+
type Runtime = 'claude' | 'opencode' | 'gemini' | 'codex' | 'copilot' | 'antigravity';
|
|
11
|
+
type Attribution = string | null | undefined;
|
|
12
|
+
|
|
13
|
+
// Colors
|
|
14
|
+
const cyan = '\x1b[36m';
|
|
15
|
+
const green = '\x1b[32m';
|
|
16
|
+
const yellow = '\x1b[33m';
|
|
17
|
+
const dim = '\x1b[2m';
|
|
18
|
+
const reset = '\x1b[0m';
|
|
19
|
+
|
|
20
|
+
// Codex config.toml constants
|
|
21
|
+
const GSD_CODEX_MARKER = '# Vector Agent Configuration \u2014 managed by core installer';
|
|
22
|
+
|
|
23
|
+
// Copilot instructions marker constants
|
|
24
|
+
const GSD_COPILOT_INSTRUCTIONS_MARKER = '<!-- Vector Configuration \u2014 managed by core installer -->';
|
|
25
|
+
const GSD_COPILOT_INSTRUCTIONS_CLOSE_MARKER = '<!-- /Vector Configuration -->';
|
|
26
|
+
|
|
27
|
+
const CODEX_AGENT_SANDBOX = {
|
|
28
|
+
'vector-executor': 'workspace-write',
|
|
29
|
+
'vector-planner': 'workspace-write',
|
|
30
|
+
'vector-phase-researcher': 'workspace-write',
|
|
31
|
+
'vector-project-researcher': 'workspace-write',
|
|
32
|
+
'vector-research-synthesizer': 'workspace-write',
|
|
33
|
+
'vector-verifier': 'workspace-write',
|
|
34
|
+
'vector-codebase-mapper': 'workspace-write',
|
|
35
|
+
'vector-roadmapper': 'workspace-write',
|
|
36
|
+
'vector-debugger': 'workspace-write',
|
|
37
|
+
'vector-plan-checker': 'read-only',
|
|
38
|
+
'vector-integration-checker': 'read-only',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Copilot tool name mapping — Claude Code tools to GitHub Copilot tools
|
|
42
|
+
// Tool mapping applies ONLY to agents, NOT to skills (per CONTEXT.md decision)
|
|
43
|
+
const claudeToCopilotTools = {
|
|
44
|
+
Read: 'read',
|
|
45
|
+
Write: 'edit',
|
|
46
|
+
Edit: 'edit',
|
|
47
|
+
Bash: 'execute',
|
|
48
|
+
Grep: 'search',
|
|
49
|
+
Glob: 'search',
|
|
50
|
+
Task: 'agent',
|
|
51
|
+
WebSearch: 'web',
|
|
52
|
+
WebFetch: 'web',
|
|
53
|
+
TodoWrite: 'todo',
|
|
54
|
+
AskUserQuestion: 'ask_user',
|
|
55
|
+
SlashCommand: 'skill',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// pkg imported at top of file
|
|
59
|
+
|
|
60
|
+
// Parse args
|
|
61
|
+
const args = process.argv.slice(2);
|
|
62
|
+
const hasGlobal = args.includes('--global') || args.includes('-g');
|
|
63
|
+
const hasLocal = args.includes('--local') || args.includes('-l');
|
|
64
|
+
const hasOpencode = args.includes('--opencode');
|
|
65
|
+
const hasClaude = args.includes('--claude');
|
|
66
|
+
const hasGemini = args.includes('--gemini');
|
|
67
|
+
const hasCodex = args.includes('--codex');
|
|
68
|
+
const hasCopilot = args.includes('--copilot');
|
|
69
|
+
const hasAntigravity = args.includes('--antigravity');
|
|
70
|
+
const hasBoth = args.includes('--both'); // Legacy flag, keeps working
|
|
71
|
+
const hasAll = args.includes('--all');
|
|
72
|
+
const hasUninstall = args.includes('--uninstall') || args.includes('-u');
|
|
73
|
+
|
|
74
|
+
// Runtime selection - can be set by flags or interactive prompt
|
|
75
|
+
let selectedRuntimes: Runtime[] = [];
|
|
76
|
+
if (hasAll) {
|
|
77
|
+
selectedRuntimes = ['claude', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity'];
|
|
78
|
+
} else if (hasBoth) {
|
|
79
|
+
selectedRuntimes = ['claude', 'opencode'];
|
|
80
|
+
} else {
|
|
81
|
+
if (hasOpencode) selectedRuntimes.push('opencode');
|
|
82
|
+
if (hasClaude) selectedRuntimes.push('claude');
|
|
83
|
+
if (hasGemini) selectedRuntimes.push('gemini');
|
|
84
|
+
if (hasCodex) selectedRuntimes.push('codex');
|
|
85
|
+
if (hasCopilot) selectedRuntimes.push('copilot');
|
|
86
|
+
if (hasAntigravity) selectedRuntimes.push('antigravity');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// WSL + Windows Node.js detection
|
|
90
|
+
// When Windows-native Node runs on WSL, os.homedir() and path.join() produce
|
|
91
|
+
// backslash paths that don't resolve correctly on the Linux filesystem.
|
|
92
|
+
if (process.platform === 'win32') {
|
|
93
|
+
let isWSL = false;
|
|
94
|
+
try {
|
|
95
|
+
if (process.env.WSL_DISTRO_NAME) {
|
|
96
|
+
isWSL = true;
|
|
97
|
+
} else if (fs.existsSync('/proc/version')) {
|
|
98
|
+
const procVersion = fs.readFileSync('/proc/version', 'utf8').toLowerCase();
|
|
99
|
+
if (procVersion.includes('microsoft') || procVersion.includes('wsl')) {
|
|
100
|
+
isWSL = true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
// Ignore read errors — not WSL
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (isWSL) {
|
|
108
|
+
console.error(`
|
|
109
|
+
${yellow}⚠ Detected WSL with Windows-native Node.js.${reset}
|
|
110
|
+
|
|
111
|
+
This causes path resolution issues that prevent correct installation.
|
|
112
|
+
Please install a Linux-native Node.js inside WSL:
|
|
113
|
+
|
|
114
|
+
curl -fsSL https://fnm.vercel.app/install | bash
|
|
115
|
+
fnm install --lts
|
|
116
|
+
|
|
117
|
+
Then re-run: npx vector@latest
|
|
118
|
+
`);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Helper to get directory name for a runtime (used for local/project installs)
|
|
124
|
+
function getDirName(runtime: Runtime): string {
|
|
125
|
+
if (runtime === 'copilot') return '.github';
|
|
126
|
+
if (runtime === 'opencode') return '.opencode';
|
|
127
|
+
if (runtime === 'gemini') return '.gemini';
|
|
128
|
+
if (runtime === 'codex') return '.codex';
|
|
129
|
+
if (runtime === 'antigravity') return '.agent';
|
|
130
|
+
return '.claude';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get the config directory path relative to home directory for a runtime
|
|
135
|
+
* Used for templating hooks that use path.join(homeDir, '<configDir>', ...)
|
|
136
|
+
* @param {string} runtime - 'claude', 'opencode', 'gemini', 'codex', or 'copilot'
|
|
137
|
+
* @param {boolean} isGlobal - Whether this is a global install
|
|
138
|
+
*/
|
|
139
|
+
function getConfigDirFromHome(runtime: Runtime, isGlobal: boolean): string {
|
|
140
|
+
if (!isGlobal) {
|
|
141
|
+
// Local installs use the same dir name pattern
|
|
142
|
+
return `'${getDirName(runtime)}'`;
|
|
143
|
+
}
|
|
144
|
+
// Global installs - OpenCode uses XDG path structure
|
|
145
|
+
if (runtime === 'copilot') return "'.copilot'";
|
|
146
|
+
if (runtime === 'opencode') {
|
|
147
|
+
// OpenCode: ~/.config/opencode -> '.config', 'opencode'
|
|
148
|
+
// Return as comma-separated for path.join() replacement
|
|
149
|
+
return "'.config', 'opencode'";
|
|
150
|
+
}
|
|
151
|
+
if (runtime === 'gemini') return "'.gemini'";
|
|
152
|
+
if (runtime === 'codex') return "'.codex'";
|
|
153
|
+
if (runtime === 'antigravity') {
|
|
154
|
+
if (!isGlobal) return "'.agent'";
|
|
155
|
+
return "'.gemini', 'antigravity'";
|
|
156
|
+
}
|
|
157
|
+
return "'.claude'";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get the global config directory for OpenCode
|
|
162
|
+
* OpenCode follows XDG Base Directory spec and uses ~/.config/opencode/
|
|
163
|
+
* Priority: OPENCODE_CONFIG_DIR > dirname(OPENCODE_CONFIG) > XDG_CONFIG_HOME/opencode > ~/.config/opencode
|
|
164
|
+
*/
|
|
165
|
+
function getOpencodeGlobalDir() {
|
|
166
|
+
// 1. Explicit OPENCODE_CONFIG_DIR env var
|
|
167
|
+
if (process.env.OPENCODE_CONFIG_DIR) {
|
|
168
|
+
return expandTilde(process.env.OPENCODE_CONFIG_DIR);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 2. OPENCODE_CONFIG env var (use its directory)
|
|
172
|
+
if (process.env.OPENCODE_CONFIG) {
|
|
173
|
+
return path.dirname(expandTilde(process.env.OPENCODE_CONFIG));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 3. XDG_CONFIG_HOME/opencode
|
|
177
|
+
if (process.env.XDG_CONFIG_HOME) {
|
|
178
|
+
return path.join(expandTilde(process.env.XDG_CONFIG_HOME), 'opencode');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 4. Default: ~/.config/opencode (XDG default)
|
|
182
|
+
return path.join(os.homedir(), '.config', 'opencode');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get the global config directory for a runtime
|
|
187
|
+
* @param {string} runtime - 'claude', 'opencode', 'gemini', 'codex', or 'copilot'
|
|
188
|
+
* @param {string|null} explicitDir - Explicit directory from --config-dir flag
|
|
189
|
+
*/
|
|
190
|
+
function getGlobalDir(runtime: Runtime, explicitDir: string | null = null): string {
|
|
191
|
+
if (runtime === 'opencode') {
|
|
192
|
+
// For OpenCode, --config-dir overrides env vars
|
|
193
|
+
if (explicitDir) {
|
|
194
|
+
return expandTilde(explicitDir);
|
|
195
|
+
}
|
|
196
|
+
return getOpencodeGlobalDir();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (runtime === 'gemini') {
|
|
200
|
+
// Gemini: --config-dir > GEMINI_CONFIG_DIR > ~/.gemini
|
|
201
|
+
if (explicitDir) {
|
|
202
|
+
return expandTilde(explicitDir);
|
|
203
|
+
}
|
|
204
|
+
if (process.env.GEMINI_CONFIG_DIR) {
|
|
205
|
+
return expandTilde(process.env.GEMINI_CONFIG_DIR);
|
|
206
|
+
}
|
|
207
|
+
return path.join(os.homedir(), '.gemini');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (runtime === 'codex') {
|
|
211
|
+
// Codex: --config-dir > CODEX_HOME > ~/.codex
|
|
212
|
+
if (explicitDir) {
|
|
213
|
+
return expandTilde(explicitDir);
|
|
214
|
+
}
|
|
215
|
+
if (process.env.CODEX_HOME) {
|
|
216
|
+
return expandTilde(process.env.CODEX_HOME);
|
|
217
|
+
}
|
|
218
|
+
return path.join(os.homedir(), '.codex');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (runtime === 'copilot') {
|
|
222
|
+
// Copilot: --config-dir > COPILOT_CONFIG_DIR > ~/.copilot
|
|
223
|
+
if (explicitDir) {
|
|
224
|
+
return expandTilde(explicitDir);
|
|
225
|
+
}
|
|
226
|
+
if (process.env.COPILOT_CONFIG_DIR) {
|
|
227
|
+
return expandTilde(process.env.COPILOT_CONFIG_DIR);
|
|
228
|
+
}
|
|
229
|
+
return path.join(os.homedir(), '.copilot');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (runtime === 'antigravity') {
|
|
233
|
+
// Antigravity: --config-dir > ANTIGRAVITY_CONFIG_DIR > ~/.gemini/antigravity
|
|
234
|
+
if (explicitDir) {
|
|
235
|
+
return expandTilde(explicitDir);
|
|
236
|
+
}
|
|
237
|
+
if (process.env.ANTIGRAVITY_CONFIG_DIR) {
|
|
238
|
+
return expandTilde(process.env.ANTIGRAVITY_CONFIG_DIR);
|
|
239
|
+
}
|
|
240
|
+
return path.join(os.homedir(), '.gemini', 'antigravity');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Claude Code: --config-dir > CLAUDE_CONFIG_DIR > ~/.claude
|
|
244
|
+
if (explicitDir) {
|
|
245
|
+
return expandTilde(explicitDir);
|
|
246
|
+
}
|
|
247
|
+
if (process.env.CLAUDE_CONFIG_DIR) {
|
|
248
|
+
return expandTilde(process.env.CLAUDE_CONFIG_DIR);
|
|
249
|
+
}
|
|
250
|
+
return path.join(os.homedir(), '.claude');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const banner = '\n' +
|
|
254
|
+
cyan + ' ██████╗ ███████╗██████╗\n' +
|
|
255
|
+
' ██╔════╝ ██╔════╝██╔══██╗\n' +
|
|
256
|
+
' ██║ ███╗███████╗██║ ██║\n' +
|
|
257
|
+
' ██║ ██║╚════██║██║ ██║\n' +
|
|
258
|
+
' ╚██████╔╝███████║██████╔╝\n' +
|
|
259
|
+
' ╚═════╝ ╚══════╝╚═════╝' + reset + '\n' +
|
|
260
|
+
'\n' +
|
|
261
|
+
' Vector ' + dim + 'v' + pkg.version + reset + '\n' +
|
|
262
|
+
' A meta-prompting, context engineering and spec-driven\n' +
|
|
263
|
+
' development system for Claude Code, OpenCode, Gemini, Codex, Copilot, and Antigravity by TÂCHES.\n';
|
|
264
|
+
|
|
265
|
+
// Parse --config-dir argument
|
|
266
|
+
function parseConfigDirArg() {
|
|
267
|
+
const configDirIndex = args.findIndex(arg => arg === '--config-dir' || arg === '-c');
|
|
268
|
+
if (configDirIndex !== -1) {
|
|
269
|
+
const nextArg = args[configDirIndex + 1];
|
|
270
|
+
// Error if --config-dir is provided without a value or next arg is another flag
|
|
271
|
+
if (!nextArg || nextArg.startsWith('-')) {
|
|
272
|
+
console.error(` ${yellow}--config-dir requires a path argument${reset}`);
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
return nextArg;
|
|
276
|
+
}
|
|
277
|
+
// Also handle --config-dir=value format
|
|
278
|
+
const configDirArg = args.find(arg => arg.startsWith('--config-dir=') || arg.startsWith('-c='));
|
|
279
|
+
if (configDirArg) {
|
|
280
|
+
const value = configDirArg.split('=')[1];
|
|
281
|
+
if (!value) {
|
|
282
|
+
console.error(` ${yellow}--config-dir requires a non-empty path${reset}`);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
return value;
|
|
286
|
+
}
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
const explicitConfigDir = parseConfigDirArg();
|
|
290
|
+
const hasHelp = args.includes('--help') || args.includes('-h');
|
|
291
|
+
const forceStatusline = args.includes('--force-statusline');
|
|
292
|
+
|
|
293
|
+
console.log(banner);
|
|
294
|
+
|
|
295
|
+
if (hasUninstall) {
|
|
296
|
+
console.log(' Mode: Uninstall\n');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Show help if requested
|
|
300
|
+
if (hasHelp) {
|
|
301
|
+
console.log(` ${yellow}Usage:${reset} npx vector [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall Vector (remove all Vector files)\n ${cyan}-c, --config-dir <path>${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx vector\n\n ${dim}# Install for Claude Code globally${reset}\n npx vector --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx vector --gemini --global\n\n ${dim}# Install for Codex globally${reset}\n npx vector --codex --global\n\n ${dim}# Install for Copilot globally${reset}\n npx vector --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx vector --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx vector --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx vector --antigravity --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx vector --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx vector --codex --global --config-dir ~/.codex-work\n\n ${dim}# Install to current project only${reset}\n npx vector --claude --local\n\n ${dim}# Uninstall Vector from Codex globally${reset}\n npx vector --codex --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / GEMINI_CONFIG_DIR / CODEX_HOME / COPILOT_CONFIG_DIR / ANTIGRAVITY_CONFIG_DIR environment variables.\n`);
|
|
302
|
+
process.exit(0);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Expand ~ to home directory (shell doesn't expand in env vars passed to node)
|
|
307
|
+
*/
|
|
308
|
+
function expandTilde(filePath: string): string {
|
|
309
|
+
if (filePath && filePath.startsWith('~/')) {
|
|
310
|
+
return path.join(os.homedir(), filePath.slice(2));
|
|
311
|
+
}
|
|
312
|
+
return filePath;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Build a hook command path using forward slashes for cross-platform compatibility.
|
|
317
|
+
* On Windows, $HOME is not expanded by cmd.exe/PowerShell, so we use the actual path.
|
|
318
|
+
*/
|
|
319
|
+
function buildHookCommand(configDir: string, hookName: string): string {
|
|
320
|
+
// Use forward slashes for Node.js compatibility on all platforms
|
|
321
|
+
const hooksPath = configDir.replace(/\\/g, '/') + '/hooks/' + hookName;
|
|
322
|
+
return `node "${hooksPath}"`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Resolve the opencode config file path, preferring .jsonc if it exists.
|
|
327
|
+
*/
|
|
328
|
+
function resolveOpencodeConfigPath(configDir: string): string {
|
|
329
|
+
const jsoncPath = path.join(configDir, 'opencode.jsonc');
|
|
330
|
+
if (fs.existsSync(jsoncPath)) {
|
|
331
|
+
return jsoncPath;
|
|
332
|
+
}
|
|
333
|
+
return path.join(configDir, 'opencode.json');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Read and parse settings.json, returning empty object if it doesn't exist
|
|
338
|
+
*/
|
|
339
|
+
function readSettings(settingsPath: string): Record<string, unknown> {
|
|
340
|
+
if (fs.existsSync(settingsPath)) {
|
|
341
|
+
try {
|
|
342
|
+
return JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
343
|
+
} catch (e) {
|
|
344
|
+
return {};
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return {};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Write settings.json with proper formatting
|
|
352
|
+
*/
|
|
353
|
+
function writeSettings(settingsPath: string, settings: Record<string, unknown>): void {
|
|
354
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Cache for attribution settings (populated once per runtime during install)
|
|
358
|
+
const attributionCache = new Map();
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Get commit attribution setting for a runtime
|
|
362
|
+
* @param {string} runtime - 'claude', 'opencode', 'gemini', 'codex', or 'copilot'
|
|
363
|
+
* @returns {null|undefined|string} null = remove, undefined = keep default, string = custom
|
|
364
|
+
*/
|
|
365
|
+
function getCommitAttribution(runtime: Runtime): Attribution {
|
|
366
|
+
// Return cached value if available
|
|
367
|
+
if (attributionCache.has(runtime)) {
|
|
368
|
+
return attributionCache.get(runtime);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
let result;
|
|
372
|
+
|
|
373
|
+
if (runtime === 'opencode') {
|
|
374
|
+
const config = readSettings(resolveOpencodeConfigPath(getGlobalDir('opencode', null as string | null)));
|
|
375
|
+
result = config.disable_ai_attribution === true ? null : undefined;
|
|
376
|
+
} else if (runtime === 'gemini') {
|
|
377
|
+
// Gemini: check gemini settings.json for attribution config
|
|
378
|
+
const settings = readSettings(path.join(getGlobalDir('gemini', explicitConfigDir), 'settings.json'));
|
|
379
|
+
const attribution = settings.attribution as Record<string, unknown> | undefined;
|
|
380
|
+
if (!attribution || attribution.commit === undefined) {
|
|
381
|
+
result = undefined;
|
|
382
|
+
} else if (attribution.commit === '') {
|
|
383
|
+
result = null;
|
|
384
|
+
} else {
|
|
385
|
+
result = attribution.commit as string;
|
|
386
|
+
}
|
|
387
|
+
} else if (runtime === 'claude') {
|
|
388
|
+
// Claude Code
|
|
389
|
+
const settings = readSettings(path.join(getGlobalDir('claude', explicitConfigDir), 'settings.json'));
|
|
390
|
+
const attribution = settings.attribution as Record<string, unknown> | undefined;
|
|
391
|
+
if (!attribution || attribution.commit === undefined) {
|
|
392
|
+
result = undefined;
|
|
393
|
+
} else if (attribution.commit === '') {
|
|
394
|
+
result = null;
|
|
395
|
+
} else {
|
|
396
|
+
result = attribution.commit as string;
|
|
397
|
+
}
|
|
398
|
+
} else {
|
|
399
|
+
// Codex and Copilot currently have no attribution setting equivalent
|
|
400
|
+
result = undefined;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Cache and return
|
|
404
|
+
attributionCache.set(runtime, result);
|
|
405
|
+
return result;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Process Co-Authored-By lines based on attribution setting
|
|
410
|
+
* @param {string} content - File content to process
|
|
411
|
+
* @param {null|undefined|string} attribution - null=remove, undefined=keep, string=replace
|
|
412
|
+
* @returns {string} Processed content
|
|
413
|
+
*/
|
|
414
|
+
function processAttribution(content: string, attribution: Attribution): string {
|
|
415
|
+
if (attribution === null) {
|
|
416
|
+
// Remove Co-Authored-By lines and the preceding blank line
|
|
417
|
+
return content.replace(/(\r?\n){2}Co-Authored-By:.*$/gim, '');
|
|
418
|
+
}
|
|
419
|
+
if (attribution === undefined) {
|
|
420
|
+
return content;
|
|
421
|
+
}
|
|
422
|
+
// Replace with custom attribution (escape $ to prevent backreference injection)
|
|
423
|
+
const safeAttribution = attribution.replace(/\$/g, '$$$$');
|
|
424
|
+
return content.replace(/Co-Authored-By:.*$/gim, `Co-Authored-By: ${safeAttribution}`);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Convert Claude Code frontmatter to opencode format
|
|
429
|
+
* - Converts 'allowed-tools:' array to 'permission:' object
|
|
430
|
+
* @param {string} content - Markdown file content with YAML frontmatter
|
|
431
|
+
* @returns {string} - Content with converted frontmatter
|
|
432
|
+
*/
|
|
433
|
+
// Color name to hex mapping for opencode compatibility
|
|
434
|
+
const colorNameToHex = {
|
|
435
|
+
cyan: '#00FFFF',
|
|
436
|
+
red: '#FF0000',
|
|
437
|
+
green: '#00FF00',
|
|
438
|
+
blue: '#0000FF',
|
|
439
|
+
yellow: '#FFFF00',
|
|
440
|
+
magenta: '#FF00FF',
|
|
441
|
+
orange: '#FFA500',
|
|
442
|
+
purple: '#800080',
|
|
443
|
+
pink: '#FFC0CB',
|
|
444
|
+
white: '#FFFFFF',
|
|
445
|
+
black: '#000000',
|
|
446
|
+
gray: '#808080',
|
|
447
|
+
grey: '#808080',
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
// Tool name mapping from Claude Code to OpenCode
|
|
451
|
+
// OpenCode uses lowercase tool names; special mappings for renamed tools
|
|
452
|
+
const claudeToOpencodeTools = {
|
|
453
|
+
AskUserQuestion: 'question',
|
|
454
|
+
SlashCommand: 'skill',
|
|
455
|
+
TodoWrite: 'todowrite',
|
|
456
|
+
WebFetch: 'webfetch',
|
|
457
|
+
WebSearch: 'websearch', // Plugin/MCP - keep for compatibility
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
// Tool name mapping from Claude Code to Gemini CLI
|
|
461
|
+
// Gemini CLI uses snake_case built-in tool names
|
|
462
|
+
const claudeToGeminiTools = {
|
|
463
|
+
Read: 'read_file',
|
|
464
|
+
Write: 'write_file',
|
|
465
|
+
Edit: 'replace',
|
|
466
|
+
Bash: 'run_shell_command',
|
|
467
|
+
Glob: 'glob',
|
|
468
|
+
Grep: 'search_file_content',
|
|
469
|
+
WebSearch: 'google_web_search',
|
|
470
|
+
WebFetch: 'web_fetch',
|
|
471
|
+
TodoWrite: 'write_todos',
|
|
472
|
+
AskUserQuestion: 'ask_user',
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Convert a Claude Code tool name to OpenCode format
|
|
477
|
+
* - Applies special mappings (AskUserQuestion -> question, etc.)
|
|
478
|
+
* - Converts to lowercase (except MCP tools which keep their format)
|
|
479
|
+
*/
|
|
480
|
+
function convertToolName(claudeTool: string): string {
|
|
481
|
+
// Check for special mapping first
|
|
482
|
+
if ((claudeToOpencodeTools as Record<string, string>)[claudeTool]) {
|
|
483
|
+
return (claudeToOpencodeTools as Record<string, string>)[claudeTool];
|
|
484
|
+
}
|
|
485
|
+
// MCP tools (mcp__*) keep their format
|
|
486
|
+
if (claudeTool.startsWith('mcp__')) {
|
|
487
|
+
return claudeTool;
|
|
488
|
+
}
|
|
489
|
+
// Default: convert to lowercase
|
|
490
|
+
return claudeTool.toLowerCase();
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Convert a Claude Code tool name to Gemini CLI format
|
|
495
|
+
* - Applies Claude→Gemini mapping (Read→read_file, Bash→run_shell_command, etc.)
|
|
496
|
+
* - Filters out MCP tools (mcp__*) — they are auto-discovered at runtime in Gemini
|
|
497
|
+
* - Filters out Task — agents are auto-registered as tools in Gemini
|
|
498
|
+
* @returns {string|null} Gemini tool name, or null if tool should be excluded
|
|
499
|
+
*/
|
|
500
|
+
function convertGeminiToolName(claudeTool: string): string | null {
|
|
501
|
+
// MCP tools: exclude — auto-discovered from mcpServers config at runtime
|
|
502
|
+
if (claudeTool.startsWith('mcp__')) {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
// Task: exclude — agents are auto-registered as callable tools
|
|
506
|
+
if (claudeTool === 'Task') {
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
// Check for explicit mapping
|
|
510
|
+
if ((claudeToGeminiTools as Record<string, string>)[claudeTool]) {
|
|
511
|
+
return (claudeToGeminiTools as Record<string, string>)[claudeTool];
|
|
512
|
+
}
|
|
513
|
+
// Default: lowercase
|
|
514
|
+
return claudeTool.toLowerCase();
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Convert a Claude Code tool name to GitHub Copilot format.
|
|
519
|
+
* - Applies explicit mapping from claudeToCopilotTools
|
|
520
|
+
* - Handles mcp__context7__* prefix → io.github.upstash/context7/*
|
|
521
|
+
* - Falls back to lowercase for unknown tools
|
|
522
|
+
*/
|
|
523
|
+
function convertCopilotToolName(claudeTool: string): string {
|
|
524
|
+
// mcp__context7__* wildcard → io.github.upstash/context7/*
|
|
525
|
+
if (claudeTool.startsWith('mcp__context7__')) {
|
|
526
|
+
return 'io.github.upstash/context7/' + claudeTool.slice('mcp__context7__'.length);
|
|
527
|
+
}
|
|
528
|
+
// Check explicit mapping
|
|
529
|
+
if ((claudeToCopilotTools as Record<string, string>)[claudeTool]) {
|
|
530
|
+
return (claudeToCopilotTools as Record<string, string>)[claudeTool];
|
|
531
|
+
}
|
|
532
|
+
// Default: lowercase
|
|
533
|
+
return claudeTool.toLowerCase();
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Apply Copilot-specific content conversion — CONV-06 (paths) + CONV-07 (command names).
|
|
538
|
+
* Path mappings depend on install mode:
|
|
539
|
+
* Global: ~/.claude/ → ~/.copilot/, ./.claude/ → ./.github/
|
|
540
|
+
* Local: ~/.claude/ → ./.github/, ./.claude/ → ./.github/
|
|
541
|
+
* Applied to ALL Copilot content (skills, agents, engine files).
|
|
542
|
+
* @param {string} content - Source content to convert
|
|
543
|
+
* @param {boolean} [isGlobal=false] - Whether this is a global install
|
|
544
|
+
*/
|
|
545
|
+
function convertClaudeToCopilotContent(content: string, isGlobal = false) {
|
|
546
|
+
let c = content;
|
|
547
|
+
// CONV-06: Path replacement — most specific first to avoid substring matches
|
|
548
|
+
if (isGlobal) {
|
|
549
|
+
c = c.replace(/\$HOME\/\.claude\//g, '$HOME/.copilot/');
|
|
550
|
+
c = c.replace(/~\/\.claude\//g, '~/.copilot/');
|
|
551
|
+
} else {
|
|
552
|
+
c = c.replace(/\$HOME\/\.claude\//g, '.github/');
|
|
553
|
+
c = c.replace(/~\/\.claude\//g, '.github/');
|
|
554
|
+
}
|
|
555
|
+
c = c.replace(/\.\/\.claude\//g, './.github/');
|
|
556
|
+
c = c.replace(/\.claude\//g, '.github/');
|
|
557
|
+
// CONV-07: Command name conversion (all vector: references → vector-)
|
|
558
|
+
c = c.replace(/vector:/g, 'vector-');
|
|
559
|
+
return c;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Convert a Claude command (.md) to a Copilot skill (SKILL.md).
|
|
564
|
+
* Transforms frontmatter only — body passes through with CONV-06/07 applied.
|
|
565
|
+
* Skills keep original tool names (no mapping) per CONTEXT.md decision.
|
|
566
|
+
*/
|
|
567
|
+
function convertClaudeCommandToCopilotSkill(content: string, skillName: string, isGlobal = false) {
|
|
568
|
+
const converted = convertClaudeToCopilotContent(content, isGlobal);
|
|
569
|
+
const { frontmatter, body } = extractFrontmatterAndBody(converted);
|
|
570
|
+
if (!frontmatter) return converted;
|
|
571
|
+
|
|
572
|
+
const description = extractFrontmatterField(frontmatter, 'description') || '';
|
|
573
|
+
const argumentHint = extractFrontmatterField(frontmatter, 'argument-hint');
|
|
574
|
+
const agent = extractFrontmatterField(frontmatter, 'agent');
|
|
575
|
+
|
|
576
|
+
// CONV-02: Extract allowed-tools YAML multiline list → comma-separated string
|
|
577
|
+
const toolsMatch = frontmatter.match(/^allowed-tools:\s*\n((?:\s+-\s+.+\n?)*)/m);
|
|
578
|
+
let toolsLine = '';
|
|
579
|
+
if (toolsMatch) {
|
|
580
|
+
const tools = toolsMatch[1].match(/^\s+-\s+(.+)/gm);
|
|
581
|
+
if (tools) {
|
|
582
|
+
toolsLine = tools.map((t: string) => t.replace(/^\s+-\s+/, '').trim()).join(', ');
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Reconstruct frontmatter in Copilot format
|
|
587
|
+
let fm = `---\nname: ${skillName}\ndescription: ${description}\n`;
|
|
588
|
+
if (argumentHint) fm += `argument-hint: ${yamlQuote(argumentHint)}\n`;
|
|
589
|
+
if (agent) fm += `agent: ${agent}\n`;
|
|
590
|
+
if (toolsLine) fm += `allowed-tools: ${toolsLine}\n`;
|
|
591
|
+
fm += '---';
|
|
592
|
+
|
|
593
|
+
return `${fm}\n${body}`;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Convert a Claude agent (.md) to a Copilot agent (.agent.md).
|
|
598
|
+
* Applies tool mapping + deduplication, formats tools as JSON array.
|
|
599
|
+
* CONV-04: JSON array format. CONV-05: Tool name mapping.
|
|
600
|
+
*/
|
|
601
|
+
function convertClaudeAgentToCopilotAgent(content: string, isGlobal = false) {
|
|
602
|
+
const converted = convertClaudeToCopilotContent(content, isGlobal);
|
|
603
|
+
const { frontmatter, body } = extractFrontmatterAndBody(converted);
|
|
604
|
+
if (!frontmatter) return converted;
|
|
605
|
+
|
|
606
|
+
const name = extractFrontmatterField(frontmatter, 'name') || 'unknown';
|
|
607
|
+
const description = extractFrontmatterField(frontmatter, 'description') || '';
|
|
608
|
+
const color = extractFrontmatterField(frontmatter, 'color');
|
|
609
|
+
const toolsRaw = extractFrontmatterField(frontmatter, 'tools') || '';
|
|
610
|
+
|
|
611
|
+
// CONV-04 + CONV-05: Map tools, deduplicate, format as JSON array
|
|
612
|
+
const claudeTools = toolsRaw.split(',').map((t: string) => t.trim()).filter(Boolean);
|
|
613
|
+
const mappedTools = claudeTools.map((t: string) => convertCopilotToolName(t));
|
|
614
|
+
const uniqueTools = [...new Set(mappedTools)];
|
|
615
|
+
const toolsArray = uniqueTools.length > 0
|
|
616
|
+
? "['" + uniqueTools.join("', '") + "']"
|
|
617
|
+
: '[]';
|
|
618
|
+
|
|
619
|
+
// Reconstruct frontmatter in Copilot format
|
|
620
|
+
let fm = `---\nname: ${name}\ndescription: ${description}\ntools: ${toolsArray}\n`;
|
|
621
|
+
if (color) fm += `color: ${color}\n`;
|
|
622
|
+
fm += '---';
|
|
623
|
+
|
|
624
|
+
return `${fm}\n${body}`;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Apply Antigravity-specific content conversion — path replacement + command name conversion.
|
|
629
|
+
* Path mappings depend on install mode:
|
|
630
|
+
* Global: ~/.claude/ → ~/.gemini/antigravity/, ./.claude/ → ./.agent/
|
|
631
|
+
* Local: ~/.claude/ → .agent/, ./.claude/ → ./.agent/
|
|
632
|
+
* Applied to ALL Antigravity content (skills, agents, engine files).
|
|
633
|
+
* @param {string} content - Source content to convert
|
|
634
|
+
* @param {boolean} [isGlobal=false] - Whether this is a global install
|
|
635
|
+
*/
|
|
636
|
+
function convertClaudeToAntigravityContent(content: string, isGlobal = false) {
|
|
637
|
+
let c = content;
|
|
638
|
+
if (isGlobal) {
|
|
639
|
+
c = c.replace(/\$HOME\/\.claude\//g, '$HOME/.gemini/antigravity/');
|
|
640
|
+
c = c.replace(/~\/\.claude\//g, '~/.gemini/antigravity/');
|
|
641
|
+
} else {
|
|
642
|
+
c = c.replace(/\$HOME\/\.claude\//g, '.agent/');
|
|
643
|
+
c = c.replace(/~\/\.claude\//g, '.agent/');
|
|
644
|
+
}
|
|
645
|
+
c = c.replace(/\.\/\.claude\//g, './.agent/');
|
|
646
|
+
c = c.replace(/\.claude\//g, '.agent/');
|
|
647
|
+
// Command name conversion (all vector: references → vector-)
|
|
648
|
+
c = c.replace(/vector:/g, 'vector-');
|
|
649
|
+
return c;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Convert a Claude command (.md) to an Antigravity skill (SKILL.md).
|
|
654
|
+
* Transforms frontmatter to minimal name + description only.
|
|
655
|
+
* Body passes through with path/command conversions applied.
|
|
656
|
+
*/
|
|
657
|
+
function convertClaudeCommandToAntigravitySkill(content: string, skillName: string, isGlobal = false) {
|
|
658
|
+
const converted = convertClaudeToAntigravityContent(content, isGlobal);
|
|
659
|
+
const { frontmatter, body } = extractFrontmatterAndBody(converted);
|
|
660
|
+
if (!frontmatter) return converted;
|
|
661
|
+
|
|
662
|
+
const name = skillName || extractFrontmatterField(frontmatter, 'name') || 'unknown';
|
|
663
|
+
const description = extractFrontmatterField(frontmatter, 'description') || '';
|
|
664
|
+
|
|
665
|
+
const fm = `---\nname: ${name}\ndescription: ${description}\n---`;
|
|
666
|
+
return `${fm}\n${body}`;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Convert a Claude agent (.md) to an Antigravity agent.
|
|
671
|
+
* Uses Gemini tool names since Antigravity runs on Gemini 3 backend.
|
|
672
|
+
*/
|
|
673
|
+
function convertClaudeAgentToAntigravityAgent(content: string, isGlobal = false) {
|
|
674
|
+
const converted = convertClaudeToAntigravityContent(content, isGlobal);
|
|
675
|
+
const { frontmatter, body } = extractFrontmatterAndBody(converted);
|
|
676
|
+
if (!frontmatter) return converted;
|
|
677
|
+
|
|
678
|
+
const name = extractFrontmatterField(frontmatter, 'name') || 'unknown';
|
|
679
|
+
const description = extractFrontmatterField(frontmatter, 'description') || '';
|
|
680
|
+
const color = extractFrontmatterField(frontmatter, 'color');
|
|
681
|
+
const toolsRaw = extractFrontmatterField(frontmatter, 'tools') || '';
|
|
682
|
+
|
|
683
|
+
// Map tools to Gemini equivalents (reuse existing convertGeminiToolName)
|
|
684
|
+
const claudeTools = toolsRaw.split(',').map((t: string) => t.trim()).filter(Boolean);
|
|
685
|
+
const mappedTools = claudeTools.map((t: string) => convertGeminiToolName(t)).filter(Boolean);
|
|
686
|
+
|
|
687
|
+
let fm = `---\nname: ${name}\ndescription: ${description}\ntools: ${mappedTools.join(', ')}\n`;
|
|
688
|
+
if (color) fm += `color: ${color}\n`;
|
|
689
|
+
fm += '---';
|
|
690
|
+
|
|
691
|
+
return `${fm}\n${body}`;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function toSingleLine(value: string): string {
|
|
695
|
+
return value.replace(/\s+/g, ' ').trim();
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function yamlQuote(value: string): string {
|
|
699
|
+
return JSON.stringify(value);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function extractFrontmatterAndBody(content: string) {
|
|
703
|
+
if (!content.startsWith('---')) {
|
|
704
|
+
return { frontmatter: null, body: content };
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const endIndex = content.indexOf('---', 3);
|
|
708
|
+
if (endIndex === -1) {
|
|
709
|
+
return { frontmatter: null, body: content };
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return {
|
|
713
|
+
frontmatter: content.substring(3, endIndex).trim(),
|
|
714
|
+
body: content.substring(endIndex + 3),
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function extractFrontmatterField(frontmatter: string, fieldName: string) {
|
|
719
|
+
const regex = new RegExp(`^${fieldName}:\\s*(.+)$`, 'm');
|
|
720
|
+
const match = frontmatter.match(regex);
|
|
721
|
+
if (!match) return null;
|
|
722
|
+
return match[1].trim().replace(/^['"]|['"]$/g, '');
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function convertSlashCommandsToCodexSkillMentions(content: string) {
|
|
726
|
+
let converted = content.replace(/\/vector:([a-z0-9-]+)/gi, (_: string, commandName: string) => {
|
|
727
|
+
return `$vector-${String(commandName).toLowerCase()}`;
|
|
728
|
+
});
|
|
729
|
+
converted = converted.replace(/\/vector-help\b/g, '$vector-help');
|
|
730
|
+
return converted;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function convertClaudeToCodexMarkdown(content: string) {
|
|
734
|
+
let converted = convertSlashCommandsToCodexSkillMentions(content);
|
|
735
|
+
converted = converted.replace(/\$ARGUMENTS\b/g, '{{VECTOR_ARGS}}');
|
|
736
|
+
return converted;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function getCodexSkillAdapterHeader(skillName: string) {
|
|
740
|
+
const invocation = `$${skillName}`;
|
|
741
|
+
return `<codex_skill_adapter>
|
|
742
|
+
## A. Skill Invocation
|
|
743
|
+
- This skill is invoked by mentioning \`${invocation}\`.
|
|
744
|
+
- Treat all user text after \`${invocation}\` as \`{{VECTOR_ARGS}}\`.
|
|
745
|
+
- If no arguments are present, treat \`{{VECTOR_ARGS}}\` as empty.
|
|
746
|
+
|
|
747
|
+
## B. AskUserQuestion → request_user_input Mapping
|
|
748
|
+
Vector workflows use \`AskUserQuestion\` (Claude Code syntax). Translate to Codex \`request_user_input\`:
|
|
749
|
+
|
|
750
|
+
Parameter mapping:
|
|
751
|
+
- \`header\` → \`header\`
|
|
752
|
+
- \`question\` → \`question\`
|
|
753
|
+
- Options formatted as \`"Label" — description\` → \`{label: "Label", description: "description"}\`
|
|
754
|
+
- Generate \`id\` from header: lowercase, replace spaces with underscores
|
|
755
|
+
|
|
756
|
+
Batched calls:
|
|
757
|
+
- \`AskUserQuestion([q1, q2])\` → single \`request_user_input\` with multiple entries in \`questions[]\`
|
|
758
|
+
|
|
759
|
+
Multi-select workaround:
|
|
760
|
+
- Codex has no \`multiSelect\`. Use sequential single-selects, or present a numbered freeform list asking the user to enter comma-separated numbers.
|
|
761
|
+
|
|
762
|
+
Execute mode fallback:
|
|
763
|
+
- When \`request_user_input\` is rejected (Execute mode), present a plain-text numbered list and pick a reasonable default.
|
|
764
|
+
|
|
765
|
+
## C. Task() → spawn_agent Mapping
|
|
766
|
+
Vector workflows use \`Task(...)\` (Claude Code syntax). Translate to Codex collaboration tools:
|
|
767
|
+
|
|
768
|
+
Direct mapping:
|
|
769
|
+
- \`Task(subagent_type="X", prompt="Y")\` → \`spawn_agent(agent_type="X", message="Y")\`
|
|
770
|
+
- \`Task(model="...")\` → omit (Codex uses per-role config, not inline model selection)
|
|
771
|
+
- \`fork_context: false\` by default — Vector agents load their own context via \`<files_to_read>\` blocks
|
|
772
|
+
|
|
773
|
+
Parallel fan-out:
|
|
774
|
+
- Spawn multiple agents → collect agent IDs → \`wait(ids)\` for all to complete
|
|
775
|
+
|
|
776
|
+
Result parsing:
|
|
777
|
+
- Look for structured markers in agent output: \`CHECKPOINT\`, \`PLAN COMPLETE\`, \`SUMMARY\`, etc.
|
|
778
|
+
- \`close_agent(id)\` after collecting results from each agent
|
|
779
|
+
</codex_skill_adapter>`;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function convertClaudeCommandToCodexSkill(content: string, skillName: string) {
|
|
783
|
+
const converted = convertClaudeToCodexMarkdown(content);
|
|
784
|
+
const { frontmatter, body } = extractFrontmatterAndBody(converted);
|
|
785
|
+
let description = `Run Vector workflow ${skillName}.`;
|
|
786
|
+
if (frontmatter) {
|
|
787
|
+
const maybeDescription = extractFrontmatterField(frontmatter, 'description');
|
|
788
|
+
if (maybeDescription) {
|
|
789
|
+
description = maybeDescription;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
description = toSingleLine(description);
|
|
793
|
+
const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
|
|
794
|
+
const adapter = getCodexSkillAdapterHeader(skillName);
|
|
795
|
+
|
|
796
|
+
return `---\nname: ${yamlQuote(skillName)}\ndescription: ${yamlQuote(description)}\nmetadata:\n short-description: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Convert Claude Code agent markdown to Codex agent format.
|
|
801
|
+
* Applies base markdown conversions, then adds a <codex_agent_role> header
|
|
802
|
+
* and cleans up frontmatter (removes tools/color fields).
|
|
803
|
+
*/
|
|
804
|
+
function convertClaudeAgentToCodexAgent(content: string) {
|
|
805
|
+
let converted = convertClaudeToCodexMarkdown(content);
|
|
806
|
+
|
|
807
|
+
const { frontmatter, body } = extractFrontmatterAndBody(converted);
|
|
808
|
+
if (!frontmatter) return converted;
|
|
809
|
+
|
|
810
|
+
const name = extractFrontmatterField(frontmatter, 'name') || 'unknown';
|
|
811
|
+
const description = extractFrontmatterField(frontmatter, 'description') || '';
|
|
812
|
+
const tools = extractFrontmatterField(frontmatter, 'tools') || '';
|
|
813
|
+
|
|
814
|
+
const roleHeader = `<codex_agent_role>
|
|
815
|
+
role: ${name}
|
|
816
|
+
tools: ${tools}
|
|
817
|
+
purpose: ${toSingleLine(description)}
|
|
818
|
+
</codex_agent_role>`;
|
|
819
|
+
|
|
820
|
+
const cleanFrontmatter = `---\nname: ${yamlQuote(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`;
|
|
821
|
+
|
|
822
|
+
return `${cleanFrontmatter}\n\n${roleHeader}\n${body}`;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Generate a per-agent .toml config file for Codex.
|
|
827
|
+
* Sets sandbox_mode and developer_instructions from the agent markdown body.
|
|
828
|
+
*/
|
|
829
|
+
function generateCodexAgentToml(agentName: string, agentContent: string) {
|
|
830
|
+
const sandboxMode = (CODEX_AGENT_SANDBOX as Record<string, string>)[agentName] || 'read-only';
|
|
831
|
+
const { body } = extractFrontmatterAndBody(agentContent);
|
|
832
|
+
const instructions = body.trim();
|
|
833
|
+
|
|
834
|
+
const lines = [
|
|
835
|
+
`sandbox_mode = "${sandboxMode}"`,
|
|
836
|
+
// Agent prompts contain raw backslashes in regexes and shell snippets.
|
|
837
|
+
// TOML literal multiline strings preserve them without escape parsing.
|
|
838
|
+
`developer_instructions = '''`,
|
|
839
|
+
instructions,
|
|
840
|
+
`'''`,
|
|
841
|
+
];
|
|
842
|
+
return lines.join('\n') + '\n';
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Generate the Vector config block for Codex config.toml.
|
|
847
|
+
* @param {Array<{name: string, description: string}>} agents
|
|
848
|
+
*/
|
|
849
|
+
function generateCodexConfigBlock(agents: Array<{name: string, description: string}>) {
|
|
850
|
+
const lines = [
|
|
851
|
+
GSD_CODEX_MARKER,
|
|
852
|
+
'',
|
|
853
|
+
];
|
|
854
|
+
|
|
855
|
+
for (const { name, description } of agents) {
|
|
856
|
+
lines.push(`[agents.${name}]`);
|
|
857
|
+
lines.push(`description = ${JSON.stringify(description)}`);
|
|
858
|
+
lines.push(`config_file = "agents/${name}.toml"`);
|
|
859
|
+
lines.push('');
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
return lines.join('\n');
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Strip Vector sections from Codex config.toml content.
|
|
867
|
+
* Returns cleaned content, or null if file would be empty.
|
|
868
|
+
*/
|
|
869
|
+
function stripGsdFromCodexConfig(content: string) {
|
|
870
|
+
const markerIndex = content.indexOf(GSD_CODEX_MARKER);
|
|
871
|
+
|
|
872
|
+
if (markerIndex !== -1) {
|
|
873
|
+
// Has Vector marker — remove everything from marker to EOF
|
|
874
|
+
let before = content.substring(0, markerIndex).trimEnd();
|
|
875
|
+
// Also strip Vector-injected feature keys above the marker (Case 3 inject)
|
|
876
|
+
before = before.replace(/^multi_agent\s*=\s*true\s*\n?/m, '');
|
|
877
|
+
before = before.replace(/^default_mode_request_user_input\s*=\s*true\s*\n?/m, '');
|
|
878
|
+
before = before.replace(/^\[features\]\s*\n(?=\[|$)/m, '');
|
|
879
|
+
before = before.replace(/\n{3,}/g, '\n\n').trim();
|
|
880
|
+
if (!before) return null;
|
|
881
|
+
return before + '\n';
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// No marker but may have Vector-injected feature keys
|
|
885
|
+
let cleaned = content;
|
|
886
|
+
cleaned = cleaned.replace(/^multi_agent\s*=\s*true\s*\n?/m, '');
|
|
887
|
+
cleaned = cleaned.replace(/^default_mode_request_user_input\s*=\s*true\s*\n?/m, '');
|
|
888
|
+
|
|
889
|
+
// Remove [agents.vector-*] sections (from header to next section or EOF)
|
|
890
|
+
cleaned = cleaned.replace(/^\[agents\.vector-[^\]]+\]\n(?:(?!\[)[^\n]*\n?)*/gm, '');
|
|
891
|
+
|
|
892
|
+
// Remove [features] section if now empty (only header, no keys before next section)
|
|
893
|
+
cleaned = cleaned.replace(/^\[features\]\s*\n(?=\[|$)/m, '');
|
|
894
|
+
|
|
895
|
+
// Remove [agents] section if now empty
|
|
896
|
+
cleaned = cleaned.replace(/^\[agents\]\s*\n(?=\[|$)/m, '');
|
|
897
|
+
|
|
898
|
+
// Clean up excessive blank lines
|
|
899
|
+
cleaned = cleaned.replace(/\n{3,}/g, '\n\n').trim();
|
|
900
|
+
|
|
901
|
+
if (!cleaned) return null;
|
|
902
|
+
return cleaned + '\n';
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* Merge Vector config block into an existing or new config.toml.
|
|
907
|
+
* Three cases: new file, existing with Vector marker, existing without marker.
|
|
908
|
+
*/
|
|
909
|
+
function mergeCodexConfig(configPath: string, vectorBlock: string) {
|
|
910
|
+
// Case 1: No config.toml — create fresh
|
|
911
|
+
if (!fs.existsSync(configPath)) {
|
|
912
|
+
fs.writeFileSync(configPath, vectorBlock + '\n');
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const existing = fs.readFileSync(configPath, 'utf8');
|
|
917
|
+
const markerIndex = existing.indexOf(GSD_CODEX_MARKER);
|
|
918
|
+
|
|
919
|
+
// Case 2: Has Vector marker — truncate and re-append
|
|
920
|
+
if (markerIndex !== -1) {
|
|
921
|
+
let before = existing.substring(0, markerIndex).trimEnd();
|
|
922
|
+
if (before) {
|
|
923
|
+
// Strip any Vector-managed sections that leaked above the marker from previous installs
|
|
924
|
+
before = before.replace(/^\[agents\.vector-[^\]]+\]\n(?:(?!\[)[^\n]*\n?)*/gm, '');
|
|
925
|
+
before = before.replace(/^\[agents\]\n(?:(?!\[)[^\n]*\n?)*/m, '');
|
|
926
|
+
before = before.replace(/\n{3,}/g, '\n\n').trimEnd();
|
|
927
|
+
|
|
928
|
+
fs.writeFileSync(configPath, before + '\n\n' + vectorBlock + '\n');
|
|
929
|
+
} else {
|
|
930
|
+
fs.writeFileSync(configPath, vectorBlock + '\n');
|
|
931
|
+
}
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// Case 3: No marker — append Vector block
|
|
936
|
+
let content = existing;
|
|
937
|
+
content = content.trimEnd() + '\n\n' + vectorBlock + '\n';
|
|
938
|
+
|
|
939
|
+
fs.writeFileSync(configPath, content);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Merge Vector instructions into copilot-instructions.md.
|
|
944
|
+
* Three cases: new file, existing with markers, existing without markers.
|
|
945
|
+
* @param {string} filePath - Full path to copilot-instructions.md
|
|
946
|
+
* @param {string} vectorContent - Template content (without markers)
|
|
947
|
+
*/
|
|
948
|
+
function mergeCopilotInstructions(filePath: string, vectorContent: string) {
|
|
949
|
+
const vectorBlock = GSD_COPILOT_INSTRUCTIONS_MARKER + '\n' +
|
|
950
|
+
vectorContent.trim() + '\n' +
|
|
951
|
+
GSD_COPILOT_INSTRUCTIONS_CLOSE_MARKER;
|
|
952
|
+
|
|
953
|
+
// Case 1: No file — create fresh
|
|
954
|
+
if (!fs.existsSync(filePath)) {
|
|
955
|
+
fs.writeFileSync(filePath, vectorBlock + '\n');
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
const existing = fs.readFileSync(filePath, 'utf8');
|
|
960
|
+
const openIndex = existing.indexOf(GSD_COPILOT_INSTRUCTIONS_MARKER);
|
|
961
|
+
const closeIndex = existing.indexOf(GSD_COPILOT_INSTRUCTIONS_CLOSE_MARKER);
|
|
962
|
+
|
|
963
|
+
// Case 2: Has Vector markers — replace between markers
|
|
964
|
+
if (openIndex !== -1 && closeIndex !== -1) {
|
|
965
|
+
const before = existing.substring(0, openIndex).trimEnd();
|
|
966
|
+
const after = existing.substring(closeIndex + GSD_COPILOT_INSTRUCTIONS_CLOSE_MARKER.length).trimStart();
|
|
967
|
+
let newContent = '';
|
|
968
|
+
if (before) newContent += before + '\n\n';
|
|
969
|
+
newContent += vectorBlock;
|
|
970
|
+
if (after) newContent += '\n\n' + after;
|
|
971
|
+
newContent += '\n';
|
|
972
|
+
fs.writeFileSync(filePath, newContent);
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Case 3: No markers — append at end
|
|
977
|
+
const content = existing.trimEnd() + '\n\n' + vectorBlock + '\n';
|
|
978
|
+
fs.writeFileSync(filePath, content);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Strip Vector section from copilot-instructions.md content.
|
|
983
|
+
* Returns cleaned content, or null if file should be deleted (was Vector-only).
|
|
984
|
+
* @param {string} content - File content
|
|
985
|
+
* @returns {string|null} - Cleaned content or null if empty
|
|
986
|
+
*/
|
|
987
|
+
function stripGsdFromCopilotInstructions(content: string) {
|
|
988
|
+
const openIndex = content.indexOf(GSD_COPILOT_INSTRUCTIONS_MARKER);
|
|
989
|
+
const closeIndex = content.indexOf(GSD_COPILOT_INSTRUCTIONS_CLOSE_MARKER);
|
|
990
|
+
|
|
991
|
+
if (openIndex !== -1 && closeIndex !== -1) {
|
|
992
|
+
const before = content.substring(0, openIndex).trimEnd();
|
|
993
|
+
const after = content.substring(closeIndex + GSD_COPILOT_INSTRUCTIONS_CLOSE_MARKER.length).trimStart();
|
|
994
|
+
const cleaned = (before + (before && after ? '\n\n' : '') + after).trim();
|
|
995
|
+
if (!cleaned) return null;
|
|
996
|
+
return cleaned + '\n';
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// No markers found — nothing to strip
|
|
1000
|
+
return content;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* Generate config.toml and per-agent .toml files for Codex.
|
|
1005
|
+
* Reads agent .md files from source, extracts metadata, writes .toml configs.
|
|
1006
|
+
*/
|
|
1007
|
+
function installCodexConfig(targetDir: string, agentsSrc: string) {
|
|
1008
|
+
const configPath = path.join(targetDir, 'config.toml');
|
|
1009
|
+
const agentsTomlDir = path.join(targetDir, 'agents');
|
|
1010
|
+
fs.mkdirSync(agentsTomlDir, { recursive: true });
|
|
1011
|
+
|
|
1012
|
+
const agentEntries = fs.readdirSync(agentsSrc).filter(f => f.startsWith('vector-') && f.endsWith('.md'));
|
|
1013
|
+
const agents = [];
|
|
1014
|
+
|
|
1015
|
+
// Compute the Codex Vector install path (absolute, so subagents with empty $HOME work — #820)
|
|
1016
|
+
const codexGsdPath = `${path.resolve(targetDir, 'core').replace(/\\/g, '/')}/`;
|
|
1017
|
+
|
|
1018
|
+
for (const file of agentEntries) {
|
|
1019
|
+
let content = fs.readFileSync(path.join(agentsSrc, file), 'utf8');
|
|
1020
|
+
// Replace full .claude/core prefix so path resolves to codex Vector install
|
|
1021
|
+
content = content.replace(/~\/\.claude\/core\//g, codexGsdPath);
|
|
1022
|
+
content = content.replace(/\$HOME\/\.claude\/core\//g, codexGsdPath);
|
|
1023
|
+
const { frontmatter } = extractFrontmatterAndBody(content);
|
|
1024
|
+
const name = (frontmatter ? extractFrontmatterField(frontmatter, 'name') : null) || file.replace('.md', '');
|
|
1025
|
+
const description = (frontmatter ? extractFrontmatterField(frontmatter, 'description') : null) || '';
|
|
1026
|
+
|
|
1027
|
+
agents.push({ name, description: toSingleLine(description) });
|
|
1028
|
+
|
|
1029
|
+
const tomlContent = generateCodexAgentToml(name, content);
|
|
1030
|
+
fs.writeFileSync(path.join(agentsTomlDir, `${name}.toml`), tomlContent);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const vectorBlock = generateCodexConfigBlock(agents);
|
|
1034
|
+
mergeCodexConfig(configPath, vectorBlock);
|
|
1035
|
+
|
|
1036
|
+
return agents.length;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* Strip HTML <sub> tags for Gemini CLI output
|
|
1041
|
+
* Terminals don't support subscript — Gemini renders these as raw HTML.
|
|
1042
|
+
* Converts <sub>text</sub> to italic *(text)* for readable terminal output.
|
|
1043
|
+
*/
|
|
1044
|
+
function stripSubTags(content: string) {
|
|
1045
|
+
return content.replace(/<sub>(.*?)<\/sub>/g, '*($1)*');
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Convert Claude Code agent frontmatter to Gemini CLI format
|
|
1050
|
+
* Gemini agents use .md files with YAML frontmatter, same as Claude,
|
|
1051
|
+
* but with different field names and formats:
|
|
1052
|
+
* - tools: must be a YAML array (not comma-separated string)
|
|
1053
|
+
* - tool names: must use Gemini built-in names (read_file, not Read)
|
|
1054
|
+
* - color: must be removed (causes validation error)
|
|
1055
|
+
* - skills: must be removed (causes validation error)
|
|
1056
|
+
* - mcp__* tools: must be excluded (auto-discovered at runtime)
|
|
1057
|
+
*/
|
|
1058
|
+
function convertClaudeToGeminiAgent(content: string) {
|
|
1059
|
+
if (!content.startsWith('---')) return content;
|
|
1060
|
+
|
|
1061
|
+
const endIndex = content.indexOf('---', 3);
|
|
1062
|
+
if (endIndex === -1) return content;
|
|
1063
|
+
|
|
1064
|
+
const frontmatter = content.substring(3, endIndex).trim();
|
|
1065
|
+
const body = content.substring(endIndex + 3);
|
|
1066
|
+
|
|
1067
|
+
const lines = frontmatter.split('\n');
|
|
1068
|
+
const newLines = [];
|
|
1069
|
+
let inAllowedTools = false;
|
|
1070
|
+
let inSkippedArrayField = false;
|
|
1071
|
+
const tools = [];
|
|
1072
|
+
|
|
1073
|
+
for (const line of lines) {
|
|
1074
|
+
const trimmed = line.trim();
|
|
1075
|
+
|
|
1076
|
+
if (inSkippedArrayField) {
|
|
1077
|
+
if (!trimmed || trimmed.startsWith('- ')) {
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
inSkippedArrayField = false;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// Convert allowed-tools YAML array to tools list
|
|
1084
|
+
if (trimmed.startsWith('allowed-tools:')) {
|
|
1085
|
+
inAllowedTools = true;
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// Handle inline tools: field (comma-separated string)
|
|
1090
|
+
if (trimmed.startsWith('tools:')) {
|
|
1091
|
+
const toolsValue = trimmed.substring(6).trim();
|
|
1092
|
+
if (toolsValue) {
|
|
1093
|
+
const parsed = toolsValue.split(',').map((t: string) => t.trim()).filter((t: string) => t);
|
|
1094
|
+
for (const t of parsed) {
|
|
1095
|
+
const mapped = convertGeminiToolName(t);
|
|
1096
|
+
if (mapped) tools.push(mapped);
|
|
1097
|
+
}
|
|
1098
|
+
} else {
|
|
1099
|
+
// tools: with no value means YAML array follows
|
|
1100
|
+
inAllowedTools = true;
|
|
1101
|
+
}
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// Strip color field (not supported by Gemini CLI, causes validation error)
|
|
1106
|
+
if (trimmed.startsWith('color:')) continue;
|
|
1107
|
+
|
|
1108
|
+
// Strip skills field (not supported by Gemini CLI, causes validation error)
|
|
1109
|
+
if (trimmed.startsWith('skills:')) {
|
|
1110
|
+
inSkippedArrayField = true;
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// Collect allowed-tools/tools array items
|
|
1115
|
+
if (inAllowedTools) {
|
|
1116
|
+
if (trimmed.startsWith('- ')) {
|
|
1117
|
+
const mapped = convertGeminiToolName(trimmed.substring(2).trim());
|
|
1118
|
+
if (mapped) tools.push(mapped);
|
|
1119
|
+
continue;
|
|
1120
|
+
} else if (trimmed && !trimmed.startsWith('-')) {
|
|
1121
|
+
inAllowedTools = false;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
if (!inAllowedTools) {
|
|
1126
|
+
newLines.push(line);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// Add tools as YAML array (Gemini requires array format)
|
|
1131
|
+
if (tools.length > 0) {
|
|
1132
|
+
newLines.push('tools:');
|
|
1133
|
+
for (const tool of tools) {
|
|
1134
|
+
newLines.push(` - ${tool}`);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
const newFrontmatter = newLines.join('\n').trim();
|
|
1139
|
+
|
|
1140
|
+
// Escape ${VAR} patterns in agent body for Gemini CLI compatibility.
|
|
1141
|
+
// Gemini's templateString() treats all ${word} patterns as template variables
|
|
1142
|
+
// and throws "Template validation failed: Missing required input parameters"
|
|
1143
|
+
// when they can't be resolved. Vector agents use ${PHASE}, ${PLAN}, etc. as
|
|
1144
|
+
// shell variables in bash code blocks — convert to $VAR (no braces) which
|
|
1145
|
+
// is equivalent bash and invisible to Gemini's /\$\{(\w+)\}/g regex.
|
|
1146
|
+
const escapedBody = body.replace(/\$\{(\w+)\}/g, '$$$1');
|
|
1147
|
+
|
|
1148
|
+
return `---\n${newFrontmatter}\n---${stripSubTags(escapedBody)}`;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function convertClaudeToOpencodeFrontmatter(content: string, { isAgent = false } = {}) {
|
|
1152
|
+
// Replace tool name references in content (applies to all files)
|
|
1153
|
+
let convertedContent = content;
|
|
1154
|
+
convertedContent = convertedContent.replace(/\bAskUserQuestion\b/g, 'question');
|
|
1155
|
+
convertedContent = convertedContent.replace(/\bSlashCommand\b/g, 'skill');
|
|
1156
|
+
convertedContent = convertedContent.replace(/\bTodoWrite\b/g, 'todowrite');
|
|
1157
|
+
// Replace /vector:command with /vector-command for opencode (flat command structure)
|
|
1158
|
+
convertedContent = convertedContent.replace(/\/vector:/g, '/vector-');
|
|
1159
|
+
// Replace ~/.claude and $HOME/.claude with OpenCode's config location
|
|
1160
|
+
convertedContent = convertedContent.replace(/~\/\.claude\b/g, '~/.config/opencode');
|
|
1161
|
+
convertedContent = convertedContent.replace(/\$HOME\/\.claude\b/g, '$HOME/.config/opencode');
|
|
1162
|
+
// Replace general-purpose subagent type with OpenCode's equivalent "general"
|
|
1163
|
+
convertedContent = convertedContent.replace(/subagent_type="general-purpose"/g, 'subagent_type="general"');
|
|
1164
|
+
|
|
1165
|
+
// Check if content has frontmatter
|
|
1166
|
+
if (!convertedContent.startsWith('---')) {
|
|
1167
|
+
return convertedContent;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// Find the end of frontmatter
|
|
1171
|
+
const endIndex = convertedContent.indexOf('---', 3);
|
|
1172
|
+
if (endIndex === -1) {
|
|
1173
|
+
return convertedContent;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
const frontmatter = convertedContent.substring(3, endIndex).trim();
|
|
1177
|
+
const body = convertedContent.substring(endIndex + 3);
|
|
1178
|
+
|
|
1179
|
+
// Parse frontmatter line by line (simple YAML parsing)
|
|
1180
|
+
const lines = frontmatter.split('\n');
|
|
1181
|
+
const newLines = [];
|
|
1182
|
+
let inAllowedTools = false;
|
|
1183
|
+
let inSkippedArray = false;
|
|
1184
|
+
const allowedTools = [];
|
|
1185
|
+
|
|
1186
|
+
for (const line of lines) {
|
|
1187
|
+
const trimmed = line.trim();
|
|
1188
|
+
|
|
1189
|
+
// For agents: skip commented-out lines (e.g. hooks blocks)
|
|
1190
|
+
if (isAgent && trimmed.startsWith('#')) {
|
|
1191
|
+
continue;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// Detect start of allowed-tools array
|
|
1195
|
+
if (trimmed.startsWith('allowed-tools:')) {
|
|
1196
|
+
inAllowedTools = true;
|
|
1197
|
+
continue;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// Detect inline tools: field (comma-separated string)
|
|
1201
|
+
if (trimmed.startsWith('tools:')) {
|
|
1202
|
+
if (isAgent) {
|
|
1203
|
+
// Agents: strip tools entirely (not supported in OpenCode agent frontmatter)
|
|
1204
|
+
inSkippedArray = true;
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1207
|
+
const toolsValue = trimmed.substring(6).trim();
|
|
1208
|
+
if (toolsValue) {
|
|
1209
|
+
// Parse comma-separated tools
|
|
1210
|
+
const tools = toolsValue.split(',').map((t: string) => t.trim()).filter((t: string) => t);
|
|
1211
|
+
allowedTools.push(...tools);
|
|
1212
|
+
}
|
|
1213
|
+
continue;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// For agents: strip skills:, color:, memory:, maxTurns:, permissionMode:, disallowedTools:
|
|
1217
|
+
if (isAgent && /^(skills|color|memory|maxTurns|permissionMode|disallowedTools):/.test(trimmed)) {
|
|
1218
|
+
inSkippedArray = true;
|
|
1219
|
+
continue;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// Skip continuation lines of a stripped array/object field
|
|
1223
|
+
if (inSkippedArray) {
|
|
1224
|
+
if (trimmed.startsWith('- ') || trimmed.startsWith('#') || /^\s/.test(line)) {
|
|
1225
|
+
continue;
|
|
1226
|
+
}
|
|
1227
|
+
inSkippedArray = false;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// For commands: remove name: field (opencode uses filename for command name)
|
|
1231
|
+
// For agents: keep name: (required by OpenCode agents)
|
|
1232
|
+
if (!isAgent && trimmed.startsWith('name:')) {
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// Convert color names to hex for opencode (commands only; agents strip color above)
|
|
1237
|
+
if (trimmed.startsWith('color:')) {
|
|
1238
|
+
const colorValue = trimmed.substring(6).trim().toLowerCase();
|
|
1239
|
+
const hexColor = (colorNameToHex as Record<string, string>)[colorValue];
|
|
1240
|
+
if (hexColor) {
|
|
1241
|
+
newLines.push(`color: "${hexColor}"`);
|
|
1242
|
+
} else if (colorValue.startsWith('#')) {
|
|
1243
|
+
// Validate hex color format (#RGB or #RRGGBB)
|
|
1244
|
+
if (/^#[0-9a-f]{3}$|^#[0-9a-f]{6}$/i.test(colorValue)) {
|
|
1245
|
+
// Already hex and valid, keep as is
|
|
1246
|
+
newLines.push(line);
|
|
1247
|
+
}
|
|
1248
|
+
// Skip invalid hex colors
|
|
1249
|
+
}
|
|
1250
|
+
// Skip unknown color names
|
|
1251
|
+
continue;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// Collect allowed-tools items
|
|
1255
|
+
if (inAllowedTools) {
|
|
1256
|
+
if (trimmed.startsWith('- ')) {
|
|
1257
|
+
allowedTools.push(trimmed.substring(2).trim());
|
|
1258
|
+
continue;
|
|
1259
|
+
} else if (trimmed && !trimmed.startsWith('-')) {
|
|
1260
|
+
// End of array, new field started
|
|
1261
|
+
inAllowedTools = false;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// Keep other fields
|
|
1266
|
+
if (!inAllowedTools) {
|
|
1267
|
+
newLines.push(line);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// For agents: add required OpenCode agent fields
|
|
1272
|
+
if (isAgent) {
|
|
1273
|
+
newLines.push('model: inherit');
|
|
1274
|
+
newLines.push('mode: subagent');
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// For commands: add tools object if we had allowed-tools or tools
|
|
1278
|
+
if (!isAgent && allowedTools.length > 0) {
|
|
1279
|
+
newLines.push('tools:');
|
|
1280
|
+
for (const tool of allowedTools) {
|
|
1281
|
+
newLines.push(` ${convertToolName(tool)}: true`);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
// Rebuild frontmatter (body already has tool names converted)
|
|
1286
|
+
const newFrontmatter = newLines.join('\n').trim();
|
|
1287
|
+
return `---\n${newFrontmatter}\n---${body}`;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
/**
|
|
1291
|
+
* Convert Claude Code markdown command to Gemini TOML format
|
|
1292
|
+
* @param {string} content - Markdown file content with YAML frontmatter
|
|
1293
|
+
* @returns {string} - TOML content
|
|
1294
|
+
*/
|
|
1295
|
+
function convertClaudeToGeminiToml(content: string) {
|
|
1296
|
+
// Check if content has frontmatter
|
|
1297
|
+
if (!content.startsWith('---')) {
|
|
1298
|
+
return `prompt = ${JSON.stringify(content)}\n`;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
const endIndex = content.indexOf('---', 3);
|
|
1302
|
+
if (endIndex === -1) {
|
|
1303
|
+
return `prompt = ${JSON.stringify(content)}\n`;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
const frontmatter = content.substring(3, endIndex).trim();
|
|
1307
|
+
const body = content.substring(endIndex + 3).trim();
|
|
1308
|
+
|
|
1309
|
+
// Extract description from frontmatter
|
|
1310
|
+
let description = '';
|
|
1311
|
+
const lines = frontmatter.split('\n');
|
|
1312
|
+
for (const line of lines) {
|
|
1313
|
+
const trimmed = line.trim();
|
|
1314
|
+
if (trimmed.startsWith('description:')) {
|
|
1315
|
+
description = trimmed.substring(12).trim();
|
|
1316
|
+
break;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// Construct TOML
|
|
1321
|
+
let toml = '';
|
|
1322
|
+
if (description) {
|
|
1323
|
+
toml += `description = ${JSON.stringify(description)}\n`;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
toml += `prompt = ${JSON.stringify(body)}\n`;
|
|
1327
|
+
|
|
1328
|
+
return toml;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
/**
|
|
1332
|
+
* Copy commands to a flat structure for OpenCode
|
|
1333
|
+
* OpenCode expects: command/vector-help.md (invoked as /vector-help)
|
|
1334
|
+
* Source structure: commands/vector/help.md
|
|
1335
|
+
*
|
|
1336
|
+
* @param {string} srcDir - Source directory (e.g., commands/vector/)
|
|
1337
|
+
* @param {string} destDir - Destination directory (e.g., command/)
|
|
1338
|
+
* @param {string} prefix - Prefix for filenames (e.g., 'vector')
|
|
1339
|
+
* @param {string} pathPrefix - Path prefix for file references
|
|
1340
|
+
* @param {string} runtime - Target runtime ('claude' or 'opencode')
|
|
1341
|
+
*/
|
|
1342
|
+
function copyFlattenedCommands(srcDir: string, destDir: string, prefix: string, pathPrefix: string, runtime: Runtime) {
|
|
1343
|
+
if (!fs.existsSync(srcDir)) {
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// Remove old vector-*.md files before copying new ones
|
|
1348
|
+
if (fs.existsSync(destDir)) {
|
|
1349
|
+
for (const file of fs.readdirSync(destDir)) {
|
|
1350
|
+
if (file.startsWith(`${prefix}-`) && file.endsWith('.md')) {
|
|
1351
|
+
fs.unlinkSync(path.join(destDir, file));
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
} else {
|
|
1355
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
1359
|
+
|
|
1360
|
+
for (const entry of entries) {
|
|
1361
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
1362
|
+
|
|
1363
|
+
if (entry.isDirectory()) {
|
|
1364
|
+
// Recurse into subdirectories, adding to prefix
|
|
1365
|
+
// e.g., commands/vector/debug/start.md -> command/vector-debug-start.md
|
|
1366
|
+
copyFlattenedCommands(srcPath, destDir, `${prefix}-${entry.name}`, pathPrefix, runtime);
|
|
1367
|
+
} else if (entry.name.endsWith('.md')) {
|
|
1368
|
+
// Flatten: help.md -> vector-help.md
|
|
1369
|
+
const baseName = entry.name.replace('.md', '');
|
|
1370
|
+
const destName = `${prefix}-${baseName}.md`;
|
|
1371
|
+
const destPath = path.join(destDir, destName);
|
|
1372
|
+
|
|
1373
|
+
let content = fs.readFileSync(srcPath, 'utf8');
|
|
1374
|
+
const globalClaudeRegex = /~\/\.claude\//g;
|
|
1375
|
+
const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
|
|
1376
|
+
const localClaudeRegex = /\.\/\.claude\//g;
|
|
1377
|
+
const opencodeDirRegex = /~\/\.opencode\//g;
|
|
1378
|
+
content = content.replace(globalClaudeRegex, pathPrefix);
|
|
1379
|
+
content = content.replace(globalClaudeHomeRegex, pathPrefix);
|
|
1380
|
+
content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
|
|
1381
|
+
content = content.replace(opencodeDirRegex, pathPrefix);
|
|
1382
|
+
content = processAttribution(content, getCommitAttribution(runtime));
|
|
1383
|
+
content = convertClaudeToOpencodeFrontmatter(content);
|
|
1384
|
+
|
|
1385
|
+
fs.writeFileSync(destPath, content);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
function listCodexSkillNames(skillsDir: string, prefix = 'vector-') {
|
|
1391
|
+
if (!fs.existsSync(skillsDir)) return [];
|
|
1392
|
+
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
1393
|
+
return entries
|
|
1394
|
+
.filter(entry => entry.isDirectory() && entry.name.startsWith(prefix))
|
|
1395
|
+
.filter(entry => fs.existsSync(path.join(skillsDir, entry.name, 'SKILL.md')))
|
|
1396
|
+
.map(entry => entry.name)
|
|
1397
|
+
.sort();
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
function copyCommandsAsCodexSkills(srcDir: string, skillsDir: string, prefix: string, pathPrefix: string, runtime: Runtime) {
|
|
1401
|
+
if (!fs.existsSync(srcDir)) {
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
1406
|
+
|
|
1407
|
+
// Remove previous Vector Codex skills to avoid stale command skills.
|
|
1408
|
+
const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
1409
|
+
for (const entry of existing) {
|
|
1410
|
+
if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
|
|
1411
|
+
fs.rmSync(path.join(skillsDir, entry.name), { recursive: true });
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
function recurse(currentSrcDir: string, currentPrefix: string) {
|
|
1416
|
+
const entries = fs.readdirSync(currentSrcDir, { withFileTypes: true });
|
|
1417
|
+
|
|
1418
|
+
for (const entry of entries) {
|
|
1419
|
+
const srcPath = path.join(currentSrcDir, entry.name);
|
|
1420
|
+
if (entry.isDirectory()) {
|
|
1421
|
+
recurse(srcPath, `${currentPrefix}-${entry.name}`);
|
|
1422
|
+
continue;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
if (!entry.name.endsWith('.md')) {
|
|
1426
|
+
continue;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
const baseName = entry.name.replace('.md', '');
|
|
1430
|
+
const skillName = `${currentPrefix}-${baseName}`;
|
|
1431
|
+
const skillDir = path.join(skillsDir, skillName);
|
|
1432
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
1433
|
+
|
|
1434
|
+
let content = fs.readFileSync(srcPath, 'utf8');
|
|
1435
|
+
const globalClaudeRegex = /~\/\.claude\//g;
|
|
1436
|
+
const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
|
|
1437
|
+
const localClaudeRegex = /\.\/\.claude\//g;
|
|
1438
|
+
const codexDirRegex = /~\/\.codex\//g;
|
|
1439
|
+
content = content.replace(globalClaudeRegex, pathPrefix);
|
|
1440
|
+
content = content.replace(globalClaudeHomeRegex, pathPrefix);
|
|
1441
|
+
content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
|
|
1442
|
+
content = content.replace(codexDirRegex, pathPrefix);
|
|
1443
|
+
content = processAttribution(content, getCommitAttribution(runtime));
|
|
1444
|
+
content = convertClaudeCommandToCodexSkill(content, skillName);
|
|
1445
|
+
|
|
1446
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
recurse(srcDir, prefix);
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
/**
|
|
1454
|
+
* Copy Claude commands as Copilot skills — one folder per skill with SKILL.md.
|
|
1455
|
+
* Applies CONV-01 (structure), CONV-02 (allowed-tools), CONV-06 (paths), CONV-07 (command names).
|
|
1456
|
+
*/
|
|
1457
|
+
function copyCommandsAsCopilotSkills(srcDir: string, skillsDir: string, prefix: string, isGlobal = false) {
|
|
1458
|
+
if (!fs.existsSync(srcDir)) {
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
1463
|
+
|
|
1464
|
+
// Remove previous Vector Copilot skills
|
|
1465
|
+
const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
1466
|
+
for (const entry of existing) {
|
|
1467
|
+
if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
|
|
1468
|
+
fs.rmSync(path.join(skillsDir, entry.name), { recursive: true });
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
function recurse(currentSrcDir: string, currentPrefix: string) {
|
|
1473
|
+
const entries = fs.readdirSync(currentSrcDir, { withFileTypes: true });
|
|
1474
|
+
|
|
1475
|
+
for (const entry of entries) {
|
|
1476
|
+
const srcPath = path.join(currentSrcDir, entry.name);
|
|
1477
|
+
if (entry.isDirectory()) {
|
|
1478
|
+
recurse(srcPath, `${currentPrefix}-${entry.name}`);
|
|
1479
|
+
continue;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
if (!entry.name.endsWith('.md')) {
|
|
1483
|
+
continue;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
const baseName = entry.name.replace('.md', '');
|
|
1487
|
+
const skillName = `${currentPrefix}-${baseName}`;
|
|
1488
|
+
const skillDir = path.join(skillsDir, skillName);
|
|
1489
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
1490
|
+
|
|
1491
|
+
let content = fs.readFileSync(srcPath, 'utf8');
|
|
1492
|
+
content = convertClaudeCommandToCopilotSkill(content, skillName, isGlobal);
|
|
1493
|
+
content = processAttribution(content, getCommitAttribution('copilot'));
|
|
1494
|
+
|
|
1495
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
recurse(srcDir, prefix);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
/**
|
|
1503
|
+
* Recursively install Vector commands as Antigravity skills.
|
|
1504
|
+
* Each command becomes a skill-name/ folder containing SKILL.md.
|
|
1505
|
+
* Mirrors copyCommandsAsCopilotSkills but uses Antigravity converters.
|
|
1506
|
+
* @param {string} srcDir - Source commands directory
|
|
1507
|
+
* @param {string} skillsDir - Target skills directory
|
|
1508
|
+
* @param {string} prefix - Skill name prefix (e.g. 'vector')
|
|
1509
|
+
* @param {boolean} isGlobal - Whether this is a global install
|
|
1510
|
+
*/
|
|
1511
|
+
function copyCommandsAsAntigravitySkills(srcDir: string, skillsDir: string, prefix: string, isGlobal = false) {
|
|
1512
|
+
if (!fs.existsSync(srcDir)) {
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
1517
|
+
|
|
1518
|
+
// Remove previous Vector Antigravity skills
|
|
1519
|
+
const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
1520
|
+
for (const entry of existing) {
|
|
1521
|
+
if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
|
|
1522
|
+
fs.rmSync(path.join(skillsDir, entry.name), { recursive: true });
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
function recurse(currentSrcDir: string, currentPrefix: string) {
|
|
1527
|
+
const entries = fs.readdirSync(currentSrcDir, { withFileTypes: true });
|
|
1528
|
+
|
|
1529
|
+
for (const entry of entries) {
|
|
1530
|
+
const srcPath = path.join(currentSrcDir, entry.name);
|
|
1531
|
+
if (entry.isDirectory()) {
|
|
1532
|
+
recurse(srcPath, `${currentPrefix}-${entry.name}`);
|
|
1533
|
+
continue;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
if (!entry.name.endsWith('.md')) {
|
|
1537
|
+
continue;
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
const baseName = entry.name.replace('.md', '');
|
|
1541
|
+
const skillName = `${currentPrefix}-${baseName}`;
|
|
1542
|
+
const skillDir = path.join(skillsDir, skillName);
|
|
1543
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
1544
|
+
|
|
1545
|
+
let content = fs.readFileSync(srcPath, 'utf8');
|
|
1546
|
+
content = convertClaudeCommandToAntigravitySkill(content, skillName, isGlobal);
|
|
1547
|
+
content = processAttribution(content, getCommitAttribution('antigravity'));
|
|
1548
|
+
|
|
1549
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
recurse(srcDir, prefix);
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
/**
|
|
1557
|
+
* Recursively copy directory, replacing paths in .md files
|
|
1558
|
+
* Deletes existing destDir first to remove orphaned files from previous versions
|
|
1559
|
+
* @param {string} srcDir - Source directory
|
|
1560
|
+
* @param {string} destDir - Destination directory
|
|
1561
|
+
* @param {string} pathPrefix - Path prefix for file references
|
|
1562
|
+
* @param {string} runtime - Target runtime ('claude', 'opencode', 'gemini', 'codex')
|
|
1563
|
+
*/
|
|
1564
|
+
function copyWithPathReplacement(srcDir: string, destDir: string, pathPrefix: string, runtime: Runtime, isCommand = false, isGlobal = false) {
|
|
1565
|
+
const isOpencode = runtime === 'opencode';
|
|
1566
|
+
const isCodex = runtime === 'codex';
|
|
1567
|
+
const isCopilot = runtime === 'copilot';
|
|
1568
|
+
const isAntigravity = runtime === 'antigravity';
|
|
1569
|
+
const dirName = getDirName(runtime);
|
|
1570
|
+
|
|
1571
|
+
// Clean install: remove existing destination to prevent orphaned files
|
|
1572
|
+
if (fs.existsSync(destDir)) {
|
|
1573
|
+
fs.rmSync(destDir, { recursive: true });
|
|
1574
|
+
}
|
|
1575
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
1576
|
+
|
|
1577
|
+
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
1578
|
+
|
|
1579
|
+
for (const entry of entries) {
|
|
1580
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
1581
|
+
const destPath = path.join(destDir, entry.name);
|
|
1582
|
+
|
|
1583
|
+
if (entry.isDirectory()) {
|
|
1584
|
+
copyWithPathReplacement(srcPath, destPath, pathPrefix, runtime, isCommand, isGlobal);
|
|
1585
|
+
} else if (entry.name.endsWith('.md')) {
|
|
1586
|
+
// Replace ~/.claude/ and $HOME/.claude/ and ./.claude/ with runtime-appropriate paths
|
|
1587
|
+
// Skip generic replacement for Copilot — convertClaudeToCopilotContent handles all paths
|
|
1588
|
+
let content = fs.readFileSync(srcPath, 'utf8');
|
|
1589
|
+
if (!isCopilot && !isAntigravity) {
|
|
1590
|
+
const globalClaudeRegex = /~\/\.claude\//g;
|
|
1591
|
+
const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
|
|
1592
|
+
const localClaudeRegex = /\.\/\.claude\//g;
|
|
1593
|
+
content = content.replace(globalClaudeRegex, pathPrefix);
|
|
1594
|
+
content = content.replace(globalClaudeHomeRegex, pathPrefix);
|
|
1595
|
+
content = content.replace(localClaudeRegex, `./${dirName}/`);
|
|
1596
|
+
}
|
|
1597
|
+
content = processAttribution(content, getCommitAttribution(runtime));
|
|
1598
|
+
|
|
1599
|
+
// Convert frontmatter for opencode compatibility
|
|
1600
|
+
if (isOpencode) {
|
|
1601
|
+
content = convertClaudeToOpencodeFrontmatter(content);
|
|
1602
|
+
fs.writeFileSync(destPath, content);
|
|
1603
|
+
} else if (runtime === 'gemini') {
|
|
1604
|
+
if (isCommand) {
|
|
1605
|
+
// Convert to TOML for Gemini (strip <sub> tags — terminals can't render subscript)
|
|
1606
|
+
content = stripSubTags(content);
|
|
1607
|
+
const tomlContent = convertClaudeToGeminiToml(content);
|
|
1608
|
+
// Replace extension with .toml
|
|
1609
|
+
const tomlPath = destPath.replace(/\.md$/, '.toml');
|
|
1610
|
+
fs.writeFileSync(tomlPath, tomlContent);
|
|
1611
|
+
} else {
|
|
1612
|
+
fs.writeFileSync(destPath, content);
|
|
1613
|
+
}
|
|
1614
|
+
} else if (isCodex) {
|
|
1615
|
+
content = convertClaudeToCodexMarkdown(content);
|
|
1616
|
+
fs.writeFileSync(destPath, content);
|
|
1617
|
+
} else if (isCopilot) {
|
|
1618
|
+
content = convertClaudeToCopilotContent(content, isGlobal);
|
|
1619
|
+
content = processAttribution(content, getCommitAttribution(runtime));
|
|
1620
|
+
fs.writeFileSync(destPath, content);
|
|
1621
|
+
} else if (isAntigravity) {
|
|
1622
|
+
content = convertClaudeToAntigravityContent(content, isGlobal);
|
|
1623
|
+
content = processAttribution(content, getCommitAttribution(runtime));
|
|
1624
|
+
fs.writeFileSync(destPath, content);
|
|
1625
|
+
} else {
|
|
1626
|
+
fs.writeFileSync(destPath, content);
|
|
1627
|
+
}
|
|
1628
|
+
} else if (isCopilot && (entry.name.endsWith('.cjs') || entry.name.endsWith('.js'))) {
|
|
1629
|
+
// Copilot: also transform .cjs/.js files for CONV-06 and CONV-07
|
|
1630
|
+
let content = fs.readFileSync(srcPath, 'utf8');
|
|
1631
|
+
content = convertClaudeToCopilotContent(content, isGlobal);
|
|
1632
|
+
fs.writeFileSync(destPath, content);
|
|
1633
|
+
} else if (isAntigravity && (entry.name.endsWith('.cjs') || entry.name.endsWith('.js'))) {
|
|
1634
|
+
// Antigravity: also transform .cjs/.js files for path/command conversions
|
|
1635
|
+
let content = fs.readFileSync(srcPath, 'utf8');
|
|
1636
|
+
content = convertClaudeToAntigravityContent(content, isGlobal);
|
|
1637
|
+
fs.writeFileSync(destPath, content);
|
|
1638
|
+
} else {
|
|
1639
|
+
fs.copyFileSync(srcPath, destPath);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
/**
|
|
1645
|
+
* Clean up orphaned files from previous Vector versions
|
|
1646
|
+
*/
|
|
1647
|
+
function cleanupOrphanedFiles(configDir: string) {
|
|
1648
|
+
const orphanedFiles = [
|
|
1649
|
+
'hooks/vector-notify.sh', // Removed in v1.6.x
|
|
1650
|
+
'hooks/statusline.js', // Renamed to vector-statusline.js in v1.9.0
|
|
1651
|
+
];
|
|
1652
|
+
|
|
1653
|
+
for (const relPath of orphanedFiles) {
|
|
1654
|
+
const fullPath = path.join(configDir, relPath);
|
|
1655
|
+
if (fs.existsSync(fullPath)) {
|
|
1656
|
+
fs.unlinkSync(fullPath);
|
|
1657
|
+
console.log(` ${green}✓${reset} Removed orphaned ${relPath}`);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
/**
|
|
1663
|
+
* Clean up orphaned hook registrations from settings.json
|
|
1664
|
+
*/
|
|
1665
|
+
function cleanupOrphanedHooks(settings: Record<string, unknown>) {
|
|
1666
|
+
const orphanedHookPatterns = [
|
|
1667
|
+
'vector-notify.sh', // Removed in v1.6.x
|
|
1668
|
+
'hooks/statusline.js', // Renamed to vector-statusline.js in v1.9.0
|
|
1669
|
+
'vector-intel-index.js', // Removed in v1.9.2
|
|
1670
|
+
'vector-intel-session.js', // Removed in v1.9.2
|
|
1671
|
+
'vector-intel-prune.js', // Removed in v1.9.2
|
|
1672
|
+
];
|
|
1673
|
+
|
|
1674
|
+
let cleanedHooks = false;
|
|
1675
|
+
|
|
1676
|
+
// Check all hook event types (Stop, SessionStart, etc.)
|
|
1677
|
+
if (settings.hooks) {
|
|
1678
|
+
const hooks = settings.hooks as Record<string, unknown[]>;
|
|
1679
|
+
for (const eventType of Object.keys(hooks)) {
|
|
1680
|
+
const hookEntries = hooks[eventType];
|
|
1681
|
+
if (Array.isArray(hookEntries)) {
|
|
1682
|
+
// Filter out entries that contain orphaned hooks
|
|
1683
|
+
const filtered = hookEntries.filter(entry => {
|
|
1684
|
+
const e = entry as Record<string, unknown>;
|
|
1685
|
+
if (e.hooks && Array.isArray(e.hooks)) {
|
|
1686
|
+
// Check if any hook in this entry matches orphaned patterns
|
|
1687
|
+
const hasOrphaned = (e.hooks as Record<string, unknown>[]).some((h: Record<string, unknown>) =>
|
|
1688
|
+
h.command && orphanedHookPatterns.some(pattern => (h.command as string).includes(pattern))
|
|
1689
|
+
);
|
|
1690
|
+
if (hasOrphaned) {
|
|
1691
|
+
cleanedHooks = true;
|
|
1692
|
+
return false; // Remove this entry
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
return true; // Keep this entry
|
|
1696
|
+
});
|
|
1697
|
+
hooks[eventType] = filtered;
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
if (cleanedHooks) {
|
|
1703
|
+
console.log(` ${green}✓${reset} Removed orphaned hook registrations`);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
// Fix #330: Update statusLine if it points to old Vector statusline.js path
|
|
1707
|
+
// Only match the specific old Vector path pattern (hooks/statusline.js),
|
|
1708
|
+
// not third-party statusline scripts that happen to contain 'statusline.js'
|
|
1709
|
+
const statusLine = settings.statusLine as Record<string, string> | undefined;
|
|
1710
|
+
if (statusLine && statusLine.command &&
|
|
1711
|
+
/hooks[\/\\]statusline\.js/.test(statusLine.command)) {
|
|
1712
|
+
statusLine.command = statusLine.command.replace(
|
|
1713
|
+
/hooks([\/\\])statusline\.js/,
|
|
1714
|
+
'hooks$1vector-statusline.js'
|
|
1715
|
+
);
|
|
1716
|
+
console.log(` ${green}✓${reset} Updated statusline path (hooks/statusline.js → hooks/vector-statusline.js)`);
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
return settings;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
/**
|
|
1723
|
+
* Uninstall Vector from the specified directory for a specific runtime
|
|
1724
|
+
* Removes only Vector-specific files/directories, preserves user content
|
|
1725
|
+
* @param {boolean} isGlobal - Whether to uninstall from global or local
|
|
1726
|
+
* @param {string} runtime - Target runtime ('claude', 'opencode', 'gemini', 'codex', 'copilot')
|
|
1727
|
+
*/
|
|
1728
|
+
function uninstall(isGlobal: boolean, runtime: Runtime = 'claude') {
|
|
1729
|
+
const isOpencode = runtime === 'opencode';
|
|
1730
|
+
const isCodex = runtime === 'codex';
|
|
1731
|
+
const isCopilot = runtime === 'copilot';
|
|
1732
|
+
const isAntigravity = runtime === 'antigravity';
|
|
1733
|
+
const dirName = getDirName(runtime);
|
|
1734
|
+
|
|
1735
|
+
// Get the target directory based on runtime and install type
|
|
1736
|
+
const targetDir = isGlobal
|
|
1737
|
+
? getGlobalDir(runtime, explicitConfigDir)
|
|
1738
|
+
: path.join(process.cwd(), dirName);
|
|
1739
|
+
|
|
1740
|
+
const locationLabel = isGlobal
|
|
1741
|
+
? targetDir.replace(os.homedir(), '~')
|
|
1742
|
+
: targetDir.replace(process.cwd(), '.');
|
|
1743
|
+
|
|
1744
|
+
let runtimeLabel = 'Claude Code';
|
|
1745
|
+
if (runtime === 'opencode') runtimeLabel = 'OpenCode';
|
|
1746
|
+
if (runtime === 'gemini') runtimeLabel = 'Gemini';
|
|
1747
|
+
if (runtime === 'codex') runtimeLabel = 'Codex';
|
|
1748
|
+
if (runtime === 'copilot') runtimeLabel = 'Copilot';
|
|
1749
|
+
if (runtime === 'antigravity') runtimeLabel = 'Antigravity';
|
|
1750
|
+
|
|
1751
|
+
console.log(` Uninstalling Vector from ${cyan}${runtimeLabel}${reset} at ${cyan}${locationLabel}${reset}\n`);
|
|
1752
|
+
|
|
1753
|
+
// Check if target directory exists
|
|
1754
|
+
if (!fs.existsSync(targetDir)) {
|
|
1755
|
+
console.log(` ${yellow}⚠${reset} Directory does not exist: ${locationLabel}`);
|
|
1756
|
+
console.log(` Nothing to uninstall.\n`);
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
let removedCount = 0;
|
|
1761
|
+
|
|
1762
|
+
// 1. Remove Vector commands/skills
|
|
1763
|
+
if (isOpencode) {
|
|
1764
|
+
// OpenCode: remove command/vector-*.md files
|
|
1765
|
+
const commandDir = path.join(targetDir, 'command');
|
|
1766
|
+
if (fs.existsSync(commandDir)) {
|
|
1767
|
+
const files = fs.readdirSync(commandDir);
|
|
1768
|
+
for (const file of files) {
|
|
1769
|
+
if (file.startsWith('vector-') && file.endsWith('.md')) {
|
|
1770
|
+
fs.unlinkSync(path.join(commandDir, file));
|
|
1771
|
+
removedCount++;
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
console.log(` ${green}✓${reset} Removed Vector commands from command/`);
|
|
1775
|
+
}
|
|
1776
|
+
} else if (isCodex) {
|
|
1777
|
+
// Codex: remove skills/vector-*/SKILL.md skill directories
|
|
1778
|
+
const skillsDir = path.join(targetDir, 'skills');
|
|
1779
|
+
if (fs.existsSync(skillsDir)) {
|
|
1780
|
+
let skillCount = 0;
|
|
1781
|
+
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
1782
|
+
for (const entry of entries) {
|
|
1783
|
+
if (entry.isDirectory() && entry.name.startsWith('vector-')) {
|
|
1784
|
+
fs.rmSync(path.join(skillsDir, entry.name), { recursive: true });
|
|
1785
|
+
skillCount++;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
if (skillCount > 0) {
|
|
1789
|
+
removedCount++;
|
|
1790
|
+
console.log(` ${green}✓${reset} Removed ${skillCount} Codex skills`);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
// Codex: remove Vector agent .toml config files
|
|
1795
|
+
const codexAgentsDir = path.join(targetDir, 'agents');
|
|
1796
|
+
if (fs.existsSync(codexAgentsDir)) {
|
|
1797
|
+
const tomlFiles = fs.readdirSync(codexAgentsDir);
|
|
1798
|
+
let tomlCount = 0;
|
|
1799
|
+
for (const file of tomlFiles) {
|
|
1800
|
+
if (file.startsWith('vector-') && file.endsWith('.toml')) {
|
|
1801
|
+
fs.unlinkSync(path.join(codexAgentsDir, file));
|
|
1802
|
+
tomlCount++;
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
if (tomlCount > 0) {
|
|
1806
|
+
removedCount++;
|
|
1807
|
+
console.log(` ${green}✓${reset} Removed ${tomlCount} agent .toml configs`);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
// Codex: clean Vector sections from config.toml
|
|
1812
|
+
const configPath = path.join(targetDir, 'config.toml');
|
|
1813
|
+
if (fs.existsSync(configPath)) {
|
|
1814
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
1815
|
+
const cleaned = stripGsdFromCodexConfig(content);
|
|
1816
|
+
if (cleaned === null) {
|
|
1817
|
+
// File is empty after stripping — delete it
|
|
1818
|
+
fs.unlinkSync(configPath);
|
|
1819
|
+
removedCount++;
|
|
1820
|
+
console.log(` ${green}✓${reset} Removed config.toml (was Vector-only)`);
|
|
1821
|
+
} else if (cleaned !== content) {
|
|
1822
|
+
fs.writeFileSync(configPath, cleaned);
|
|
1823
|
+
removedCount++;
|
|
1824
|
+
console.log(` ${green}✓${reset} Cleaned Vector sections from config.toml`);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
} else if (isCopilot) {
|
|
1828
|
+
// Copilot: remove skills/vector-/ directories (same layout as Codex skills)
|
|
1829
|
+
const skillsDir = path.join(targetDir, 'skills');
|
|
1830
|
+
if (fs.existsSync(skillsDir)) {
|
|
1831
|
+
let skillCount = 0;
|
|
1832
|
+
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
1833
|
+
for (const entry of entries) {
|
|
1834
|
+
if (entry.isDirectory() && entry.name.startsWith('vector-')) {
|
|
1835
|
+
fs.rmSync(path.join(skillsDir, entry.name), { recursive: true });
|
|
1836
|
+
skillCount++;
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
if (skillCount > 0) {
|
|
1840
|
+
removedCount++;
|
|
1841
|
+
console.log(` ${green}✓${reset} Removed ${skillCount} Copilot skills`);
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
// Copilot: clean Vector section from copilot-instructions.md
|
|
1846
|
+
const instructionsPath = path.join(targetDir, 'copilot-instructions.md');
|
|
1847
|
+
if (fs.existsSync(instructionsPath)) {
|
|
1848
|
+
const content = fs.readFileSync(instructionsPath, 'utf8');
|
|
1849
|
+
const cleaned = stripGsdFromCopilotInstructions(content);
|
|
1850
|
+
if (cleaned === null) {
|
|
1851
|
+
fs.unlinkSync(instructionsPath);
|
|
1852
|
+
removedCount++;
|
|
1853
|
+
console.log(` ${green}✓${reset} Removed copilot-instructions.md (was Vector-only)`);
|
|
1854
|
+
} else if (cleaned !== content) {
|
|
1855
|
+
fs.writeFileSync(instructionsPath, cleaned);
|
|
1856
|
+
removedCount++;
|
|
1857
|
+
console.log(` ${green}✓${reset} Cleaned Vector section from copilot-instructions.md`);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
} else if (isAntigravity) {
|
|
1861
|
+
// Antigravity: remove skills/vector-/ directories (same layout as Copilot skills)
|
|
1862
|
+
const skillsDir = path.join(targetDir, 'skills');
|
|
1863
|
+
if (fs.existsSync(skillsDir)) {
|
|
1864
|
+
let skillCount = 0;
|
|
1865
|
+
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
1866
|
+
for (const entry of entries) {
|
|
1867
|
+
if (entry.isDirectory() && entry.name.startsWith('vector-')) {
|
|
1868
|
+
fs.rmSync(path.join(skillsDir, entry.name), { recursive: true });
|
|
1869
|
+
skillCount++;
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
if (skillCount > 0) {
|
|
1873
|
+
removedCount++;
|
|
1874
|
+
console.log(` ${green}✓${reset} Removed ${skillCount} Antigravity skills`);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
} else {
|
|
1878
|
+
const vectorCommandsDir = path.join(targetDir, 'commands', 'vector');
|
|
1879
|
+
if (fs.existsSync(vectorCommandsDir)) {
|
|
1880
|
+
fs.rmSync(vectorCommandsDir, { recursive: true });
|
|
1881
|
+
removedCount++;
|
|
1882
|
+
console.log(` ${green}✓${reset} Removed commands/vector/`);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
// 2. Remove core directory
|
|
1887
|
+
const vectorDir = path.join(targetDir, 'core');
|
|
1888
|
+
if (fs.existsSync(vectorDir)) {
|
|
1889
|
+
fs.rmSync(vectorDir, { recursive: true });
|
|
1890
|
+
removedCount++;
|
|
1891
|
+
console.log(` ${green}✓${reset} Removed core/`);
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
// 3. Remove Vector agents (vector-*.md files only)
|
|
1895
|
+
const agentsDir = path.join(targetDir, 'agents');
|
|
1896
|
+
if (fs.existsSync(agentsDir)) {
|
|
1897
|
+
const files = fs.readdirSync(agentsDir);
|
|
1898
|
+
let agentCount = 0;
|
|
1899
|
+
for (const file of files) {
|
|
1900
|
+
if (file.startsWith('vector-') && file.endsWith('.md')) {
|
|
1901
|
+
fs.unlinkSync(path.join(agentsDir, file));
|
|
1902
|
+
agentCount++;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
if (agentCount > 0) {
|
|
1906
|
+
removedCount++;
|
|
1907
|
+
console.log(` ${green}✓${reset} Removed ${agentCount} Vector agents`);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
// 4. Remove Vector hooks
|
|
1912
|
+
const hooksDir = path.join(targetDir, 'hooks');
|
|
1913
|
+
if (fs.existsSync(hooksDir)) {
|
|
1914
|
+
const vectorHooks = ['vector-statusline.js', 'vector-check-update.js', 'vector-check-update.sh', 'vector-context-monitor.js'];
|
|
1915
|
+
let hookCount = 0;
|
|
1916
|
+
for (const hook of vectorHooks) {
|
|
1917
|
+
const hookPath = path.join(hooksDir, hook);
|
|
1918
|
+
if (fs.existsSync(hookPath)) {
|
|
1919
|
+
fs.unlinkSync(hookPath);
|
|
1920
|
+
hookCount++;
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
if (hookCount > 0) {
|
|
1924
|
+
removedCount++;
|
|
1925
|
+
console.log(` ${green}✓${reset} Removed ${hookCount} Vector hooks`);
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
// 5. Remove Vector package.json (CommonJS mode marker)
|
|
1930
|
+
const pkgJsonPath = path.join(targetDir, 'package.json');
|
|
1931
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
1932
|
+
try {
|
|
1933
|
+
const content = fs.readFileSync(pkgJsonPath, 'utf8').trim();
|
|
1934
|
+
// Only remove if it's our minimal CommonJS marker
|
|
1935
|
+
if (content === '{"type":"commonjs"}') {
|
|
1936
|
+
fs.unlinkSync(pkgJsonPath);
|
|
1937
|
+
removedCount++;
|
|
1938
|
+
console.log(` ${green}✓${reset} Removed Vector package.json`);
|
|
1939
|
+
}
|
|
1940
|
+
} catch (e) {
|
|
1941
|
+
// Ignore read errors
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
// 6. Clean up settings.json (remove Vector hooks and statusline)
|
|
1946
|
+
const settingsPath = path.join(targetDir, 'settings.json');
|
|
1947
|
+
if (fs.existsSync(settingsPath)) {
|
|
1948
|
+
const settings = readSettings(settingsPath);
|
|
1949
|
+
let settingsModified = false;
|
|
1950
|
+
|
|
1951
|
+
// Remove Vector statusline if it references our hook
|
|
1952
|
+
const settingsStatusLine = settings.statusLine as Record<string, string> | undefined;
|
|
1953
|
+
if (settingsStatusLine && settingsStatusLine.command &&
|
|
1954
|
+
settingsStatusLine.command.includes('vector-statusline')) {
|
|
1955
|
+
delete settings.statusLine;
|
|
1956
|
+
settingsModified = true;
|
|
1957
|
+
console.log(` ${green}✓${reset} Removed Vector statusline from settings`);
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
// Remove Vector hooks from SessionStart
|
|
1961
|
+
const settingsHooks = settings.hooks as Record<string, unknown[]> | undefined;
|
|
1962
|
+
if (settingsHooks && settingsHooks.SessionStart) {
|
|
1963
|
+
const before = settingsHooks.SessionStart.length;
|
|
1964
|
+
settingsHooks.SessionStart = settingsHooks.SessionStart.filter(entry => {
|
|
1965
|
+
const e = entry as Record<string, unknown>;
|
|
1966
|
+
if (e.hooks && Array.isArray(e.hooks)) {
|
|
1967
|
+
// Filter out Vector hooks
|
|
1968
|
+
const hasGsdHook = (e.hooks as Record<string, unknown>[]).some((h: Record<string, unknown>) =>
|
|
1969
|
+
h.command && ((h.command as string).includes('vector-check-update') || (h.command as string).includes('vector-statusline'))
|
|
1970
|
+
);
|
|
1971
|
+
return !hasGsdHook;
|
|
1972
|
+
}
|
|
1973
|
+
return true;
|
|
1974
|
+
});
|
|
1975
|
+
if (settingsHooks.SessionStart.length < before) {
|
|
1976
|
+
settingsModified = true;
|
|
1977
|
+
console.log(` ${green}✓${reset} Removed Vector hooks from settings`);
|
|
1978
|
+
}
|
|
1979
|
+
// Clean up empty array
|
|
1980
|
+
if (settingsHooks.SessionStart.length === 0) {
|
|
1981
|
+
delete settingsHooks.SessionStart;
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
// Remove Vector hooks from PostToolUse and AfterTool (Gemini uses AfterTool)
|
|
1986
|
+
for (const eventName of ['PostToolUse', 'AfterTool']) {
|
|
1987
|
+
if (settingsHooks && settingsHooks[eventName]) {
|
|
1988
|
+
const before = settingsHooks[eventName].length;
|
|
1989
|
+
settingsHooks[eventName] = settingsHooks[eventName].filter(entry => {
|
|
1990
|
+
const e = entry as Record<string, unknown>;
|
|
1991
|
+
if (e.hooks && Array.isArray(e.hooks)) {
|
|
1992
|
+
const hasGsdHook = (e.hooks as Record<string, unknown>[]).some((h: Record<string, unknown>) =>
|
|
1993
|
+
h.command && (h.command as string).includes('vector-context-monitor')
|
|
1994
|
+
);
|
|
1995
|
+
return !hasGsdHook;
|
|
1996
|
+
}
|
|
1997
|
+
return true;
|
|
1998
|
+
});
|
|
1999
|
+
if (settingsHooks[eventName].length < before) {
|
|
2000
|
+
settingsModified = true;
|
|
2001
|
+
console.log(` ${green}✓${reset} Removed context monitor hook from settings`);
|
|
2002
|
+
}
|
|
2003
|
+
if (settingsHooks[eventName].length === 0) {
|
|
2004
|
+
delete settingsHooks[eventName];
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
// Clean up empty hooks object
|
|
2010
|
+
if (settingsHooks && Object.keys(settingsHooks).length === 0) {
|
|
2011
|
+
delete settings.hooks;
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
if (settingsModified) {
|
|
2015
|
+
writeSettings(settingsPath, settings);
|
|
2016
|
+
removedCount++;
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
// 6. For OpenCode, clean up permissions from opencode.json or opencode.jsonc
|
|
2021
|
+
if (isOpencode) {
|
|
2022
|
+
const opencodeConfigDir = isGlobal
|
|
2023
|
+
? getOpencodeGlobalDir()
|
|
2024
|
+
: path.join(process.cwd(), '.opencode');
|
|
2025
|
+
const configPath = resolveOpencodeConfigPath(opencodeConfigDir);
|
|
2026
|
+
if (fs.existsSync(configPath)) {
|
|
2027
|
+
try {
|
|
2028
|
+
const config = parseJsonc(fs.readFileSync(configPath, 'utf8'));
|
|
2029
|
+
let modified = false;
|
|
2030
|
+
|
|
2031
|
+
// Remove Vector permission entries
|
|
2032
|
+
if (config.permission) {
|
|
2033
|
+
const permission = config.permission as Record<string, Record<string, string>>;
|
|
2034
|
+
for (const permType of ['read', 'external_directory']) {
|
|
2035
|
+
if (permission[permType]) {
|
|
2036
|
+
const keys = Object.keys(permission[permType]);
|
|
2037
|
+
for (const key of keys) {
|
|
2038
|
+
if (key.includes('core')) {
|
|
2039
|
+
delete permission[permType][key];
|
|
2040
|
+
modified = true;
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
// Clean up empty objects
|
|
2044
|
+
if (Object.keys(permission[permType]).length === 0) {
|
|
2045
|
+
delete permission[permType];
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
if (Object.keys(permission).length === 0) {
|
|
2050
|
+
delete config.permission;
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
if (modified) {
|
|
2055
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
2056
|
+
removedCount++;
|
|
2057
|
+
console.log(` ${green}✓${reset} Removed Vector permissions from ${path.basename(configPath)}`);
|
|
2058
|
+
}
|
|
2059
|
+
} catch (e) {
|
|
2060
|
+
// Ignore JSON parse errors
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
if (removedCount === 0) {
|
|
2066
|
+
console.log(` ${yellow}⚠${reset} No Vector files found to remove.`);
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
console.log(`
|
|
2070
|
+
${green}Done!${reset} Vector has been uninstalled from ${runtimeLabel}.
|
|
2071
|
+
Your other files and settings have been preserved.
|
|
2072
|
+
`);
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
/**
|
|
2076
|
+
* Parse JSONC (JSON with Comments) by stripping comments and trailing commas.
|
|
2077
|
+
* OpenCode supports JSONC format via jsonc-parser, so users may have comments.
|
|
2078
|
+
* This is a lightweight inline parser to avoid adding dependencies.
|
|
2079
|
+
*/
|
|
2080
|
+
function parseJsonc(content: string): Record<string, unknown> {
|
|
2081
|
+
// Strip BOM if present
|
|
2082
|
+
if (content.charCodeAt(0) === 0xFEFF) {
|
|
2083
|
+
content = content.slice(1);
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
// Remove single-line and block comments while preserving strings
|
|
2087
|
+
let result = '';
|
|
2088
|
+
let inString = false;
|
|
2089
|
+
let i = 0;
|
|
2090
|
+
while (i < content.length) {
|
|
2091
|
+
const char = content[i];
|
|
2092
|
+
const next = content[i + 1];
|
|
2093
|
+
|
|
2094
|
+
if (inString) {
|
|
2095
|
+
result += char;
|
|
2096
|
+
// Handle escape sequences
|
|
2097
|
+
if (char === '\\' && i + 1 < content.length) {
|
|
2098
|
+
result += next;
|
|
2099
|
+
i += 2;
|
|
2100
|
+
continue;
|
|
2101
|
+
}
|
|
2102
|
+
if (char === '"') {
|
|
2103
|
+
inString = false;
|
|
2104
|
+
}
|
|
2105
|
+
i++;
|
|
2106
|
+
} else {
|
|
2107
|
+
if (char === '"') {
|
|
2108
|
+
inString = true;
|
|
2109
|
+
result += char;
|
|
2110
|
+
i++;
|
|
2111
|
+
} else if (char === '/' && next === '/') {
|
|
2112
|
+
// Skip single-line comment until end of line
|
|
2113
|
+
while (i < content.length && content[i] !== '\n') {
|
|
2114
|
+
i++;
|
|
2115
|
+
}
|
|
2116
|
+
} else if (char === '/' && next === '*') {
|
|
2117
|
+
// Skip block comment
|
|
2118
|
+
i += 2;
|
|
2119
|
+
while (i < content.length - 1 && !(content[i] === '*' && content[i + 1] === '/')) {
|
|
2120
|
+
i++;
|
|
2121
|
+
}
|
|
2122
|
+
i += 2; // Skip closing */
|
|
2123
|
+
} else {
|
|
2124
|
+
result += char;
|
|
2125
|
+
i++;
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
// Remove trailing commas before } or ]
|
|
2131
|
+
result = result.replace(/,(\s*[}\]])/g, '$1');
|
|
2132
|
+
|
|
2133
|
+
return JSON.parse(result);
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
/**
|
|
2137
|
+
* Configure OpenCode permissions to allow reading Vector reference docs
|
|
2138
|
+
* This prevents permission prompts when Vector accesses the core directory
|
|
2139
|
+
* @param {boolean} isGlobal - Whether this is a global or local install
|
|
2140
|
+
*/
|
|
2141
|
+
function configureOpencodePermissions(isGlobal = true) {
|
|
2142
|
+
// For local installs, use ./.opencode/
|
|
2143
|
+
// For global installs, use ~/.config/opencode/
|
|
2144
|
+
const opencodeConfigDir = isGlobal
|
|
2145
|
+
? getOpencodeGlobalDir()
|
|
2146
|
+
: path.join(process.cwd(), '.opencode');
|
|
2147
|
+
// Ensure config directory exists
|
|
2148
|
+
fs.mkdirSync(opencodeConfigDir, { recursive: true });
|
|
2149
|
+
|
|
2150
|
+
const configPath = resolveOpencodeConfigPath(opencodeConfigDir);
|
|
2151
|
+
|
|
2152
|
+
// Read existing config or create empty object
|
|
2153
|
+
let config: Record<string, unknown> = {};
|
|
2154
|
+
if (fs.existsSync(configPath)) {
|
|
2155
|
+
try {
|
|
2156
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
2157
|
+
config = parseJsonc(content);
|
|
2158
|
+
} catch (e) {
|
|
2159
|
+
// Cannot parse - DO NOT overwrite user's config
|
|
2160
|
+
const configFile = path.basename(configPath);
|
|
2161
|
+
console.log(` ${yellow}⚠${reset} Could not parse ${configFile} - skipping permission config`);
|
|
2162
|
+
console.log(` ${dim}Reason: ${(e as Error).message}${reset}`);
|
|
2163
|
+
console.log(` ${dim}Your config was NOT modified. Fix the syntax manually if needed.${reset}`);
|
|
2164
|
+
return;
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
// Ensure permission structure exists
|
|
2169
|
+
if (!config.permission) {
|
|
2170
|
+
config.permission = {} as Record<string, Record<string, string>>;
|
|
2171
|
+
}
|
|
2172
|
+
const permission = config.permission as Record<string, Record<string, string>>;
|
|
2173
|
+
|
|
2174
|
+
// Build the Vector path using the actual config directory
|
|
2175
|
+
// Use ~ shorthand if it's in the default location, otherwise use full path
|
|
2176
|
+
const defaultConfigDir = path.join(os.homedir(), '.config', 'opencode');
|
|
2177
|
+
const vectorPath = opencodeConfigDir === defaultConfigDir
|
|
2178
|
+
? '~/.config/opencode/core/*'
|
|
2179
|
+
: `${opencodeConfigDir.replace(/\\/g, '/')}/core/*`;
|
|
2180
|
+
|
|
2181
|
+
let modified = false;
|
|
2182
|
+
|
|
2183
|
+
// Configure read permission
|
|
2184
|
+
if (!permission.read || typeof permission.read !== 'object') {
|
|
2185
|
+
permission.read = {};
|
|
2186
|
+
}
|
|
2187
|
+
if (permission.read[vectorPath] !== 'allow') {
|
|
2188
|
+
permission.read[vectorPath] = 'allow';
|
|
2189
|
+
modified = true;
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
// Configure external_directory permission (the safety guard for paths outside project)
|
|
2193
|
+
if (!permission.external_directory || typeof permission.external_directory !== 'object') {
|
|
2194
|
+
permission.external_directory = {};
|
|
2195
|
+
}
|
|
2196
|
+
if (permission.external_directory[vectorPath] !== 'allow') {
|
|
2197
|
+
permission.external_directory[vectorPath] = 'allow';
|
|
2198
|
+
modified = true;
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
if (!modified) {
|
|
2202
|
+
return; // Already configured
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
// Write config back
|
|
2206
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
2207
|
+
console.log(` ${green}✓${reset} Configured read permission for Vector docs`);
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
/**
|
|
2211
|
+
* Verify a directory exists and contains files
|
|
2212
|
+
*/
|
|
2213
|
+
function verifyInstalled(dirPath: string, description: string) {
|
|
2214
|
+
if (!fs.existsSync(dirPath)) {
|
|
2215
|
+
console.error(` ${yellow}✗${reset} Failed to install ${description}: directory not created`);
|
|
2216
|
+
return false;
|
|
2217
|
+
}
|
|
2218
|
+
try {
|
|
2219
|
+
const entries = fs.readdirSync(dirPath);
|
|
2220
|
+
if (entries.length === 0) {
|
|
2221
|
+
console.error(` ${yellow}✗${reset} Failed to install ${description}: directory is empty`);
|
|
2222
|
+
return false;
|
|
2223
|
+
}
|
|
2224
|
+
} catch (e) {
|
|
2225
|
+
console.error(` ${yellow}✗${reset} Failed to install ${description}: ${(e as Error).message}`);
|
|
2226
|
+
return false;
|
|
2227
|
+
}
|
|
2228
|
+
return true;
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
/**
|
|
2232
|
+
* Verify a file exists
|
|
2233
|
+
*/
|
|
2234
|
+
function verifyFileInstalled(filePath: string, description: string) {
|
|
2235
|
+
if (!fs.existsSync(filePath)) {
|
|
2236
|
+
console.error(` ${yellow}✗${reset} Failed to install ${description}: file not created`);
|
|
2237
|
+
return false;
|
|
2238
|
+
}
|
|
2239
|
+
return true;
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
/**
|
|
2243
|
+
* Install to the specified directory for a specific runtime
|
|
2244
|
+
* @param {boolean} isGlobal - Whether to install globally or locally
|
|
2245
|
+
* @param {string} runtime - Target runtime ('claude', 'opencode', 'gemini', 'codex')
|
|
2246
|
+
*/
|
|
2247
|
+
|
|
2248
|
+
// ──────────────────────────────────────────────────────
|
|
2249
|
+
// Local Patch Persistence
|
|
2250
|
+
// ──────────────────────────────────────────────────────
|
|
2251
|
+
|
|
2252
|
+
const PATCHES_DIR_NAME = 'vector-local-patches';
|
|
2253
|
+
const MANIFEST_NAME = 'vector-file-manifest.json';
|
|
2254
|
+
|
|
2255
|
+
/**
|
|
2256
|
+
* Compute SHA256 hash of file contents
|
|
2257
|
+
*/
|
|
2258
|
+
function fileHash(filePath: string): string {
|
|
2259
|
+
const content = fs.readFileSync(filePath);
|
|
2260
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
/**
|
|
2264
|
+
* Recursively collect all files in dir with their hashes
|
|
2265
|
+
*/
|
|
2266
|
+
function generateManifest(dir: string, baseDir?: string): Record<string, string> {
|
|
2267
|
+
if (!baseDir) baseDir = dir;
|
|
2268
|
+
const manifest: Record<string, string> = {};
|
|
2269
|
+
if (!fs.existsSync(dir)) return manifest;
|
|
2270
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
2271
|
+
for (const entry of entries) {
|
|
2272
|
+
const fullPath = path.join(dir, entry.name);
|
|
2273
|
+
const relPath = path.relative(baseDir, fullPath).replace(/\\/g, '/');
|
|
2274
|
+
if (entry.isDirectory()) {
|
|
2275
|
+
Object.assign(manifest, generateManifest(fullPath, baseDir));
|
|
2276
|
+
} else {
|
|
2277
|
+
manifest[relPath] = fileHash(fullPath);
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
return manifest;
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
/**
|
|
2284
|
+
* Write file manifest after installation for future modification detection
|
|
2285
|
+
*/
|
|
2286
|
+
function writeManifest(configDir: string, runtime: Runtime = 'claude') {
|
|
2287
|
+
const isOpencode = runtime === 'opencode';
|
|
2288
|
+
const isCodex = runtime === 'codex';
|
|
2289
|
+
const isCopilot = runtime === 'copilot';
|
|
2290
|
+
const isAntigravity = runtime === 'antigravity';
|
|
2291
|
+
const vectorDir = path.join(configDir, 'core');
|
|
2292
|
+
const commandsDir = path.join(configDir, 'commands', 'vector');
|
|
2293
|
+
const opencodeCommandDir = path.join(configDir, 'command');
|
|
2294
|
+
const codexSkillsDir = path.join(configDir, 'skills');
|
|
2295
|
+
const agentsDir = path.join(configDir, 'agents');
|
|
2296
|
+
const manifest: { version: string; timestamp: string; files: Record<string, string> } = { version: pkg.version, timestamp: new Date().toISOString(), files: {} };
|
|
2297
|
+
|
|
2298
|
+
const vectorHashes = generateManifest(vectorDir);
|
|
2299
|
+
for (const [rel, hash] of Object.entries(vectorHashes)) {
|
|
2300
|
+
manifest.files['core/' + rel] = hash;
|
|
2301
|
+
}
|
|
2302
|
+
if (!isOpencode && !isCodex && !isCopilot && !isAntigravity && fs.existsSync(commandsDir)) {
|
|
2303
|
+
const cmdHashes = generateManifest(commandsDir);
|
|
2304
|
+
for (const [rel, hash] of Object.entries(cmdHashes)) {
|
|
2305
|
+
manifest.files['commands/vector/' + rel] = hash;
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
if (isOpencode && fs.existsSync(opencodeCommandDir)) {
|
|
2309
|
+
for (const file of fs.readdirSync(opencodeCommandDir)) {
|
|
2310
|
+
if (file.startsWith('vector-') && file.endsWith('.md')) {
|
|
2311
|
+
manifest.files['command/' + file] = fileHash(path.join(opencodeCommandDir, file));
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
if ((isCodex || isCopilot || isAntigravity) && fs.existsSync(codexSkillsDir)) {
|
|
2316
|
+
for (const skillName of listCodexSkillNames(codexSkillsDir)) {
|
|
2317
|
+
const skillRoot = path.join(codexSkillsDir, skillName);
|
|
2318
|
+
const skillHashes = generateManifest(skillRoot);
|
|
2319
|
+
for (const [rel, hash] of Object.entries(skillHashes)) {
|
|
2320
|
+
manifest.files[`skills/${skillName}/${rel}`] = hash;
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
if (fs.existsSync(agentsDir)) {
|
|
2325
|
+
for (const file of fs.readdirSync(agentsDir)) {
|
|
2326
|
+
if (file.startsWith('vector-') && file.endsWith('.md')) {
|
|
2327
|
+
manifest.files['agents/' + file] = fileHash(path.join(agentsDir, file));
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
fs.writeFileSync(path.join(configDir, MANIFEST_NAME), JSON.stringify(manifest, null, 2));
|
|
2333
|
+
return manifest;
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
/**
|
|
2337
|
+
* Detect user-modified Vector files by comparing against install manifest.
|
|
2338
|
+
* Backs up modified files to vector-local-patches/ for reapply after update.
|
|
2339
|
+
*/
|
|
2340
|
+
function saveLocalPatches(configDir: string) {
|
|
2341
|
+
const manifestPath = path.join(configDir, MANIFEST_NAME);
|
|
2342
|
+
if (!fs.existsSync(manifestPath)) return [];
|
|
2343
|
+
|
|
2344
|
+
let manifest;
|
|
2345
|
+
try { manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); } catch { return []; }
|
|
2346
|
+
|
|
2347
|
+
const patchesDir = path.join(configDir, PATCHES_DIR_NAME);
|
|
2348
|
+
const modified = [];
|
|
2349
|
+
|
|
2350
|
+
for (const [relPath, originalHash] of Object.entries(manifest.files || {})) {
|
|
2351
|
+
const fullPath = path.join(configDir, relPath);
|
|
2352
|
+
if (!fs.existsSync(fullPath)) continue;
|
|
2353
|
+
const currentHash = fileHash(fullPath);
|
|
2354
|
+
if (currentHash !== originalHash) {
|
|
2355
|
+
const backupPath = path.join(patchesDir, relPath);
|
|
2356
|
+
fs.mkdirSync(path.dirname(backupPath), { recursive: true });
|
|
2357
|
+
fs.copyFileSync(fullPath, backupPath);
|
|
2358
|
+
modified.push(relPath);
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
if (modified.length > 0) {
|
|
2363
|
+
const meta = {
|
|
2364
|
+
backed_up_at: new Date().toISOString(),
|
|
2365
|
+
from_version: manifest.version,
|
|
2366
|
+
files: modified
|
|
2367
|
+
};
|
|
2368
|
+
fs.writeFileSync(path.join(patchesDir, 'backup-meta.json'), JSON.stringify(meta, null, 2));
|
|
2369
|
+
console.log(' ' + yellow + 'i' + reset + ' Found ' + modified.length + ' locally modified Vector file(s) — backed up to ' + PATCHES_DIR_NAME + '/');
|
|
2370
|
+
for (const f of modified) {
|
|
2371
|
+
console.log(' ' + dim + f + reset);
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
return modified;
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
/**
|
|
2378
|
+
* After install, report backed-up patches for user to reapply.
|
|
2379
|
+
*/
|
|
2380
|
+
function reportLocalPatches(configDir: string, runtime: Runtime = 'claude') {
|
|
2381
|
+
const patchesDir = path.join(configDir, PATCHES_DIR_NAME);
|
|
2382
|
+
const metaPath = path.join(patchesDir, 'backup-meta.json');
|
|
2383
|
+
if (!fs.existsSync(metaPath)) return [];
|
|
2384
|
+
|
|
2385
|
+
let meta;
|
|
2386
|
+
try { meta = JSON.parse(fs.readFileSync(metaPath, 'utf8')); } catch { return []; }
|
|
2387
|
+
|
|
2388
|
+
if (meta.files && meta.files.length > 0) {
|
|
2389
|
+
const reapplyCommand = (runtime === 'opencode' || runtime === 'copilot')
|
|
2390
|
+
? '/vector-reapply-patches'
|
|
2391
|
+
: runtime === 'codex'
|
|
2392
|
+
? '$vector-reapply-patches'
|
|
2393
|
+
: '/vector:reapply-patches';
|
|
2394
|
+
console.log('');
|
|
2395
|
+
console.log(' ' + yellow + 'Local patches detected' + reset + ' (from v' + meta.from_version + '):');
|
|
2396
|
+
for (const f of meta.files) {
|
|
2397
|
+
console.log(' ' + cyan + f + reset);
|
|
2398
|
+
}
|
|
2399
|
+
console.log('');
|
|
2400
|
+
console.log(' Your modifications are saved in ' + cyan + PATCHES_DIR_NAME + '/' + reset);
|
|
2401
|
+
console.log(' Run ' + cyan + reapplyCommand + reset + ' to merge them into the new version.');
|
|
2402
|
+
console.log(' Or manually compare and merge the files.');
|
|
2403
|
+
console.log('');
|
|
2404
|
+
}
|
|
2405
|
+
return meta.files || [];
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
function install(isGlobal: boolean, runtime: Runtime = 'claude') {
|
|
2409
|
+
const isOpencode = runtime === 'opencode';
|
|
2410
|
+
const isGemini = runtime === 'gemini';
|
|
2411
|
+
const isCodex = runtime === 'codex';
|
|
2412
|
+
const isCopilot = runtime === 'copilot';
|
|
2413
|
+
const isAntigravity = runtime === 'antigravity';
|
|
2414
|
+
const dirName = getDirName(runtime);
|
|
2415
|
+
const src = path.join(__dirname, '..');
|
|
2416
|
+
|
|
2417
|
+
// Get the target directory based on runtime and install type
|
|
2418
|
+
const targetDir = isGlobal
|
|
2419
|
+
? getGlobalDir(runtime, explicitConfigDir)
|
|
2420
|
+
: path.join(process.cwd(), dirName);
|
|
2421
|
+
|
|
2422
|
+
const locationLabel = isGlobal
|
|
2423
|
+
? targetDir.replace(os.homedir(), '~')
|
|
2424
|
+
: targetDir.replace(process.cwd(), '.');
|
|
2425
|
+
|
|
2426
|
+
// Path prefix for file references in markdown content (e.g. vector-tools.cjs).
|
|
2427
|
+
// Replaces $HOME/.claude/ or ~/.claude/ so the result is <pathPrefix>core/bin/...
|
|
2428
|
+
// Always use absolute path so: (1) local installs work when Vector is outside $HOME,
|
|
2429
|
+
// (2) spawned subagents with empty $HOME still resolve the path (fixes #820).
|
|
2430
|
+
const pathPrefix = `${path.resolve(targetDir).replace(/\\/g, '/')}/`;
|
|
2431
|
+
|
|
2432
|
+
let runtimeLabel = 'Claude Code';
|
|
2433
|
+
if (isOpencode) runtimeLabel = 'OpenCode';
|
|
2434
|
+
if (isGemini) runtimeLabel = 'Gemini';
|
|
2435
|
+
if (isCodex) runtimeLabel = 'Codex';
|
|
2436
|
+
if (isCopilot) runtimeLabel = 'Copilot';
|
|
2437
|
+
if (isAntigravity) runtimeLabel = 'Antigravity';
|
|
2438
|
+
|
|
2439
|
+
console.log(` Installing for ${cyan}${runtimeLabel}${reset} to ${cyan}${locationLabel}${reset}\n`);
|
|
2440
|
+
|
|
2441
|
+
// Track installation failures
|
|
2442
|
+
const failures = [];
|
|
2443
|
+
|
|
2444
|
+
// Save any locally modified Vector files before they get wiped
|
|
2445
|
+
saveLocalPatches(targetDir);
|
|
2446
|
+
|
|
2447
|
+
// Clean up orphaned files from previous versions
|
|
2448
|
+
cleanupOrphanedFiles(targetDir);
|
|
2449
|
+
|
|
2450
|
+
// OpenCode uses command/ (flat), Codex uses skills/, Claude/Gemini use commands/vector/
|
|
2451
|
+
if (isOpencode) {
|
|
2452
|
+
// OpenCode: flat structure in command/ directory
|
|
2453
|
+
const commandDir = path.join(targetDir, 'command');
|
|
2454
|
+
fs.mkdirSync(commandDir, { recursive: true });
|
|
2455
|
+
|
|
2456
|
+
// Copy commands/vector/*.md as command/vector-*.md (flatten structure)
|
|
2457
|
+
const vectorSrc = path.join(src, 'commands', 'vector');
|
|
2458
|
+
copyFlattenedCommands(vectorSrc, commandDir, 'vector', pathPrefix, runtime);
|
|
2459
|
+
if (verifyInstalled(commandDir, 'command/vector-*')) {
|
|
2460
|
+
const count = fs.readdirSync(commandDir).filter(f => f.startsWith('vector-')).length;
|
|
2461
|
+
console.log(` ${green}✓${reset} Installed ${count} commands to command/`);
|
|
2462
|
+
} else {
|
|
2463
|
+
failures.push('command/vector-*');
|
|
2464
|
+
}
|
|
2465
|
+
} else if (isCodex) {
|
|
2466
|
+
const skillsDir = path.join(targetDir, 'skills');
|
|
2467
|
+
const vectorSrc = path.join(src, 'commands', 'vector');
|
|
2468
|
+
copyCommandsAsCodexSkills(vectorSrc, skillsDir, 'vector', pathPrefix, runtime);
|
|
2469
|
+
const installedSkillNames = listCodexSkillNames(skillsDir);
|
|
2470
|
+
if (installedSkillNames.length > 0) {
|
|
2471
|
+
console.log(` ${green}✓${reset} Installed ${installedSkillNames.length} skills to skills/`);
|
|
2472
|
+
} else {
|
|
2473
|
+
failures.push('skills/vector-*');
|
|
2474
|
+
}
|
|
2475
|
+
} else if (isCopilot) {
|
|
2476
|
+
const skillsDir = path.join(targetDir, 'skills');
|
|
2477
|
+
const vectorSrc = path.join(src, 'commands', 'vector');
|
|
2478
|
+
copyCommandsAsCopilotSkills(vectorSrc, skillsDir, 'vector', isGlobal);
|
|
2479
|
+
if (fs.existsSync(skillsDir)) {
|
|
2480
|
+
const count = fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
2481
|
+
.filter(e => e.isDirectory() && e.name.startsWith('vector-')).length;
|
|
2482
|
+
if (count > 0) {
|
|
2483
|
+
console.log(` ${green}✓${reset} Installed ${count} skills to skills/`);
|
|
2484
|
+
} else {
|
|
2485
|
+
failures.push('skills/vector-*');
|
|
2486
|
+
}
|
|
2487
|
+
} else {
|
|
2488
|
+
failures.push('skills/vector-*');
|
|
2489
|
+
}
|
|
2490
|
+
} else if (isAntigravity) {
|
|
2491
|
+
const skillsDir = path.join(targetDir, 'skills');
|
|
2492
|
+
const vectorSrc = path.join(src, 'commands', 'vector');
|
|
2493
|
+
copyCommandsAsAntigravitySkills(vectorSrc, skillsDir, 'vector', isGlobal);
|
|
2494
|
+
if (fs.existsSync(skillsDir)) {
|
|
2495
|
+
const count = fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
2496
|
+
.filter(e => e.isDirectory() && e.name.startsWith('vector-')).length;
|
|
2497
|
+
if (count > 0) {
|
|
2498
|
+
console.log(` ${green}✓${reset} Installed ${count} skills to skills/`);
|
|
2499
|
+
} else {
|
|
2500
|
+
failures.push('skills/vector-*');
|
|
2501
|
+
}
|
|
2502
|
+
} else {
|
|
2503
|
+
failures.push('skills/vector-*');
|
|
2504
|
+
}
|
|
2505
|
+
} else {
|
|
2506
|
+
// Claude Code & Gemini: nested structure in commands/ directory
|
|
2507
|
+
const commandsDir = path.join(targetDir, 'commands');
|
|
2508
|
+
fs.mkdirSync(commandsDir, { recursive: true });
|
|
2509
|
+
|
|
2510
|
+
const vectorSrc = path.join(src, 'commands', 'vector');
|
|
2511
|
+
const vectorDest = path.join(commandsDir, 'vector');
|
|
2512
|
+
copyWithPathReplacement(vectorSrc, vectorDest, pathPrefix, runtime, true, isGlobal);
|
|
2513
|
+
if (verifyInstalled(vectorDest, 'commands/vector')) {
|
|
2514
|
+
console.log(` ${green}✓${reset} Installed commands/vector`);
|
|
2515
|
+
} else {
|
|
2516
|
+
failures.push('commands/vector');
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
// Copy core skill with path replacement
|
|
2521
|
+
const skillSrc = path.join(src, 'core');
|
|
2522
|
+
const skillDest = path.join(targetDir, 'core');
|
|
2523
|
+
copyWithPathReplacement(skillSrc, skillDest, pathPrefix, runtime, false, isGlobal);
|
|
2524
|
+
if (verifyInstalled(skillDest, 'core')) {
|
|
2525
|
+
console.log(` ${green}✓${reset} Installed core`);
|
|
2526
|
+
} else {
|
|
2527
|
+
failures.push('core');
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
// Copy agents to agents directory
|
|
2531
|
+
const agentsSrc = path.join(src, 'agents');
|
|
2532
|
+
if (fs.existsSync(agentsSrc)) {
|
|
2533
|
+
const agentsDest = path.join(targetDir, 'agents');
|
|
2534
|
+
fs.mkdirSync(agentsDest, { recursive: true });
|
|
2535
|
+
|
|
2536
|
+
// Remove old Vector agents (vector-*.md) before copying new ones
|
|
2537
|
+
if (fs.existsSync(agentsDest)) {
|
|
2538
|
+
for (const file of fs.readdirSync(agentsDest)) {
|
|
2539
|
+
if (file.startsWith('vector-') && file.endsWith('.md')) {
|
|
2540
|
+
fs.unlinkSync(path.join(agentsDest, file));
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
// Copy new agents
|
|
2546
|
+
const agentEntries = fs.readdirSync(agentsSrc, { withFileTypes: true });
|
|
2547
|
+
for (const entry of agentEntries) {
|
|
2548
|
+
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
2549
|
+
let content = fs.readFileSync(path.join(agentsSrc, entry.name), 'utf8');
|
|
2550
|
+
// Replace ~/.claude/ and $HOME/.claude/ as they are the source of truth in the repo
|
|
2551
|
+
const dirRegex = /~\/\.claude\//g;
|
|
2552
|
+
const homeDirRegex = /\$HOME\/\.claude\//g;
|
|
2553
|
+
if (!isCopilot && !isAntigravity) {
|
|
2554
|
+
content = content.replace(dirRegex, pathPrefix);
|
|
2555
|
+
content = content.replace(homeDirRegex, pathPrefix);
|
|
2556
|
+
}
|
|
2557
|
+
content = processAttribution(content, getCommitAttribution(runtime));
|
|
2558
|
+
// Convert frontmatter for runtime compatibility (agents need different handling)
|
|
2559
|
+
if (isOpencode) {
|
|
2560
|
+
content = convertClaudeToOpencodeFrontmatter(content, { isAgent: true });
|
|
2561
|
+
} else if (isGemini) {
|
|
2562
|
+
content = convertClaudeToGeminiAgent(content);
|
|
2563
|
+
} else if (isCodex) {
|
|
2564
|
+
content = convertClaudeAgentToCodexAgent(content);
|
|
2565
|
+
} else if (isCopilot) {
|
|
2566
|
+
content = convertClaudeAgentToCopilotAgent(content, isGlobal);
|
|
2567
|
+
} else if (isAntigravity) {
|
|
2568
|
+
content = convertClaudeAgentToAntigravityAgent(content, isGlobal);
|
|
2569
|
+
}
|
|
2570
|
+
const destName = isCopilot ? entry.name.replace('.md', '.agent.md') : entry.name;
|
|
2571
|
+
fs.writeFileSync(path.join(agentsDest, destName), content);
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
if (verifyInstalled(agentsDest, 'agents')) {
|
|
2575
|
+
console.log(` ${green}✓${reset} Installed agents`);
|
|
2576
|
+
} else {
|
|
2577
|
+
failures.push('agents');
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
// Copy CHANGELOG.md
|
|
2582
|
+
const changelogSrc = path.join(src, 'CHANGELOG.md');
|
|
2583
|
+
const changelogDest = path.join(targetDir, 'core', 'CHANGELOG.md');
|
|
2584
|
+
if (fs.existsSync(changelogSrc)) {
|
|
2585
|
+
fs.copyFileSync(changelogSrc, changelogDest);
|
|
2586
|
+
if (verifyFileInstalled(changelogDest, 'CHANGELOG.md')) {
|
|
2587
|
+
console.log(` ${green}✓${reset} Installed CHANGELOG.md`);
|
|
2588
|
+
} else {
|
|
2589
|
+
failures.push('CHANGELOG.md');
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
// Write VERSION file
|
|
2594
|
+
const versionDest = path.join(targetDir, 'core', 'VERSION');
|
|
2595
|
+
fs.writeFileSync(versionDest, pkg.version);
|
|
2596
|
+
if (verifyFileInstalled(versionDest, 'VERSION')) {
|
|
2597
|
+
console.log(` ${green}✓${reset} Wrote VERSION (${pkg.version})`);
|
|
2598
|
+
} else {
|
|
2599
|
+
failures.push('VERSION');
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
if (!isCodex && !isCopilot) {
|
|
2603
|
+
// Write package.json to force CommonJS mode for Vector scripts
|
|
2604
|
+
// Prevents "require is not defined" errors when project has "type": "module"
|
|
2605
|
+
// Node.js walks up looking for package.json - this stops inheritance from project
|
|
2606
|
+
const pkgJsonDest = path.join(targetDir, 'package.json');
|
|
2607
|
+
fs.writeFileSync(pkgJsonDest, '{"type":"commonjs"}\n');
|
|
2608
|
+
console.log(` ${green}✓${reset} Wrote package.json (CommonJS mode)`);
|
|
2609
|
+
|
|
2610
|
+
// Copy hooks from dist/ (bundled with dependencies)
|
|
2611
|
+
// Template paths for the target runtime (replaces '.claude' with correct config dir)
|
|
2612
|
+
const hooksSrc = path.join(src, 'hooks', 'dist');
|
|
2613
|
+
if (fs.existsSync(hooksSrc)) {
|
|
2614
|
+
const hooksDest = path.join(targetDir, 'hooks');
|
|
2615
|
+
fs.mkdirSync(hooksDest, { recursive: true });
|
|
2616
|
+
const hookEntries = fs.readdirSync(hooksSrc);
|
|
2617
|
+
const configDirReplacement = getConfigDirFromHome(runtime, isGlobal);
|
|
2618
|
+
for (const entry of hookEntries) {
|
|
2619
|
+
const srcFile = path.join(hooksSrc, entry);
|
|
2620
|
+
if (fs.statSync(srcFile).isFile()) {
|
|
2621
|
+
const destFile = path.join(hooksDest, entry);
|
|
2622
|
+
// Template .js files to replace '.claude' with runtime-specific config dir
|
|
2623
|
+
if (entry.endsWith('.js')) {
|
|
2624
|
+
let content = fs.readFileSync(srcFile, 'utf8');
|
|
2625
|
+
content = content.replace(/'\.claude'/g, configDirReplacement);
|
|
2626
|
+
fs.writeFileSync(destFile, content);
|
|
2627
|
+
} else {
|
|
2628
|
+
fs.copyFileSync(srcFile, destFile);
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
if (verifyInstalled(hooksDest, 'hooks')) {
|
|
2633
|
+
console.log(` ${green}✓${reset} Installed hooks (bundled)`);
|
|
2634
|
+
} else {
|
|
2635
|
+
failures.push('hooks');
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
if (failures.length > 0) {
|
|
2641
|
+
console.error(`\n ${yellow}Installation incomplete!${reset} Failed: ${failures.join(', ')}`);
|
|
2642
|
+
process.exit(1);
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
// Write file manifest for future modification detection
|
|
2646
|
+
writeManifest(targetDir, runtime);
|
|
2647
|
+
console.log(` ${green}✓${reset} Wrote file manifest (${MANIFEST_NAME})`);
|
|
2648
|
+
|
|
2649
|
+
// Report any backed-up local patches
|
|
2650
|
+
reportLocalPatches(targetDir, runtime);
|
|
2651
|
+
|
|
2652
|
+
// Verify no leaked .claude paths in non-Claude runtimes
|
|
2653
|
+
if (runtime !== 'claude') {
|
|
2654
|
+
const leakedPaths: Array<{file: string; count: number}> = [];
|
|
2655
|
+
function scanForLeakedPaths(dir: string) {
|
|
2656
|
+
if (!fs.existsSync(dir)) return;
|
|
2657
|
+
let entries;
|
|
2658
|
+
try {
|
|
2659
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
2660
|
+
} catch (err) {
|
|
2661
|
+
if ((err as NodeJS.ErrnoException).code === 'EPERM' || (err as NodeJS.ErrnoException).code === 'EACCES') {
|
|
2662
|
+
return; // skip inaccessible directories
|
|
2663
|
+
}
|
|
2664
|
+
throw err;
|
|
2665
|
+
}
|
|
2666
|
+
for (const entry of entries) {
|
|
2667
|
+
const fullPath = path.join(dir, entry.name);
|
|
2668
|
+
if (entry.isDirectory()) {
|
|
2669
|
+
scanForLeakedPaths(fullPath);
|
|
2670
|
+
} else if ((entry.name.endsWith('.md') || entry.name.endsWith('.toml')) && entry.name !== 'CHANGELOG.md') {
|
|
2671
|
+
let content;
|
|
2672
|
+
try {
|
|
2673
|
+
content = fs.readFileSync(fullPath, 'utf8');
|
|
2674
|
+
} catch (err) {
|
|
2675
|
+
if ((err as NodeJS.ErrnoException).code === 'EPERM' || (err as NodeJS.ErrnoException).code === 'EACCES') {
|
|
2676
|
+
continue; // skip inaccessible files
|
|
2677
|
+
}
|
|
2678
|
+
throw err;
|
|
2679
|
+
}
|
|
2680
|
+
const matches = content.match(/(?:~|\$HOME)\/\.claude\b/g);
|
|
2681
|
+
if (matches) {
|
|
2682
|
+
leakedPaths.push({ file: fullPath.replace(targetDir + '/', ''), count: matches.length });
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
scanForLeakedPaths(targetDir);
|
|
2688
|
+
if (leakedPaths.length > 0) {
|
|
2689
|
+
const totalLeaks = leakedPaths.reduce((sum, l) => sum + l.count, 0);
|
|
2690
|
+
console.warn(`\n ${yellow}⚠${reset} Found ${totalLeaks} unreplaced .claude path reference(s) in ${leakedPaths.length} file(s):`);
|
|
2691
|
+
for (const leak of leakedPaths.slice(0, 5)) {
|
|
2692
|
+
console.warn(` ${dim}${leak.file}${reset} (${leak.count})`);
|
|
2693
|
+
}
|
|
2694
|
+
if (leakedPaths.length > 5) {
|
|
2695
|
+
console.warn(` ${dim}... and ${leakedPaths.length - 5} more file(s)${reset}`);
|
|
2696
|
+
}
|
|
2697
|
+
console.warn(` ${dim}These paths may not resolve correctly for ${runtimeLabel}.${reset}`);
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
if (isCodex) {
|
|
2702
|
+
// Generate Codex config.toml and per-agent .toml files
|
|
2703
|
+
const agentCount = installCodexConfig(targetDir, agentsSrc);
|
|
2704
|
+
console.log(` ${green}✓${reset} Generated config.toml with ${agentCount} agent roles`);
|
|
2705
|
+
console.log(` ${green}✓${reset} Generated ${agentCount} agent .toml config files`);
|
|
2706
|
+
return { settingsPath: null, settings: null, statuslineCommand: null, runtime };
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
if (isCopilot) {
|
|
2710
|
+
// Generate copilot-instructions.md
|
|
2711
|
+
const templatePath = path.join(targetDir, 'core', 'templates', 'copilot-instructions.md');
|
|
2712
|
+
const instructionsPath = path.join(targetDir, 'copilot-instructions.md');
|
|
2713
|
+
if (fs.existsSync(templatePath)) {
|
|
2714
|
+
const template = fs.readFileSync(templatePath, 'utf8');
|
|
2715
|
+
mergeCopilotInstructions(instructionsPath, template);
|
|
2716
|
+
console.log(` ${green}✓${reset} Generated copilot-instructions.md`);
|
|
2717
|
+
}
|
|
2718
|
+
// Copilot: no settings.json, no hooks, no statusline (like Codex)
|
|
2719
|
+
return { settingsPath: null, settings: null, statuslineCommand: null, runtime };
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
// Configure statusline and hooks in settings.json
|
|
2723
|
+
// Gemini and Antigravity use AfterTool instead of PostToolUse for post-tool hooks
|
|
2724
|
+
const postToolEvent = (runtime === 'gemini' || runtime === 'antigravity') ? 'AfterTool' : 'PostToolUse';
|
|
2725
|
+
const settingsPath = path.join(targetDir, 'settings.json');
|
|
2726
|
+
const settings = cleanupOrphanedHooks(readSettings(settingsPath));
|
|
2727
|
+
const statuslineCommand = isGlobal
|
|
2728
|
+
? buildHookCommand(targetDir, 'vector-statusline.js')
|
|
2729
|
+
: 'node ' + dirName + '/hooks/vector-statusline.js';
|
|
2730
|
+
const updateCheckCommand = isGlobal
|
|
2731
|
+
? buildHookCommand(targetDir, 'vector-check-update.js')
|
|
2732
|
+
: 'node ' + dirName + '/hooks/vector-check-update.js';
|
|
2733
|
+
const contextMonitorCommand = isGlobal
|
|
2734
|
+
? buildHookCommand(targetDir, 'vector-context-monitor.js')
|
|
2735
|
+
: 'node ' + dirName + '/hooks/vector-context-monitor.js';
|
|
2736
|
+
|
|
2737
|
+
// Enable experimental agents for Gemini CLI (required for custom sub-agents)
|
|
2738
|
+
if (isGemini) {
|
|
2739
|
+
if (!settings.experimental) {
|
|
2740
|
+
settings.experimental = {} as Record<string, unknown>;
|
|
2741
|
+
}
|
|
2742
|
+
const experimental = settings.experimental as Record<string, unknown>;
|
|
2743
|
+
if (!experimental.enableAgents) {
|
|
2744
|
+
experimental.enableAgents = true;
|
|
2745
|
+
console.log(` ${green}✓${reset} Enabled experimental agents`);
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
// Configure SessionStart hook for update checking (skip for opencode)
|
|
2750
|
+
if (!isOpencode) {
|
|
2751
|
+
if (!settings.hooks) {
|
|
2752
|
+
settings.hooks = {} as Record<string, unknown[]>;
|
|
2753
|
+
}
|
|
2754
|
+
const hooksMap = settings.hooks as Record<string, unknown[]>;
|
|
2755
|
+
if (!hooksMap.SessionStart) {
|
|
2756
|
+
hooksMap.SessionStart = [];
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
const hasGsdUpdateHook = hooksMap.SessionStart.some((entry: unknown) => {
|
|
2760
|
+
const e = entry as Record<string, unknown>;
|
|
2761
|
+
return e.hooks && Array.isArray(e.hooks) && (e.hooks as Record<string, unknown>[]).some((h: Record<string, unknown>) => h.command && (h.command as string).includes('vector-check-update'));
|
|
2762
|
+
});
|
|
2763
|
+
|
|
2764
|
+
if (!hasGsdUpdateHook) {
|
|
2765
|
+
hooksMap.SessionStart.push({
|
|
2766
|
+
hooks: [
|
|
2767
|
+
{
|
|
2768
|
+
type: 'command',
|
|
2769
|
+
command: updateCheckCommand
|
|
2770
|
+
}
|
|
2771
|
+
]
|
|
2772
|
+
});
|
|
2773
|
+
console.log(` ${green}✓${reset} Configured update check hook`);
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2776
|
+
// Configure post-tool hook for context window monitoring
|
|
2777
|
+
if (!hooksMap[postToolEvent]) {
|
|
2778
|
+
hooksMap[postToolEvent] = [];
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
const hasContextMonitorHook = hooksMap[postToolEvent].some((entry: unknown) => {
|
|
2782
|
+
const e = entry as Record<string, unknown>;
|
|
2783
|
+
return e.hooks && Array.isArray(e.hooks) && (e.hooks as Record<string, unknown>[]).some((h: Record<string, unknown>) => h.command && (h.command as string).includes('vector-context-monitor'));
|
|
2784
|
+
});
|
|
2785
|
+
|
|
2786
|
+
if (!hasContextMonitorHook) {
|
|
2787
|
+
hooksMap[postToolEvent].push({
|
|
2788
|
+
hooks: [
|
|
2789
|
+
{
|
|
2790
|
+
type: 'command',
|
|
2791
|
+
command: contextMonitorCommand
|
|
2792
|
+
}
|
|
2793
|
+
]
|
|
2794
|
+
});
|
|
2795
|
+
console.log(` ${green}✓${reset} Configured context window monitor hook`);
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
return { settingsPath, settings, statuslineCommand, runtime };
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
/**
|
|
2803
|
+
* Apply statusline config, then print completion message
|
|
2804
|
+
*/
|
|
2805
|
+
function finishInstall(settingsPath: string | null, settings: Record<string, unknown> | null, statuslineCommand: string | null, shouldInstallStatusline: boolean, runtime: Runtime = 'claude', isGlobal = true) {
|
|
2806
|
+
const isOpencode = runtime === 'opencode';
|
|
2807
|
+
const isCodex = runtime === 'codex';
|
|
2808
|
+
const isCopilot = runtime === 'copilot';
|
|
2809
|
+
|
|
2810
|
+
if (shouldInstallStatusline && !isOpencode && !isCodex && !isCopilot && settings) {
|
|
2811
|
+
settings.statusLine = {
|
|
2812
|
+
type: 'command',
|
|
2813
|
+
command: statuslineCommand
|
|
2814
|
+
};
|
|
2815
|
+
console.log(` ${green}✓${reset} Configured statusline`);
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
// Write settings when runtime supports settings.json
|
|
2819
|
+
if (!isCodex && !isCopilot && settingsPath && settings) {
|
|
2820
|
+
writeSettings(settingsPath, settings);
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
// Configure OpenCode permissions
|
|
2824
|
+
if (isOpencode) {
|
|
2825
|
+
configureOpencodePermissions(isGlobal);
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
let program = 'Claude Code';
|
|
2829
|
+
if (runtime === 'opencode') program = 'OpenCode';
|
|
2830
|
+
if (runtime === 'gemini') program = 'Gemini';
|
|
2831
|
+
if (runtime === 'codex') program = 'Codex';
|
|
2832
|
+
if (runtime === 'copilot') program = 'Copilot';
|
|
2833
|
+
if (runtime === 'antigravity') program = 'Antigravity';
|
|
2834
|
+
|
|
2835
|
+
let command = '/vector:new-project';
|
|
2836
|
+
if (runtime === 'opencode') command = '/vector-new-project';
|
|
2837
|
+
if (runtime === 'codex') command = '$vector-new-project';
|
|
2838
|
+
if (runtime === 'copilot') command = '/vector-new-project';
|
|
2839
|
+
if (runtime === 'antigravity') command = '/vector-new-project';
|
|
2840
|
+
console.log(`
|
|
2841
|
+
${green}Done!${reset} Open a blank directory in ${program} and run ${cyan}${command}${reset}.
|
|
2842
|
+
|
|
2843
|
+
${cyan}Join the community:${reset} https://discord.gg/gsd
|
|
2844
|
+
`);
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
/**
|
|
2848
|
+
* Handle statusline configuration with optional prompt
|
|
2849
|
+
*/
|
|
2850
|
+
function handleStatusline(settings: Record<string, unknown> | null, isInteractive: boolean, callback: (shouldInstall: boolean) => void) {
|
|
2851
|
+
const statusLine = settings ? (settings.statusLine as Record<string, string> | undefined) : undefined;
|
|
2852
|
+
const hasExisting = statusLine != null;
|
|
2853
|
+
|
|
2854
|
+
if (!hasExisting) {
|
|
2855
|
+
callback(true);
|
|
2856
|
+
return;
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
if (forceStatusline) {
|
|
2860
|
+
callback(true);
|
|
2861
|
+
return;
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
if (!isInteractive) {
|
|
2865
|
+
console.log(` ${yellow}⚠${reset} Skipping statusline (already configured)`);
|
|
2866
|
+
console.log(` Use ${cyan}--force-statusline${reset} to replace\n`);
|
|
2867
|
+
callback(false);
|
|
2868
|
+
return;
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
const existingCmd = (statusLine && (statusLine.command || statusLine.url)) || '(custom)';
|
|
2872
|
+
|
|
2873
|
+
const rl = readline.createInterface({
|
|
2874
|
+
input: process.stdin,
|
|
2875
|
+
output: process.stdout
|
|
2876
|
+
});
|
|
2877
|
+
|
|
2878
|
+
console.log(`
|
|
2879
|
+
${yellow}⚠${reset} Existing statusline detected\n
|
|
2880
|
+
Your current statusline:
|
|
2881
|
+
${dim}command: ${existingCmd}${reset}
|
|
2882
|
+
|
|
2883
|
+
Vector includes a statusline showing:
|
|
2884
|
+
• Model name
|
|
2885
|
+
• Current task (from todo list)
|
|
2886
|
+
• Context window usage (color-coded)
|
|
2887
|
+
|
|
2888
|
+
${cyan}1${reset}) Keep existing
|
|
2889
|
+
${cyan}2${reset}) Replace with Vector statusline
|
|
2890
|
+
`);
|
|
2891
|
+
|
|
2892
|
+
rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
|
|
2893
|
+
rl.close();
|
|
2894
|
+
const choice = answer.trim() || '1';
|
|
2895
|
+
callback(choice === '2');
|
|
2896
|
+
});
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
/**
|
|
2900
|
+
* Prompt for runtime selection
|
|
2901
|
+
*/
|
|
2902
|
+
function promptRuntime(callback: (runtimes: Runtime[]) => void) {
|
|
2903
|
+
const rl = readline.createInterface({
|
|
2904
|
+
input: process.stdin,
|
|
2905
|
+
output: process.stdout
|
|
2906
|
+
});
|
|
2907
|
+
|
|
2908
|
+
let answered = false;
|
|
2909
|
+
|
|
2910
|
+
rl.on('close', () => {
|
|
2911
|
+
if (!answered) {
|
|
2912
|
+
answered = true;
|
|
2913
|
+
console.log(`\n ${yellow}Installation cancelled${reset}\n`);
|
|
2914
|
+
process.exit(0);
|
|
2915
|
+
}
|
|
2916
|
+
});
|
|
2917
|
+
|
|
2918
|
+
console.log(` ${yellow}Which runtime(s) would you like to install for?${reset}\n\n ${cyan}1${reset}) Claude Code ${dim}(~/.claude)${reset}
|
|
2919
|
+
${cyan}2${reset}) OpenCode ${dim}(~/.config/opencode)${reset} - open source, free models
|
|
2920
|
+
${cyan}3${reset}) Gemini ${dim}(~/.gemini)${reset}
|
|
2921
|
+
${cyan}4${reset}) Codex ${dim}(~/.codex)${reset}
|
|
2922
|
+
${cyan}5${reset}) Copilot ${dim}(~/.copilot)${reset}
|
|
2923
|
+
${cyan}6${reset}) Antigravity ${dim}(~/.gemini/antigravity)${reset}
|
|
2924
|
+
${cyan}7${reset}) All
|
|
2925
|
+
`);
|
|
2926
|
+
|
|
2927
|
+
rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
|
|
2928
|
+
answered = true;
|
|
2929
|
+
rl.close();
|
|
2930
|
+
const choice = answer.trim() || '1';
|
|
2931
|
+
if (choice === '7') {
|
|
2932
|
+
callback(['claude', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity']);
|
|
2933
|
+
} else if (choice === '6') {
|
|
2934
|
+
callback(['antigravity']);
|
|
2935
|
+
} else if (choice === '5') {
|
|
2936
|
+
callback(['copilot']);
|
|
2937
|
+
} else if (choice === '4') {
|
|
2938
|
+
callback(['codex']);
|
|
2939
|
+
} else if (choice === '3') {
|
|
2940
|
+
callback(['gemini']);
|
|
2941
|
+
} else if (choice === '2') {
|
|
2942
|
+
callback(['opencode']);
|
|
2943
|
+
} else {
|
|
2944
|
+
callback(['claude']);
|
|
2945
|
+
}
|
|
2946
|
+
});
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
/**
|
|
2950
|
+
* Prompt for install location
|
|
2951
|
+
*/
|
|
2952
|
+
function promptLocation(runtimes: Runtime[]) {
|
|
2953
|
+
if (!process.stdin.isTTY) {
|
|
2954
|
+
console.log(` ${yellow}Non-interactive terminal detected, defaulting to global install${reset}\n`);
|
|
2955
|
+
installAllRuntimes(runtimes, true, false);
|
|
2956
|
+
return;
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
const rl = readline.createInterface({
|
|
2960
|
+
input: process.stdin,
|
|
2961
|
+
output: process.stdout
|
|
2962
|
+
});
|
|
2963
|
+
|
|
2964
|
+
let answered = false;
|
|
2965
|
+
|
|
2966
|
+
rl.on('close', () => {
|
|
2967
|
+
if (!answered) {
|
|
2968
|
+
answered = true;
|
|
2969
|
+
console.log(`\n ${yellow}Installation cancelled${reset}\n`);
|
|
2970
|
+
process.exit(0);
|
|
2971
|
+
}
|
|
2972
|
+
});
|
|
2973
|
+
|
|
2974
|
+
const pathExamples = runtimes.map(r => {
|
|
2975
|
+
const globalPath = getGlobalDir(r, explicitConfigDir);
|
|
2976
|
+
return globalPath.replace(os.homedir(), '~');
|
|
2977
|
+
}).join(', ');
|
|
2978
|
+
|
|
2979
|
+
const localExamples = runtimes.map(r => `./${getDirName(r)}`).join(', ');
|
|
2980
|
+
|
|
2981
|
+
console.log(` ${yellow}Where would you like to install?${reset}\n\n ${cyan}1${reset}) Global ${dim}(${pathExamples})${reset} - available in all projects
|
|
2982
|
+
${cyan}2${reset}) Local ${dim}(${localExamples})${reset} - this project only
|
|
2983
|
+
`);
|
|
2984
|
+
|
|
2985
|
+
rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
|
|
2986
|
+
answered = true;
|
|
2987
|
+
rl.close();
|
|
2988
|
+
const choice = answer.trim() || '1';
|
|
2989
|
+
const isGlobal = choice !== '2';
|
|
2990
|
+
installAllRuntimes(runtimes, isGlobal, true);
|
|
2991
|
+
});
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
/**
|
|
2995
|
+
* Install Vector for all selected runtimes
|
|
2996
|
+
*/
|
|
2997
|
+
function installAllRuntimes(runtimes: Runtime[], isGlobal: boolean, isInteractive: boolean) {
|
|
2998
|
+
const results: Array<{ settingsPath: string | null; settings: Record<string, unknown> | null; statuslineCommand: string | null; runtime: Runtime }> = [];
|
|
2999
|
+
|
|
3000
|
+
for (const runtime of runtimes) {
|
|
3001
|
+
const result = install(isGlobal, runtime);
|
|
3002
|
+
results.push(result);
|
|
3003
|
+
}
|
|
3004
|
+
|
|
3005
|
+
const statuslineRuntimes = ['claude', 'gemini'];
|
|
3006
|
+
const primaryStatuslineResult = results.find(r => statuslineRuntimes.includes(r.runtime));
|
|
3007
|
+
|
|
3008
|
+
const finalize = (shouldInstallStatusline: boolean) => {
|
|
3009
|
+
for (const result of results) {
|
|
3010
|
+
const useStatusline = statuslineRuntimes.includes(result.runtime) && shouldInstallStatusline;
|
|
3011
|
+
finishInstall(
|
|
3012
|
+
result.settingsPath,
|
|
3013
|
+
result.settings,
|
|
3014
|
+
result.statuslineCommand,
|
|
3015
|
+
useStatusline,
|
|
3016
|
+
result.runtime,
|
|
3017
|
+
isGlobal
|
|
3018
|
+
);
|
|
3019
|
+
}
|
|
3020
|
+
};
|
|
3021
|
+
|
|
3022
|
+
if (primaryStatuslineResult) {
|
|
3023
|
+
handleStatusline(primaryStatuslineResult.settings, isInteractive, finalize);
|
|
3024
|
+
} else {
|
|
3025
|
+
finalize(false);
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
|
|
3029
|
+
// Test-only exports — skip main logic when loaded as a module for testing
|
|
3030
|
+
if (process.env.VECTOR_TEST_MODE) {
|
|
3031
|
+
module.exports = {
|
|
3032
|
+
getCodexSkillAdapterHeader,
|
|
3033
|
+
convertClaudeToGeminiAgent,
|
|
3034
|
+
convertClaudeAgentToCodexAgent,
|
|
3035
|
+
generateCodexAgentToml,
|
|
3036
|
+
generateCodexConfigBlock,
|
|
3037
|
+
stripGsdFromCodexConfig,
|
|
3038
|
+
mergeCodexConfig,
|
|
3039
|
+
installCodexConfig,
|
|
3040
|
+
convertClaudeCommandToCodexSkill,
|
|
3041
|
+
convertClaudeToOpencodeFrontmatter,
|
|
3042
|
+
GSD_CODEX_MARKER,
|
|
3043
|
+
CODEX_AGENT_SANDBOX,
|
|
3044
|
+
getDirName,
|
|
3045
|
+
getGlobalDir,
|
|
3046
|
+
getConfigDirFromHome,
|
|
3047
|
+
claudeToCopilotTools,
|
|
3048
|
+
convertCopilotToolName,
|
|
3049
|
+
convertClaudeToCopilotContent,
|
|
3050
|
+
convertClaudeCommandToCopilotSkill,
|
|
3051
|
+
convertClaudeAgentToCopilotAgent,
|
|
3052
|
+
copyCommandsAsCopilotSkills,
|
|
3053
|
+
GSD_COPILOT_INSTRUCTIONS_MARKER,
|
|
3054
|
+
GSD_COPILOT_INSTRUCTIONS_CLOSE_MARKER,
|
|
3055
|
+
mergeCopilotInstructions,
|
|
3056
|
+
stripGsdFromCopilotInstructions,
|
|
3057
|
+
convertClaudeToAntigravityContent,
|
|
3058
|
+
convertClaudeCommandToAntigravitySkill,
|
|
3059
|
+
convertClaudeAgentToAntigravityAgent,
|
|
3060
|
+
copyCommandsAsAntigravitySkills,
|
|
3061
|
+
writeManifest,
|
|
3062
|
+
reportLocalPatches,
|
|
3063
|
+
};
|
|
3064
|
+
} else {
|
|
3065
|
+
|
|
3066
|
+
// Main logic
|
|
3067
|
+
if (hasGlobal && hasLocal) {
|
|
3068
|
+
console.error(` ${yellow}Cannot specify both --global and --local${reset}`);
|
|
3069
|
+
process.exit(1);
|
|
3070
|
+
} else if (explicitConfigDir && hasLocal) {
|
|
3071
|
+
console.error(` ${yellow}Cannot use --config-dir with --local${reset}`);
|
|
3072
|
+
process.exit(1);
|
|
3073
|
+
} else if (hasUninstall) {
|
|
3074
|
+
if (!hasGlobal && !hasLocal) {
|
|
3075
|
+
console.error(` ${yellow}--uninstall requires --global or --local${reset}`);
|
|
3076
|
+
process.exit(1);
|
|
3077
|
+
}
|
|
3078
|
+
const runtimes: Runtime[] = selectedRuntimes.length > 0 ? selectedRuntimes : ['claude'];
|
|
3079
|
+
for (const runtime of runtimes) {
|
|
3080
|
+
uninstall(hasGlobal, runtime);
|
|
3081
|
+
}
|
|
3082
|
+
} else if (selectedRuntimes.length > 0) {
|
|
3083
|
+
if (!hasGlobal && !hasLocal) {
|
|
3084
|
+
promptLocation(selectedRuntimes);
|
|
3085
|
+
} else {
|
|
3086
|
+
installAllRuntimes(selectedRuntimes, hasGlobal, false);
|
|
3087
|
+
}
|
|
3088
|
+
} else if (hasGlobal || hasLocal) {
|
|
3089
|
+
// Default to Claude if no runtime specified but location is
|
|
3090
|
+
installAllRuntimes(['claude'], hasGlobal, false);
|
|
3091
|
+
} else {
|
|
3092
|
+
// Interactive
|
|
3093
|
+
if (!process.stdin.isTTY) {
|
|
3094
|
+
console.log(` ${yellow}Non-interactive terminal detected, defaulting to Claude Code global install${reset}\n`);
|
|
3095
|
+
installAllRuntimes(['claude'], true, false);
|
|
3096
|
+
} else {
|
|
3097
|
+
promptRuntime((runtimes) => {
|
|
3098
|
+
promptLocation(runtimes);
|
|
3099
|
+
});
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
|
|
3103
|
+
} // end of else block for VECTOR_TEST_MODE
|