@bhargavvc/sdd-cc 1.30.0 → 1.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja-JP.md +144 -110
- package/README.ko-KR.md +143 -107
- package/README.md +183 -112
- package/README.pt-BR.md +90 -52
- package/README.zh-CN.md +141 -101
- package/agents/sdd-advisor-researcher.md +23 -0
- package/agents/sdd-ai-researcher.md +133 -0
- package/agents/sdd-code-fixer.md +516 -0
- package/agents/sdd-code-reviewer.md +355 -0
- package/agents/sdd-codebase-mapper.md +3 -3
- package/agents/sdd-debugger.md +17 -5
- package/agents/sdd-doc-verifier.md +201 -0
- package/agents/sdd-doc-writer.md +602 -0
- package/agents/sdd-domain-researcher.md +153 -0
- package/agents/sdd-eval-auditor.md +164 -0
- package/agents/sdd-eval-planner.md +154 -0
- package/agents/sdd-executor.md +87 -4
- package/agents/sdd-framework-selector.md +160 -0
- package/agents/sdd-intel-updater.md +314 -0
- package/agents/sdd-nyquist-auditor.md +1 -1
- package/agents/sdd-phase-researcher.md +71 -4
- package/agents/sdd-plan-checker.md +100 -6
- package/agents/sdd-planner.md +145 -206
- package/agents/sdd-project-researcher.md +25 -2
- package/agents/sdd-research-synthesizer.md +3 -3
- package/agents/sdd-roadmapper.md +6 -6
- package/agents/sdd-security-auditor.md +128 -0
- package/agents/sdd-ui-auditor.md +43 -3
- package/agents/sdd-ui-checker.md +5 -5
- package/agents/sdd-ui-researcher.md +27 -4
- package/agents/sdd-user-profiler.md +2 -2
- package/agents/sdd-verifier.md +142 -22
- package/bin/install.js +2151 -551
- package/commands/sdd/add-backlog.md +5 -5
- package/commands/sdd/add-tests.md +2 -2
- package/commands/sdd/ai-integration-phase.md +36 -0
- package/commands/sdd/analyze-dependencies.md +34 -0
- package/commands/sdd/audit-fix.md +33 -0
- package/commands/sdd/autonomous.md +7 -2
- package/commands/sdd/cleanup.md +5 -0
- package/commands/sdd/code-review-fix.md +52 -0
- package/commands/sdd/code-review.md +55 -0
- package/commands/sdd/complete-milestone.md +6 -6
- package/commands/sdd/debug.md +22 -9
- package/commands/sdd/discuss-phase.md +7 -2
- package/commands/sdd/do.md +1 -1
- package/commands/sdd/docs-update.md +48 -0
- package/commands/sdd/eval-review.md +32 -0
- package/commands/sdd/execute-phase.md +4 -0
- package/commands/sdd/explore.md +27 -0
- package/commands/sdd/fast.md +2 -2
- package/commands/sdd/from-sdd2.md +45 -0
- package/commands/sdd/help.md +2 -0
- package/commands/sdd/import.md +36 -0
- package/commands/sdd/intel.md +179 -0
- package/commands/sdd/join-discord.md +2 -1
- package/commands/sdd/manager.md +1 -0
- package/commands/sdd/map-codebase.md +3 -3
- package/commands/sdd/new-milestone.md +1 -1
- package/commands/sdd/new-project.md +5 -1
- package/commands/sdd/new-workspace.md +1 -1
- package/commands/sdd/next.md +2 -0
- package/commands/sdd/plan-milestone-gaps.md +2 -2
- package/commands/sdd/plan-phase.md +6 -1
- package/commands/sdd/plant-seed.md +1 -1
- package/commands/sdd/profile-user.md +1 -1
- package/commands/sdd/quick.md +5 -3
- package/commands/sdd/reapply-patches.md +230 -42
- package/commands/sdd/research-phase.md +3 -3
- package/commands/sdd/review-backlog.md +1 -0
- package/commands/sdd/review.md +6 -3
- package/commands/sdd/scan.md +26 -0
- package/commands/sdd/secure-phase.md +35 -0
- package/commands/sdd/ship.md +1 -1
- package/commands/sdd/thread.md +5 -5
- package/commands/sdd/undo.md +34 -0
- package/commands/sdd/verify-work.md +1 -1
- package/commands/sdd/workstreams.md +17 -11
- package/hooks/dist/sdd-check-update.js +33 -8
- package/hooks/dist/sdd-context-monitor.js +17 -8
- package/hooks/dist/sdd-phase-boundary.sh +27 -0
- package/hooks/dist/sdd-prompt-guard.js +1 -0
- package/hooks/dist/sdd-read-guard.js +82 -0
- package/hooks/dist/sdd-session-state.sh +33 -0
- package/hooks/dist/sdd-statusline.js +137 -15
- package/hooks/dist/sdd-validate-commit.sh +47 -0
- package/hooks/dist/sdd-workflow-guard.js +4 -4
- package/hooks/sdd-check-update.js +139 -0
- package/hooks/sdd-context-monitor.js +165 -0
- package/hooks/sdd-phase-boundary.sh +27 -0
- package/hooks/sdd-prompt-guard.js +97 -0
- package/hooks/sdd-read-guard.js +82 -0
- package/hooks/sdd-session-state.sh +33 -0
- package/hooks/sdd-statusline.js +241 -0
- package/hooks/sdd-validate-commit.sh +47 -0
- package/hooks/sdd-workflow-guard.js +94 -0
- package/package.json +3 -3
- package/scripts/build-hooks.js +18 -7
- package/scripts/prompt-injection-scan.sh +1 -0
- package/scripts/rebrand-gsd-to-sdd.sh +221 -220
- package/scripts/run-tests.cjs +5 -1
- package/scripts/sync-upstream.sh +1 -1
- package/sdd/bin/lib/commands.cjs +79 -17
- package/sdd/bin/lib/config.cjs +90 -48
- package/sdd/bin/lib/core.cjs +452 -87
- package/sdd/bin/lib/docs.cjs +267 -0
- package/sdd/bin/lib/frontmatter.cjs +381 -336
- package/sdd/bin/lib/init.cjs +110 -16
- package/sdd/bin/lib/intel.cjs +660 -0
- package/sdd/bin/lib/learnings.cjs +378 -0
- package/sdd/bin/lib/milestone.cjs +42 -11
- package/sdd/bin/lib/model-profiles.cjs +17 -15
- package/sdd/bin/lib/phase.cjs +367 -288
- package/sdd/bin/lib/profile-output.cjs +106 -10
- package/sdd/bin/lib/roadmap.cjs +146 -115
- package/sdd/bin/lib/schema-detect.cjs +238 -0
- package/sdd/bin/lib/sdd2-import.cjs +511 -0
- package/sdd/bin/lib/security.cjs +124 -3
- package/sdd/bin/lib/state.cjs +648 -264
- package/sdd/bin/lib/template.cjs +8 -4
- package/sdd/bin/lib/verify.cjs +209 -28
- package/sdd/bin/lib/workstream.cjs +7 -3
- package/sdd/bin/sdd-tools.cjs +184 -12
- package/sdd/contexts/dev.md +21 -0
- package/sdd/contexts/research.md +22 -0
- package/sdd/contexts/review.md +22 -0
- package/sdd/references/agent-contracts.md +79 -0
- package/sdd/references/ai-evals.md +156 -0
- package/sdd/references/ai-frameworks.md +186 -0
- package/sdd/references/artifact-types.md +113 -0
- package/sdd/references/common-bug-patterns.md +114 -0
- package/sdd/references/context-budget.md +49 -0
- package/sdd/references/continuation-format.md +25 -25
- package/sdd/references/domain-probes.md +125 -0
- package/sdd/references/few-shot-examples/plan-checker.md +73 -0
- package/sdd/references/few-shot-examples/verifier.md +109 -0
- package/sdd/references/gate-prompts.md +100 -0
- package/sdd/references/gates.md +70 -0
- package/sdd/references/git-integration.md +1 -1
- package/sdd/references/ios-scaffold.md +123 -0
- package/sdd/references/model-profile-resolution.md +2 -0
- package/sdd/references/model-profiles.md +24 -18
- package/sdd/references/planner-gap-closure.md +62 -0
- package/sdd/references/planner-reviews.md +39 -0
- package/sdd/references/planner-revision.md +87 -0
- package/sdd/references/planning-config.md +252 -0
- package/sdd/references/revision-loop.md +97 -0
- package/sdd/references/thinking-models-debug.md +44 -0
- package/sdd/references/thinking-models-execution.md +50 -0
- package/sdd/references/thinking-models-planning.md +62 -0
- package/sdd/references/thinking-models-research.md +50 -0
- package/sdd/references/thinking-models-verification.md +55 -0
- package/sdd/references/thinking-partner.md +96 -0
- package/sdd/references/ui-brand.md +4 -4
- package/sdd/references/universal-anti-patterns.md +63 -0
- package/sdd/references/verification-overrides.md +227 -0
- package/sdd/references/workstream-flag.md +56 -3
- package/sdd/templates/AI-SPEC.md +246 -0
- package/sdd/templates/DEBUG.md +1 -1
- package/sdd/templates/SECURITY.md +61 -0
- package/sdd/templates/UAT.md +4 -4
- package/sdd/templates/VALIDATION.md +4 -4
- package/sdd/templates/claude-md.md +32 -9
- package/sdd/templates/config.json +4 -0
- package/sdd/templates/debug-subagent-prompt.md +1 -1
- package/sdd/templates/dev-preferences.md +1 -1
- package/sdd/templates/discovery.md +2 -2
- package/sdd/templates/phase-prompt.md +1 -1
- package/sdd/templates/planner-subagent-prompt.md +3 -3
- package/sdd/templates/project.md +1 -1
- package/sdd/templates/research.md +1 -1
- package/sdd/templates/state.md +2 -2
- package/sdd/workflows/add-phase.md +8 -8
- package/sdd/workflows/add-tests.md +12 -9
- package/sdd/workflows/add-todo.md +5 -3
- package/sdd/workflows/ai-integration-phase.md +284 -0
- package/sdd/workflows/analyze-dependencies.md +96 -0
- package/sdd/workflows/audit-fix.md +157 -0
- package/sdd/workflows/audit-milestone.md +11 -11
- package/sdd/workflows/audit-uat.md +2 -2
- package/sdd/workflows/autonomous.md +195 -27
- package/sdd/workflows/check-todos.md +12 -10
- package/sdd/workflows/cleanup.md +2 -0
- package/sdd/workflows/code-review-fix.md +497 -0
- package/sdd/workflows/code-review.md +515 -0
- package/sdd/workflows/complete-milestone.md +56 -22
- package/sdd/workflows/diagnose-issues.md +10 -3
- package/sdd/workflows/discovery-phase.md +5 -3
- package/sdd/workflows/discuss-phase-assumptions.md +24 -6
- package/sdd/workflows/discuss-phase-power.md +291 -0
- package/sdd/workflows/discuss-phase.md +173 -21
- package/sdd/workflows/do.md +23 -21
- package/sdd/workflows/docs-update.md +1155 -0
- package/sdd/workflows/eval-review.md +155 -0
- package/sdd/workflows/execute-phase.md +594 -38
- package/sdd/workflows/execute-plan.md +67 -96
- package/sdd/workflows/explore.md +139 -0
- package/sdd/workflows/fast.md +5 -5
- package/sdd/workflows/forensics.md +2 -2
- package/sdd/workflows/health.md +4 -4
- package/sdd/workflows/help.md +122 -119
- package/sdd/workflows/import.md +276 -0
- package/sdd/workflows/inbox.md +387 -0
- package/sdd/workflows/insert-phase.md +7 -7
- package/sdd/workflows/list-phase-assumptions.md +4 -4
- package/sdd/workflows/list-workspaces.md +2 -2
- package/sdd/workflows/manager.md +35 -32
- package/sdd/workflows/map-codebase.md +7 -5
- package/sdd/workflows/milestone-summary.md +2 -2
- package/sdd/workflows/new-milestone.md +17 -9
- package/sdd/workflows/new-project.md +50 -25
- package/sdd/workflows/new-workspace.md +7 -5
- package/sdd/workflows/next.md +67 -11
- package/sdd/workflows/note.md +9 -7
- package/sdd/workflows/pause-work.md +75 -12
- package/sdd/workflows/plan-milestone-gaps.md +8 -8
- package/sdd/workflows/plan-phase.md +294 -42
- package/sdd/workflows/plant-seed.md +6 -3
- package/sdd/workflows/pr-branch.md +42 -14
- package/sdd/workflows/profile-user.md +9 -7
- package/sdd/workflows/progress.md +45 -45
- package/sdd/workflows/quick.md +195 -47
- package/sdd/workflows/remove-phase.md +6 -6
- package/sdd/workflows/remove-workspace.md +3 -1
- package/sdd/workflows/research-phase.md +2 -2
- package/sdd/workflows/resume-project.md +12 -12
- package/sdd/workflows/review.md +109 -9
- package/sdd/workflows/scan.md +102 -0
- package/sdd/workflows/secure-phase.md +166 -0
- package/sdd/workflows/session-report.md +2 -2
- package/sdd/workflows/settings.md +38 -12
- package/sdd/workflows/ship.md +21 -9
- package/sdd/workflows/stats.md +1 -1
- package/sdd/workflows/transition.md +23 -23
- package/sdd/workflows/ui-phase.md +15 -7
- package/sdd/workflows/ui-review.md +29 -4
- package/sdd/workflows/undo.md +314 -0
- package/sdd/workflows/update.md +171 -20
- package/sdd/workflows/validate-phase.md +6 -4
- package/sdd/workflows/verify-phase.md +210 -6
- package/sdd/workflows/verify-work.md +83 -9
- package/sdd/commands/sdd/workstreams.md +0 -63
package/bin/install.js
CHANGED
|
@@ -62,12 +62,17 @@ const hasLocal = args.includes('--local') || args.includes('-l');
|
|
|
62
62
|
const hasOpencode = args.includes('--opencode');
|
|
63
63
|
const hasClaude = args.includes('--claude');
|
|
64
64
|
const hasGemini = args.includes('--gemini');
|
|
65
|
+
const hasKilo = args.includes('--kilo');
|
|
65
66
|
const hasCodex = args.includes('--codex');
|
|
66
67
|
const hasCopilot = args.includes('--copilot');
|
|
67
68
|
const hasAntigravity = args.includes('--antigravity');
|
|
68
69
|
const hasCursor = args.includes('--cursor');
|
|
69
70
|
const hasWindsurf = args.includes('--windsurf');
|
|
70
|
-
const
|
|
71
|
+
const hasAugment = args.includes('--augment');
|
|
72
|
+
const hasTrae = args.includes('--trae');
|
|
73
|
+
const hasQwen = args.includes('--qwen');
|
|
74
|
+
const hasCodebuddy = args.includes('--codebuddy');
|
|
75
|
+
const hasCline = args.includes('--cline');
|
|
71
76
|
const hasBoth = args.includes('--both'); // Legacy flag, keeps working
|
|
72
77
|
const hasAll = args.includes('--all');
|
|
73
78
|
const hasUninstall = args.includes('--uninstall') || args.includes('-u');
|
|
@@ -75,18 +80,24 @@ const hasUninstall = args.includes('--uninstall') || args.includes('-u');
|
|
|
75
80
|
// Runtime selection - can be set by flags or interactive prompt
|
|
76
81
|
let selectedRuntimes = [];
|
|
77
82
|
if (hasAll) {
|
|
78
|
-
selectedRuntimes = ['claude', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf'];
|
|
83
|
+
selectedRuntimes = ['claude', 'kilo', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf', 'augment', 'trae', 'qwen', 'codebuddy', 'cline'];
|
|
79
84
|
} else if (hasBoth) {
|
|
80
85
|
selectedRuntimes = ['claude', 'opencode'];
|
|
81
86
|
} else {
|
|
82
|
-
if (hasOpencode) selectedRuntimes.push('opencode');
|
|
83
87
|
if (hasClaude) selectedRuntimes.push('claude');
|
|
88
|
+
if (hasOpencode) selectedRuntimes.push('opencode');
|
|
84
89
|
if (hasGemini) selectedRuntimes.push('gemini');
|
|
90
|
+
if (hasKilo) selectedRuntimes.push('kilo');
|
|
85
91
|
if (hasCodex) selectedRuntimes.push('codex');
|
|
86
92
|
if (hasCopilot) selectedRuntimes.push('copilot');
|
|
87
93
|
if (hasAntigravity) selectedRuntimes.push('antigravity');
|
|
88
94
|
if (hasCursor) selectedRuntimes.push('cursor');
|
|
89
95
|
if (hasWindsurf) selectedRuntimes.push('windsurf');
|
|
96
|
+
if (hasAugment) selectedRuntimes.push('augment');
|
|
97
|
+
if (hasTrae) selectedRuntimes.push('trae');
|
|
98
|
+
if (hasQwen) selectedRuntimes.push('qwen');
|
|
99
|
+
if (hasCodebuddy) selectedRuntimes.push('codebuddy');
|
|
100
|
+
if (hasCline) selectedRuntimes.push('cline');
|
|
90
101
|
}
|
|
91
102
|
|
|
92
103
|
// WSL + Windows Node.js detection
|
|
@@ -128,10 +139,16 @@ function getDirName(runtime) {
|
|
|
128
139
|
if (runtime === 'copilot') return '.github';
|
|
129
140
|
if (runtime === 'opencode') return '.opencode';
|
|
130
141
|
if (runtime === 'gemini') return '.gemini';
|
|
142
|
+
if (runtime === 'kilo') return '.kilo';
|
|
131
143
|
if (runtime === 'codex') return '.codex';
|
|
132
144
|
if (runtime === 'antigravity') return '.agent';
|
|
133
145
|
if (runtime === 'cursor') return '.cursor';
|
|
134
146
|
if (runtime === 'windsurf') return '.windsurf';
|
|
147
|
+
if (runtime === 'augment') return '.augment';
|
|
148
|
+
if (runtime === 'trae') return '.trae';
|
|
149
|
+
if (runtime === 'qwen') return '.qwen';
|
|
150
|
+
if (runtime === 'codebuddy') return '.codebuddy';
|
|
151
|
+
if (runtime === 'cline') return '.cline';
|
|
135
152
|
return '.claude';
|
|
136
153
|
}
|
|
137
154
|
|
|
@@ -154,6 +171,7 @@ function getConfigDirFromHome(runtime, isGlobal) {
|
|
|
154
171
|
return "'.config', 'opencode'";
|
|
155
172
|
}
|
|
156
173
|
if (runtime === 'gemini') return "'.gemini'";
|
|
174
|
+
if (runtime === 'kilo') return "'.config', 'kilo'";
|
|
157
175
|
if (runtime === 'codex') return "'.codex'";
|
|
158
176
|
if (runtime === 'antigravity') {
|
|
159
177
|
if (!isGlobal) return "'.agent'";
|
|
@@ -161,6 +179,11 @@ function getConfigDirFromHome(runtime, isGlobal) {
|
|
|
161
179
|
}
|
|
162
180
|
if (runtime === 'cursor') return "'.cursor'";
|
|
163
181
|
if (runtime === 'windsurf') return "'.windsurf'";
|
|
182
|
+
if (runtime === 'augment') return "'.augment'";
|
|
183
|
+
if (runtime === 'trae') return "'.trae'";
|
|
184
|
+
if (runtime === 'qwen') return "'.qwen'";
|
|
185
|
+
if (runtime === 'codebuddy') return "'.codebuddy'";
|
|
186
|
+
if (runtime === 'cline') return "'.cline'";
|
|
164
187
|
return "'.claude'";
|
|
165
188
|
}
|
|
166
189
|
|
|
@@ -174,21 +197,46 @@ function getOpencodeGlobalDir() {
|
|
|
174
197
|
if (process.env.OPENCODE_CONFIG_DIR) {
|
|
175
198
|
return expandTilde(process.env.OPENCODE_CONFIG_DIR);
|
|
176
199
|
}
|
|
177
|
-
|
|
200
|
+
|
|
178
201
|
// 2. OPENCODE_CONFIG env var (use its directory)
|
|
179
202
|
if (process.env.OPENCODE_CONFIG) {
|
|
180
203
|
return path.dirname(expandTilde(process.env.OPENCODE_CONFIG));
|
|
181
204
|
}
|
|
182
|
-
|
|
205
|
+
|
|
183
206
|
// 3. XDG_CONFIG_HOME/opencode
|
|
184
207
|
if (process.env.XDG_CONFIG_HOME) {
|
|
185
208
|
return path.join(expandTilde(process.env.XDG_CONFIG_HOME), 'opencode');
|
|
186
209
|
}
|
|
187
|
-
|
|
210
|
+
|
|
188
211
|
// 4. Default: ~/.config/opencode (XDG default)
|
|
189
212
|
return path.join(os.homedir(), '.config', 'opencode');
|
|
190
213
|
}
|
|
191
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Get the global config directory for Kilo
|
|
217
|
+
* Kilo follows XDG Base Directory spec and uses ~/.config/kilo/
|
|
218
|
+
* Priority: KILO_CONFIG_DIR > dirname(KILO_CONFIG) > XDG_CONFIG_HOME/kilo > ~/.config/kilo
|
|
219
|
+
*/
|
|
220
|
+
function getKiloGlobalDir() {
|
|
221
|
+
// 1. Explicit KILO_CONFIG_DIR env var
|
|
222
|
+
if (process.env.KILO_CONFIG_DIR) {
|
|
223
|
+
return expandTilde(process.env.KILO_CONFIG_DIR);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 2. KILO_CONFIG env var (use its directory)
|
|
227
|
+
if (process.env.KILO_CONFIG) {
|
|
228
|
+
return path.dirname(expandTilde(process.env.KILO_CONFIG));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 3. XDG_CONFIG_HOME/kilo
|
|
232
|
+
if (process.env.XDG_CONFIG_HOME) {
|
|
233
|
+
return path.join(expandTilde(process.env.XDG_CONFIG_HOME), 'kilo');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 4. Default: ~/.config/kilo (XDG default)
|
|
237
|
+
return path.join(os.homedir(), '.config', 'kilo');
|
|
238
|
+
}
|
|
239
|
+
|
|
192
240
|
/**
|
|
193
241
|
* Get the global config directory for a runtime
|
|
194
242
|
* @param {string} runtime - 'claude', 'opencode', 'gemini', 'codex', or 'copilot'
|
|
@@ -202,7 +250,15 @@ function getGlobalDir(runtime, explicitDir = null) {
|
|
|
202
250
|
}
|
|
203
251
|
return getOpencodeGlobalDir();
|
|
204
252
|
}
|
|
205
|
-
|
|
253
|
+
|
|
254
|
+
if (runtime === 'kilo') {
|
|
255
|
+
// For Kilo, --config-dir overrides env vars
|
|
256
|
+
if (explicitDir) {
|
|
257
|
+
return expandTilde(explicitDir);
|
|
258
|
+
}
|
|
259
|
+
return getKiloGlobalDir();
|
|
260
|
+
}
|
|
261
|
+
|
|
206
262
|
if (runtime === 'gemini') {
|
|
207
263
|
// Gemini: --config-dir > GEMINI_CONFIG_DIR > ~/.gemini
|
|
208
264
|
if (explicitDir) {
|
|
@@ -259,16 +315,68 @@ function getGlobalDir(runtime, explicitDir = null) {
|
|
|
259
315
|
}
|
|
260
316
|
|
|
261
317
|
if (runtime === 'windsurf') {
|
|
262
|
-
// Windsurf: --config-dir > WINDSURF_CONFIG_DIR > ~/.windsurf
|
|
318
|
+
// Windsurf: --config-dir > WINDSURF_CONFIG_DIR > ~/.codeium/windsurf
|
|
263
319
|
if (explicitDir) {
|
|
264
320
|
return expandTilde(explicitDir);
|
|
265
321
|
}
|
|
266
322
|
if (process.env.WINDSURF_CONFIG_DIR) {
|
|
267
323
|
return expandTilde(process.env.WINDSURF_CONFIG_DIR);
|
|
268
324
|
}
|
|
269
|
-
return path.join(os.homedir(), '.windsurf');
|
|
325
|
+
return path.join(os.homedir(), '.codeium', 'windsurf');
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (runtime === 'augment') {
|
|
329
|
+
// Augment: --config-dir > AUGMENT_CONFIG_DIR > ~/.augment
|
|
330
|
+
if (explicitDir) {
|
|
331
|
+
return expandTilde(explicitDir);
|
|
332
|
+
}
|
|
333
|
+
if (process.env.AUGMENT_CONFIG_DIR) {
|
|
334
|
+
return expandTilde(process.env.AUGMENT_CONFIG_DIR);
|
|
335
|
+
}
|
|
336
|
+
return path.join(os.homedir(), '.augment');
|
|
337
|
+
}
|
|
338
|
+
if (runtime === 'trae') {
|
|
339
|
+
// Trae: --config-dir > TRAE_CONFIG_DIR > ~/.trae
|
|
340
|
+
if (explicitDir) {
|
|
341
|
+
return expandTilde(explicitDir);
|
|
342
|
+
}
|
|
343
|
+
if (process.env.TRAE_CONFIG_DIR) {
|
|
344
|
+
return expandTilde(process.env.TRAE_CONFIG_DIR);
|
|
345
|
+
}
|
|
346
|
+
return path.join(os.homedir(), '.trae');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (runtime === 'qwen') {
|
|
350
|
+
if (explicitDir) {
|
|
351
|
+
return expandTilde(explicitDir);
|
|
352
|
+
}
|
|
353
|
+
if (process.env.QWEN_CONFIG_DIR) {
|
|
354
|
+
return expandTilde(process.env.QWEN_CONFIG_DIR);
|
|
355
|
+
}
|
|
356
|
+
return path.join(os.homedir(), '.qwen');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (runtime === 'codebuddy') {
|
|
360
|
+
// CodeBuddy: --config-dir > CODEBUDDY_CONFIG_DIR > ~/.codebuddy
|
|
361
|
+
if (explicitDir) {
|
|
362
|
+
return expandTilde(explicitDir);
|
|
363
|
+
}
|
|
364
|
+
if (process.env.CODEBUDDY_CONFIG_DIR) {
|
|
365
|
+
return expandTilde(process.env.CODEBUDDY_CONFIG_DIR);
|
|
366
|
+
}
|
|
367
|
+
return path.join(os.homedir(), '.codebuddy');
|
|
270
368
|
}
|
|
271
369
|
|
|
370
|
+
if (runtime === 'cline') {
|
|
371
|
+
// Cline: --config-dir > CLINE_CONFIG_DIR > ~/.cline
|
|
372
|
+
if (explicitDir) {
|
|
373
|
+
return expandTilde(explicitDir);
|
|
374
|
+
}
|
|
375
|
+
if (process.env.CLINE_CONFIG_DIR) {
|
|
376
|
+
return expandTilde(process.env.CLINE_CONFIG_DIR);
|
|
377
|
+
}
|
|
378
|
+
return path.join(os.homedir(), '.cline');
|
|
379
|
+
}
|
|
272
380
|
|
|
273
381
|
// Claude Code: --config-dir > CLAUDE_CONFIG_DIR > ~/.claude
|
|
274
382
|
if (explicitDir) {
|
|
@@ -281,16 +389,16 @@ function getGlobalDir(runtime, explicitDir = null) {
|
|
|
281
389
|
}
|
|
282
390
|
|
|
283
391
|
const banner = '\n' +
|
|
284
|
-
cyan + '
|
|
285
|
-
'
|
|
286
|
-
'
|
|
287
|
-
'
|
|
288
|
-
'
|
|
289
|
-
'
|
|
392
|
+
cyan + ' ███████╗██████╗ ██████╗\n' +
|
|
393
|
+
' ██╔════╝██╔══██╗██╔══██╗\n' +
|
|
394
|
+
' ███████╗██║ ██║██║ ██║\n' +
|
|
395
|
+
' ╚════██║██║ ██║██║ ██║\n' +
|
|
396
|
+
' ███████║██████╔╝██████╔╝\n' +
|
|
397
|
+
' ╚══════╝╚═════╝ ╚═════╝' + reset + '\n' +
|
|
290
398
|
'\n' +
|
|
291
399
|
' Spec-Driven Development ' + dim + 'v' + pkg.version + reset + '\n' +
|
|
292
400
|
' A meta-prompting, context engineering and spec-driven\n' +
|
|
293
|
-
' development system for Claude Code, OpenCode, Gemini, Codex, Copilot, Antigravity, Cursor, and
|
|
401
|
+
' development system for Claude Code, OpenCode, Gemini, Kilo, Codex, Copilot, Antigravity, Cursor, Windsurf, Augment, Trae, Qwen Code, Cline and CodeBuddy by TÂCHES.\n';
|
|
294
402
|
|
|
295
403
|
// Parse --config-dir argument
|
|
296
404
|
function parseConfigDirArg() {
|
|
@@ -328,7 +436,7 @@ if (hasUninstall) {
|
|
|
328
436
|
|
|
329
437
|
// Show help if requested
|
|
330
438
|
if (hasHelp) {
|
|
331
|
-
console.log(` ${yellow}Usage:${reset} npx @bhargavvc/sdd-cc [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}--cursor${reset} Install for Cursor only\n ${cyan}--windsurf${reset} Install for Windsurf only\n ${cyan}--
|
|
439
|
+
console.log(` ${yellow}Usage:${reset} npx @bhargavvc/sdd-cc [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}--kilo${reset} Install for Kilo 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}--cursor${reset} Install for Cursor only\n ${cyan}--windsurf${reset} Install for Windsurf only\n ${cyan}--augment${reset} Install for Augment only\n ${cyan}--trae${reset} Install for Trae only\n ${cyan}--qwen${reset} Install for Qwen Code only\n ${cyan}--cline${reset} Install for Cline only\n ${cyan}--codebuddy${reset} Install for CodeBuddy only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall SDD (remove all SDD 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 @bhargavvc/sdd-cc\n\n ${dim}# Install for Claude Code globally${reset}\n npx @bhargavvc/sdd-cc --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx @bhargavvc/sdd-cc --gemini --global\n\n ${dim}# Install for Kilo globally${reset}\n npx @bhargavvc/sdd-cc --kilo --global\n\n ${dim}# Install for Codex globally${reset}\n npx @bhargavvc/sdd-cc --codex --global\n\n ${dim}# Install for Copilot globally${reset}\n npx @bhargavvc/sdd-cc --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx @bhargavvc/sdd-cc --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx @bhargavvc/sdd-cc --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx @bhargavvc/sdd-cc --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx @bhargavvc/sdd-cc --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx @bhargavvc/sdd-cc --cursor --local\n\n ${dim}# Install for Windsurf globally${reset}\n npx @bhargavvc/sdd-cc --windsurf --global\n\n ${dim}# Install for Windsurf locally${reset}\n npx @bhargavvc/sdd-cc --windsurf --local\n\n ${dim}# Install for Augment globally${reset}\n npx @bhargavvc/sdd-cc --augment --global\n\n ${dim}# Install for Augment locally${reset}\n npx @bhargavvc/sdd-cc --augment --local\n\n ${dim}# Install for Trae globally${reset}\n npx @bhargavvc/sdd-cc --trae --global\n\n ${dim}# Install for Trae locally${reset}\n npx @bhargavvc/sdd-cc --trae --local\n\n ${dim}# Install for Cline locally${reset}\n npx @bhargavvc/sdd-cc --cline --local\n\n ${dim}# Install for CodeBuddy globally${reset}\n npx @bhargavvc/sdd-cc --codebuddy --global\n\n ${dim}# Install for CodeBuddy locally${reset}\n npx @bhargavvc/sdd-cc --codebuddy --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx @bhargavvc/sdd-cc --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx @bhargavvc/sdd-cc --kilo --global --config-dir ~/.kilo-work\n\n ${dim}# Install to current project only${reset}\n npx @bhargavvc/sdd-cc --claude --local\n\n ${dim}# Uninstall SDD from Cursor globally${reset}\n npx @bhargavvc/sdd-cc --cursor --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 / OPENCODE_CONFIG_DIR / GEMINI_CONFIG_DIR / KILO_CONFIG_DIR / CODEX_HOME / COPILOT_CONFIG_DIR / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR / WINDSURF_CONFIG_DIR / AUGMENT_CONFIG_DIR / TRAE_CONFIG_DIR / QWEN_CONFIG_DIR / CLINE_CONFIG_DIR / CODEBUDDY_CONFIG_DIR environment variables.\n`);
|
|
332
440
|
process.exit(0);
|
|
333
441
|
}
|
|
334
442
|
|
|
@@ -349,7 +457,13 @@ function expandTilde(filePath) {
|
|
|
349
457
|
function buildHookCommand(configDir, hookName) {
|
|
350
458
|
// Use forward slashes for Node.js compatibility on all platforms
|
|
351
459
|
const hooksPath = configDir.replace(/\\/g, '/') + '/hooks/' + hookName;
|
|
352
|
-
|
|
460
|
+
// .sh hooks use bash; .js hooks use node. Both wrap the path in double quotes
|
|
461
|
+
// so that paths with spaces (e.g. Windows "C:/Users/First Last/") work correctly
|
|
462
|
+
// (fixes #2045). Routing .sh hooks through this function also ensures they always
|
|
463
|
+
// receive an absolute path rather than the bare relative string that the old manual
|
|
464
|
+
// concatenation produced (fixes #2046).
|
|
465
|
+
const runner = hookName.endsWith('.sh') ? 'bash' : 'node';
|
|
466
|
+
return `${runner} "${hooksPath}"`;
|
|
353
467
|
}
|
|
354
468
|
|
|
355
469
|
/**
|
|
@@ -364,14 +478,89 @@ function resolveOpencodeConfigPath(configDir) {
|
|
|
364
478
|
}
|
|
365
479
|
|
|
366
480
|
/**
|
|
367
|
-
*
|
|
481
|
+
* Resolve the Kilo config file path, preferring .jsonc if it exists.
|
|
482
|
+
*/
|
|
483
|
+
function resolveKiloConfigPath(configDir) {
|
|
484
|
+
const jsoncPath = path.join(configDir, 'kilo.jsonc');
|
|
485
|
+
if (fs.existsSync(jsoncPath)) {
|
|
486
|
+
return jsoncPath;
|
|
487
|
+
}
|
|
488
|
+
return path.join(configDir, 'kilo.json');
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Strip JSONC comments (// and /* */) from a string to produce valid JSON.
|
|
493
|
+
* Handles comments inside strings correctly (does not strip them).
|
|
494
|
+
*/
|
|
495
|
+
function stripJsonComments(text) {
|
|
496
|
+
let result = '';
|
|
497
|
+
let i = 0;
|
|
498
|
+
let inString = false;
|
|
499
|
+
let stringChar = '';
|
|
500
|
+
while (i < text.length) {
|
|
501
|
+
// Handle string literals — don't strip comments inside strings
|
|
502
|
+
if (inString) {
|
|
503
|
+
if (text[i] === '\\') {
|
|
504
|
+
result += text[i] + (text[i + 1] || '');
|
|
505
|
+
i += 2;
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
if (text[i] === stringChar) {
|
|
509
|
+
inString = false;
|
|
510
|
+
}
|
|
511
|
+
result += text[i];
|
|
512
|
+
i++;
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
// Start of string
|
|
516
|
+
if (text[i] === '"' || text[i] === "'") {
|
|
517
|
+
inString = true;
|
|
518
|
+
stringChar = text[i];
|
|
519
|
+
result += text[i];
|
|
520
|
+
i++;
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
// Line comment
|
|
524
|
+
if (text[i] === '/' && text[i + 1] === '/') {
|
|
525
|
+
// Skip to end of line
|
|
526
|
+
while (i < text.length && text[i] !== '\n') i++;
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
// Block comment
|
|
530
|
+
if (text[i] === '/' && text[i + 1] === '*') {
|
|
531
|
+
i += 2;
|
|
532
|
+
while (i < text.length && !(text[i] === '*' && text[i + 1] === '/')) i++;
|
|
533
|
+
i += 2; // skip closing */
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
result += text[i];
|
|
537
|
+
i++;
|
|
538
|
+
}
|
|
539
|
+
// Remove trailing commas before } or ] (common in JSONC)
|
|
540
|
+
return result.replace(/,\s*([}\]])/g, '$1');
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Read and parse settings.json, returning empty object if it doesn't exist.
|
|
545
|
+
* Supports JSONC (JSON with comments) — many CLI tools allow comments in
|
|
546
|
+
* their settings files, so we strip them before parsing to avoid silent
|
|
547
|
+
* data loss from JSON.parse failures.
|
|
368
548
|
*/
|
|
369
549
|
function readSettings(settingsPath) {
|
|
370
550
|
if (fs.existsSync(settingsPath)) {
|
|
371
551
|
try {
|
|
372
|
-
|
|
552
|
+
const raw = fs.readFileSync(settingsPath, 'utf8');
|
|
553
|
+
// Try standard JSON first (fast path)
|
|
554
|
+
try {
|
|
555
|
+
return JSON.parse(raw);
|
|
556
|
+
} catch {
|
|
557
|
+
// Fall back to JSONC stripping
|
|
558
|
+
return JSON.parse(stripJsonComments(raw));
|
|
559
|
+
}
|
|
373
560
|
} catch (e) {
|
|
374
|
-
|
|
561
|
+
// If even JSONC stripping fails, warn instead of silently returning {}
|
|
562
|
+
console.warn(' ' + yellow + '⚠' + reset + ' Warning: Could not parse ' + settingsPath + ' — file may be malformed. Existing settings preserved.');
|
|
563
|
+
return null;
|
|
375
564
|
}
|
|
376
565
|
}
|
|
377
566
|
return {};
|
|
@@ -400,13 +589,16 @@ function getCommitAttribution(runtime) {
|
|
|
400
589
|
|
|
401
590
|
let result;
|
|
402
591
|
|
|
403
|
-
if (runtime === 'opencode') {
|
|
404
|
-
const
|
|
405
|
-
|
|
592
|
+
if (runtime === 'opencode' || runtime === 'kilo') {
|
|
593
|
+
const resolveConfigPath = runtime === 'opencode'
|
|
594
|
+
? resolveOpencodeConfigPath
|
|
595
|
+
: resolveKiloConfigPath;
|
|
596
|
+
const config = readSettings(resolveConfigPath(getGlobalDir(runtime, null)));
|
|
597
|
+
result = (config && config.disable_ai_attribution === true) ? null : undefined;
|
|
406
598
|
} else if (runtime === 'gemini') {
|
|
407
599
|
// Gemini: check gemini settings.json for attribution config
|
|
408
600
|
const settings = readSettings(path.join(getGlobalDir('gemini', explicitConfigDir), 'settings.json'));
|
|
409
|
-
if (!settings.attribution || settings.attribution.commit === undefined) {
|
|
601
|
+
if (!settings || !settings.attribution || settings.attribution.commit === undefined) {
|
|
410
602
|
result = undefined;
|
|
411
603
|
} else if (settings.attribution.commit === '') {
|
|
412
604
|
result = null;
|
|
@@ -416,7 +608,7 @@ function getCommitAttribution(runtime) {
|
|
|
416
608
|
} else if (runtime === 'claude') {
|
|
417
609
|
// Claude Code
|
|
418
610
|
const settings = readSettings(path.join(getGlobalDir('claude', explicitConfigDir), 'settings.json'));
|
|
419
|
-
if (!settings.attribution || settings.attribution.commit === undefined) {
|
|
611
|
+
if (!settings || !settings.attribution || settings.attribution.commit === undefined) {
|
|
420
612
|
result = undefined;
|
|
421
613
|
} else if (settings.attribution.commit === '') {
|
|
422
614
|
result = null;
|
|
@@ -542,6 +734,72 @@ function convertGeminiToolName(claudeTool) {
|
|
|
542
734
|
return claudeTool.toLowerCase();
|
|
543
735
|
}
|
|
544
736
|
|
|
737
|
+
const claudeToKiloAgentPermissions = {
|
|
738
|
+
Read: 'read',
|
|
739
|
+
Write: 'edit',
|
|
740
|
+
Edit: 'edit',
|
|
741
|
+
Bash: 'bash',
|
|
742
|
+
Grep: 'grep',
|
|
743
|
+
Glob: 'glob',
|
|
744
|
+
Task: 'task',
|
|
745
|
+
WebFetch: 'webfetch',
|
|
746
|
+
WebSearch: 'websearch',
|
|
747
|
+
TodoWrite: 'todowrite',
|
|
748
|
+
AskUserQuestion: 'question',
|
|
749
|
+
SlashCommand: 'skill',
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
const kiloAgentPermissionOrder = [
|
|
753
|
+
'read',
|
|
754
|
+
'edit',
|
|
755
|
+
'bash',
|
|
756
|
+
'grep',
|
|
757
|
+
'glob',
|
|
758
|
+
'task',
|
|
759
|
+
'webfetch',
|
|
760
|
+
'websearch',
|
|
761
|
+
'skill',
|
|
762
|
+
'question',
|
|
763
|
+
'todowrite',
|
|
764
|
+
'list',
|
|
765
|
+
'codesearch',
|
|
766
|
+
'lsp',
|
|
767
|
+
];
|
|
768
|
+
|
|
769
|
+
function convertClaudeToKiloPermissionTool(claudeTool) {
|
|
770
|
+
return claudeToKiloAgentPermissions[claudeTool] || null;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function buildKiloAgentPermissionBlock(claudeTools) {
|
|
774
|
+
const allowedPermissions = new Set();
|
|
775
|
+
|
|
776
|
+
for (const tool of claudeTools) {
|
|
777
|
+
const mapped = convertClaudeToKiloPermissionTool(tool);
|
|
778
|
+
if (mapped) {
|
|
779
|
+
allowedPermissions.add(mapped);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const lines = ['permission:'];
|
|
784
|
+
for (const permission of kiloAgentPermissionOrder) {
|
|
785
|
+
lines.push(` ${permission}: ${allowedPermissions.has(permission) ? 'allow' : 'deny'}`);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return lines;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function escapeRegExp(value) {
|
|
792
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function replaceRelativePathReference(content, fromPath, toPath) {
|
|
796
|
+
const escapedPath = escapeRegExp(fromPath);
|
|
797
|
+
return content.replace(
|
|
798
|
+
new RegExp(`(^|[^A-Za-z0-9_./-])${escapedPath}`, 'g'),
|
|
799
|
+
(_, prefix) => `${prefix}${toPath}`,
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
|
|
545
803
|
/**
|
|
546
804
|
* Convert a Claude Code tool name to GitHub Copilot format.
|
|
547
805
|
* - Applies explicit mapping from claudeToCopilotTools
|
|
@@ -579,6 +837,7 @@ function convertClaudeToCopilotContent(content, isGlobal = false) {
|
|
|
579
837
|
} else {
|
|
580
838
|
c = c.replace(/\$HOME\/\.claude\//g, '.github/');
|
|
581
839
|
c = c.replace(/~\/\.claude\//g, '.github/');
|
|
840
|
+
c = c.replace(/~\/\.claude\n/g, '.github/');
|
|
582
841
|
}
|
|
583
842
|
c = c.replace(/\.\/\.claude\//g, './.github/');
|
|
584
843
|
c = c.replace(/\.claude\//g, '.github/');
|
|
@@ -623,6 +882,39 @@ function convertClaudeCommandToCopilotSkill(content, skillName, isGlobal = false
|
|
|
623
882
|
return `${fm}\n${body}`;
|
|
624
883
|
}
|
|
625
884
|
|
|
885
|
+
/**
|
|
886
|
+
* Convert a Claude command (.md) to a Claude skill (SKILL.md).
|
|
887
|
+
* Claude Code is the native format, so minimal conversion needed —
|
|
888
|
+
* preserve allowed-tools as YAML multiline list, preserve argument-hint,
|
|
889
|
+
* convert name from sdd:xxx to sdd-xxx format.
|
|
890
|
+
*/
|
|
891
|
+
function convertClaudeCommandToClaudeSkill(content, skillName) {
|
|
892
|
+
const { frontmatter, body } = extractFrontmatterAndBody(content);
|
|
893
|
+
if (!frontmatter) return content;
|
|
894
|
+
|
|
895
|
+
const description = extractFrontmatterField(frontmatter, 'description') || '';
|
|
896
|
+
const argumentHint = extractFrontmatterField(frontmatter, 'argument-hint');
|
|
897
|
+
const agent = extractFrontmatterField(frontmatter, 'agent');
|
|
898
|
+
|
|
899
|
+
// Preserve allowed-tools as YAML multiline list (Claude native format)
|
|
900
|
+
const toolsMatch = frontmatter.match(/^allowed-tools:\s*\n((?:\s+-\s+.+\n?)*)/m);
|
|
901
|
+
let toolsBlock = '';
|
|
902
|
+
if (toolsMatch) {
|
|
903
|
+
toolsBlock = 'allowed-tools:\n' + toolsMatch[1];
|
|
904
|
+
// Ensure trailing newline
|
|
905
|
+
if (!toolsBlock.endsWith('\n')) toolsBlock += '\n';
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// Reconstruct frontmatter in Claude skill format
|
|
909
|
+
let fm = `---\nname: ${skillName}\ndescription: ${yamlQuote(description)}\n`;
|
|
910
|
+
if (argumentHint) fm += `argument-hint: ${yamlQuote(argumentHint)}\n`;
|
|
911
|
+
if (agent) fm += `agent: ${agent}\n`;
|
|
912
|
+
if (toolsBlock) fm += toolsBlock;
|
|
913
|
+
fm += '---';
|
|
914
|
+
|
|
915
|
+
return `${fm}\n${body}`;
|
|
916
|
+
}
|
|
917
|
+
|
|
626
918
|
/**
|
|
627
919
|
* Convert a Claude agent (.md) to a Copilot agent (.agent.md).
|
|
628
920
|
* Applies tool mapping + deduplication, formats tools as JSON array.
|
|
@@ -878,8 +1170,8 @@ function convertClaudeAgentToCursorAgent(content) {
|
|
|
878
1170
|
}
|
|
879
1171
|
|
|
880
1172
|
// --- Windsurf converters ---
|
|
881
|
-
// Windsurf
|
|
882
|
-
// Config lives in .windsurf/ (local) and ~/.windsurf/ (global).
|
|
1173
|
+
// Windsurf uses a tool set similar to Cursor.
|
|
1174
|
+
// Config lives in .windsurf/ (local) and ~/.codeium/windsurf/ (global).
|
|
883
1175
|
|
|
884
1176
|
// Tool name mapping from Claude Code to Windsurf Cascade
|
|
885
1177
|
const claudeToWindsurfTools = {
|
|
@@ -920,10 +1212,10 @@ function convertClaudeToWindsurfMarkdown(content) {
|
|
|
920
1212
|
converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"');
|
|
921
1213
|
converted = converted.replace(/\$ARGUMENTS\b/g, '{{SDD_ARGS}}');
|
|
922
1214
|
// Replace project-level Claude conventions with Windsurf equivalents
|
|
923
|
-
converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.windsurf/rules
|
|
924
|
-
converted = converted.replace(/\.\/CLAUDE\.md/g, '.windsurf/rules
|
|
925
|
-
converted = converted.replace(/`CLAUDE\.md`/g, '`.windsurf/rules
|
|
926
|
-
converted = converted.replace(/\bCLAUDE\.md\b/g, '.windsurf/rules
|
|
1215
|
+
converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.windsurf/rules`');
|
|
1216
|
+
converted = converted.replace(/\.\/CLAUDE\.md/g, '.windsurf/rules');
|
|
1217
|
+
converted = converted.replace(/`CLAUDE\.md`/g, '`.windsurf/rules`');
|
|
1218
|
+
converted = converted.replace(/\bCLAUDE\.md\b/g, '.windsurf/rules');
|
|
927
1219
|
converted = converted.replace(/\.claude\/skills\//g, '.windsurf/skills/');
|
|
928
1220
|
// Remove Claude Code-specific bug workarounds before brand replacement
|
|
929
1221
|
converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
|
|
@@ -995,67 +1287,98 @@ function convertClaudeAgentToWindsurfAgent(content) {
|
|
|
995
1287
|
return `${cleanFrontmatter}\n${body}`;
|
|
996
1288
|
}
|
|
997
1289
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1290
|
+
// --- Augment converters ---
|
|
1291
|
+
// Augment uses a tool set similar to Cursor/Windsurf.
|
|
1292
|
+
// Config lives in .augment/ (local) and ~/.augment/ (global).
|
|
1293
|
+
|
|
1294
|
+
const claudeToAugmentTools = {
|
|
1295
|
+
Bash: 'launch-process',
|
|
1296
|
+
Edit: 'str-replace-editor',
|
|
1297
|
+
AskUserQuestion: null,
|
|
1298
|
+
SlashCommand: null,
|
|
1299
|
+
TodoWrite: 'add_tasks',
|
|
1300
|
+
};
|
|
1301
|
+
|
|
1302
|
+
function convertAugmentToolName(claudeTool) {
|
|
1303
|
+
if (claudeTool in claudeToAugmentTools) {
|
|
1304
|
+
return claudeToAugmentTools[claudeTool];
|
|
1305
|
+
}
|
|
1306
|
+
if (claudeTool.startsWith('mcp__')) {
|
|
1307
|
+
return claudeTool;
|
|
1308
|
+
}
|
|
1309
|
+
const toolMapping = {
|
|
1310
|
+
Read: 'view',
|
|
1311
|
+
Write: 'save-file',
|
|
1312
|
+
Glob: 'view',
|
|
1313
|
+
Grep: 'grep',
|
|
1314
|
+
Task: null,
|
|
1315
|
+
WebSearch: 'web-search',
|
|
1316
|
+
WebFetch: 'web-fetch',
|
|
1317
|
+
};
|
|
1318
|
+
return toolMapping[claudeTool] || claudeTool;
|
|
1004
1319
|
}
|
|
1005
1320
|
|
|
1006
|
-
function
|
|
1007
|
-
|
|
1321
|
+
function convertSlashCommandsToAugmentSkillMentions(content) {
|
|
1322
|
+
return content.replace(/sdd:/gi, 'sdd-');
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
function convertClaudeToAugmentMarkdown(content) {
|
|
1326
|
+
let converted = convertSlashCommandsToAugmentSkillMentions(content);
|
|
1327
|
+
converted = converted.replace(/\bBash\(/g, 'launch-process(');
|
|
1328
|
+
converted = converted.replace(/\bEdit\(/g, 'str-replace-editor(');
|
|
1329
|
+
converted = converted.replace(/\bRead\(/g, 'view(');
|
|
1330
|
+
converted = converted.replace(/\bWrite\(/g, 'save-file(');
|
|
1331
|
+
converted = converted.replace(/\bTodoWrite\(/g, 'add_tasks(');
|
|
1332
|
+
converted = converted.replace(/\bAskUserQuestion\b/g, 'conversational prompting');
|
|
1333
|
+
// Replace subagent_type from Claude to Augment format
|
|
1334
|
+
converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"');
|
|
1008
1335
|
converted = converted.replace(/\$ARGUMENTS\b/g, '{{SDD_ARGS}}');
|
|
1009
|
-
//
|
|
1010
|
-
converted =
|
|
1336
|
+
// Replace project-level Claude conventions with Augment equivalents
|
|
1337
|
+
converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.augment/rules/`');
|
|
1338
|
+
converted = converted.replace(/\.\/CLAUDE\.md/g, '.augment/rules/');
|
|
1339
|
+
converted = converted.replace(/`CLAUDE\.md`/g, '`.augment/rules/`');
|
|
1340
|
+
converted = converted.replace(/\bCLAUDE\.md\b/g, '.augment/rules/');
|
|
1341
|
+
converted = converted.replace(/\.claude\/skills\//g, '.augment/skills/');
|
|
1342
|
+
// Remove Claude Code-specific bug workarounds before brand replacement
|
|
1343
|
+
converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
|
|
1344
|
+
converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
|
|
1345
|
+
// Replace "Claude Code" brand references with "Augment"
|
|
1346
|
+
converted = converted.replace(/\bClaude Code\b/g, 'Augment');
|
|
1011
1347
|
return converted;
|
|
1012
1348
|
}
|
|
1013
1349
|
|
|
1014
|
-
function
|
|
1015
|
-
|
|
1016
|
-
return `<codex_skill_adapter>
|
|
1350
|
+
function getAugmentSkillAdapterHeader(skillName) {
|
|
1351
|
+
return `<augment_skill_adapter>
|
|
1017
1352
|
## A. Skill Invocation
|
|
1018
|
-
- This skill is invoked
|
|
1019
|
-
- Treat all user text after
|
|
1353
|
+
- This skill is invoked when the user mentions \`${skillName}\` or describes a task matching this skill.
|
|
1354
|
+
- Treat all user text after the skill mention as \`{{SDD_ARGS}}\`.
|
|
1020
1355
|
- If no arguments are present, treat \`{{SDD_ARGS}}\` as empty.
|
|
1021
1356
|
|
|
1022
|
-
## B.
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
-
|
|
1027
|
-
- \`question\` → \`question\`
|
|
1028
|
-
- Options formatted as \`"Label" — description\` → \`{label: "Label", description: "description"}\`
|
|
1029
|
-
- Generate \`id\` from header: lowercase, replace spaces with underscores
|
|
1030
|
-
|
|
1031
|
-
Batched calls:
|
|
1032
|
-
- \`AskUserQuestion([q1, q2])\` → single \`request_user_input\` with multiple entries in \`questions[]\`
|
|
1033
|
-
|
|
1034
|
-
Multi-select workaround:
|
|
1035
|
-
- Codex has no \`multiSelect\`. Use sequential single-selects, or present a numbered freeform list asking the user to enter comma-separated numbers.
|
|
1036
|
-
|
|
1037
|
-
Execute mode fallback:
|
|
1038
|
-
- When \`request_user_input\` is rejected (Execute mode), present a plain-text numbered list and pick a reasonable default.
|
|
1039
|
-
|
|
1040
|
-
## C. Task() → spawn_agent Mapping
|
|
1041
|
-
SDD workflows use \`Task(...)\` (Claude Code syntax). Translate to Codex collaboration tools:
|
|
1042
|
-
|
|
1043
|
-
Direct mapping:
|
|
1044
|
-
- \`Task(subagent_type="X", prompt="Y")\` → \`spawn_agent(agent_type="X", message="Y")\`
|
|
1045
|
-
- \`Task(model="...")\` → omit (Codex uses per-role config, not inline model selection)
|
|
1046
|
-
- \`fork_context: false\` by default — SDD agents load their own context via \`<files_to_read>\` blocks
|
|
1357
|
+
## B. User Prompting
|
|
1358
|
+
When the workflow needs user input, prompt the user conversationally:
|
|
1359
|
+
- Present options as a numbered list in your response text
|
|
1360
|
+
- Ask the user to reply with their choice
|
|
1361
|
+
- For multi-select, ask for comma-separated numbers
|
|
1047
1362
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1363
|
+
## C. Tool Usage
|
|
1364
|
+
Use these Augment tools when executing SDD workflows:
|
|
1365
|
+
- \`launch-process\` for running commands (terminal operations)
|
|
1366
|
+
- \`str-replace-editor\` for editing existing files
|
|
1367
|
+
- \`view\` for reading files and listing directories
|
|
1368
|
+
- \`save-file\` for creating new files
|
|
1369
|
+
- \`grep\` for searching code (or use MCP servers for advanced search)
|
|
1370
|
+
- \`web-search\`, \`web-fetch\` for web queries
|
|
1371
|
+
- \`add_tasks\`, \`view_tasklist\`, \`update_tasks\` for task management
|
|
1050
1372
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
-
|
|
1054
|
-
|
|
1373
|
+
## D. Subagent Spawning
|
|
1374
|
+
When the workflow needs to spawn a subagent:
|
|
1375
|
+
- Use the built-in subagent spawning capability
|
|
1376
|
+
- Define agent prompts in \`.augment/agents/\` directory
|
|
1377
|
+
</augment_skill_adapter>`;
|
|
1055
1378
|
}
|
|
1056
1379
|
|
|
1057
|
-
function
|
|
1058
|
-
const converted =
|
|
1380
|
+
function convertClaudeCommandToAugmentSkill(content, skillName) {
|
|
1381
|
+
const converted = convertClaudeToAugmentMarkdown(content);
|
|
1059
1382
|
const { frontmatter, body } = extractFrontmatterAndBody(converted);
|
|
1060
1383
|
let description = `Run SDD workflow ${skillName}.`;
|
|
1061
1384
|
if (frontmatter) {
|
|
@@ -1066,72 +1389,379 @@ function convertClaudeCommandToCodexSkill(content, skillName) {
|
|
|
1066
1389
|
}
|
|
1067
1390
|
description = toSingleLine(description);
|
|
1068
1391
|
const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
|
|
1069
|
-
const adapter =
|
|
1392
|
+
const adapter = getAugmentSkillAdapterHeader(skillName);
|
|
1070
1393
|
|
|
1071
|
-
return `---\nname: ${
|
|
1394
|
+
return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
|
|
1072
1395
|
}
|
|
1073
1396
|
|
|
1074
1397
|
/**
|
|
1075
|
-
* Convert Claude Code agent markdown to
|
|
1076
|
-
*
|
|
1077
|
-
* and cleans up
|
|
1398
|
+
* Convert Claude Code agent markdown to Augment agent format.
|
|
1399
|
+
* Strips frontmatter fields Augment doesn't support (color, skills),
|
|
1400
|
+
* converts tool references, and cleans up for Augment agents.
|
|
1078
1401
|
*/
|
|
1079
|
-
function
|
|
1080
|
-
let converted =
|
|
1402
|
+
function convertClaudeAgentToAugmentAgent(content) {
|
|
1403
|
+
let converted = convertClaudeToAugmentMarkdown(content);
|
|
1081
1404
|
|
|
1082
1405
|
const { frontmatter, body } = extractFrontmatterAndBody(converted);
|
|
1083
1406
|
if (!frontmatter) return converted;
|
|
1084
1407
|
|
|
1085
1408
|
const name = extractFrontmatterField(frontmatter, 'name') || 'unknown';
|
|
1086
1409
|
const description = extractFrontmatterField(frontmatter, 'description') || '';
|
|
1087
|
-
const tools = extractFrontmatterField(frontmatter, 'tools') || '';
|
|
1088
|
-
|
|
1089
|
-
const roleHeader = `<codex_agent_role>
|
|
1090
|
-
role: ${name}
|
|
1091
|
-
tools: ${tools}
|
|
1092
|
-
purpose: ${toSingleLine(description)}
|
|
1093
|
-
</codex_agent_role>`;
|
|
1094
1410
|
|
|
1095
|
-
const cleanFrontmatter = `---\nname: ${
|
|
1411
|
+
const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`;
|
|
1096
1412
|
|
|
1097
|
-
return `${cleanFrontmatter}\n
|
|
1413
|
+
return `${cleanFrontmatter}\n${body}`;
|
|
1098
1414
|
}
|
|
1099
1415
|
|
|
1100
1416
|
/**
|
|
1101
|
-
*
|
|
1102
|
-
*
|
|
1103
|
-
* from the agent markdown content.
|
|
1417
|
+
* Copy Claude commands as Augment skills — one folder per skill with SKILL.md.
|
|
1418
|
+
* Mirrors copyCommandsAsCursorSkills but uses Augment converters.
|
|
1104
1419
|
*/
|
|
1105
|
-
function
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
const resolvedName = extractFrontmatterField(frontmatterText, 'name') || agentName;
|
|
1110
|
-
const resolvedDescription = toSingleLine(
|
|
1111
|
-
extractFrontmatterField(frontmatterText, 'description') || `SDD agent ${resolvedName}`
|
|
1112
|
-
);
|
|
1113
|
-
const instructions = body.trim();
|
|
1420
|
+
function copyCommandsAsAugmentSkills(srcDir, skillsDir, prefix, pathPrefix, runtime) {
|
|
1421
|
+
if (!fs.existsSync(srcDir)) {
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1114
1424
|
|
|
1115
|
-
|
|
1116
|
-
`name = ${JSON.stringify(resolvedName)}`,
|
|
1117
|
-
`description = ${JSON.stringify(resolvedDescription)}`,
|
|
1118
|
-
`sandbox_mode = "${sandboxMode}"`,
|
|
1119
|
-
// Agent prompts contain raw backslashes in regexes and shell snippets.
|
|
1120
|
-
// TOML literal multiline strings preserve them without escape parsing.
|
|
1121
|
-
`developer_instructions = '''`,
|
|
1122
|
-
instructions,
|
|
1123
|
-
`'''`,
|
|
1124
|
-
];
|
|
1125
|
-
return lines.join('\n') + '\n';
|
|
1126
|
-
}
|
|
1425
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
1127
1426
|
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1427
|
+
// Remove previous SDD Augment skills to avoid stale command skills
|
|
1428
|
+
const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
1429
|
+
for (const entry of existing) {
|
|
1430
|
+
if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
|
|
1431
|
+
fs.rmSync(path.join(skillsDir, entry.name), { recursive: true });
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
function recurse(currentSrcDir, currentPrefix) {
|
|
1436
|
+
const entries = fs.readdirSync(currentSrcDir, { withFileTypes: true });
|
|
1437
|
+
|
|
1438
|
+
for (const entry of entries) {
|
|
1439
|
+
const srcPath = path.join(currentSrcDir, entry.name);
|
|
1440
|
+
if (entry.isDirectory()) {
|
|
1441
|
+
recurse(srcPath, `${currentPrefix}-${entry.name}`);
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
if (!entry.name.endsWith('.md')) {
|
|
1446
|
+
continue;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
const baseName = entry.name.replace('.md', '');
|
|
1450
|
+
const skillName = `${currentPrefix}-${baseName}`;
|
|
1451
|
+
const skillDir = path.join(skillsDir, skillName);
|
|
1452
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
1453
|
+
|
|
1454
|
+
let content = fs.readFileSync(srcPath, 'utf8');
|
|
1455
|
+
const globalClaudeRegex = /~\/\.claude\//g;
|
|
1456
|
+
const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
|
|
1457
|
+
const localClaudeRegex = /\.\/\.claude\//g;
|
|
1458
|
+
const augmentDirRegex = /~\/\.augment\//g;
|
|
1459
|
+
content = content.replace(globalClaudeRegex, pathPrefix);
|
|
1460
|
+
content = content.replace(globalClaudeHomeRegex, pathPrefix);
|
|
1461
|
+
content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
|
|
1462
|
+
content = content.replace(augmentDirRegex, pathPrefix);
|
|
1463
|
+
content = processAttribution(content, getCommitAttribution(runtime));
|
|
1464
|
+
content = convertClaudeCommandToAugmentSkill(content, skillName);
|
|
1465
|
+
|
|
1466
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
recurse(srcDir, prefix);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
function convertSlashCommandsToTraeSkillMentions(content) {
|
|
1474
|
+
return content.replace(/\/sdd:([a-z0-9-]+)/g, (_, commandName) => {
|
|
1475
|
+
return `/sdd-${commandName}`;
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
function convertClaudeToTraeMarkdown(content) {
|
|
1480
|
+
let converted = convertSlashCommandsToTraeSkillMentions(content);
|
|
1481
|
+
converted = converted.replace(/\bBash\(/g, 'Shell(');
|
|
1482
|
+
converted = converted.replace(/\bEdit\(/g, 'StrReplace(');
|
|
1483
|
+
// Replace general-purpose subagent type with Trae's equivalent "general_purpose_task"
|
|
1484
|
+
converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="general_purpose_task"');
|
|
1485
|
+
converted = converted.replace(/\$ARGUMENTS\b/g, '{{SDD_ARGS}}');
|
|
1486
|
+
converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.trae/rules/`');
|
|
1487
|
+
converted = converted.replace(/\.\/CLAUDE\.md/g, '.trae/rules/');
|
|
1488
|
+
converted = converted.replace(/`CLAUDE\.md`/g, '`.trae/rules/`');
|
|
1489
|
+
converted = converted.replace(/\bCLAUDE\.md\b/g, '.trae/rules/');
|
|
1490
|
+
converted = converted.replace(/\.claude\/skills\//g, '.trae/skills/');
|
|
1491
|
+
converted = converted.replace(/\.\/\.claude\//g, './.trae/');
|
|
1492
|
+
converted = converted.replace(/\.claude\//g, '.trae/');
|
|
1493
|
+
converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
|
|
1494
|
+
converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
|
|
1495
|
+
converted = converted.replace(/\bClaude Code\b/g, 'Trae');
|
|
1496
|
+
return converted;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
function convertClaudeCommandToTraeSkill(content, skillName) {
|
|
1500
|
+
const converted = convertClaudeToTraeMarkdown(content);
|
|
1501
|
+
const { frontmatter, body } = extractFrontmatterAndBody(converted);
|
|
1502
|
+
let description = `Run SDD workflow ${skillName}.`;
|
|
1503
|
+
if (frontmatter) {
|
|
1504
|
+
const maybeDescription = extractFrontmatterField(frontmatter, 'description');
|
|
1505
|
+
if (maybeDescription) {
|
|
1506
|
+
description = maybeDescription;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
description = toSingleLine(description);
|
|
1510
|
+
const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
|
|
1511
|
+
return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${shortDescription}\n---\n${body}`;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
function convertClaudeAgentToTraeAgent(content) {
|
|
1515
|
+
let converted = convertClaudeToTraeMarkdown(content);
|
|
1516
|
+
|
|
1517
|
+
const { frontmatter, body } = extractFrontmatterAndBody(converted);
|
|
1518
|
+
if (!frontmatter) return converted;
|
|
1519
|
+
|
|
1520
|
+
const name = extractFrontmatterField(frontmatter, 'name') || 'unknown';
|
|
1521
|
+
const description = extractFrontmatterField(frontmatter, 'description') || '';
|
|
1522
|
+
|
|
1523
|
+
const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`;
|
|
1524
|
+
|
|
1525
|
+
return `${cleanFrontmatter}\n${body}`;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
function convertSlashCommandsToCodebuddySkillMentions(content) {
|
|
1529
|
+
return content.replace(/\/sdd:([a-z0-9-]+)/g, (_, commandName) => {
|
|
1530
|
+
return `/sdd-${commandName}`;
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
function convertClaudeToCodebuddyMarkdown(content) {
|
|
1535
|
+
let converted = convertSlashCommandsToCodebuddySkillMentions(content);
|
|
1536
|
+
// CodeBuddy uses the same tool names as Claude Code (Bash, Edit, Read, Write, etc.)
|
|
1537
|
+
// No tool name conversion needed
|
|
1538
|
+
converted = converted.replace(/\$ARGUMENTS\b/g, '{{SDD_ARGS}}');
|
|
1539
|
+
converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`CODEBUDDY.md`');
|
|
1540
|
+
converted = converted.replace(/\.\/CLAUDE\.md/g, 'CODEBUDDY.md');
|
|
1541
|
+
converted = converted.replace(/`CLAUDE\.md`/g, '`CODEBUDDY.md`');
|
|
1542
|
+
converted = converted.replace(/\bCLAUDE\.md\b/g, 'CODEBUDDY.md');
|
|
1543
|
+
converted = converted.replace(/\.claude\/skills\//g, '.codebuddy/skills/');
|
|
1544
|
+
converted = converted.replace(/\.\/\.claude\//g, './.codebuddy/');
|
|
1545
|
+
converted = converted.replace(/\.claude\//g, '.codebuddy/');
|
|
1546
|
+
converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
|
|
1547
|
+
converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
|
|
1548
|
+
converted = converted.replace(/\bClaude Code\b/g, 'CodeBuddy');
|
|
1549
|
+
return converted;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
function convertClaudeCommandToCodebuddySkill(content, skillName) {
|
|
1553
|
+
const converted = convertClaudeToCodebuddyMarkdown(content);
|
|
1554
|
+
const { frontmatter, body } = extractFrontmatterAndBody(converted);
|
|
1555
|
+
let description = `Run SDD workflow ${skillName}.`;
|
|
1556
|
+
if (frontmatter) {
|
|
1557
|
+
const maybeDescription = extractFrontmatterField(frontmatter, 'description');
|
|
1558
|
+
if (maybeDescription) {
|
|
1559
|
+
description = maybeDescription;
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
description = toSingleLine(description);
|
|
1563
|
+
const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
|
|
1564
|
+
return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${shortDescription}\n---\n${body}`;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
function convertClaudeAgentToCodebuddyAgent(content) {
|
|
1568
|
+
let converted = convertClaudeToCodebuddyMarkdown(content);
|
|
1569
|
+
|
|
1570
|
+
const { frontmatter, body } = extractFrontmatterAndBody(converted);
|
|
1571
|
+
if (!frontmatter) return converted;
|
|
1572
|
+
|
|
1573
|
+
const name = extractFrontmatterField(frontmatter, 'name') || 'unknown';
|
|
1574
|
+
const description = extractFrontmatterField(frontmatter, 'description') || '';
|
|
1575
|
+
|
|
1576
|
+
const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`;
|
|
1577
|
+
|
|
1578
|
+
return `${cleanFrontmatter}\n${body}`;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
// ── Cline converters ────────────────────────────────────────────────────────
|
|
1582
|
+
|
|
1583
|
+
function convertClaudeToCliineMarkdown(content) {
|
|
1584
|
+
let converted = content;
|
|
1585
|
+
// Cline uses the same tool names as Claude Code — no tool name conversion needed
|
|
1586
|
+
converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.clinerules`');
|
|
1587
|
+
converted = converted.replace(/\.\/CLAUDE\.md/g, '.clinerules');
|
|
1588
|
+
converted = converted.replace(/`CLAUDE\.md`/g, '`.clinerules`');
|
|
1589
|
+
converted = converted.replace(/\bCLAUDE\.md\b/g, '.clinerules');
|
|
1590
|
+
converted = converted.replace(/\.claude\/skills\//g, '.cline/skills/');
|
|
1591
|
+
converted = converted.replace(/\.\/\.claude\//g, './.cline/');
|
|
1592
|
+
converted = converted.replace(/\.claude\//g, '.cline/');
|
|
1593
|
+
converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
|
|
1594
|
+
converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
|
|
1595
|
+
converted = converted.replace(/\bClaude Code\b/g, 'Cline');
|
|
1596
|
+
return converted;
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
function convertClaudeAgentToClineAgent(content) {
|
|
1600
|
+
let converted = convertClaudeToCliineMarkdown(content);
|
|
1601
|
+
const { frontmatter, body } = extractFrontmatterAndBody(converted);
|
|
1602
|
+
if (!frontmatter) return converted;
|
|
1603
|
+
const name = extractFrontmatterField(frontmatter, 'name') || 'unknown';
|
|
1604
|
+
const description = extractFrontmatterField(frontmatter, 'description') || '';
|
|
1605
|
+
const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`;
|
|
1606
|
+
return `${cleanFrontmatter}\n${body}`;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// ── End Cline converters ─────────────────────────────────────────────────────
|
|
1610
|
+
|
|
1611
|
+
function convertSlashCommandsToCodexSkillMentions(content) {
|
|
1612
|
+
// Convert colon-style skill invocations to Codex $ prefix
|
|
1613
|
+
let converted = content.replace(/\/sdd:([a-z0-9-]+)/gi, (_, commandName) => {
|
|
1614
|
+
return `$sdd-${String(commandName).toLowerCase()}`;
|
|
1615
|
+
});
|
|
1616
|
+
// Convert hyphen-style command references (workflow output) to Codex $ prefix.
|
|
1617
|
+
// Negative lookbehind excludes file paths like bin/sdd-tools.cjs where
|
|
1618
|
+
// the slash is preceded by a word char, dot, or another slash.
|
|
1619
|
+
converted = converted.replace(/(?<![a-zA-Z0-9./])\/sdd-([a-z0-9-]+)/gi, (_, commandName) => {
|
|
1620
|
+
return `$sdd-${String(commandName).toLowerCase()}`;
|
|
1621
|
+
});
|
|
1622
|
+
return converted;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
function convertClaudeToCodexMarkdown(content) {
|
|
1626
|
+
let converted = convertSlashCommandsToCodexSkillMentions(content);
|
|
1627
|
+
converted = converted.replace(/\$ARGUMENTS\b/g, '{{SDD_ARGS}}');
|
|
1628
|
+
// Remove /clear references — Codex has no equivalent command
|
|
1629
|
+
// Handle backtick-wrapped: `\/clear` then: → (removed)
|
|
1630
|
+
converted = converted.replace(/`\/clear`\s*,?\s*then:?\s*\n?/gi, '');
|
|
1631
|
+
// Handle bare: /clear then: → (removed)
|
|
1632
|
+
converted = converted.replace(/\/clear\s*,?\s*then:?\s*\n?/gi, '');
|
|
1633
|
+
// Handle standalone /clear on its own line
|
|
1634
|
+
converted = converted.replace(/^\s*`?\/clear`?\s*$/gm, '');
|
|
1635
|
+
// Path replacement: .claude → .codex (#1430)
|
|
1636
|
+
converted = converted.replace(/\$HOME\/\.claude\//g, '$HOME/.codex/');
|
|
1637
|
+
converted = converted.replace(/~\/\.claude\//g, '~/.codex/');
|
|
1638
|
+
converted = converted.replace(/\.\/\.claude\//g, './.codex/');
|
|
1639
|
+
// Runtime-neutral agent name replacement (#766)
|
|
1640
|
+
converted = neutralizeAgentReferences(converted, 'AGENTS.md');
|
|
1641
|
+
return converted;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
function getCodexSkillAdapterHeader(skillName) {
|
|
1645
|
+
const invocation = `$${skillName}`;
|
|
1646
|
+
return `<codex_skill_adapter>
|
|
1647
|
+
## A. Skill Invocation
|
|
1648
|
+
- This skill is invoked by mentioning \`${invocation}\`.
|
|
1649
|
+
- Treat all user text after \`${invocation}\` as \`{{SDD_ARGS}}\`.
|
|
1650
|
+
- If no arguments are present, treat \`{{SDD_ARGS}}\` as empty.
|
|
1651
|
+
|
|
1652
|
+
## B. AskUserQuestion → request_user_input Mapping
|
|
1653
|
+
SDD workflows use \`AskUserQuestion\` (Claude Code syntax). Translate to Codex \`request_user_input\`:
|
|
1654
|
+
|
|
1655
|
+
Parameter mapping:
|
|
1656
|
+
- \`header\` → \`header\`
|
|
1657
|
+
- \`question\` → \`question\`
|
|
1658
|
+
- Options formatted as \`"Label" — description\` → \`{label: "Label", description: "description"}\`
|
|
1659
|
+
- Generate \`id\` from header: lowercase, replace spaces with underscores
|
|
1660
|
+
|
|
1661
|
+
Batched calls:
|
|
1662
|
+
- \`AskUserQuestion([q1, q2])\` → single \`request_user_input\` with multiple entries in \`questions[]\`
|
|
1663
|
+
|
|
1664
|
+
Multi-select workaround:
|
|
1665
|
+
- Codex has no \`multiSelect\`. Use sequential single-selects, or present a numbered freeform list asking the user to enter comma-separated numbers.
|
|
1666
|
+
|
|
1667
|
+
Execute mode fallback:
|
|
1668
|
+
- When \`request_user_input\` is rejected (Execute mode), present a plain-text numbered list and pick a reasonable default.
|
|
1669
|
+
|
|
1670
|
+
## C. Task() → spawn_agent Mapping
|
|
1671
|
+
SDD workflows use \`Task(...)\` (Claude Code syntax). Translate to Codex collaboration tools:
|
|
1672
|
+
|
|
1673
|
+
Direct mapping:
|
|
1674
|
+
- \`Task(subagent_type="X", prompt="Y")\` → \`spawn_agent(agent_type="X", message="Y")\`
|
|
1675
|
+
- \`Task(model="...")\` → omit (Codex uses per-role config, not inline model selection)
|
|
1676
|
+
- \`fork_context: false\` by default — SDD agents load their own context via \`<files_to_read>\` blocks
|
|
1677
|
+
|
|
1678
|
+
Parallel fan-out:
|
|
1679
|
+
- Spawn multiple agents → collect agent IDs → \`wait(ids)\` for all to complete
|
|
1680
|
+
|
|
1681
|
+
Result parsing:
|
|
1682
|
+
- Look for structured markers in agent output: \`CHECKPOINT\`, \`PLAN COMPLETE\`, \`SUMMARY\`, etc.
|
|
1683
|
+
- \`close_agent(id)\` after collecting results from each agent
|
|
1684
|
+
</codex_skill_adapter>`;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
function convertClaudeCommandToCodexSkill(content, skillName) {
|
|
1688
|
+
const converted = convertClaudeToCodexMarkdown(content);
|
|
1689
|
+
const { frontmatter, body } = extractFrontmatterAndBody(converted);
|
|
1690
|
+
let description = `Run SDD workflow ${skillName}.`;
|
|
1691
|
+
if (frontmatter) {
|
|
1692
|
+
const maybeDescription = extractFrontmatterField(frontmatter, 'description');
|
|
1693
|
+
if (maybeDescription) {
|
|
1694
|
+
description = maybeDescription;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
description = toSingleLine(description);
|
|
1698
|
+
const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
|
|
1699
|
+
const adapter = getCodexSkillAdapterHeader(skillName);
|
|
1700
|
+
|
|
1701
|
+
return `---\nname: ${yamlQuote(skillName)}\ndescription: ${yamlQuote(description)}\nmetadata:\n short-description: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
/**
|
|
1705
|
+
* Convert Claude Code agent markdown to Codex agent format.
|
|
1706
|
+
* Applies base markdown conversions, then adds a <codex_agent_role> header
|
|
1707
|
+
* and cleans up frontmatter (removes tools/color fields).
|
|
1708
|
+
*/
|
|
1709
|
+
function convertClaudeAgentToCodexAgent(content) {
|
|
1710
|
+
let converted = convertClaudeToCodexMarkdown(content);
|
|
1711
|
+
|
|
1712
|
+
const { frontmatter, body } = extractFrontmatterAndBody(converted);
|
|
1713
|
+
if (!frontmatter) return converted;
|
|
1714
|
+
|
|
1715
|
+
const name = extractFrontmatterField(frontmatter, 'name') || 'unknown';
|
|
1716
|
+
const description = extractFrontmatterField(frontmatter, 'description') || '';
|
|
1717
|
+
const tools = extractFrontmatterField(frontmatter, 'tools') || '';
|
|
1718
|
+
|
|
1719
|
+
const roleHeader = `<codex_agent_role>
|
|
1720
|
+
role: ${name}
|
|
1721
|
+
tools: ${tools}
|
|
1722
|
+
purpose: ${toSingleLine(description)}
|
|
1723
|
+
</codex_agent_role>`;
|
|
1724
|
+
|
|
1725
|
+
const cleanFrontmatter = `---\nname: ${yamlQuote(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`;
|
|
1726
|
+
|
|
1727
|
+
return `${cleanFrontmatter}\n\n${roleHeader}\n${body}`;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
/**
|
|
1731
|
+
* Generate a per-agent .toml config file for Codex.
|
|
1732
|
+
* Sets required agent metadata, sandbox_mode, and developer_instructions
|
|
1733
|
+
* from the agent markdown content.
|
|
1734
|
+
*/
|
|
1735
|
+
function generateCodexAgentToml(agentName, agentContent) {
|
|
1736
|
+
const sandboxMode = CODEX_AGENT_SANDBOX[agentName] || 'read-only';
|
|
1737
|
+
const { frontmatter, body } = extractFrontmatterAndBody(agentContent);
|
|
1738
|
+
const frontmatterText = frontmatter || '';
|
|
1739
|
+
const resolvedName = extractFrontmatterField(frontmatterText, 'name') || agentName;
|
|
1740
|
+
const resolvedDescription = toSingleLine(
|
|
1741
|
+
extractFrontmatterField(frontmatterText, 'description') || `SDD agent ${resolvedName}`
|
|
1742
|
+
);
|
|
1743
|
+
const instructions = body.trim();
|
|
1744
|
+
|
|
1745
|
+
const lines = [
|
|
1746
|
+
`name = ${JSON.stringify(resolvedName)}`,
|
|
1747
|
+
`description = ${JSON.stringify(resolvedDescription)}`,
|
|
1748
|
+
`sandbox_mode = "${sandboxMode}"`,
|
|
1749
|
+
// Agent prompts contain raw backslashes in regexes and shell snippets.
|
|
1750
|
+
// TOML literal multiline strings preserve them without escape parsing.
|
|
1751
|
+
`developer_instructions = '''`,
|
|
1752
|
+
instructions,
|
|
1753
|
+
`'''`,
|
|
1754
|
+
];
|
|
1755
|
+
return lines.join('\n') + '\n';
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
/**
|
|
1759
|
+
* Generate the SDD config block for Codex config.toml.
|
|
1760
|
+
* @param {Array<{name: string, description: string}>} agents
|
|
1761
|
+
*/
|
|
1762
|
+
function generateCodexConfigBlock(agents, targetDir) {
|
|
1763
|
+
// Use absolute paths when targetDir is provided — Codex ≥0.116 requires
|
|
1764
|
+
// AbsolutePathBuf for config_file and cannot resolve relative paths.
|
|
1135
1765
|
const agentsPrefix = targetDir
|
|
1136
1766
|
? path.join(targetDir, 'agents').replace(/\\/g, '/')
|
|
1137
1767
|
: 'agents';
|
|
@@ -1150,7 +1780,7 @@ function generateCodexConfigBlock(agents, targetDir) {
|
|
|
1150
1780
|
return lines.join('\n');
|
|
1151
1781
|
}
|
|
1152
1782
|
|
|
1153
|
-
function
|
|
1783
|
+
function stripCodexSddAgentSections(content) {
|
|
1154
1784
|
return content.replace(/^\[agents\.sdd-[^\]]+\]\n(?:(?!\[)[^\n]*\n?)*/gm, '');
|
|
1155
1785
|
}
|
|
1156
1786
|
|
|
@@ -1158,7 +1788,7 @@ function stripCodexGsdAgentSections(content) {
|
|
|
1158
1788
|
* Strip SDD sections from Codex config.toml content.
|
|
1159
1789
|
* Returns cleaned content, or null if file would be empty.
|
|
1160
1790
|
*/
|
|
1161
|
-
function
|
|
1791
|
+
function stripSddFromCodexConfig(content) {
|
|
1162
1792
|
const eol = detectLineEnding(content);
|
|
1163
1793
|
const markerIndex = content.indexOf(SDD_CODEX_MARKER);
|
|
1164
1794
|
const codexHooksOwnership = getManagedCodexHooksOwnership(content);
|
|
@@ -1184,7 +1814,7 @@ function stripGsdFromCodexConfig(content) {
|
|
|
1184
1814
|
cleaned = cleaned.replace(/^default_mode_request_user_input\s*=\s*true\s*(?:\r?\n)?/m, '');
|
|
1185
1815
|
|
|
1186
1816
|
// Remove [agents.sdd-*] sections (from header to next section or EOF)
|
|
1187
|
-
cleaned =
|
|
1817
|
+
cleaned = stripCodexSddAgentSections(cleaned);
|
|
1188
1818
|
|
|
1189
1819
|
// Remove [features] section if now empty (only header, no keys before next section)
|
|
1190
1820
|
cleaned = cleaned.replace(/^\[features\]\s*\n(?=\[|$)/m, '');
|
|
@@ -1818,7 +2448,7 @@ function setManagedCodexHooksOwnership(content, ownership) {
|
|
|
1818
2448
|
remainder;
|
|
1819
2449
|
}
|
|
1820
2450
|
|
|
1821
|
-
function
|
|
2451
|
+
function isLegacySddAgentsSection(body) {
|
|
1822
2452
|
const lineRecords = getTomlLineRecords(body);
|
|
1823
2453
|
const legacyKeys = new Set(['max_threads', 'max_depth']);
|
|
1824
2454
|
let sawLegacyKey = false;
|
|
@@ -1847,13 +2477,13 @@ function isLegacyGsdAgentsSection(body) {
|
|
|
1847
2477
|
return sawLegacyKey;
|
|
1848
2478
|
}
|
|
1849
2479
|
|
|
1850
|
-
function
|
|
2480
|
+
function stripLeakedSddCodexSections(content) {
|
|
1851
2481
|
const leakedSections = getTomlTableSections(content)
|
|
1852
2482
|
.filter((section) =>
|
|
1853
2483
|
section.path.startsWith('agents.sdd-') ||
|
|
1854
2484
|
(
|
|
1855
2485
|
section.path === 'agents' &&
|
|
1856
|
-
|
|
2486
|
+
isLegacySddAgentsSection(content.slice(section.headerEnd, section.end))
|
|
1857
2487
|
)
|
|
1858
2488
|
);
|
|
1859
2489
|
|
|
@@ -2023,7 +2653,7 @@ function mergeCodexConfig(configPath, sddBlock) {
|
|
|
2023
2653
|
|
|
2024
2654
|
const existing = fs.readFileSync(configPath, 'utf8');
|
|
2025
2655
|
const eol = detectLineEnding(existing);
|
|
2026
|
-
const
|
|
2656
|
+
const normalizedSddBlock = sddBlock.replace(/\r?\n/g, eol);
|
|
2027
2657
|
const markerIndex = existing.indexOf(SDD_CODEX_MARKER);
|
|
2028
2658
|
|
|
2029
2659
|
// Case 2: Has SDD marker — truncate and re-append
|
|
@@ -2031,21 +2661,21 @@ function mergeCodexConfig(configPath, sddBlock) {
|
|
|
2031
2661
|
let before = existing.substring(0, markerIndex).trimEnd();
|
|
2032
2662
|
if (before) {
|
|
2033
2663
|
// Strip any SDD-managed sections that leaked above the marker from previous installs
|
|
2034
|
-
before =
|
|
2664
|
+
before = stripLeakedSddCodexSections(before).trimEnd();
|
|
2035
2665
|
|
|
2036
|
-
fs.writeFileSync(configPath, before + eol + eol +
|
|
2666
|
+
fs.writeFileSync(configPath, before + eol + eol + normalizedSddBlock + eol);
|
|
2037
2667
|
} else {
|
|
2038
|
-
fs.writeFileSync(configPath,
|
|
2668
|
+
fs.writeFileSync(configPath, normalizedSddBlock + eol);
|
|
2039
2669
|
}
|
|
2040
2670
|
return;
|
|
2041
2671
|
}
|
|
2042
2672
|
|
|
2043
2673
|
// Case 3: No marker — append SDD block
|
|
2044
|
-
let content =
|
|
2674
|
+
let content = stripLeakedSddCodexSections(existing).trimEnd();
|
|
2045
2675
|
if (content) {
|
|
2046
|
-
content = content + eol + eol +
|
|
2676
|
+
content = content + eol + eol + normalizedSddBlock + eol;
|
|
2047
2677
|
} else {
|
|
2048
|
-
content =
|
|
2678
|
+
content = normalizedSddBlock + eol;
|
|
2049
2679
|
}
|
|
2050
2680
|
|
|
2051
2681
|
fs.writeFileSync(configPath, content);
|
|
@@ -2303,7 +2933,7 @@ function mergeCopilotInstructions(filePath, sddContent) {
|
|
|
2303
2933
|
* @param {string} content - File content
|
|
2304
2934
|
* @returns {string|null} - Cleaned content or null if empty
|
|
2305
2935
|
*/
|
|
2306
|
-
function
|
|
2936
|
+
function stripSddFromCopilotInstructions(content) {
|
|
2307
2937
|
const openIndex = content.indexOf(SDD_COPILOT_INSTRUCTIONS_MARKER);
|
|
2308
2938
|
const closeIndex = content.indexOf(SDD_COPILOT_INSTRUCTIONS_CLOSE_MARKER);
|
|
2309
2939
|
|
|
@@ -2332,13 +2962,13 @@ function installCodexConfig(targetDir, agentsSrc) {
|
|
|
2332
2962
|
const agents = [];
|
|
2333
2963
|
|
|
2334
2964
|
// Compute the Codex SDD install path (absolute, so subagents with empty $HOME work — #820)
|
|
2335
|
-
const
|
|
2965
|
+
const codexSddPath = `${path.resolve(targetDir, 'sdd').replace(/\\/g, '/')}/`;
|
|
2336
2966
|
|
|
2337
2967
|
for (const file of agentEntries) {
|
|
2338
2968
|
let content = fs.readFileSync(path.join(agentsSrc, file), 'utf8');
|
|
2339
2969
|
// Replace full .claude/sdd prefix so path resolves to codex SDD install
|
|
2340
|
-
content = content.replace(/~\/\.claude\/sdd\//g,
|
|
2341
|
-
content = content.replace(/\$HOME\/\.claude\/sdd\//g,
|
|
2970
|
+
content = content.replace(/~\/\.claude\/sdd\//g, codexSddPath);
|
|
2971
|
+
content = content.replace(/\$HOME\/\.claude\/sdd\//g, codexSddPath);
|
|
2342
2972
|
const { frontmatter } = extractFrontmatterAndBody(content);
|
|
2343
2973
|
const name = extractFrontmatterField(frontmatter, 'name') || file.replace('.md', '');
|
|
2344
2974
|
const description = extractFrontmatterField(frontmatter, 'description') || '';
|
|
@@ -2460,57 +3090,213 @@ function convertClaudeToGeminiAgent(content) {
|
|
|
2460
3090
|
continue;
|
|
2461
3091
|
}
|
|
2462
3092
|
|
|
2463
|
-
// Collect allowed-tools/tools array items
|
|
3093
|
+
// Collect allowed-tools/tools array items
|
|
3094
|
+
if (inAllowedTools) {
|
|
3095
|
+
if (trimmed.startsWith('- ')) {
|
|
3096
|
+
const mapped = convertGeminiToolName(trimmed.substring(2).trim());
|
|
3097
|
+
if (mapped) tools.push(mapped);
|
|
3098
|
+
continue;
|
|
3099
|
+
} else if (trimmed && !trimmed.startsWith('-')) {
|
|
3100
|
+
inAllowedTools = false;
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
|
|
3104
|
+
if (!inAllowedTools) {
|
|
3105
|
+
newLines.push(line);
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
// Add tools as YAML array (Gemini requires array format)
|
|
3110
|
+
if (tools.length > 0) {
|
|
3111
|
+
newLines.push('tools:');
|
|
3112
|
+
for (const tool of tools) {
|
|
3113
|
+
newLines.push(` - ${tool}`);
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
const newFrontmatter = newLines.join('\n').trim();
|
|
3118
|
+
|
|
3119
|
+
// Escape ${VAR} patterns in agent body for Gemini CLI compatibility.
|
|
3120
|
+
// Gemini's templateString() treats all ${word} patterns as template variables
|
|
3121
|
+
// and throws "Template validation failed: Missing required input parameters"
|
|
3122
|
+
// when they can't be resolved. SDD agents use ${PHASE}, ${PLAN}, etc. as
|
|
3123
|
+
// shell variables in bash code blocks — convert to $VAR (no braces) which
|
|
3124
|
+
// is equivalent bash and invisible to Gemini's /\$\{(\w+)\}/g regex.
|
|
3125
|
+
const escapedBody = body.replace(/\$\{(\w+)\}/g, '$$$1');
|
|
3126
|
+
|
|
3127
|
+
// Runtime-neutral agent name replacement (#766)
|
|
3128
|
+
const neutralBody = neutralizeAgentReferences(escapedBody, 'GEMINI.md');
|
|
3129
|
+
return `---\n${newFrontmatter}\n---${stripSubTags(neutralBody)}`;
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
function convertClaudeToOpencodeFrontmatter(content, { isAgent = false } = {}) {
|
|
3133
|
+
// Replace tool name references in content (applies to all files)
|
|
3134
|
+
let convertedContent = content;
|
|
3135
|
+
convertedContent = convertedContent.replace(/\bAskUserQuestion\b/g, 'question');
|
|
3136
|
+
convertedContent = convertedContent.replace(/\bSlashCommand\b/g, 'skill');
|
|
3137
|
+
convertedContent = convertedContent.replace(/\bTodoWrite\b/g, 'todowrite');
|
|
3138
|
+
// Replace /sdd-command colon variant with /sdd-command for opencode (flat command structure)
|
|
3139
|
+
convertedContent = convertedContent.replace(/\/sdd:/g, '/sdd-');
|
|
3140
|
+
// Replace ~/.claude and $HOME/.claude with OpenCode's config location
|
|
3141
|
+
convertedContent = convertedContent.replace(/~\/\.claude\b/g, '~/.config/opencode');
|
|
3142
|
+
convertedContent = convertedContent.replace(/\$HOME\/\.claude\b/g, '$HOME/.config/opencode');
|
|
3143
|
+
// Replace general-purpose subagent type with OpenCode's equivalent "general"
|
|
3144
|
+
convertedContent = convertedContent.replace(/subagent_type="general-purpose"/g, 'subagent_type="general"');
|
|
3145
|
+
// Runtime-neutral agent name replacement (#766)
|
|
3146
|
+
convertedContent = neutralizeAgentReferences(convertedContent, 'AGENTS.md');
|
|
3147
|
+
|
|
3148
|
+
// Check if content has frontmatter
|
|
3149
|
+
if (!convertedContent.startsWith('---')) {
|
|
3150
|
+
return convertedContent;
|
|
3151
|
+
}
|
|
3152
|
+
|
|
3153
|
+
// Find the end of frontmatter
|
|
3154
|
+
const endIndex = convertedContent.indexOf('---', 3);
|
|
3155
|
+
if (endIndex === -1) {
|
|
3156
|
+
return convertedContent;
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
const frontmatter = convertedContent.substring(3, endIndex).trim();
|
|
3160
|
+
const body = convertedContent.substring(endIndex + 3);
|
|
3161
|
+
|
|
3162
|
+
// Parse frontmatter line by line (simple YAML parsing)
|
|
3163
|
+
const lines = frontmatter.split('\n');
|
|
3164
|
+
const newLines = [];
|
|
3165
|
+
let inAllowedTools = false;
|
|
3166
|
+
let inSkippedArray = false;
|
|
3167
|
+
const allowedTools = [];
|
|
3168
|
+
|
|
3169
|
+
for (const line of lines) {
|
|
3170
|
+
const trimmed = line.trim();
|
|
3171
|
+
|
|
3172
|
+
// For agents: skip commented-out lines (e.g. hooks blocks)
|
|
3173
|
+
if (isAgent && trimmed.startsWith('#')) {
|
|
3174
|
+
continue;
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
// Detect start of allowed-tools array
|
|
3178
|
+
if (trimmed.startsWith('allowed-tools:')) {
|
|
3179
|
+
inAllowedTools = true;
|
|
3180
|
+
continue;
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
// Detect inline tools: field (comma-separated string)
|
|
3184
|
+
if (trimmed.startsWith('tools:')) {
|
|
3185
|
+
if (isAgent) {
|
|
3186
|
+
// Agents: strip tools entirely (not supported in OpenCode agent frontmatter)
|
|
3187
|
+
inSkippedArray = true;
|
|
3188
|
+
continue;
|
|
3189
|
+
}
|
|
3190
|
+
const toolsValue = trimmed.substring(6).trim();
|
|
3191
|
+
if (toolsValue) {
|
|
3192
|
+
// Parse comma-separated tools
|
|
3193
|
+
const tools = toolsValue.split(',').map(t => t.trim()).filter(t => t);
|
|
3194
|
+
allowedTools.push(...tools);
|
|
3195
|
+
}
|
|
3196
|
+
continue;
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
// For agents: strip skills:, color:, memory:, maxTurns:, permissionMode:, disallowedTools:
|
|
3200
|
+
if (isAgent && /^(skills|color|memory|maxTurns|permissionMode|disallowedTools):/.test(trimmed)) {
|
|
3201
|
+
inSkippedArray = true;
|
|
3202
|
+
continue;
|
|
3203
|
+
}
|
|
3204
|
+
|
|
3205
|
+
// Skip continuation lines of a stripped array/object field
|
|
3206
|
+
if (inSkippedArray) {
|
|
3207
|
+
if (trimmed.startsWith('- ') || trimmed.startsWith('#') || /^\s/.test(line)) {
|
|
3208
|
+
continue;
|
|
3209
|
+
}
|
|
3210
|
+
inSkippedArray = false;
|
|
3211
|
+
}
|
|
3212
|
+
|
|
3213
|
+
// For commands: remove name: field (opencode uses filename for command name)
|
|
3214
|
+
// For agents: keep name: (required by OpenCode agents)
|
|
3215
|
+
if (!isAgent && trimmed.startsWith('name:')) {
|
|
3216
|
+
continue;
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
// Strip model: field — OpenCode doesn't support Claude Code model aliases
|
|
3220
|
+
// like 'haiku', 'sonnet', 'opus', or 'inherit'. Omitting lets OpenCode use
|
|
3221
|
+
// its configured default model. See #1156.
|
|
3222
|
+
if (trimmed.startsWith('model:')) {
|
|
3223
|
+
continue;
|
|
3224
|
+
}
|
|
3225
|
+
|
|
3226
|
+
// Convert color names to hex for opencode (commands only; agents strip color above)
|
|
3227
|
+
if (trimmed.startsWith('color:')) {
|
|
3228
|
+
const colorValue = trimmed.substring(6).trim().toLowerCase();
|
|
3229
|
+
const hexColor = colorNameToHex[colorValue];
|
|
3230
|
+
if (hexColor) {
|
|
3231
|
+
newLines.push(`color: "${hexColor}"`);
|
|
3232
|
+
} else if (colorValue.startsWith('#')) {
|
|
3233
|
+
// Validate hex color format (#RGB or #RRGGBB)
|
|
3234
|
+
if (/^#[0-9a-f]{3}$|^#[0-9a-f]{6}$/i.test(colorValue)) {
|
|
3235
|
+
// Already hex and valid, keep as is
|
|
3236
|
+
newLines.push(line);
|
|
3237
|
+
}
|
|
3238
|
+
// Skip invalid hex colors
|
|
3239
|
+
}
|
|
3240
|
+
// Skip unknown color names
|
|
3241
|
+
continue;
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3244
|
+
// Collect allowed-tools items
|
|
2464
3245
|
if (inAllowedTools) {
|
|
2465
3246
|
if (trimmed.startsWith('- ')) {
|
|
2466
|
-
|
|
2467
|
-
if (mapped) tools.push(mapped);
|
|
3247
|
+
allowedTools.push(trimmed.substring(2).trim());
|
|
2468
3248
|
continue;
|
|
2469
3249
|
} else if (trimmed && !trimmed.startsWith('-')) {
|
|
3250
|
+
// End of array, new field started
|
|
2470
3251
|
inAllowedTools = false;
|
|
2471
3252
|
}
|
|
2472
3253
|
}
|
|
2473
3254
|
|
|
3255
|
+
// Keep other fields
|
|
2474
3256
|
if (!inAllowedTools) {
|
|
2475
3257
|
newLines.push(line);
|
|
2476
3258
|
}
|
|
2477
3259
|
}
|
|
2478
3260
|
|
|
2479
|
-
//
|
|
2480
|
-
|
|
3261
|
+
// For agents: add required OpenCode agent fields
|
|
3262
|
+
// Note: Do NOT add 'model: inherit' — OpenCode does not recognize the 'inherit'
|
|
3263
|
+
// keyword and throws ProviderModelNotFoundError. Omitting model: lets OpenCode
|
|
3264
|
+
// use its default model for subagents. See #1156.
|
|
3265
|
+
if (isAgent) {
|
|
3266
|
+
newLines.push('mode: subagent');
|
|
3267
|
+
}
|
|
3268
|
+
|
|
3269
|
+
// For commands: add tools object if we had allowed-tools or tools
|
|
3270
|
+
if (!isAgent && allowedTools.length > 0) {
|
|
2481
3271
|
newLines.push('tools:');
|
|
2482
|
-
for (const tool of
|
|
2483
|
-
newLines.push(`
|
|
3272
|
+
for (const tool of allowedTools) {
|
|
3273
|
+
newLines.push(` ${convertToolName(tool)}: true`);
|
|
2484
3274
|
}
|
|
2485
3275
|
}
|
|
2486
3276
|
|
|
3277
|
+
// Rebuild frontmatter (body already has tool names converted)
|
|
2487
3278
|
const newFrontmatter = newLines.join('\n').trim();
|
|
2488
|
-
|
|
2489
|
-
// Escape ${VAR} patterns in agent body for Gemini CLI compatibility.
|
|
2490
|
-
// Gemini's templateString() treats all ${word} patterns as template variables
|
|
2491
|
-
// and throws "Template validation failed: Missing required input parameters"
|
|
2492
|
-
// when they can't be resolved. SDD agents use ${PHASE}, ${PLAN}, etc. as
|
|
2493
|
-
// shell variables in bash code blocks — convert to $VAR (no braces) which
|
|
2494
|
-
// is equivalent bash and invisible to Gemini's /\$\{(\w+)\}/g regex.
|
|
2495
|
-
const escapedBody = body.replace(/\$\{(\w+)\}/g, '$$$1');
|
|
2496
|
-
|
|
2497
|
-
// Runtime-neutral agent name replacement (#766)
|
|
2498
|
-
const neutralBody = neutralizeAgentReferences(escapedBody, 'GEMINI.md');
|
|
2499
|
-
return `---\n${newFrontmatter}\n---${stripSubTags(neutralBody)}`;
|
|
3279
|
+
return `---\n${newFrontmatter}\n---${body}`;
|
|
2500
3280
|
}
|
|
2501
3281
|
|
|
2502
|
-
|
|
3282
|
+
// Kilo CLI — same conversion logic as OpenCode, different config paths.
|
|
3283
|
+
function convertClaudeToKiloFrontmatter(content, { isAgent = false } = {}) {
|
|
2503
3284
|
// Replace tool name references in content (applies to all files)
|
|
2504
3285
|
let convertedContent = content;
|
|
2505
3286
|
convertedContent = convertedContent.replace(/\bAskUserQuestion\b/g, 'question');
|
|
2506
3287
|
convertedContent = convertedContent.replace(/\bSlashCommand\b/g, 'skill');
|
|
2507
3288
|
convertedContent = convertedContent.replace(/\bTodoWrite\b/g, 'todowrite');
|
|
2508
|
-
// Replace /sdd
|
|
3289
|
+
// Replace /sdd-command colon variant with /sdd-command for Kilo (flat command structure)
|
|
2509
3290
|
convertedContent = convertedContent.replace(/\/sdd:/g, '/sdd-');
|
|
2510
|
-
// Replace ~/.claude and $HOME/.claude with
|
|
2511
|
-
convertedContent = convertedContent.replace(/~\/\.claude\b/g, '~/.config/
|
|
2512
|
-
convertedContent = convertedContent.replace(/\$HOME\/\.claude\b/g, '$HOME/.config/
|
|
2513
|
-
|
|
3291
|
+
// Replace ~/.claude and $HOME/.claude with Kilo's config location
|
|
3292
|
+
convertedContent = convertedContent.replace(/~\/\.claude\b/g, '~/.config/kilo');
|
|
3293
|
+
convertedContent = convertedContent.replace(/\$HOME\/\.claude\b/g, '$HOME/.config/kilo');
|
|
3294
|
+
convertedContent = convertedContent.replace(/\.\/\.claude\//g, './.kilo/');
|
|
3295
|
+
// Normalize both Claude skill directory variants to Kilo's canonical skills dir.
|
|
3296
|
+
convertedContent = replaceRelativePathReference(convertedContent, '.claude/skills/', '.kilo/skills/');
|
|
3297
|
+
convertedContent = replaceRelativePathReference(convertedContent, '.agents/skills/', '.kilo/skills/');
|
|
3298
|
+
convertedContent = replaceRelativePathReference(convertedContent, '.claude/agents/', '.kilo/agents/');
|
|
3299
|
+
// Replace general-purpose subagent type with Kilo's equivalent "general"
|
|
2514
3300
|
convertedContent = convertedContent.replace(/subagent_type="general-purpose"/g, 'subagent_type="general"');
|
|
2515
3301
|
// Runtime-neutral agent name replacement (#766)
|
|
2516
3302
|
convertedContent = neutralizeAgentReferences(convertedContent, 'AGENTS.md');
|
|
@@ -2533,8 +3319,10 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false } = {}) {
|
|
|
2533
3319
|
const lines = frontmatter.split('\n');
|
|
2534
3320
|
const newLines = [];
|
|
2535
3321
|
let inAllowedTools = false;
|
|
3322
|
+
let inAgentTools = false;
|
|
2536
3323
|
let inSkippedArray = false;
|
|
2537
3324
|
const allowedTools = [];
|
|
3325
|
+
const agentTools = [];
|
|
2538
3326
|
|
|
2539
3327
|
for (const line of lines) {
|
|
2540
3328
|
const trimmed = line.trim();
|
|
@@ -2550,11 +3338,26 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false } = {}) {
|
|
|
2550
3338
|
continue;
|
|
2551
3339
|
}
|
|
2552
3340
|
|
|
3341
|
+
if (isAgent && inAgentTools) {
|
|
3342
|
+
if (trimmed.startsWith('- ')) {
|
|
3343
|
+
agentTools.push(trimmed.substring(2).trim());
|
|
3344
|
+
continue;
|
|
3345
|
+
}
|
|
3346
|
+
if (trimmed && !trimmed.startsWith('-')) {
|
|
3347
|
+
inAgentTools = false;
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
|
|
2553
3351
|
// Detect inline tools: field (comma-separated string)
|
|
2554
3352
|
if (trimmed.startsWith('tools:')) {
|
|
2555
3353
|
if (isAgent) {
|
|
2556
|
-
|
|
2557
|
-
|
|
3354
|
+
const toolsValue = trimmed.substring(6).trim();
|
|
3355
|
+
if (toolsValue) {
|
|
3356
|
+
const tools = toolsValue.split(',').map(t => t.trim()).filter(t => t);
|
|
3357
|
+
agentTools.push(...tools);
|
|
3358
|
+
} else {
|
|
3359
|
+
inAgentTools = true;
|
|
3360
|
+
}
|
|
2558
3361
|
continue;
|
|
2559
3362
|
}
|
|
2560
3363
|
const toolsValue = trimmed.substring(6).trim();
|
|
@@ -2580,20 +3383,20 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false } = {}) {
|
|
|
2580
3383
|
inSkippedArray = false;
|
|
2581
3384
|
}
|
|
2582
3385
|
|
|
2583
|
-
// For commands: remove name: field (
|
|
2584
|
-
// For agents: keep name: (required by
|
|
3386
|
+
// For commands: remove name: field (Kilo uses filename for command name)
|
|
3387
|
+
// For agents: keep name: (required by Kilo agents)
|
|
2585
3388
|
if (!isAgent && trimmed.startsWith('name:')) {
|
|
2586
3389
|
continue;
|
|
2587
3390
|
}
|
|
2588
3391
|
|
|
2589
|
-
// Strip model: field —
|
|
2590
|
-
// like 'haiku', 'sonnet', 'opus', or 'inherit'. Omitting lets
|
|
2591
|
-
// its configured default model.
|
|
3392
|
+
// Strip model: field — Kilo doesn't support Claude Code model aliases
|
|
3393
|
+
// like 'haiku', 'sonnet', 'opus', or 'inherit'. Omitting lets Kilo use
|
|
3394
|
+
// its configured default model.
|
|
2592
3395
|
if (trimmed.startsWith('model:')) {
|
|
2593
3396
|
continue;
|
|
2594
3397
|
}
|
|
2595
3398
|
|
|
2596
|
-
// Convert color names to hex for
|
|
3399
|
+
// Convert color names to hex for Kilo (commands only; agents strip color above)
|
|
2597
3400
|
if (trimmed.startsWith('color:')) {
|
|
2598
3401
|
const colorValue = trimmed.substring(6).trim().toLowerCase();
|
|
2599
3402
|
const hexColor = colorNameToHex[colorValue];
|
|
@@ -2614,7 +3417,12 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false } = {}) {
|
|
|
2614
3417
|
// Collect allowed-tools items
|
|
2615
3418
|
if (inAllowedTools) {
|
|
2616
3419
|
if (trimmed.startsWith('- ')) {
|
|
2617
|
-
|
|
3420
|
+
const tool = trimmed.substring(2).trim();
|
|
3421
|
+
if (isAgent) {
|
|
3422
|
+
agentTools.push(tool);
|
|
3423
|
+
} else {
|
|
3424
|
+
allowedTools.push(tool);
|
|
3425
|
+
}
|
|
2618
3426
|
continue;
|
|
2619
3427
|
} else if (trimmed && !trimmed.startsWith('-')) {
|
|
2620
3428
|
// End of array, new field started
|
|
@@ -2628,12 +3436,10 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false } = {}) {
|
|
|
2628
3436
|
}
|
|
2629
3437
|
}
|
|
2630
3438
|
|
|
2631
|
-
// For agents: add required
|
|
2632
|
-
// Note: Do NOT add 'model: inherit' — OpenCode does not recognize the 'inherit'
|
|
2633
|
-
// keyword and throws ProviderModelNotFoundError. Omitting model: lets OpenCode
|
|
2634
|
-
// use its default model for subagents. See #1156.
|
|
3439
|
+
// For agents: add required Kilo agent fields
|
|
2635
3440
|
if (isAgent) {
|
|
2636
3441
|
newLines.push('mode: subagent');
|
|
3442
|
+
newLines.push(...buildKiloAgentPermissionBlock(agentTools));
|
|
2637
3443
|
}
|
|
2638
3444
|
|
|
2639
3445
|
// For commands: add tools object if we had allowed-tools or tools
|
|
@@ -2667,7 +3473,7 @@ function convertClaudeToGeminiToml(content) {
|
|
|
2667
3473
|
|
|
2668
3474
|
const frontmatter = content.substring(3, endIndex).trim();
|
|
2669
3475
|
const body = content.substring(endIndex + 3).trim();
|
|
2670
|
-
|
|
3476
|
+
|
|
2671
3477
|
// Extract description from frontmatter
|
|
2672
3478
|
let description = '';
|
|
2673
3479
|
const lines = frontmatter.split('\n');
|
|
@@ -2684,9 +3490,9 @@ function convertClaudeToGeminiToml(content) {
|
|
|
2684
3490
|
if (description) {
|
|
2685
3491
|
toml += `description = ${JSON.stringify(description)}\n`;
|
|
2686
3492
|
}
|
|
2687
|
-
|
|
3493
|
+
|
|
2688
3494
|
toml += `prompt = ${JSON.stringify(body)}\n`;
|
|
2689
|
-
|
|
3495
|
+
|
|
2690
3496
|
return toml;
|
|
2691
3497
|
}
|
|
2692
3498
|
|
|
@@ -2699,74 +3505,240 @@ function convertClaudeToGeminiToml(content) {
|
|
|
2699
3505
|
* @param {string} destDir - Destination directory (e.g., command/)
|
|
2700
3506
|
* @param {string} prefix - Prefix for filenames (e.g., 'sdd')
|
|
2701
3507
|
* @param {string} pathPrefix - Path prefix for file references
|
|
2702
|
-
* @param {string} runtime - Target runtime ('claude' or '
|
|
3508
|
+
* @param {string} runtime - Target runtime ('claude', 'opencode', or 'kilo')
|
|
2703
3509
|
*/
|
|
2704
3510
|
function copyFlattenedCommands(srcDir, destDir, prefix, pathPrefix, runtime) {
|
|
2705
3511
|
if (!fs.existsSync(srcDir)) {
|
|
2706
3512
|
return;
|
|
2707
3513
|
}
|
|
2708
|
-
|
|
3514
|
+
|
|
2709
3515
|
// Remove old sdd-*.md files before copying new ones
|
|
2710
3516
|
if (fs.existsSync(destDir)) {
|
|
2711
3517
|
for (const file of fs.readdirSync(destDir)) {
|
|
2712
3518
|
if (file.startsWith(`${prefix}-`) && file.endsWith('.md')) {
|
|
2713
3519
|
fs.unlinkSync(path.join(destDir, file));
|
|
2714
3520
|
}
|
|
2715
|
-
}
|
|
2716
|
-
} else {
|
|
2717
|
-
fs.mkdirSync(destDir, { recursive: true });
|
|
2718
|
-
}
|
|
2719
|
-
|
|
2720
|
-
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
2721
|
-
|
|
2722
|
-
for (const entry of entries) {
|
|
2723
|
-
const srcPath = path.join(srcDir, entry.name);
|
|
2724
|
-
|
|
2725
|
-
if (entry.isDirectory()) {
|
|
2726
|
-
// Recurse into subdirectories, adding to prefix
|
|
2727
|
-
// e.g., commands/sdd/debug/start.md -> command/sdd-debug-start.md
|
|
2728
|
-
copyFlattenedCommands(srcPath, destDir, `${prefix}-${entry.name}`, pathPrefix, runtime);
|
|
2729
|
-
} else if (entry.name.endsWith('.md')) {
|
|
2730
|
-
// Flatten: help.md -> sdd-help.md
|
|
3521
|
+
}
|
|
3522
|
+
} else {
|
|
3523
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3526
|
+
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
3527
|
+
|
|
3528
|
+
for (const entry of entries) {
|
|
3529
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
3530
|
+
|
|
3531
|
+
if (entry.isDirectory()) {
|
|
3532
|
+
// Recurse into subdirectories, adding to prefix
|
|
3533
|
+
// e.g., commands/sdd/debug/start.md -> command/sdd-debug-start.md
|
|
3534
|
+
copyFlattenedCommands(srcPath, destDir, `${prefix}-${entry.name}`, pathPrefix, runtime);
|
|
3535
|
+
} else if (entry.name.endsWith('.md')) {
|
|
3536
|
+
// Flatten: help.md -> sdd-help.md
|
|
3537
|
+
const baseName = entry.name.replace('.md', '');
|
|
3538
|
+
const destName = `${prefix}-${baseName}.md`;
|
|
3539
|
+
const destPath = path.join(destDir, destName);
|
|
3540
|
+
|
|
3541
|
+
let content = fs.readFileSync(srcPath, 'utf8');
|
|
3542
|
+
const globalClaudeRegex = /~\/\.claude\//g;
|
|
3543
|
+
const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
|
|
3544
|
+
const localClaudeRegex = /\.\/\.claude\//g;
|
|
3545
|
+
const opencodeDirRegex = /~\/\.opencode\//g;
|
|
3546
|
+
const kiloDirRegex = /~\/\.kilo\//g;
|
|
3547
|
+
content = content.replace(globalClaudeRegex, pathPrefix);
|
|
3548
|
+
content = content.replace(globalClaudeHomeRegex, pathPrefix);
|
|
3549
|
+
content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
|
|
3550
|
+
content = content.replace(opencodeDirRegex, pathPrefix);
|
|
3551
|
+
content = content.replace(kiloDirRegex, pathPrefix);
|
|
3552
|
+
content = processAttribution(content, getCommitAttribution(runtime));
|
|
3553
|
+
content = runtime === 'kilo'
|
|
3554
|
+
? convertClaudeToKiloFrontmatter(content)
|
|
3555
|
+
: convertClaudeToOpencodeFrontmatter(content);
|
|
3556
|
+
|
|
3557
|
+
fs.writeFileSync(destPath, content);
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
|
|
3562
|
+
function listCodexSkillNames(skillsDir, prefix = 'sdd-') {
|
|
3563
|
+
if (!fs.existsSync(skillsDir)) return [];
|
|
3564
|
+
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
3565
|
+
return entries
|
|
3566
|
+
.filter(entry => entry.isDirectory() && entry.name.startsWith(prefix))
|
|
3567
|
+
.filter(entry => fs.existsSync(path.join(skillsDir, entry.name, 'SKILL.md')))
|
|
3568
|
+
.map(entry => entry.name)
|
|
3569
|
+
.sort();
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3572
|
+
function copyCommandsAsCodexSkills(srcDir, skillsDir, prefix, pathPrefix, runtime) {
|
|
3573
|
+
if (!fs.existsSync(srcDir)) {
|
|
3574
|
+
return;
|
|
3575
|
+
}
|
|
3576
|
+
|
|
3577
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
3578
|
+
|
|
3579
|
+
// Remove previous SDD Codex skills to avoid stale command skills.
|
|
3580
|
+
const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
3581
|
+
for (const entry of existing) {
|
|
3582
|
+
if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
|
|
3583
|
+
fs.rmSync(path.join(skillsDir, entry.name), { recursive: true });
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
|
|
3587
|
+
function recurse(currentSrcDir, currentPrefix) {
|
|
3588
|
+
const entries = fs.readdirSync(currentSrcDir, { withFileTypes: true });
|
|
3589
|
+
|
|
3590
|
+
for (const entry of entries) {
|
|
3591
|
+
const srcPath = path.join(currentSrcDir, entry.name);
|
|
3592
|
+
if (entry.isDirectory()) {
|
|
3593
|
+
recurse(srcPath, `${currentPrefix}-${entry.name}`);
|
|
3594
|
+
continue;
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3597
|
+
if (!entry.name.endsWith('.md')) {
|
|
3598
|
+
continue;
|
|
3599
|
+
}
|
|
3600
|
+
|
|
3601
|
+
const baseName = entry.name.replace('.md', '');
|
|
3602
|
+
const skillName = `${currentPrefix}-${baseName}`;
|
|
3603
|
+
const skillDir = path.join(skillsDir, skillName);
|
|
3604
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
3605
|
+
|
|
3606
|
+
let content = fs.readFileSync(srcPath, 'utf8');
|
|
3607
|
+
const globalClaudeRegex = /~\/\.claude\//g;
|
|
3608
|
+
const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
|
|
3609
|
+
const localClaudeRegex = /\.\/\.claude\//g;
|
|
3610
|
+
const codexDirRegex = /~\/\.codex\//g;
|
|
3611
|
+
content = content.replace(globalClaudeRegex, pathPrefix);
|
|
3612
|
+
content = content.replace(globalClaudeHomeRegex, pathPrefix);
|
|
3613
|
+
content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
|
|
3614
|
+
content = content.replace(codexDirRegex, pathPrefix);
|
|
3615
|
+
content = processAttribution(content, getCommitAttribution(runtime));
|
|
3616
|
+
content = convertClaudeCommandToCodexSkill(content, skillName);
|
|
3617
|
+
|
|
3618
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
|
|
3622
|
+
recurse(srcDir, prefix);
|
|
3623
|
+
}
|
|
3624
|
+
|
|
3625
|
+
function copyCommandsAsCursorSkills(srcDir, skillsDir, prefix, pathPrefix, runtime) {
|
|
3626
|
+
if (!fs.existsSync(srcDir)) {
|
|
3627
|
+
return;
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3630
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
3631
|
+
|
|
3632
|
+
// Remove previous SDD Cursor skills to avoid stale command skills
|
|
3633
|
+
const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
3634
|
+
for (const entry of existing) {
|
|
3635
|
+
if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
|
|
3636
|
+
fs.rmSync(path.join(skillsDir, entry.name), { recursive: true });
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
|
|
3640
|
+
function recurse(currentSrcDir, currentPrefix) {
|
|
3641
|
+
const entries = fs.readdirSync(currentSrcDir, { withFileTypes: true });
|
|
3642
|
+
|
|
3643
|
+
for (const entry of entries) {
|
|
3644
|
+
const srcPath = path.join(currentSrcDir, entry.name);
|
|
3645
|
+
if (entry.isDirectory()) {
|
|
3646
|
+
recurse(srcPath, `${currentPrefix}-${entry.name}`);
|
|
3647
|
+
continue;
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
if (!entry.name.endsWith('.md')) {
|
|
3651
|
+
continue;
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3654
|
+
const baseName = entry.name.replace('.md', '');
|
|
3655
|
+
const skillName = `${currentPrefix}-${baseName}`;
|
|
3656
|
+
const skillDir = path.join(skillsDir, skillName);
|
|
3657
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
3658
|
+
|
|
3659
|
+
let content = fs.readFileSync(srcPath, 'utf8');
|
|
3660
|
+
const globalClaudeRegex = /~\/\.claude\//g;
|
|
3661
|
+
const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
|
|
3662
|
+
const localClaudeRegex = /\.\/\.claude\//g;
|
|
3663
|
+
const cursorDirRegex = /~\/\.cursor\//g;
|
|
3664
|
+
content = content.replace(globalClaudeRegex, pathPrefix);
|
|
3665
|
+
content = content.replace(globalClaudeHomeRegex, pathPrefix);
|
|
3666
|
+
content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
|
|
3667
|
+
content = content.replace(cursorDirRegex, pathPrefix);
|
|
3668
|
+
content = processAttribution(content, getCommitAttribution(runtime));
|
|
3669
|
+
content = convertClaudeCommandToCursorSkill(content, skillName);
|
|
3670
|
+
|
|
3671
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
|
|
3675
|
+
recurse(srcDir, prefix);
|
|
3676
|
+
}
|
|
3677
|
+
|
|
3678
|
+
/**
|
|
3679
|
+
* Copy Claude commands as Windsurf skills — one folder per skill with SKILL.md.
|
|
3680
|
+
* Mirrors copyCommandsAsCursorSkills but uses Windsurf converters.
|
|
3681
|
+
*/
|
|
3682
|
+
function copyCommandsAsWindsurfSkills(srcDir, skillsDir, prefix, pathPrefix, runtime) {
|
|
3683
|
+
if (!fs.existsSync(srcDir)) {
|
|
3684
|
+
return;
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3687
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
3688
|
+
|
|
3689
|
+
// Remove previous SDD Windsurf skills to avoid stale command skills
|
|
3690
|
+
const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
3691
|
+
for (const entry of existing) {
|
|
3692
|
+
if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
|
|
3693
|
+
fs.rmSync(path.join(skillsDir, entry.name), { recursive: true });
|
|
3694
|
+
}
|
|
3695
|
+
}
|
|
3696
|
+
|
|
3697
|
+
function recurse(currentSrcDir, currentPrefix) {
|
|
3698
|
+
const entries = fs.readdirSync(currentSrcDir, { withFileTypes: true });
|
|
3699
|
+
|
|
3700
|
+
for (const entry of entries) {
|
|
3701
|
+
const srcPath = path.join(currentSrcDir, entry.name);
|
|
3702
|
+
if (entry.isDirectory()) {
|
|
3703
|
+
recurse(srcPath, `${currentPrefix}-${entry.name}`);
|
|
3704
|
+
continue;
|
|
3705
|
+
}
|
|
3706
|
+
|
|
3707
|
+
if (!entry.name.endsWith('.md')) {
|
|
3708
|
+
continue;
|
|
3709
|
+
}
|
|
3710
|
+
|
|
2731
3711
|
const baseName = entry.name.replace('.md', '');
|
|
2732
|
-
const
|
|
2733
|
-
const
|
|
3712
|
+
const skillName = `${currentPrefix}-${baseName}`;
|
|
3713
|
+
const skillDir = path.join(skillsDir, skillName);
|
|
3714
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
2734
3715
|
|
|
2735
3716
|
let content = fs.readFileSync(srcPath, 'utf8');
|
|
2736
3717
|
const globalClaudeRegex = /~\/\.claude\//g;
|
|
2737
3718
|
const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
|
|
2738
3719
|
const localClaudeRegex = /\.\/\.claude\//g;
|
|
2739
|
-
const
|
|
3720
|
+
const windsurfDirRegex = /~\/\.codeium\/windsurf\//g;
|
|
2740
3721
|
content = content.replace(globalClaudeRegex, pathPrefix);
|
|
2741
3722
|
content = content.replace(globalClaudeHomeRegex, pathPrefix);
|
|
2742
3723
|
content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
|
|
2743
|
-
content = content.replace(
|
|
3724
|
+
content = content.replace(windsurfDirRegex, pathPrefix);
|
|
2744
3725
|
content = processAttribution(content, getCommitAttribution(runtime));
|
|
2745
|
-
content =
|
|
3726
|
+
content = convertClaudeCommandToWindsurfSkill(content, skillName);
|
|
2746
3727
|
|
|
2747
|
-
fs.writeFileSync(
|
|
3728
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
|
|
2748
3729
|
}
|
|
2749
3730
|
}
|
|
2750
|
-
}
|
|
2751
3731
|
|
|
2752
|
-
|
|
2753
|
-
if (!fs.existsSync(skillsDir)) return [];
|
|
2754
|
-
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
2755
|
-
return entries
|
|
2756
|
-
.filter(entry => entry.isDirectory() && entry.name.startsWith(prefix))
|
|
2757
|
-
.filter(entry => fs.existsSync(path.join(skillsDir, entry.name, 'SKILL.md')))
|
|
2758
|
-
.map(entry => entry.name)
|
|
2759
|
-
.sort();
|
|
3732
|
+
recurse(srcDir, prefix);
|
|
2760
3733
|
}
|
|
2761
3734
|
|
|
2762
|
-
function
|
|
3735
|
+
function copyCommandsAsTraeSkills(srcDir, skillsDir, prefix, pathPrefix, runtime) {
|
|
2763
3736
|
if (!fs.existsSync(srcDir)) {
|
|
2764
3737
|
return;
|
|
2765
3738
|
}
|
|
2766
3739
|
|
|
2767
3740
|
fs.mkdirSync(skillsDir, { recursive: true });
|
|
2768
3741
|
|
|
2769
|
-
// Remove previous SDD Codex skills to avoid stale command skills.
|
|
2770
3742
|
const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
2771
3743
|
for (const entry of existing) {
|
|
2772
3744
|
if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
|
|
@@ -2797,13 +3769,20 @@ function copyCommandsAsCodexSkills(srcDir, skillsDir, prefix, pathPrefix, runtim
|
|
|
2797
3769
|
const globalClaudeRegex = /~\/\.claude\//g;
|
|
2798
3770
|
const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
|
|
2799
3771
|
const localClaudeRegex = /\.\/\.claude\//g;
|
|
2800
|
-
const
|
|
3772
|
+
const bareGlobalClaudeRegex = /~\/\.claude\b/g;
|
|
3773
|
+
const bareGlobalClaudeHomeRegex = /\$HOME\/\.claude\b/g;
|
|
3774
|
+
const bareLocalClaudeRegex = /\.\/\.claude\b/g;
|
|
3775
|
+
const traeDirRegex = /~\/\.trae\//g;
|
|
3776
|
+
const normalizedPathPrefix = pathPrefix.replace(/\/$/, '');
|
|
2801
3777
|
content = content.replace(globalClaudeRegex, pathPrefix);
|
|
2802
3778
|
content = content.replace(globalClaudeHomeRegex, pathPrefix);
|
|
2803
3779
|
content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
|
|
2804
|
-
content = content.replace(
|
|
3780
|
+
content = content.replace(bareGlobalClaudeRegex, normalizedPathPrefix);
|
|
3781
|
+
content = content.replace(bareGlobalClaudeHomeRegex, normalizedPathPrefix);
|
|
3782
|
+
content = content.replace(bareLocalClaudeRegex, `./${getDirName(runtime)}`);
|
|
3783
|
+
content = content.replace(traeDirRegex, pathPrefix);
|
|
2805
3784
|
content = processAttribution(content, getCommitAttribution(runtime));
|
|
2806
|
-
content =
|
|
3785
|
+
content = convertClaudeCommandToTraeSkill(content, skillName);
|
|
2807
3786
|
|
|
2808
3787
|
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
|
|
2809
3788
|
}
|
|
@@ -2812,14 +3791,17 @@ function copyCommandsAsCodexSkills(srcDir, skillsDir, prefix, pathPrefix, runtim
|
|
|
2812
3791
|
recurse(srcDir, prefix);
|
|
2813
3792
|
}
|
|
2814
3793
|
|
|
2815
|
-
|
|
3794
|
+
/**
|
|
3795
|
+
* Copy Claude commands as CodeBuddy skills — one folder per skill with SKILL.md.
|
|
3796
|
+
* CodeBuddy uses the same tool names as Claude Code, but has its own config directory structure.
|
|
3797
|
+
*/
|
|
3798
|
+
function copyCommandsAsCodebuddySkills(srcDir, skillsDir, prefix, pathPrefix, runtime) {
|
|
2816
3799
|
if (!fs.existsSync(srcDir)) {
|
|
2817
3800
|
return;
|
|
2818
3801
|
}
|
|
2819
3802
|
|
|
2820
3803
|
fs.mkdirSync(skillsDir, { recursive: true });
|
|
2821
3804
|
|
|
2822
|
-
// Remove previous SDD Cursor skills to avoid stale command skills
|
|
2823
3805
|
const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
2824
3806
|
for (const entry of existing) {
|
|
2825
3807
|
if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
|
|
@@ -2850,13 +3832,20 @@ function copyCommandsAsCursorSkills(srcDir, skillsDir, prefix, pathPrefix, runti
|
|
|
2850
3832
|
const globalClaudeRegex = /~\/\.claude\//g;
|
|
2851
3833
|
const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
|
|
2852
3834
|
const localClaudeRegex = /\.\/\.claude\//g;
|
|
2853
|
-
const
|
|
3835
|
+
const bareGlobalClaudeRegex = /~\/\.claude\b/g;
|
|
3836
|
+
const bareGlobalClaudeHomeRegex = /\$HOME\/\.claude\b/g;
|
|
3837
|
+
const bareLocalClaudeRegex = /\.\/\.claude\b/g;
|
|
3838
|
+
const codebuddyDirRegex = /~\/\.codebuddy\//g;
|
|
3839
|
+
const normalizedPathPrefix = pathPrefix.replace(/\/$/, '');
|
|
2854
3840
|
content = content.replace(globalClaudeRegex, pathPrefix);
|
|
2855
3841
|
content = content.replace(globalClaudeHomeRegex, pathPrefix);
|
|
2856
3842
|
content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
|
|
2857
|
-
content = content.replace(
|
|
3843
|
+
content = content.replace(bareGlobalClaudeRegex, normalizedPathPrefix);
|
|
3844
|
+
content = content.replace(bareGlobalClaudeHomeRegex, normalizedPathPrefix);
|
|
3845
|
+
content = content.replace(bareLocalClaudeRegex, `./${getDirName(runtime)}`);
|
|
3846
|
+
content = content.replace(codebuddyDirRegex, pathPrefix);
|
|
2858
3847
|
content = processAttribution(content, getCommitAttribution(runtime));
|
|
2859
|
-
content =
|
|
3848
|
+
content = convertClaudeCommandToCodebuddySkill(content, skillName);
|
|
2860
3849
|
|
|
2861
3850
|
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
|
|
2862
3851
|
}
|
|
@@ -2866,17 +3855,17 @@ function copyCommandsAsCursorSkills(srcDir, skillsDir, prefix, pathPrefix, runti
|
|
|
2866
3855
|
}
|
|
2867
3856
|
|
|
2868
3857
|
/**
|
|
2869
|
-
* Copy Claude commands as
|
|
2870
|
-
*
|
|
3858
|
+
* Copy Claude commands as Copilot skills — one folder per skill with SKILL.md.
|
|
3859
|
+
* Applies CONV-01 (structure), CONV-02 (allowed-tools), CONV-06 (paths), CONV-07 (command names).
|
|
2871
3860
|
*/
|
|
2872
|
-
function
|
|
3861
|
+
function copyCommandsAsCopilotSkills(srcDir, skillsDir, prefix, isGlobal = false) {
|
|
2873
3862
|
if (!fs.existsSync(srcDir)) {
|
|
2874
3863
|
return;
|
|
2875
3864
|
}
|
|
2876
3865
|
|
|
2877
3866
|
fs.mkdirSync(skillsDir, { recursive: true });
|
|
2878
3867
|
|
|
2879
|
-
// Remove previous SDD
|
|
3868
|
+
// Remove previous SDD Copilot skills
|
|
2880
3869
|
const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
2881
3870
|
for (const entry of existing) {
|
|
2882
3871
|
if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
|
|
@@ -2904,16 +3893,8 @@ function copyCommandsAsWindsurfSkills(srcDir, skillsDir, prefix, pathPrefix, run
|
|
|
2904
3893
|
fs.mkdirSync(skillDir, { recursive: true });
|
|
2905
3894
|
|
|
2906
3895
|
let content = fs.readFileSync(srcPath, 'utf8');
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
const localClaudeRegex = /\.\/\.claude\//g;
|
|
2910
|
-
const windsurfDirRegex = /~\/\.windsurf\//g;
|
|
2911
|
-
content = content.replace(globalClaudeRegex, pathPrefix);
|
|
2912
|
-
content = content.replace(globalClaudeHomeRegex, pathPrefix);
|
|
2913
|
-
content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
|
|
2914
|
-
content = content.replace(windsurfDirRegex, pathPrefix);
|
|
2915
|
-
content = processAttribution(content, getCommitAttribution(runtime));
|
|
2916
|
-
content = convertClaudeCommandToWindsurfSkill(content, skillName);
|
|
3896
|
+
content = convertClaudeCommandToCopilotSkill(content, skillName, isGlobal);
|
|
3897
|
+
content = processAttribution(content, getCommitAttribution('copilot'));
|
|
2917
3898
|
|
|
2918
3899
|
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
|
|
2919
3900
|
}
|
|
@@ -2923,17 +3904,25 @@ function copyCommandsAsWindsurfSkills(srcDir, skillsDir, prefix, pathPrefix, run
|
|
|
2923
3904
|
}
|
|
2924
3905
|
|
|
2925
3906
|
/**
|
|
2926
|
-
* Copy Claude commands as
|
|
2927
|
-
*
|
|
3907
|
+
* Copy Claude commands as Claude skills — one folder per skill with SKILL.md.
|
|
3908
|
+
* Claude Code 2.1.88+ uses skills/xxx/SKILL.md instead of commands/sdd/xxx.md.
|
|
3909
|
+
* Claude is the native format so no path replacement is needed — only
|
|
3910
|
+
* frontmatter restructuring via convertClaudeCommandToClaudeSkill.
|
|
3911
|
+
* @param {string} srcDir - Source commands directory
|
|
3912
|
+
* @param {string} skillsDir - Target skills directory
|
|
3913
|
+
* @param {string} prefix - Skill name prefix (e.g. 'sdd')
|
|
3914
|
+
* @param {string} pathPrefix - Path prefix for file references
|
|
3915
|
+
* @param {string} runtime - Target runtime
|
|
3916
|
+
* @param {boolean} isGlobal - Whether this is a global install
|
|
2928
3917
|
*/
|
|
2929
|
-
function
|
|
3918
|
+
function copyCommandsAsClaudeSkills(srcDir, skillsDir, prefix, pathPrefix, runtime, isGlobal = false) {
|
|
2930
3919
|
if (!fs.existsSync(srcDir)) {
|
|
2931
3920
|
return;
|
|
2932
3921
|
}
|
|
2933
3922
|
|
|
2934
3923
|
fs.mkdirSync(skillsDir, { recursive: true });
|
|
2935
3924
|
|
|
2936
|
-
// Remove previous SDD
|
|
3925
|
+
// Remove previous SDD Claude skills to avoid stale command skills
|
|
2937
3926
|
const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
2938
3927
|
for (const entry of existing) {
|
|
2939
3928
|
if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
|
|
@@ -2961,8 +3950,14 @@ function copyCommandsAsCopilotSkills(srcDir, skillsDir, prefix, isGlobal = false
|
|
|
2961
3950
|
fs.mkdirSync(skillDir, { recursive: true });
|
|
2962
3951
|
|
|
2963
3952
|
let content = fs.readFileSync(srcPath, 'utf8');
|
|
2964
|
-
content =
|
|
2965
|
-
content =
|
|
3953
|
+
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
3954
|
+
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
|
|
3955
|
+
content = content.replace(/\.\/\.claude\//g, `./${getDirName(runtime)}/`);
|
|
3956
|
+
content = content.replace(/~\/\.qwen\//g, pathPrefix);
|
|
3957
|
+
content = content.replace(/\$HOME\/\.qwen\//g, pathPrefix);
|
|
3958
|
+
content = content.replace(/\.\/\.qwen\//g, `./${getDirName(runtime)}/`);
|
|
3959
|
+
content = processAttribution(content, getCommitAttribution(runtime));
|
|
3960
|
+
content = convertClaudeCommandToClaudeSkill(content, skillName);
|
|
2966
3961
|
|
|
2967
3962
|
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
|
|
2968
3963
|
}
|
|
@@ -3025,6 +4020,42 @@ function copyCommandsAsAntigravitySkills(srcDir, skillsDir, prefix, isGlobal = f
|
|
|
3025
4020
|
recurse(srcDir, prefix);
|
|
3026
4021
|
}
|
|
3027
4022
|
|
|
4023
|
+
/**
|
|
4024
|
+
* Save user-generated files from destDir to an in-memory map before a wipe.
|
|
4025
|
+
*
|
|
4026
|
+
* @param {string} destDir - Directory that is about to be wiped
|
|
4027
|
+
* @param {string[]} fileNames - Relative file names (e.g. ['USER-PROFILE.md']) to preserve
|
|
4028
|
+
* @returns {Map<string, string>} Map of fileName → file content (only entries that existed)
|
|
4029
|
+
*/
|
|
4030
|
+
function preserveUserArtifacts(destDir, fileNames) {
|
|
4031
|
+
const saved = new Map();
|
|
4032
|
+
for (const name of fileNames) {
|
|
4033
|
+
const fullPath = path.join(destDir, name);
|
|
4034
|
+
if (fs.existsSync(fullPath)) {
|
|
4035
|
+
try {
|
|
4036
|
+
saved.set(name, fs.readFileSync(fullPath, 'utf8'));
|
|
4037
|
+
} catch { /* skip unreadable files */ }
|
|
4038
|
+
}
|
|
4039
|
+
}
|
|
4040
|
+
return saved;
|
|
4041
|
+
}
|
|
4042
|
+
|
|
4043
|
+
/**
|
|
4044
|
+
* Restore user-generated files saved by preserveUserArtifacts after a wipe.
|
|
4045
|
+
*
|
|
4046
|
+
* @param {string} destDir - Directory that was wiped and recreated
|
|
4047
|
+
* @param {Map<string, string>} saved - Map returned by preserveUserArtifacts
|
|
4048
|
+
*/
|
|
4049
|
+
function restoreUserArtifacts(destDir, saved) {
|
|
4050
|
+
for (const [name, content] of saved) {
|
|
4051
|
+
const fullPath = path.join(destDir, name);
|
|
4052
|
+
try {
|
|
4053
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
4054
|
+
fs.writeFileSync(fullPath, content, 'utf8');
|
|
4055
|
+
} catch { /* skip unwritable paths */ }
|
|
4056
|
+
}
|
|
4057
|
+
}
|
|
4058
|
+
|
|
3028
4059
|
/**
|
|
3029
4060
|
* Recursively copy directory, replacing paths in .md files
|
|
3030
4061
|
* Deletes existing destDir first to remove orphaned files from previous versions
|
|
@@ -3035,11 +4066,16 @@ function copyCommandsAsAntigravitySkills(srcDir, skillsDir, prefix, isGlobal = f
|
|
|
3035
4066
|
*/
|
|
3036
4067
|
function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand = false, isGlobal = false) {
|
|
3037
4068
|
const isOpencode = runtime === 'opencode';
|
|
4069
|
+
const isKilo = runtime === 'kilo';
|
|
3038
4070
|
const isCodex = runtime === 'codex';
|
|
3039
4071
|
const isCopilot = runtime === 'copilot';
|
|
3040
4072
|
const isAntigravity = runtime === 'antigravity';
|
|
3041
4073
|
const isCursor = runtime === 'cursor';
|
|
3042
4074
|
const isWindsurf = runtime === 'windsurf';
|
|
4075
|
+
const isAugment = runtime === 'augment';
|
|
4076
|
+
const isTrae = runtime === 'trae';
|
|
4077
|
+
const isQwen = runtime === 'qwen';
|
|
4078
|
+
const isCline = runtime === 'cline';
|
|
3043
4079
|
const dirName = getDirName(runtime);
|
|
3044
4080
|
|
|
3045
4081
|
// Clean install: remove existing destination to prevent orphaned files
|
|
@@ -3067,12 +4103,17 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
|
|
3067
4103
|
content = content.replace(globalClaudeRegex, pathPrefix);
|
|
3068
4104
|
content = content.replace(globalClaudeHomeRegex, pathPrefix);
|
|
3069
4105
|
content = content.replace(localClaudeRegex, `./${dirName}/`);
|
|
4106
|
+
content = content.replace(/~\/\.qwen\//g, pathPrefix);
|
|
4107
|
+
content = content.replace(/\$HOME\/\.qwen\//g, pathPrefix);
|
|
4108
|
+
content = content.replace(/\.\/\.qwen\//g, `./${dirName}/`);
|
|
3070
4109
|
}
|
|
3071
4110
|
content = processAttribution(content, getCommitAttribution(runtime));
|
|
3072
4111
|
|
|
3073
4112
|
// Convert frontmatter for opencode compatibility
|
|
3074
|
-
if (isOpencode) {
|
|
3075
|
-
content =
|
|
4113
|
+
if (isOpencode || isKilo) {
|
|
4114
|
+
content = isKilo
|
|
4115
|
+
? convertClaudeToKiloFrontmatter(content)
|
|
4116
|
+
: convertClaudeToOpencodeFrontmatter(content);
|
|
3076
4117
|
fs.writeFileSync(destPath, content);
|
|
3077
4118
|
} else if (runtime === 'gemini') {
|
|
3078
4119
|
if (isCommand) {
|
|
@@ -3102,6 +4143,12 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
|
|
3102
4143
|
} else if (isWindsurf) {
|
|
3103
4144
|
content = convertClaudeToWindsurfMarkdown(content);
|
|
3104
4145
|
fs.writeFileSync(destPath, content);
|
|
4146
|
+
} else if (isTrae) {
|
|
4147
|
+
content = convertClaudeToTraeMarkdown(content);
|
|
4148
|
+
fs.writeFileSync(destPath, content);
|
|
4149
|
+
} else if (isCline) {
|
|
4150
|
+
content = convertClaudeToCliineMarkdown(content);
|
|
4151
|
+
fs.writeFileSync(destPath, content);
|
|
3105
4152
|
} else {
|
|
3106
4153
|
fs.writeFileSync(destPath, content);
|
|
3107
4154
|
}
|
|
@@ -3128,9 +4175,24 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
|
|
3128
4175
|
let jsContent = fs.readFileSync(srcPath, 'utf8');
|
|
3129
4176
|
jsContent = jsContent.replace(/sdd:/gi, 'sdd-');
|
|
3130
4177
|
jsContent = jsContent.replace(/\.claude\/skills\//g, '.windsurf/skills/');
|
|
3131
|
-
jsContent = jsContent.replace(/CLAUDE\.md/g, '.windsurf/rules
|
|
4178
|
+
jsContent = jsContent.replace(/CLAUDE\.md/g, '.windsurf/rules');
|
|
3132
4179
|
jsContent = jsContent.replace(/\bClaude Code\b/g, 'Windsurf');
|
|
3133
4180
|
fs.writeFileSync(destPath, jsContent);
|
|
4181
|
+
} else if (isTrae && (entry.name.endsWith('.cjs') || entry.name.endsWith('.js'))) {
|
|
4182
|
+
let jsContent = fs.readFileSync(srcPath, 'utf8');
|
|
4183
|
+
jsContent = jsContent.replace(/\/sdd:([a-z0-9-]+)/g, (_, commandName) => {
|
|
4184
|
+
return `/sdd-${commandName}`;
|
|
4185
|
+
});
|
|
4186
|
+
jsContent = jsContent.replace(/\.claude\/skills\//g, '.trae/skills/');
|
|
4187
|
+
jsContent = jsContent.replace(/CLAUDE\.md/g, '.trae/rules/');
|
|
4188
|
+
jsContent = jsContent.replace(/\bClaude Code\b/g, 'Trae');
|
|
4189
|
+
fs.writeFileSync(destPath, jsContent);
|
|
4190
|
+
} else if (isCline && (entry.name.endsWith('.cjs') || entry.name.endsWith('.js'))) {
|
|
4191
|
+
let jsContent = fs.readFileSync(srcPath, 'utf8');
|
|
4192
|
+
jsContent = jsContent.replace(/\.claude\/skills\//g, '.cline/skills/');
|
|
4193
|
+
jsContent = jsContent.replace(/CLAUDE\.md/g, '.clinerules');
|
|
4194
|
+
jsContent = jsContent.replace(/\bClaude Code\b/g, 'Cline');
|
|
4195
|
+
fs.writeFileSync(destPath, jsContent);
|
|
3134
4196
|
} else {
|
|
3135
4197
|
fs.copyFileSync(srcPath, destPath);
|
|
3136
4198
|
}
|
|
@@ -3201,7 +4263,7 @@ function cleanupOrphanedHooks(settings) {
|
|
|
3201
4263
|
// Only match the specific old SDD path pattern (hooks/statusline.js),
|
|
3202
4264
|
// not third-party statusline scripts that happen to contain 'statusline.js'
|
|
3203
4265
|
if (settings.statusLine && settings.statusLine.command &&
|
|
3204
|
-
|
|
4266
|
+
/hooks[\/\\]statusline\.js/.test(settings.statusLine.command)) {
|
|
3205
4267
|
settings.statusLine.command = settings.statusLine.command.replace(
|
|
3206
4268
|
/hooks([\/\\])statusline\.js/,
|
|
3207
4269
|
'hooks$1sdd-statusline.js'
|
|
@@ -3299,11 +4361,17 @@ function validateHookFields(settings) {
|
|
|
3299
4361
|
*/
|
|
3300
4362
|
function uninstall(isGlobal, runtime = 'claude') {
|
|
3301
4363
|
const isOpencode = runtime === 'opencode';
|
|
4364
|
+
const isKilo = runtime === 'kilo';
|
|
4365
|
+
const isGemini = runtime === 'gemini';
|
|
3302
4366
|
const isCodex = runtime === 'codex';
|
|
3303
4367
|
const isCopilot = runtime === 'copilot';
|
|
3304
4368
|
const isAntigravity = runtime === 'antigravity';
|
|
3305
4369
|
const isCursor = runtime === 'cursor';
|
|
3306
4370
|
const isWindsurf = runtime === 'windsurf';
|
|
4371
|
+
const isAugment = runtime === 'augment';
|
|
4372
|
+
const isTrae = runtime === 'trae';
|
|
4373
|
+
const isQwen = runtime === 'qwen';
|
|
4374
|
+
const isCodebuddy = runtime === 'codebuddy';
|
|
3307
4375
|
const dirName = getDirName(runtime);
|
|
3308
4376
|
|
|
3309
4377
|
// Get the target directory based on runtime and install type
|
|
@@ -3318,11 +4386,16 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
3318
4386
|
let runtimeLabel = 'Claude Code';
|
|
3319
4387
|
if (runtime === 'opencode') runtimeLabel = 'OpenCode';
|
|
3320
4388
|
if (runtime === 'gemini') runtimeLabel = 'Gemini';
|
|
4389
|
+
if (runtime === 'kilo') runtimeLabel = 'Kilo';
|
|
3321
4390
|
if (runtime === 'codex') runtimeLabel = 'Codex';
|
|
3322
4391
|
if (runtime === 'copilot') runtimeLabel = 'Copilot';
|
|
3323
4392
|
if (runtime === 'antigravity') runtimeLabel = 'Antigravity';
|
|
3324
4393
|
if (runtime === 'cursor') runtimeLabel = 'Cursor';
|
|
3325
4394
|
if (runtime === 'windsurf') runtimeLabel = 'Windsurf';
|
|
4395
|
+
if (runtime === 'augment') runtimeLabel = 'Augment';
|
|
4396
|
+
if (runtime === 'trae') runtimeLabel = 'Trae';
|
|
4397
|
+
if (runtime === 'qwen') runtimeLabel = 'Qwen Code';
|
|
4398
|
+
if (runtime === 'codebuddy') runtimeLabel = 'CodeBuddy';
|
|
3326
4399
|
|
|
3327
4400
|
console.log(` Uninstalling SDD from ${cyan}${runtimeLabel}${reset} at ${cyan}${locationLabel}${reset}\n`);
|
|
3328
4401
|
|
|
@@ -3336,8 +4409,8 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
3336
4409
|
let removedCount = 0;
|
|
3337
4410
|
|
|
3338
4411
|
// 1. Remove SDD commands/skills
|
|
3339
|
-
if (isOpencode) {
|
|
3340
|
-
// OpenCode: remove command/sdd-*.md files
|
|
4412
|
+
if (isOpencode || isKilo) {
|
|
4413
|
+
// OpenCode/Kilo: remove command/sdd-*.md files
|
|
3341
4414
|
const commandDir = path.join(targetDir, 'command');
|
|
3342
4415
|
if (fs.existsSync(commandDir)) {
|
|
3343
4416
|
const files = fs.readdirSync(commandDir);
|
|
@@ -3349,8 +4422,8 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
3349
4422
|
}
|
|
3350
4423
|
console.log(` ${green}✓${reset} Removed SDD commands from command/`);
|
|
3351
4424
|
}
|
|
3352
|
-
} else if (isCodex || isCursor || isWindsurf) {
|
|
3353
|
-
// Codex/Cursor/Windsurf: remove skills/sdd-*/SKILL.md skill directories
|
|
4425
|
+
} else if (isCodex || isCursor || isWindsurf || isTrae || isCodebuddy) {
|
|
4426
|
+
// Codex/Cursor/Windsurf/Trae/CodeBuddy: remove skills/sdd-*/SKILL.md skill directories
|
|
3354
4427
|
const skillsDir = path.join(targetDir, 'skills');
|
|
3355
4428
|
if (fs.existsSync(skillsDir)) {
|
|
3356
4429
|
let skillCount = 0;
|
|
@@ -3369,39 +4442,39 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
3369
4442
|
|
|
3370
4443
|
// Codex-only: remove SDD agent .toml config files and config.toml sections
|
|
3371
4444
|
if (isCodex) {
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
4445
|
+
const codexAgentsDir = path.join(targetDir, 'agents');
|
|
4446
|
+
if (fs.existsSync(codexAgentsDir)) {
|
|
4447
|
+
const tomlFiles = fs.readdirSync(codexAgentsDir);
|
|
4448
|
+
let tomlCount = 0;
|
|
4449
|
+
for (const file of tomlFiles) {
|
|
4450
|
+
if (file.startsWith('sdd-') && file.endsWith('.toml')) {
|
|
4451
|
+
fs.unlinkSync(path.join(codexAgentsDir, file));
|
|
4452
|
+
tomlCount++;
|
|
4453
|
+
}
|
|
4454
|
+
}
|
|
4455
|
+
if (tomlCount > 0) {
|
|
4456
|
+
removedCount++;
|
|
4457
|
+
console.log(` ${green}✓${reset} Removed ${tomlCount} agent .toml configs`);
|
|
3380
4458
|
}
|
|
3381
4459
|
}
|
|
3382
|
-
if (tomlCount > 0) {
|
|
3383
|
-
removedCount++;
|
|
3384
|
-
console.log(` ${green}✓${reset} Removed ${tomlCount} agent .toml configs`);
|
|
3385
|
-
}
|
|
3386
|
-
}
|
|
3387
4460
|
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
4461
|
+
// Codex: clean SDD sections from config.toml
|
|
4462
|
+
const configPath = path.join(targetDir, 'config.toml');
|
|
4463
|
+
if (fs.existsSync(configPath)) {
|
|
4464
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
4465
|
+
const cleaned = stripSddFromCodexConfig(content);
|
|
4466
|
+
if (cleaned === null) {
|
|
4467
|
+
// File is empty after stripping — delete it
|
|
4468
|
+
fs.unlinkSync(configPath);
|
|
4469
|
+
removedCount++;
|
|
4470
|
+
console.log(` ${green}✓${reset} Removed config.toml (was SDD-only)`);
|
|
4471
|
+
} else if (cleaned !== content) {
|
|
4472
|
+
fs.writeFileSync(configPath, cleaned);
|
|
4473
|
+
removedCount++;
|
|
4474
|
+
console.log(` ${green}✓${reset} Cleaned SDD sections from config.toml`);
|
|
4475
|
+
}
|
|
3402
4476
|
}
|
|
3403
4477
|
}
|
|
3404
|
-
}
|
|
3405
4478
|
} else if (isCopilot) {
|
|
3406
4479
|
// Copilot: remove skills/sdd-*/ directories (same layout as Codex skills)
|
|
3407
4480
|
const skillsDir = path.join(targetDir, 'skills');
|
|
@@ -3424,7 +4497,7 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
3424
4497
|
const instructionsPath = path.join(targetDir, 'copilot-instructions.md');
|
|
3425
4498
|
if (fs.existsSync(instructionsPath)) {
|
|
3426
4499
|
const content = fs.readFileSync(instructionsPath, 'utf8');
|
|
3427
|
-
const cleaned =
|
|
4500
|
+
const cleaned = stripSddFromCopilotInstructions(content);
|
|
3428
4501
|
if (cleaned === null) {
|
|
3429
4502
|
fs.unlinkSync(instructionsPath);
|
|
3430
4503
|
removedCount++;
|
|
@@ -3452,8 +4525,7 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
3452
4525
|
console.log(` ${green}✓${reset} Removed ${skillCount} Antigravity skills`);
|
|
3453
4526
|
}
|
|
3454
4527
|
}
|
|
3455
|
-
} else if (
|
|
3456
|
-
// Cursor: remove skills/sdd-*/ directories (same layout as Codex skills)
|
|
4528
|
+
} else if (isQwen) {
|
|
3457
4529
|
const skillsDir = path.join(targetDir, 'skills');
|
|
3458
4530
|
if (fs.existsSync(skillsDir)) {
|
|
3459
4531
|
let skillCount = 0;
|
|
@@ -3466,11 +4538,45 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
3466
4538
|
}
|
|
3467
4539
|
if (skillCount > 0) {
|
|
3468
4540
|
removedCount++;
|
|
3469
|
-
console.log(` ${green}✓${reset} Removed ${skillCount}
|
|
4541
|
+
console.log(` ${green}✓${reset} Removed ${skillCount} Qwen Code skills`);
|
|
3470
4542
|
}
|
|
3471
4543
|
}
|
|
3472
|
-
|
|
3473
|
-
|
|
4544
|
+
|
|
4545
|
+
const legacyCommandsDir = path.join(targetDir, 'commands', 'sdd');
|
|
4546
|
+
if (fs.existsSync(legacyCommandsDir)) {
|
|
4547
|
+
const savedLegacyArtifacts = preserveUserArtifacts(legacyCommandsDir, ['dev-preferences.md']);
|
|
4548
|
+
fs.rmSync(legacyCommandsDir, { recursive: true });
|
|
4549
|
+
removedCount++;
|
|
4550
|
+
console.log(` ${green}✓${reset} Removed legacy commands/sdd/`);
|
|
4551
|
+
restoreUserArtifacts(legacyCommandsDir, savedLegacyArtifacts);
|
|
4552
|
+
}
|
|
4553
|
+
} else if (isGemini) {
|
|
4554
|
+
// Gemini: still uses commands/sdd/
|
|
4555
|
+
const sddCommandsDir = path.join(targetDir, 'commands', 'sdd');
|
|
4556
|
+
if (fs.existsSync(sddCommandsDir)) {
|
|
4557
|
+
// Preserve user-generated files before wipe (#1423)
|
|
4558
|
+
// Note: if more user files are added, consider a naming convention (e.g., USER-*.md)
|
|
4559
|
+
// and preserve all matching files instead of listing each one individually.
|
|
4560
|
+
const devPrefsPath = path.join(sddCommandsDir, 'dev-preferences.md');
|
|
4561
|
+
const preservedDevPrefs = fs.existsSync(devPrefsPath) ? fs.readFileSync(devPrefsPath, 'utf-8') : null;
|
|
4562
|
+
|
|
4563
|
+
fs.rmSync(sddCommandsDir, { recursive: true });
|
|
4564
|
+
removedCount++;
|
|
4565
|
+
console.log(` ${green}✓${reset} Removed commands/sdd/`);
|
|
4566
|
+
|
|
4567
|
+
// Restore user-generated files
|
|
4568
|
+
if (preservedDevPrefs) {
|
|
4569
|
+
try {
|
|
4570
|
+
fs.mkdirSync(sddCommandsDir, { recursive: true });
|
|
4571
|
+
fs.writeFileSync(devPrefsPath, preservedDevPrefs);
|
|
4572
|
+
console.log(` ${green}✓${reset} Preserved commands/sdd/dev-preferences.md`);
|
|
4573
|
+
} catch (err) {
|
|
4574
|
+
console.error(` ${red}✗${reset} Failed to restore dev-preferences.md: ${err.message}`);
|
|
4575
|
+
}
|
|
4576
|
+
}
|
|
4577
|
+
}
|
|
4578
|
+
} else if (isGlobal) {
|
|
4579
|
+
// Claude Code global: remove skills/sdd-*/ directories (primary global install location)
|
|
3474
4580
|
const skillsDir = path.join(targetDir, 'skills');
|
|
3475
4581
|
if (fs.existsSync(skillsDir)) {
|
|
3476
4582
|
let skillCount = 0;
|
|
@@ -3483,24 +4589,76 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
3483
4589
|
}
|
|
3484
4590
|
if (skillCount > 0) {
|
|
3485
4591
|
removedCount++;
|
|
3486
|
-
console.log(` ${green}✓${reset} Removed ${skillCount}
|
|
4592
|
+
console.log(` ${green}✓${reset} Removed ${skillCount} Claude Code skills`);
|
|
4593
|
+
}
|
|
4594
|
+
}
|
|
4595
|
+
|
|
4596
|
+
// Also clean up legacy commands/sdd/ from older global installs
|
|
4597
|
+
const legacyCommandsDir = path.join(targetDir, 'commands', 'sdd');
|
|
4598
|
+
if (fs.existsSync(legacyCommandsDir)) {
|
|
4599
|
+
// Preserve user-generated files before legacy wipe (#1423)
|
|
4600
|
+
const devPrefsPath = path.join(legacyCommandsDir, 'dev-preferences.md');
|
|
4601
|
+
const preservedDevPrefs = fs.existsSync(devPrefsPath) ? fs.readFileSync(devPrefsPath, 'utf-8') : null;
|
|
4602
|
+
|
|
4603
|
+
fs.rmSync(legacyCommandsDir, { recursive: true });
|
|
4604
|
+
removedCount++;
|
|
4605
|
+
console.log(` ${green}✓${reset} Removed legacy commands/sdd/`);
|
|
4606
|
+
|
|
4607
|
+
if (preservedDevPrefs) {
|
|
4608
|
+
try {
|
|
4609
|
+
fs.mkdirSync(legacyCommandsDir, { recursive: true });
|
|
4610
|
+
fs.writeFileSync(devPrefsPath, preservedDevPrefs);
|
|
4611
|
+
console.log(` ${green}✓${reset} Preserved commands/sdd/dev-preferences.md`);
|
|
4612
|
+
} catch (err) {
|
|
4613
|
+
console.error(` ${red}✗${reset} Failed to restore dev-preferences.md: ${err.message}`);
|
|
4614
|
+
}
|
|
3487
4615
|
}
|
|
3488
4616
|
}
|
|
3489
4617
|
} else {
|
|
4618
|
+
// Claude Code local: remove commands/sdd/ (primary local install location since #1736)
|
|
3490
4619
|
const sddCommandsDir = path.join(targetDir, 'commands', 'sdd');
|
|
3491
4620
|
if (fs.existsSync(sddCommandsDir)) {
|
|
4621
|
+
// Preserve user-generated files before wipe (#1423)
|
|
4622
|
+
const devPrefsPath = path.join(sddCommandsDir, 'dev-preferences.md');
|
|
4623
|
+
const preservedDevPrefs = fs.existsSync(devPrefsPath) ? fs.readFileSync(devPrefsPath, 'utf-8') : null;
|
|
4624
|
+
|
|
3492
4625
|
fs.rmSync(sddCommandsDir, { recursive: true });
|
|
3493
4626
|
removedCount++;
|
|
3494
4627
|
console.log(` ${green}✓${reset} Removed commands/sdd/`);
|
|
4628
|
+
|
|
4629
|
+
if (preservedDevPrefs) {
|
|
4630
|
+
try {
|
|
4631
|
+
fs.mkdirSync(sddCommandsDir, { recursive: true });
|
|
4632
|
+
fs.writeFileSync(devPrefsPath, preservedDevPrefs);
|
|
4633
|
+
console.log(` ${green}✓${reset} Preserved commands/sdd/dev-preferences.md`);
|
|
4634
|
+
} catch (err) {
|
|
4635
|
+
console.error(` ${red}✗${reset} Failed to restore dev-preferences.md: ${err.message}`);
|
|
4636
|
+
}
|
|
4637
|
+
}
|
|
3495
4638
|
}
|
|
3496
4639
|
}
|
|
3497
4640
|
|
|
3498
4641
|
// 2. Remove sdd directory
|
|
3499
4642
|
const sddDir = path.join(targetDir, 'sdd');
|
|
3500
4643
|
if (fs.existsSync(sddDir)) {
|
|
4644
|
+
// Preserve user-generated files before wipe (#1423)
|
|
4645
|
+
const userProfilePath = path.join(sddDir, 'USER-PROFILE.md');
|
|
4646
|
+
const preservedProfile = fs.existsSync(userProfilePath) ? fs.readFileSync(userProfilePath, 'utf-8') : null;
|
|
4647
|
+
|
|
3501
4648
|
fs.rmSync(sddDir, { recursive: true });
|
|
3502
4649
|
removedCount++;
|
|
3503
4650
|
console.log(` ${green}✓${reset} Removed sdd/`);
|
|
4651
|
+
|
|
4652
|
+
// Restore user-generated files
|
|
4653
|
+
if (preservedProfile) {
|
|
4654
|
+
try {
|
|
4655
|
+
fs.mkdirSync(sddDir, { recursive: true });
|
|
4656
|
+
fs.writeFileSync(userProfilePath, preservedProfile);
|
|
4657
|
+
console.log(` ${green}✓${reset} Preserved sdd/USER-PROFILE.md`);
|
|
4658
|
+
} catch (err) {
|
|
4659
|
+
console.error(` ${red}✗${reset} Failed to restore USER-PROFILE.md: ${err.message}`);
|
|
4660
|
+
}
|
|
4661
|
+
}
|
|
3504
4662
|
}
|
|
3505
4663
|
|
|
3506
4664
|
// 3. Remove SDD agents (sdd-*.md files only)
|
|
@@ -3523,7 +4681,7 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
3523
4681
|
// 4. Remove SDD hooks
|
|
3524
4682
|
const hooksDir = path.join(targetDir, 'hooks');
|
|
3525
4683
|
if (fs.existsSync(hooksDir)) {
|
|
3526
|
-
const sddHooks = ['sdd-statusline.js', 'sdd-check-update.js', 'sdd-
|
|
4684
|
+
const sddHooks = ['sdd-statusline.js', 'sdd-check-update.js', 'sdd-context-monitor.js', 'sdd-prompt-guard.js', 'sdd-read-guard.js', 'sdd-workflow-guard.js', 'sdd-session-state.sh', 'sdd-validate-commit.sh', 'sdd-phase-boundary.sh'];
|
|
3527
4685
|
let hookCount = 0;
|
|
3528
4686
|
for (const hook of sddHooks) {
|
|
3529
4687
|
const hookPath = path.join(hooksDir, hook);
|
|
@@ -3558,83 +4716,50 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
3558
4716
|
const settingsPath = path.join(targetDir, 'settings.json');
|
|
3559
4717
|
if (fs.existsSync(settingsPath)) {
|
|
3560
4718
|
let settings = readSettings(settingsPath);
|
|
4719
|
+
if (settings === null) {
|
|
4720
|
+
console.log(` ${yellow}i${reset} Skipping settings.json cleanup — file could not be parsed`);
|
|
4721
|
+
settings = {}; // prevent downstream crashes, but don't write back
|
|
4722
|
+
}
|
|
3561
4723
|
let settingsModified = false;
|
|
3562
4724
|
|
|
3563
4725
|
// Remove SDD statusline if it references our hook
|
|
3564
4726
|
if (settings.statusLine && settings.statusLine.command &&
|
|
3565
|
-
|
|
4727
|
+
settings.statusLine.command.includes('sdd-statusline')) {
|
|
3566
4728
|
delete settings.statusLine;
|
|
3567
4729
|
settingsModified = true;
|
|
3568
4730
|
console.log(` ${green}✓${reset} Removed SDD statusline from settings`);
|
|
3569
4731
|
}
|
|
3570
4732
|
|
|
3571
|
-
// Remove SDD hooks from
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
);
|
|
3580
|
-
return !hasGsdHook;
|
|
3581
|
-
}
|
|
3582
|
-
return true;
|
|
3583
|
-
});
|
|
3584
|
-
if (settings.hooks.SessionStart.length < before) {
|
|
3585
|
-
settingsModified = true;
|
|
3586
|
-
console.log(` ${green}✓${reset} Removed SDD hooks from settings`);
|
|
3587
|
-
}
|
|
3588
|
-
// Clean up empty array
|
|
3589
|
-
if (settings.hooks.SessionStart.length === 0) {
|
|
3590
|
-
delete settings.hooks.SessionStart;
|
|
3591
|
-
}
|
|
3592
|
-
}
|
|
4733
|
+
// Remove SDD hooks from settings — per-hook granularity to preserve
|
|
4734
|
+
// user hooks that share an entry with a SDD hook (#1755 followup)
|
|
4735
|
+
const isSddHookCommand = (cmd) =>
|
|
4736
|
+
cmd && (cmd.includes('sdd-check-update') || cmd.includes('sdd-statusline') ||
|
|
4737
|
+
cmd.includes('sdd-session-state') || cmd.includes('sdd-context-monitor') ||
|
|
4738
|
+
cmd.includes('sdd-phase-boundary') || cmd.includes('sdd-prompt-guard') ||
|
|
4739
|
+
cmd.includes('sdd-read-guard') || cmd.includes('sdd-validate-commit') ||
|
|
4740
|
+
cmd.includes('sdd-workflow-guard'));
|
|
3593
4741
|
|
|
3594
|
-
|
|
3595
|
-
for (const eventName of ['PostToolUse', 'AfterTool']) {
|
|
4742
|
+
for (const eventName of ['SessionStart', 'PostToolUse', 'AfterTool', 'PreToolUse', 'BeforeTool']) {
|
|
3596
4743
|
if (settings.hooks && settings.hooks[eventName]) {
|
|
3597
|
-
const before = settings.hooks[eventName]
|
|
3598
|
-
settings.hooks[eventName] = settings.hooks[eventName]
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
);
|
|
3603
|
-
return
|
|
3604
|
-
}
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
if (settings.hooks[eventName].length < before) {
|
|
4744
|
+
const before = JSON.stringify(settings.hooks[eventName]);
|
|
4745
|
+
settings.hooks[eventName] = settings.hooks[eventName]
|
|
4746
|
+
.map(entry => {
|
|
4747
|
+
if (!entry.hooks || !Array.isArray(entry.hooks)) return entry;
|
|
4748
|
+
// Filter out individual SDD hooks, keep user hooks
|
|
4749
|
+
entry.hooks = entry.hooks.filter(h => !isSddHookCommand(h.command));
|
|
4750
|
+
return entry.hooks.length > 0 ? entry : null;
|
|
4751
|
+
})
|
|
4752
|
+
.filter(Boolean);
|
|
4753
|
+
if (JSON.stringify(settings.hooks[eventName]) !== before) {
|
|
3608
4754
|
settingsModified = true;
|
|
3609
|
-
console.log(` ${green}✓${reset} Removed context monitor hook from settings`);
|
|
3610
4755
|
}
|
|
3611
4756
|
if (settings.hooks[eventName].length === 0) {
|
|
3612
4757
|
delete settings.hooks[eventName];
|
|
3613
4758
|
}
|
|
3614
4759
|
}
|
|
3615
4760
|
}
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
for (const eventName of ['PreToolUse', 'BeforeTool']) {
|
|
3619
|
-
if (settings.hooks && settings.hooks[eventName]) {
|
|
3620
|
-
const before = settings.hooks[eventName].length;
|
|
3621
|
-
settings.hooks[eventName] = settings.hooks[eventName].filter(entry => {
|
|
3622
|
-
if (entry.hooks && Array.isArray(entry.hooks)) {
|
|
3623
|
-
const hasGsdHook = entry.hooks.some(h =>
|
|
3624
|
-
h.command && h.command.includes('sdd-prompt-guard')
|
|
3625
|
-
);
|
|
3626
|
-
return !hasGsdHook;
|
|
3627
|
-
}
|
|
3628
|
-
return true;
|
|
3629
|
-
});
|
|
3630
|
-
if (settings.hooks[eventName].length < before) {
|
|
3631
|
-
settingsModified = true;
|
|
3632
|
-
console.log(` ${green}✓${reset} Removed prompt injection guard hook from settings`);
|
|
3633
|
-
}
|
|
3634
|
-
if (settings.hooks[eventName].length === 0) {
|
|
3635
|
-
delete settings.hooks[eventName];
|
|
3636
|
-
}
|
|
3637
|
-
}
|
|
4761
|
+
if (settingsModified) {
|
|
4762
|
+
console.log(` ${green}✓${reset} Removed SDD hooks from settings`);
|
|
3638
4763
|
}
|
|
3639
4764
|
|
|
3640
4765
|
// Clean up empty hooks object
|
|
@@ -3650,10 +4775,48 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
3650
4775
|
|
|
3651
4776
|
// 6. For OpenCode, clean up permissions from opencode.json or opencode.jsonc
|
|
3652
4777
|
if (isOpencode) {
|
|
3653
|
-
const
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
4778
|
+
const configPath = resolveOpencodeConfigPath(targetDir);
|
|
4779
|
+
if (fs.existsSync(configPath)) {
|
|
4780
|
+
try {
|
|
4781
|
+
const config = parseJsonc(fs.readFileSync(configPath, 'utf8'));
|
|
4782
|
+
let modified = false;
|
|
4783
|
+
|
|
4784
|
+
// Remove SDD permission entries
|
|
4785
|
+
if (config.permission) {
|
|
4786
|
+
for (const permType of ['read', 'external_directory']) {
|
|
4787
|
+
if (config.permission[permType]) {
|
|
4788
|
+
const keys = Object.keys(config.permission[permType]);
|
|
4789
|
+
for (const key of keys) {
|
|
4790
|
+
if (key.includes('sdd')) {
|
|
4791
|
+
delete config.permission[permType][key];
|
|
4792
|
+
modified = true;
|
|
4793
|
+
}
|
|
4794
|
+
}
|
|
4795
|
+
// Clean up empty objects
|
|
4796
|
+
if (Object.keys(config.permission[permType]).length === 0) {
|
|
4797
|
+
delete config.permission[permType];
|
|
4798
|
+
}
|
|
4799
|
+
}
|
|
4800
|
+
}
|
|
4801
|
+
if (Object.keys(config.permission).length === 0) {
|
|
4802
|
+
delete config.permission;
|
|
4803
|
+
}
|
|
4804
|
+
}
|
|
4805
|
+
|
|
4806
|
+
if (modified) {
|
|
4807
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
4808
|
+
removedCount++;
|
|
4809
|
+
console.log(` ${green}✓${reset} Removed SDD permissions from ${path.basename(configPath)}`);
|
|
4810
|
+
}
|
|
4811
|
+
} catch (e) {
|
|
4812
|
+
// Ignore JSON parse errors
|
|
4813
|
+
}
|
|
4814
|
+
}
|
|
4815
|
+
}
|
|
4816
|
+
|
|
4817
|
+
// 7. For Kilo, clean up permissions from kilo.json or kilo.jsonc
|
|
4818
|
+
if (isKilo) {
|
|
4819
|
+
const configPath = resolveKiloConfigPath(targetDir);
|
|
3657
4820
|
if (fs.existsSync(configPath)) {
|
|
3658
4821
|
try {
|
|
3659
4822
|
const config = parseJsonc(fs.readFileSync(configPath, 'utf8'));
|
|
@@ -3692,6 +4855,15 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
3692
4855
|
}
|
|
3693
4856
|
}
|
|
3694
4857
|
|
|
4858
|
+
// Remove the file manifest that the installer wrote at install time.
|
|
4859
|
+
// Without this step the metadata file persists after uninstall (#1908).
|
|
4860
|
+
const manifestPath = path.join(targetDir, MANIFEST_NAME);
|
|
4861
|
+
if (fs.existsSync(manifestPath)) {
|
|
4862
|
+
fs.rmSync(manifestPath, { force: true });
|
|
4863
|
+
removedCount++;
|
|
4864
|
+
console.log(` ${green}✓${reset} Removed ${MANIFEST_NAME}`);
|
|
4865
|
+
}
|
|
4866
|
+
|
|
3695
4867
|
if (removedCount === 0) {
|
|
3696
4868
|
console.log(` ${yellow}⚠${reset} No SDD files found to remove.`);
|
|
3697
4869
|
}
|
|
@@ -3757,27 +4929,108 @@ function parseJsonc(content) {
|
|
|
3757
4929
|
}
|
|
3758
4930
|
}
|
|
3759
4931
|
|
|
3760
|
-
// Remove trailing commas before } or ]
|
|
3761
|
-
result = result.replace(/,(\s*[}\]])/g, '$1');
|
|
4932
|
+
// Remove trailing commas before } or ]
|
|
4933
|
+
result = result.replace(/,(\s*[}\]])/g, '$1');
|
|
4934
|
+
|
|
4935
|
+
return JSON.parse(result);
|
|
4936
|
+
}
|
|
4937
|
+
|
|
4938
|
+
/**
|
|
4939
|
+
* Configure OpenCode permissions to allow reading SDD reference docs
|
|
4940
|
+
* This prevents permission prompts when SDD accesses the sdd directory
|
|
4941
|
+
* @param {boolean} isGlobal - Whether this is a global or local install
|
|
4942
|
+
* @param {string|null} configDir - Resolved config directory when already known
|
|
4943
|
+
*/
|
|
4944
|
+
function configureOpencodePermissions(isGlobal = true, configDir = null) {
|
|
4945
|
+
// For local installs, use ./.opencode/
|
|
4946
|
+
// For global installs, use ~/.config/opencode/
|
|
4947
|
+
const opencodeConfigDir = configDir || (isGlobal
|
|
4948
|
+
? getGlobalDir('opencode', explicitConfigDir)
|
|
4949
|
+
: path.join(process.cwd(), '.opencode'));
|
|
4950
|
+
// Ensure config directory exists
|
|
4951
|
+
fs.mkdirSync(opencodeConfigDir, { recursive: true });
|
|
4952
|
+
|
|
4953
|
+
const configPath = resolveOpencodeConfigPath(opencodeConfigDir);
|
|
4954
|
+
|
|
4955
|
+
// Read existing config or create empty object
|
|
4956
|
+
let config = {};
|
|
4957
|
+
if (fs.existsSync(configPath)) {
|
|
4958
|
+
try {
|
|
4959
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
4960
|
+
config = parseJsonc(content);
|
|
4961
|
+
} catch (e) {
|
|
4962
|
+
// Cannot parse - DO NOT overwrite user's config
|
|
4963
|
+
const configFile = path.basename(configPath);
|
|
4964
|
+
console.log(` ${yellow}⚠${reset} Could not parse ${configFile} - skipping permission config`);
|
|
4965
|
+
console.log(` ${dim}Reason: ${e.message}${reset}`);
|
|
4966
|
+
console.log(` ${dim}Your config was NOT modified. Fix the syntax manually if needed.${reset}`);
|
|
4967
|
+
return;
|
|
4968
|
+
}
|
|
4969
|
+
}
|
|
4970
|
+
|
|
4971
|
+
// OpenCode also allows a top-level string permission like "allow".
|
|
4972
|
+
// In that case, path-specific permission entries are unnecessary.
|
|
4973
|
+
if (typeof config.permission === 'string') {
|
|
4974
|
+
return;
|
|
4975
|
+
}
|
|
4976
|
+
|
|
4977
|
+
// Ensure permission structure exists
|
|
4978
|
+
if (!config.permission || typeof config.permission !== 'object') {
|
|
4979
|
+
config.permission = {};
|
|
4980
|
+
}
|
|
4981
|
+
|
|
4982
|
+
// Build the SDD path using the actual config directory
|
|
4983
|
+
// Use ~ shorthand if it's in the default location, otherwise use full path
|
|
4984
|
+
const defaultConfigDir = path.join(os.homedir(), '.config', 'opencode');
|
|
4985
|
+
const sddPath = opencodeConfigDir === defaultConfigDir
|
|
4986
|
+
? '~/.config/opencode/sdd/*'
|
|
4987
|
+
: `${opencodeConfigDir.replace(/\\/g, '/')}/sdd/*`;
|
|
3762
4988
|
|
|
3763
|
-
|
|
4989
|
+
let modified = false;
|
|
4990
|
+
|
|
4991
|
+
// Configure read permission
|
|
4992
|
+
if (!config.permission.read || typeof config.permission.read !== 'object') {
|
|
4993
|
+
config.permission.read = {};
|
|
4994
|
+
}
|
|
4995
|
+
if (config.permission.read[sddPath] !== 'allow') {
|
|
4996
|
+
config.permission.read[sddPath] = 'allow';
|
|
4997
|
+
modified = true;
|
|
4998
|
+
}
|
|
4999
|
+
|
|
5000
|
+
// Configure external_directory permission (the safety guard for paths outside project)
|
|
5001
|
+
if (!config.permission.external_directory || typeof config.permission.external_directory !== 'object') {
|
|
5002
|
+
config.permission.external_directory = {};
|
|
5003
|
+
}
|
|
5004
|
+
if (config.permission.external_directory[sddPath] !== 'allow') {
|
|
5005
|
+
config.permission.external_directory[sddPath] = 'allow';
|
|
5006
|
+
modified = true;
|
|
5007
|
+
}
|
|
5008
|
+
|
|
5009
|
+
if (!modified) {
|
|
5010
|
+
return; // Already configured
|
|
5011
|
+
}
|
|
5012
|
+
|
|
5013
|
+
// Write config back
|
|
5014
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
5015
|
+
console.log(` ${green}✓${reset} Configured read permission for SDD docs`);
|
|
3764
5016
|
}
|
|
3765
5017
|
|
|
3766
5018
|
/**
|
|
3767
|
-
* Configure
|
|
5019
|
+
* Configure Kilo permissions to allow reading SDD reference docs
|
|
3768
5020
|
* This prevents permission prompts when SDD accesses the sdd directory
|
|
3769
5021
|
* @param {boolean} isGlobal - Whether this is a global or local install
|
|
5022
|
+
* @param {string|null} configDir - Resolved config directory when already known
|
|
3770
5023
|
*/
|
|
3771
|
-
function
|
|
3772
|
-
// For local installs, use ./.
|
|
3773
|
-
// For global installs, use ~/.config/
|
|
3774
|
-
const
|
|
3775
|
-
?
|
|
3776
|
-
: path.join(process.cwd(), '.
|
|
5024
|
+
function configureKiloPermissions(isGlobal = true, configDir = null) {
|
|
5025
|
+
// For local installs, use ./.kilo/
|
|
5026
|
+
// For global installs, use ~/.config/kilo/
|
|
5027
|
+
const kiloConfigDir = configDir || (isGlobal
|
|
5028
|
+
? getGlobalDir('kilo', explicitConfigDir)
|
|
5029
|
+
: path.join(process.cwd(), '.kilo'));
|
|
3777
5030
|
// Ensure config directory exists
|
|
3778
|
-
fs.mkdirSync(
|
|
5031
|
+
fs.mkdirSync(kiloConfigDir, { recursive: true });
|
|
3779
5032
|
|
|
3780
|
-
const configPath =
|
|
5033
|
+
const configPath = resolveKiloConfigPath(kiloConfigDir);
|
|
3781
5034
|
|
|
3782
5035
|
// Read existing config or create empty object
|
|
3783
5036
|
let config = {};
|
|
@@ -3796,17 +5049,17 @@ function configureOpencodePermissions(isGlobal = true) {
|
|
|
3796
5049
|
}
|
|
3797
5050
|
|
|
3798
5051
|
// Ensure permission structure exists
|
|
3799
|
-
if (!config.permission) {
|
|
5052
|
+
if (!config.permission || typeof config.permission !== 'object') {
|
|
3800
5053
|
config.permission = {};
|
|
3801
5054
|
}
|
|
3802
5055
|
|
|
3803
5056
|
// Build the SDD path using the actual config directory
|
|
3804
5057
|
// Use ~ shorthand if it's in the default location, otherwise use full path
|
|
3805
|
-
const defaultConfigDir = path.join(os.homedir(), '.config', '
|
|
3806
|
-
const sddPath =
|
|
3807
|
-
? '~/.config/
|
|
3808
|
-
: `${
|
|
3809
|
-
|
|
5058
|
+
const defaultConfigDir = path.join(os.homedir(), '.config', 'kilo');
|
|
5059
|
+
const sddPath = kiloConfigDir === defaultConfigDir
|
|
5060
|
+
? '~/.config/kilo/sdd/*'
|
|
5061
|
+
: `${kiloConfigDir.replace(/\\/g, '/')}/sdd/*`;
|
|
5062
|
+
|
|
3810
5063
|
let modified = false;
|
|
3811
5064
|
|
|
3812
5065
|
// Configure read permission
|
|
@@ -3914,11 +5167,15 @@ function generateManifest(dir, baseDir) {
|
|
|
3914
5167
|
*/
|
|
3915
5168
|
function writeManifest(configDir, runtime = 'claude') {
|
|
3916
5169
|
const isOpencode = runtime === 'opencode';
|
|
5170
|
+
const isKilo = runtime === 'kilo';
|
|
5171
|
+
const isGemini = runtime === 'gemini';
|
|
3917
5172
|
const isCodex = runtime === 'codex';
|
|
3918
5173
|
const isCopilot = runtime === 'copilot';
|
|
3919
5174
|
const isAntigravity = runtime === 'antigravity';
|
|
3920
5175
|
const isCursor = runtime === 'cursor';
|
|
3921
5176
|
const isWindsurf = runtime === 'windsurf';
|
|
5177
|
+
const isTrae = runtime === 'trae';
|
|
5178
|
+
const isCline = runtime === 'cline';
|
|
3922
5179
|
const sddDir = path.join(configDir, 'sdd');
|
|
3923
5180
|
const commandsDir = path.join(configDir, 'commands', 'sdd');
|
|
3924
5181
|
const opencodeCommandDir = path.join(configDir, 'command');
|
|
@@ -3930,20 +5187,20 @@ function writeManifest(configDir, runtime = 'claude') {
|
|
|
3930
5187
|
for (const [rel, hash] of Object.entries(sddHashes)) {
|
|
3931
5188
|
manifest.files['sdd/' + rel] = hash;
|
|
3932
5189
|
}
|
|
3933
|
-
if (
|
|
5190
|
+
if (isGemini && fs.existsSync(commandsDir)) {
|
|
3934
5191
|
const cmdHashes = generateManifest(commandsDir);
|
|
3935
5192
|
for (const [rel, hash] of Object.entries(cmdHashes)) {
|
|
3936
5193
|
manifest.files['commands/sdd/' + rel] = hash;
|
|
3937
5194
|
}
|
|
3938
5195
|
}
|
|
3939
|
-
if (isOpencode && fs.existsSync(opencodeCommandDir)) {
|
|
5196
|
+
if ((isOpencode || isKilo) && fs.existsSync(opencodeCommandDir)) {
|
|
3940
5197
|
for (const file of fs.readdirSync(opencodeCommandDir)) {
|
|
3941
5198
|
if (file.startsWith('sdd-') && file.endsWith('.md')) {
|
|
3942
5199
|
manifest.files['command/' + file] = fileHash(path.join(opencodeCommandDir, file));
|
|
3943
5200
|
}
|
|
3944
5201
|
}
|
|
3945
5202
|
}
|
|
3946
|
-
if ((isCodex || isCopilot || isAntigravity || isCursor || isWindsurf) && fs.existsSync(codexSkillsDir)) {
|
|
5203
|
+
if ((isCodex || isCopilot || isAntigravity || isCursor || isWindsurf || isTrae || (!isOpencode && !isGemini)) && fs.existsSync(codexSkillsDir)) {
|
|
3947
5204
|
for (const skillName of listCodexSkillNames(codexSkillsDir)) {
|
|
3948
5205
|
const skillRoot = path.join(codexSkillsDir, skillName);
|
|
3949
5206
|
const skillHashes = generateManifest(skillRoot);
|
|
@@ -3959,13 +5216,21 @@ function writeManifest(configDir, runtime = 'claude') {
|
|
|
3959
5216
|
}
|
|
3960
5217
|
}
|
|
3961
5218
|
}
|
|
5219
|
+
// Track .clinerules file in manifest for Cline installs
|
|
5220
|
+
if (isCline) {
|
|
5221
|
+
const clinerulesDest = path.join(configDir, '.clinerules');
|
|
5222
|
+
if (fs.existsSync(clinerulesDest)) {
|
|
5223
|
+
manifest.files['.clinerules'] = fileHash(clinerulesDest);
|
|
5224
|
+
}
|
|
5225
|
+
}
|
|
5226
|
+
|
|
3962
5227
|
// Track hook files so saveLocalPatches() can detect user modifications
|
|
3963
|
-
// Hooks are only installed for runtimes that use settings.json (not Codex/Copilot)
|
|
3964
|
-
if (!isCodex && !isCopilot) {
|
|
5228
|
+
// Hooks are only installed for runtimes that use settings.json (not Codex/Copilot/Cline)
|
|
5229
|
+
if (!isCodex && !isCopilot && !isCline) {
|
|
3965
5230
|
const hooksDir = path.join(configDir, 'hooks');
|
|
3966
5231
|
if (fs.existsSync(hooksDir)) {
|
|
3967
5232
|
for (const file of fs.readdirSync(hooksDir)) {
|
|
3968
|
-
if (file.startsWith('sdd-') && file.endsWith('.js')) {
|
|
5233
|
+
if (file.startsWith('sdd-') && (file.endsWith('.js') || file.endsWith('.sh'))) {
|
|
3969
5234
|
manifest.files['hooks/' + file] = fileHash(path.join(hooksDir, file));
|
|
3970
5235
|
}
|
|
3971
5236
|
}
|
|
@@ -3979,6 +5244,8 @@ function writeManifest(configDir, runtime = 'claude') {
|
|
|
3979
5244
|
/**
|
|
3980
5245
|
* Detect user-modified SDD files by comparing against install manifest.
|
|
3981
5246
|
* Backs up modified files to sdd-local-patches/ for reapply after update.
|
|
5247
|
+
* Also saves pristine copies (from manifest) to sdd-pristine/ to enable
|
|
5248
|
+
* three-way merge during reapply-patches (pristine vs user vs new).
|
|
3982
5249
|
*/
|
|
3983
5250
|
function saveLocalPatches(configDir) {
|
|
3984
5251
|
const manifestPath = path.join(configDir, MANIFEST_NAME);
|
|
@@ -3988,6 +5255,7 @@ function saveLocalPatches(configDir) {
|
|
|
3988
5255
|
try { manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); } catch { return []; }
|
|
3989
5256
|
|
|
3990
5257
|
const patchesDir = path.join(configDir, PATCHES_DIR_NAME);
|
|
5258
|
+
const pristineDir = path.join(configDir, 'sdd-pristine');
|
|
3991
5259
|
const modified = [];
|
|
3992
5260
|
|
|
3993
5261
|
for (const [relPath, originalHash] of Object.entries(manifest.files || {})) {
|
|
@@ -3995,6 +5263,7 @@ function saveLocalPatches(configDir) {
|
|
|
3995
5263
|
if (!fs.existsSync(fullPath)) continue;
|
|
3996
5264
|
const currentHash = fileHash(fullPath);
|
|
3997
5265
|
if (currentHash !== originalHash) {
|
|
5266
|
+
// Back up the user's modified version
|
|
3998
5267
|
const backupPath = path.join(patchesDir, relPath);
|
|
3999
5268
|
fs.mkdirSync(path.dirname(backupPath), { recursive: true });
|
|
4000
5269
|
fs.copyFileSync(fullPath, backupPath);
|
|
@@ -4002,12 +5271,28 @@ function saveLocalPatches(configDir) {
|
|
|
4002
5271
|
}
|
|
4003
5272
|
}
|
|
4004
5273
|
|
|
5274
|
+
// Save pristine copies of modified files from the CURRENT install (before wipe)
|
|
5275
|
+
// These represent the original SDD distribution files that the user then modified.
|
|
5276
|
+
// The reapply-patches workflow uses these for three-way merge:
|
|
5277
|
+
// pristine (original) → user's version (what they changed) → new version (after update)
|
|
4005
5278
|
if (modified.length > 0) {
|
|
5279
|
+
// We need the pristine originals, but the current files on disk are user-modified.
|
|
5280
|
+
// The manifest records SHA-256 hashes but not content. However, we can reconstruct
|
|
5281
|
+
// the pristine version from the npm package cache or git history.
|
|
5282
|
+
// As a practical approach: save the manifest's version info so the reapply workflow
|
|
5283
|
+
// knows which SDD version these files came from, enabling npm-based reconstruction.
|
|
4006
5284
|
const meta = {
|
|
4007
5285
|
backed_up_at: new Date().toISOString(),
|
|
4008
5286
|
from_version: manifest.version,
|
|
4009
|
-
|
|
5287
|
+
from_manifest_timestamp: manifest.timestamp,
|
|
5288
|
+
files: modified,
|
|
5289
|
+
pristine_hashes: {}
|
|
4010
5290
|
};
|
|
5291
|
+
// Record the original (pristine) hash for each modified file
|
|
5292
|
+
// This lets the reapply workflow verify reconstructed pristine files
|
|
5293
|
+
for (const relPath of modified) {
|
|
5294
|
+
meta.pristine_hashes[relPath] = manifest.files[relPath];
|
|
5295
|
+
}
|
|
4011
5296
|
fs.writeFileSync(path.join(patchesDir, 'backup-meta.json'), JSON.stringify(meta, null, 2));
|
|
4012
5297
|
console.log(' ' + yellow + 'i' + reset + ' Found ' + modified.length + ' locally modified SDD file(s) — backed up to ' + PATCHES_DIR_NAME + '/');
|
|
4013
5298
|
for (const f of modified) {
|
|
@@ -4029,13 +5314,13 @@ function reportLocalPatches(configDir, runtime = 'claude') {
|
|
|
4029
5314
|
try { meta = JSON.parse(fs.readFileSync(metaPath, 'utf8')); } catch { return []; }
|
|
4030
5315
|
|
|
4031
5316
|
if (meta.files && meta.files.length > 0) {
|
|
4032
|
-
const reapplyCommand = (runtime === 'opencode' || runtime === 'copilot')
|
|
5317
|
+
const reapplyCommand = (runtime === 'opencode' || runtime === 'kilo' || runtime === 'copilot')
|
|
4033
5318
|
? '/sdd-reapply-patches'
|
|
4034
5319
|
: runtime === 'codex'
|
|
4035
5320
|
? '$sdd-reapply-patches'
|
|
4036
5321
|
: runtime === 'cursor'
|
|
4037
5322
|
? 'sdd-reapply-patches (mention the skill name)'
|
|
4038
|
-
: '/sdd
|
|
5323
|
+
: '/sdd-reapply-patches';
|
|
4039
5324
|
console.log('');
|
|
4040
5325
|
console.log(' ' + yellow + 'Local patches detected' + reset + ' (from v' + meta.from_version + '):');
|
|
4041
5326
|
for (const f of meta.files) {
|
|
@@ -4053,18 +5338,28 @@ function reportLocalPatches(configDir, runtime = 'claude') {
|
|
|
4053
5338
|
function install(isGlobal, runtime = 'claude') {
|
|
4054
5339
|
const isOpencode = runtime === 'opencode';
|
|
4055
5340
|
const isGemini = runtime === 'gemini';
|
|
5341
|
+
const isKilo = runtime === 'kilo';
|
|
4056
5342
|
const isCodex = runtime === 'codex';
|
|
4057
5343
|
const isCopilot = runtime === 'copilot';
|
|
4058
5344
|
const isAntigravity = runtime === 'antigravity';
|
|
4059
5345
|
const isCursor = runtime === 'cursor';
|
|
4060
5346
|
const isWindsurf = runtime === 'windsurf';
|
|
5347
|
+
const isAugment = runtime === 'augment';
|
|
5348
|
+
const isTrae = runtime === 'trae';
|
|
5349
|
+
const isQwen = runtime === 'qwen';
|
|
5350
|
+
const isCodebuddy = runtime === 'codebuddy';
|
|
5351
|
+
const isCline = runtime === 'cline';
|
|
4061
5352
|
const dirName = getDirName(runtime);
|
|
4062
5353
|
const src = path.join(__dirname, '..');
|
|
4063
5354
|
|
|
4064
|
-
// Get the target directory based on runtime and install type
|
|
5355
|
+
// Get the target directory based on runtime and install type.
|
|
5356
|
+
// Cline local installs write to the project root (like Claude Code) — .clinerules
|
|
5357
|
+
// lives at the root, not inside a .cline/ subdirectory.
|
|
4065
5358
|
const targetDir = isGlobal
|
|
4066
5359
|
? getGlobalDir(runtime, explicitConfigDir)
|
|
4067
|
-
:
|
|
5360
|
+
: isCline
|
|
5361
|
+
? process.cwd()
|
|
5362
|
+
: path.join(process.cwd(), dirName);
|
|
4068
5363
|
|
|
4069
5364
|
const locationLabel = isGlobal
|
|
4070
5365
|
? targetDir.replace(os.homedir(), '~')
|
|
@@ -4084,11 +5379,17 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
4084
5379
|
let runtimeLabel = 'Claude Code';
|
|
4085
5380
|
if (isOpencode) runtimeLabel = 'OpenCode';
|
|
4086
5381
|
if (isGemini) runtimeLabel = 'Gemini';
|
|
5382
|
+
if (isKilo) runtimeLabel = 'Kilo';
|
|
4087
5383
|
if (isCodex) runtimeLabel = 'Codex';
|
|
4088
5384
|
if (isCopilot) runtimeLabel = 'Copilot';
|
|
4089
5385
|
if (isAntigravity) runtimeLabel = 'Antigravity';
|
|
4090
5386
|
if (isCursor) runtimeLabel = 'Cursor';
|
|
4091
5387
|
if (isWindsurf) runtimeLabel = 'Windsurf';
|
|
5388
|
+
if (isAugment) runtimeLabel = 'Augment';
|
|
5389
|
+
if (isTrae) runtimeLabel = 'Trae';
|
|
5390
|
+
if (isQwen) runtimeLabel = 'Qwen Code';
|
|
5391
|
+
if (isCodebuddy) runtimeLabel = 'CodeBuddy';
|
|
5392
|
+
if (isCline) runtimeLabel = 'Cline';
|
|
4092
5393
|
|
|
4093
5394
|
console.log(` Installing for ${cyan}${runtimeLabel}${reset} to ${cyan}${locationLabel}${reset}\n`);
|
|
4094
5395
|
|
|
@@ -4101,12 +5402,12 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
4101
5402
|
// Clean up orphaned files from previous versions
|
|
4102
5403
|
cleanupOrphanedFiles(targetDir);
|
|
4103
5404
|
|
|
4104
|
-
// OpenCode
|
|
4105
|
-
if (isOpencode) {
|
|
4106
|
-
// OpenCode: flat structure in command/ directory
|
|
5405
|
+
// OpenCode/Kilo use command/ (flat), Codex uses skills/, Claude/Gemini use commands/sdd/
|
|
5406
|
+
if (isOpencode || isKilo) {
|
|
5407
|
+
// OpenCode/Kilo: flat structure in command/ directory
|
|
4107
5408
|
const commandDir = path.join(targetDir, 'command');
|
|
4108
5409
|
fs.mkdirSync(commandDir, { recursive: true });
|
|
4109
|
-
|
|
5410
|
+
|
|
4110
5411
|
// Copy commands/sdd/*.md as command/sdd-*.md (flatten structure)
|
|
4111
5412
|
const sddSrc = path.join(src, 'commands', 'sdd');
|
|
4112
5413
|
copyFlattenedCommands(sddSrc, commandDir, 'sdd', pathPrefix, runtime);
|
|
@@ -4176,11 +5477,66 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
4176
5477
|
} else {
|
|
4177
5478
|
failures.push('skills/sdd-*');
|
|
4178
5479
|
}
|
|
4179
|
-
} else {
|
|
4180
|
-
|
|
5480
|
+
} else if (isAugment) {
|
|
5481
|
+
const skillsDir = path.join(targetDir, 'skills');
|
|
5482
|
+
const sddSrc = path.join(src, 'commands', 'sdd');
|
|
5483
|
+
copyCommandsAsAugmentSkills(sddSrc, skillsDir, 'sdd', pathPrefix, runtime);
|
|
5484
|
+
const installedSkillNames = listCodexSkillNames(skillsDir);
|
|
5485
|
+
if (installedSkillNames.length > 0) {
|
|
5486
|
+
console.log(` ${green}✓${reset} Installed ${installedSkillNames.length} skills to skills/`);
|
|
5487
|
+
} else {
|
|
5488
|
+
failures.push('skills/sdd-*');
|
|
5489
|
+
}
|
|
5490
|
+
} else if (isTrae) {
|
|
5491
|
+
const skillsDir = path.join(targetDir, 'skills');
|
|
5492
|
+
const sddSrc = path.join(src, 'commands', 'sdd');
|
|
5493
|
+
copyCommandsAsTraeSkills(sddSrc, skillsDir, 'sdd', pathPrefix, runtime);
|
|
5494
|
+
const installedSkillNames = listCodexSkillNames(skillsDir);
|
|
5495
|
+
if (installedSkillNames.length > 0) {
|
|
5496
|
+
console.log(` ${green}✓${reset} Installed ${installedSkillNames.length} skills to skills/`);
|
|
5497
|
+
} else {
|
|
5498
|
+
failures.push('skills/sdd-*');
|
|
5499
|
+
}
|
|
5500
|
+
} else if (isQwen) {
|
|
5501
|
+
const skillsDir = path.join(targetDir, 'skills');
|
|
5502
|
+
const sddSrc = path.join(src, 'commands', 'sdd');
|
|
5503
|
+
copyCommandsAsClaudeSkills(sddSrc, skillsDir, 'sdd', pathPrefix, runtime, isGlobal);
|
|
5504
|
+
if (fs.existsSync(skillsDir)) {
|
|
5505
|
+
const count = fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
5506
|
+
.filter(e => e.isDirectory() && e.name.startsWith('sdd-')).length;
|
|
5507
|
+
if (count > 0) {
|
|
5508
|
+
console.log(` ${green}✓${reset} Installed ${count} skills to skills/`);
|
|
5509
|
+
} else {
|
|
5510
|
+
failures.push('skills/sdd-*');
|
|
5511
|
+
}
|
|
5512
|
+
} else {
|
|
5513
|
+
failures.push('skills/sdd-*');
|
|
5514
|
+
}
|
|
5515
|
+
|
|
5516
|
+
const legacyCommandsDir = path.join(targetDir, 'commands', 'sdd');
|
|
5517
|
+
if (fs.existsSync(legacyCommandsDir)) {
|
|
5518
|
+
const savedLegacyArtifacts = preserveUserArtifacts(legacyCommandsDir, ['dev-preferences.md']);
|
|
5519
|
+
fs.rmSync(legacyCommandsDir, { recursive: true });
|
|
5520
|
+
console.log(` ${green}✓${reset} Removed legacy commands/sdd/ directory`);
|
|
5521
|
+
restoreUserArtifacts(legacyCommandsDir, savedLegacyArtifacts);
|
|
5522
|
+
}
|
|
5523
|
+
} else if (isCodebuddy) {
|
|
5524
|
+
const skillsDir = path.join(targetDir, 'skills');
|
|
5525
|
+
const sddSrc = path.join(src, 'commands', 'sdd');
|
|
5526
|
+
copyCommandsAsCodebuddySkills(sddSrc, skillsDir, 'sdd', pathPrefix, runtime);
|
|
5527
|
+
const installedSkillNames = listCodexSkillNames(skillsDir);
|
|
5528
|
+
if (installedSkillNames.length > 0) {
|
|
5529
|
+
console.log(` ${green}✓${reset} Installed ${installedSkillNames.length} skills to skills/`);
|
|
5530
|
+
} else {
|
|
5531
|
+
failures.push('skills/sdd-*');
|
|
5532
|
+
}
|
|
5533
|
+
} else if (isCline) {
|
|
5534
|
+
// Cline is rules-based — commands are embedded in .clinerules (generated below).
|
|
5535
|
+
// No skills/commands directory needed. Engine is installed via copyWithPathReplacement.
|
|
5536
|
+
console.log(` ${green}✓${reset} Cline: commands will be available via .clinerules`);
|
|
5537
|
+
} else if (isGemini) {
|
|
4181
5538
|
const commandsDir = path.join(targetDir, 'commands');
|
|
4182
5539
|
fs.mkdirSync(commandsDir, { recursive: true });
|
|
4183
|
-
|
|
4184
5540
|
const sddSrc = path.join(src, 'commands', 'sdd');
|
|
4185
5541
|
const sddDest = path.join(commandsDir, 'sdd');
|
|
4186
5542
|
copyWithPathReplacement(sddSrc, sddDest, pathPrefix, runtime, true, isGlobal);
|
|
@@ -4189,12 +5545,68 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
4189
5545
|
} else {
|
|
4190
5546
|
failures.push('commands/sdd');
|
|
4191
5547
|
}
|
|
5548
|
+
} else if (isGlobal) {
|
|
5549
|
+
// Claude Code global: skills/ format (2.1.88+ compatibility)
|
|
5550
|
+
const skillsDir = path.join(targetDir, 'skills');
|
|
5551
|
+
const sddSrc = path.join(src, 'commands', 'sdd');
|
|
5552
|
+
copyCommandsAsClaudeSkills(sddSrc, skillsDir, 'sdd', pathPrefix, runtime, isGlobal);
|
|
5553
|
+
if (fs.existsSync(skillsDir)) {
|
|
5554
|
+
const count = fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
5555
|
+
.filter(e => e.isDirectory() && e.name.startsWith('sdd-')).length;
|
|
5556
|
+
if (count > 0) {
|
|
5557
|
+
console.log(` ${green}✓${reset} Installed ${count} skills to skills/`);
|
|
5558
|
+
} else {
|
|
5559
|
+
failures.push('skills/sdd-*');
|
|
5560
|
+
}
|
|
5561
|
+
} else {
|
|
5562
|
+
failures.push('skills/sdd-*');
|
|
5563
|
+
}
|
|
5564
|
+
|
|
5565
|
+
// Clean up legacy commands/sdd/ from previous global installs
|
|
5566
|
+
// Preserve user-generated files (dev-preferences.md) before wiping the directory
|
|
5567
|
+
const legacyCommandsDir = path.join(targetDir, 'commands', 'sdd');
|
|
5568
|
+
if (fs.existsSync(legacyCommandsDir)) {
|
|
5569
|
+
const savedLegacyArtifacts = preserveUserArtifacts(legacyCommandsDir, ['dev-preferences.md']);
|
|
5570
|
+
fs.rmSync(legacyCommandsDir, { recursive: true });
|
|
5571
|
+
console.log(` ${green}✓${reset} Removed legacy commands/sdd/ directory`);
|
|
5572
|
+
restoreUserArtifacts(legacyCommandsDir, savedLegacyArtifacts);
|
|
5573
|
+
}
|
|
5574
|
+
} else {
|
|
5575
|
+
// Claude Code local: commands/sdd/ format — Claude Code reads local project
|
|
5576
|
+
// commands from .claude/commands/sdd/, not .claude/skills/
|
|
5577
|
+
const commandsDir = path.join(targetDir, 'commands');
|
|
5578
|
+
fs.mkdirSync(commandsDir, { recursive: true });
|
|
5579
|
+
const sddSrc = path.join(src, 'commands', 'sdd');
|
|
5580
|
+
const sddDest = path.join(commandsDir, 'sdd');
|
|
5581
|
+
copyWithPathReplacement(sddSrc, sddDest, pathPrefix, runtime, true, isGlobal);
|
|
5582
|
+
if (verifyInstalled(sddDest, 'commands/sdd')) {
|
|
5583
|
+
const count = fs.readdirSync(sddDest).filter(f => f.endsWith('.md')).length;
|
|
5584
|
+
console.log(` ${green}✓${reset} Installed ${count} commands to commands/sdd/`);
|
|
5585
|
+
} else {
|
|
5586
|
+
failures.push('commands/sdd');
|
|
5587
|
+
}
|
|
5588
|
+
|
|
5589
|
+
// Clean up any stale skills/ from a previous local install
|
|
5590
|
+
const staleSkillsDir = path.join(targetDir, 'skills');
|
|
5591
|
+
if (fs.existsSync(staleSkillsDir)) {
|
|
5592
|
+
const staleGsd = fs.readdirSync(staleSkillsDir, { withFileTypes: true })
|
|
5593
|
+
.filter(e => e.isDirectory() && e.name.startsWith('sdd-'));
|
|
5594
|
+
for (const e of staleGsd) {
|
|
5595
|
+
fs.rmSync(path.join(staleSkillsDir, e.name), { recursive: true });
|
|
5596
|
+
}
|
|
5597
|
+
if (staleGsd.length > 0) {
|
|
5598
|
+
console.log(` ${green}✓${reset} Removed ${staleGsd.length} stale SDD skill(s) from skills/`);
|
|
5599
|
+
}
|
|
5600
|
+
}
|
|
4192
5601
|
}
|
|
4193
5602
|
|
|
4194
5603
|
// Copy sdd skill with path replacement
|
|
5604
|
+
// Preserve user-generated files before the wipe-and-copy so they survive re-install
|
|
4195
5605
|
const skillSrc = path.join(src, 'sdd');
|
|
4196
5606
|
const skillDest = path.join(targetDir, 'sdd');
|
|
5607
|
+
const savedSddArtifacts = preserveUserArtifacts(skillDest, ['USER-PROFILE.md']);
|
|
4197
5608
|
copyWithPathReplacement(skillSrc, skillDest, pathPrefix, runtime, false, isGlobal);
|
|
5609
|
+
restoreUserArtifacts(skillDest, savedSddArtifacts);
|
|
4198
5610
|
if (verifyInstalled(skillDest, 'sdd')) {
|
|
4199
5611
|
console.log(` ${green}✓${reset} Installed sdd`);
|
|
4200
5612
|
} else {
|
|
@@ -4224,14 +5636,21 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
4224
5636
|
// Replace ~/.claude/ and $HOME/.claude/ as they are the source of truth in the repo
|
|
4225
5637
|
const dirRegex = /~\/\.claude\//g;
|
|
4226
5638
|
const homeDirRegex = /\$HOME\/\.claude\//g;
|
|
5639
|
+
const bareDirRegex = /~\/\.claude\b/g;
|
|
5640
|
+
const bareHomeDirRegex = /\$HOME\/\.claude\b/g;
|
|
5641
|
+
const normalizedPathPrefix = pathPrefix.replace(/\/$/, '');
|
|
4227
5642
|
if (!isCopilot && !isAntigravity) {
|
|
4228
5643
|
content = content.replace(dirRegex, pathPrefix);
|
|
4229
5644
|
content = content.replace(homeDirRegex, pathPrefix);
|
|
5645
|
+
content = content.replace(bareDirRegex, normalizedPathPrefix);
|
|
5646
|
+
content = content.replace(bareHomeDirRegex, normalizedPathPrefix);
|
|
4230
5647
|
}
|
|
4231
5648
|
content = processAttribution(content, getCommitAttribution(runtime));
|
|
4232
5649
|
// Convert frontmatter for runtime compatibility (agents need different handling)
|
|
4233
5650
|
if (isOpencode) {
|
|
4234
5651
|
content = convertClaudeToOpencodeFrontmatter(content, { isAgent: true });
|
|
5652
|
+
} else if (isKilo) {
|
|
5653
|
+
content = convertClaudeToKiloFrontmatter(content, { isAgent: true });
|
|
4235
5654
|
} else if (isGemini) {
|
|
4236
5655
|
content = convertClaudeToGeminiAgent(content);
|
|
4237
5656
|
} else if (isCodex) {
|
|
@@ -4244,6 +5663,14 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
4244
5663
|
content = convertClaudeAgentToCursorAgent(content);
|
|
4245
5664
|
} else if (isWindsurf) {
|
|
4246
5665
|
content = convertClaudeAgentToWindsurfAgent(content);
|
|
5666
|
+
} else if (isAugment) {
|
|
5667
|
+
content = convertClaudeAgentToAugmentAgent(content);
|
|
5668
|
+
} else if (isTrae) {
|
|
5669
|
+
content = convertClaudeAgentToTraeAgent(content);
|
|
5670
|
+
} else if (isCodebuddy) {
|
|
5671
|
+
content = convertClaudeAgentToCodebuddyAgent(content);
|
|
5672
|
+
} else if (isCline) {
|
|
5673
|
+
content = convertClaudeAgentToClineAgent(content);
|
|
4247
5674
|
}
|
|
4248
5675
|
const destName = isCopilot ? entry.name.replace('.md', '.agent.md') : entry.name;
|
|
4249
5676
|
fs.writeFileSync(path.join(agentsDest, destName), content);
|
|
@@ -4277,7 +5704,7 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
4277
5704
|
failures.push('VERSION');
|
|
4278
5705
|
}
|
|
4279
5706
|
|
|
4280
|
-
if (!isCodex && !isCopilot && !isCursor && !isWindsurf) {
|
|
5707
|
+
if (!isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae && !isCline) {
|
|
4281
5708
|
// Write package.json to force CommonJS mode for SDD scripts
|
|
4282
5709
|
// Prevents "require is not defined" errors when project has "type": "module"
|
|
4283
5710
|
// Node.js walks up looking for package.json - this stops inheritance from project
|
|
@@ -4308,11 +5735,22 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
4308
5735
|
try { fs.chmodSync(destFile, 0o755); } catch (e) { /* Windows doesn't support chmod */ }
|
|
4309
5736
|
} else {
|
|
4310
5737
|
fs.copyFileSync(srcFile, destFile);
|
|
5738
|
+
// Ensure .sh hook files are executable (mirrors chmod in build-hooks.js)
|
|
5739
|
+
if (entry.endsWith('.sh')) {
|
|
5740
|
+
try { fs.chmodSync(destFile, 0o755); } catch (e) { /* Windows doesn't support chmod */ }
|
|
5741
|
+
}
|
|
4311
5742
|
}
|
|
4312
5743
|
}
|
|
4313
5744
|
}
|
|
4314
5745
|
if (verifyInstalled(hooksDest, 'hooks')) {
|
|
4315
5746
|
console.log(` ${green}✓${reset} Installed hooks (bundled)`);
|
|
5747
|
+
// Warn if expected community .sh hooks are missing (non-fatal)
|
|
5748
|
+
const expectedShHooks = ['sdd-session-state.sh', 'sdd-validate-commit.sh', 'sdd-phase-boundary.sh'];
|
|
5749
|
+
for (const sh of expectedShHooks) {
|
|
5750
|
+
if (!fs.existsSync(path.join(hooksDest, sh))) {
|
|
5751
|
+
console.warn(` ${yellow}⚠${reset} Missing expected hook: ${sh}`);
|
|
5752
|
+
}
|
|
5753
|
+
}
|
|
4316
5754
|
} else {
|
|
4317
5755
|
failures.push('hooks');
|
|
4318
5756
|
}
|
|
@@ -4320,8 +5758,8 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
4320
5758
|
}
|
|
4321
5759
|
|
|
4322
5760
|
// Clear stale update cache so next session re-evaluates hook versions
|
|
4323
|
-
//
|
|
4324
|
-
const updateCacheFile = path.join(
|
|
5761
|
+
// Cache lives at ~/.cache/sdd/ (see hooks/sdd-check-update.js line 35-36)
|
|
5762
|
+
const updateCacheFile = path.join(os.homedir(), '.cache', 'sdd', 'sdd-update-check.json');
|
|
4325
5763
|
try { fs.unlinkSync(updateCacheFile); } catch (e) { /* cache may not exist yet */ }
|
|
4326
5764
|
|
|
4327
5765
|
if (failures.length > 0) {
|
|
@@ -4400,14 +5838,21 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
4400
5838
|
configContent = setManagedCodexHooksOwnership(codexHooksFeature.content, codexHooksFeature.ownership);
|
|
4401
5839
|
|
|
4402
5840
|
// Add SessionStart hook for update checking
|
|
4403
|
-
const updateCheckScript = path.resolve(targetDir, '
|
|
5841
|
+
const updateCheckScript = path.resolve(targetDir, 'hooks', 'sdd-check-update.js').replace(/\\/g, '/');
|
|
4404
5842
|
const hookBlock =
|
|
4405
5843
|
`${eol}# SDD Hooks${eol}` +
|
|
4406
5844
|
`[[hooks]]${eol}` +
|
|
4407
5845
|
`event = "SessionStart"${eol}` +
|
|
4408
5846
|
`command = "node ${updateCheckScript}"${eol}`;
|
|
4409
5847
|
|
|
4410
|
-
|
|
5848
|
+
// Migrate legacy sdd-update-check entries from prior installs (#1755 followup)
|
|
5849
|
+
// Remove stale hook blocks that used the inverted filename or wrong path
|
|
5850
|
+
if (configContent.includes('sdd-update-check')) {
|
|
5851
|
+
configContent = configContent.replace(/\n# SDD Hooks\n\[\[hooks\]\]\nevent = "SessionStart"\ncommand = "node [^\n]*sdd-update-check\.js"\n/g, '\n');
|
|
5852
|
+
configContent = configContent.replace(/\r\n# SDD Hooks\r\n\[\[hooks\]\]\r\nevent = "SessionStart"\r\ncommand = "node [^\r\n]*sdd-update-check\.js"\r\n/g, '\r\n');
|
|
5853
|
+
}
|
|
5854
|
+
|
|
5855
|
+
if (hasEnabledCodexHooksFeature(configContent) && !configContent.includes('sdd-check-update')) {
|
|
4411
5856
|
configContent += hookBlock;
|
|
4412
5857
|
}
|
|
4413
5858
|
|
|
@@ -4417,7 +5862,7 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
4417
5862
|
console.warn(` ${yellow}⚠${reset} Could not configure Codex hooks: ${e.message}`);
|
|
4418
5863
|
}
|
|
4419
5864
|
|
|
4420
|
-
return { settingsPath: null, settings: null, statuslineCommand: null, runtime };
|
|
5865
|
+
return { settingsPath: null, settings: null, statuslineCommand: null, runtime, configDir: targetDir };
|
|
4421
5866
|
}
|
|
4422
5867
|
|
|
4423
5868
|
if (isCopilot) {
|
|
@@ -4430,36 +5875,72 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
4430
5875
|
console.log(` ${green}✓${reset} Generated copilot-instructions.md`);
|
|
4431
5876
|
}
|
|
4432
5877
|
// Copilot: no settings.json, no hooks, no statusline (like Codex)
|
|
4433
|
-
return { settingsPath: null, settings: null, statuslineCommand: null, runtime };
|
|
5878
|
+
return { settingsPath: null, settings: null, statuslineCommand: null, runtime, configDir: targetDir };
|
|
4434
5879
|
}
|
|
4435
5880
|
|
|
4436
5881
|
if (isCursor) {
|
|
4437
5882
|
// Cursor uses skills — no config.toml, no settings.json hooks needed
|
|
4438
|
-
return { settingsPath: null, settings: null, statuslineCommand: null, runtime };
|
|
5883
|
+
return { settingsPath: null, settings: null, statuslineCommand: null, runtime, configDir: targetDir };
|
|
4439
5884
|
}
|
|
4440
5885
|
|
|
4441
5886
|
if (isWindsurf) {
|
|
4442
5887
|
// Windsurf uses skills — no config.toml, no settings.json hooks needed
|
|
4443
|
-
return { settingsPath: null, settings: null, statuslineCommand: null, runtime };
|
|
5888
|
+
return { settingsPath: null, settings: null, statuslineCommand: null, runtime, configDir: targetDir };
|
|
5889
|
+
}
|
|
5890
|
+
|
|
5891
|
+
if (isTrae) {
|
|
5892
|
+
// Trae uses skills — no settings.json hooks needed
|
|
5893
|
+
return { settingsPath: null, settings: null, statuslineCommand: null, runtime, configDir: targetDir };
|
|
5894
|
+
}
|
|
5895
|
+
|
|
5896
|
+
if (isCline) {
|
|
5897
|
+
// Cline uses .clinerules — generate a rules file with SDD system instructions
|
|
5898
|
+
const clinerulesDest = path.join(targetDir, '.clinerules');
|
|
5899
|
+
const clinerules = [
|
|
5900
|
+
'# SDD — Spec-Driven Development',
|
|
5901
|
+
'',
|
|
5902
|
+
'- SDD workflows live in `sdd/workflows/`. Load the relevant workflow when',
|
|
5903
|
+
' the user runs a `/sdd-*` command.',
|
|
5904
|
+
'- SDD agents live in `agents/`. Use the matching agent when spawning subagents.',
|
|
5905
|
+
'- SDD tools are at `sdd/bin/sdd-tools.cjs`. Run with `node`.',
|
|
5906
|
+
'- Planning artifacts live in `.planning/`. Never edit them outside a SDD workflow.',
|
|
5907
|
+
'- Do not apply SDD workflows unless the user explicitly asks for them.',
|
|
5908
|
+
'- When a SDD command triggers a deliverable (feature, fix, docs), offer the next',
|
|
5909
|
+
' step to the user using Cline\'s ask_user tool after completing it.',
|
|
5910
|
+
].join('\n') + '\n';
|
|
5911
|
+
fs.writeFileSync(clinerulesDest, clinerules);
|
|
5912
|
+
console.log(` ${green}✓${reset} Wrote .clinerules`);
|
|
5913
|
+
return { settingsPath: null, settings: null, statuslineCommand: null, runtime, configDir: targetDir };
|
|
4444
5914
|
}
|
|
4445
5915
|
|
|
4446
5916
|
// Configure statusline and hooks in settings.json
|
|
4447
5917
|
// Gemini and Antigravity use AfterTool instead of PostToolUse for post-tool hooks
|
|
4448
5918
|
const postToolEvent = (runtime === 'gemini' || runtime === 'antigravity') ? 'AfterTool' : 'PostToolUse';
|
|
4449
5919
|
const settingsPath = path.join(targetDir, 'settings.json');
|
|
4450
|
-
const
|
|
5920
|
+
const rawSettings = readSettings(settingsPath);
|
|
5921
|
+
if (rawSettings === null) {
|
|
5922
|
+
console.log(' ' + yellow + 'i' + reset + ' Skipping settings.json configuration — file could not be parsed (comments or malformed JSON). Your existing settings are preserved.');
|
|
5923
|
+
return;
|
|
5924
|
+
}
|
|
5925
|
+
const settings = validateHookFields(cleanupOrphanedHooks(rawSettings));
|
|
5926
|
+
// Local installs anchor paths to $CLAUDE_PROJECT_DIR so hooks resolve
|
|
5927
|
+
// correctly regardless of the shell's current working directory (#1906).
|
|
5928
|
+
const localPrefix = '"$CLAUDE_PROJECT_DIR"/' + dirName;
|
|
4451
5929
|
const statuslineCommand = isGlobal
|
|
4452
5930
|
? buildHookCommand(targetDir, 'sdd-statusline.js')
|
|
4453
|
-
: 'node ' +
|
|
5931
|
+
: 'node ' + localPrefix + '/hooks/sdd-statusline.js';
|
|
4454
5932
|
const updateCheckCommand = isGlobal
|
|
4455
5933
|
? buildHookCommand(targetDir, 'sdd-check-update.js')
|
|
4456
|
-
: 'node ' +
|
|
5934
|
+
: 'node ' + localPrefix + '/hooks/sdd-check-update.js';
|
|
4457
5935
|
const contextMonitorCommand = isGlobal
|
|
4458
5936
|
? buildHookCommand(targetDir, 'sdd-context-monitor.js')
|
|
4459
|
-
: 'node ' +
|
|
5937
|
+
: 'node ' + localPrefix + '/hooks/sdd-context-monitor.js';
|
|
4460
5938
|
const promptGuardCommand = isGlobal
|
|
4461
5939
|
? buildHookCommand(targetDir, 'sdd-prompt-guard.js')
|
|
4462
|
-
: 'node ' +
|
|
5940
|
+
: 'node ' + localPrefix + '/hooks/sdd-prompt-guard.js';
|
|
5941
|
+
const readGuardCommand = isGlobal
|
|
5942
|
+
? buildHookCommand(targetDir, 'sdd-read-guard.js')
|
|
5943
|
+
: 'node ' + localPrefix + '/hooks/sdd-read-guard.js';
|
|
4463
5944
|
|
|
4464
5945
|
// Enable experimental agents for Gemini CLI (required for custom sub-agents)
|
|
4465
5946
|
if (isGemini) {
|
|
@@ -4473,7 +5954,7 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
4473
5954
|
}
|
|
4474
5955
|
|
|
4475
5956
|
// Configure SessionStart hook for update checking (skip for opencode)
|
|
4476
|
-
if (!isOpencode) {
|
|
5957
|
+
if (!isOpencode && !isKilo) {
|
|
4477
5958
|
if (!settings.hooks) {
|
|
4478
5959
|
settings.hooks = {};
|
|
4479
5960
|
}
|
|
@@ -4481,11 +5962,16 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
4481
5962
|
settings.hooks.SessionStart = [];
|
|
4482
5963
|
}
|
|
4483
5964
|
|
|
4484
|
-
const
|
|
5965
|
+
const hasSddUpdateHook = settings.hooks.SessionStart.some(entry =>
|
|
4485
5966
|
entry.hooks && entry.hooks.some(h => h.command && h.command.includes('sdd-check-update'))
|
|
4486
5967
|
);
|
|
4487
5968
|
|
|
4488
|
-
if (
|
|
5969
|
+
// Guard: only register if the hook file was actually installed (#1754).
|
|
5970
|
+
// When hooks/dist/ is missing from the npm package (as in v1.32.0), the
|
|
5971
|
+
// copy step produces no files but the registration step ran unconditionally,
|
|
5972
|
+
// causing "hook error" on every tool invocation.
|
|
5973
|
+
const checkUpdateFile = path.join(targetDir, 'hooks', 'sdd-check-update.js');
|
|
5974
|
+
if (!hasSddUpdateHook && fs.existsSync(checkUpdateFile)) {
|
|
4489
5975
|
settings.hooks.SessionStart.push({
|
|
4490
5976
|
hooks: [
|
|
4491
5977
|
{
|
|
@@ -4495,6 +5981,8 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
4495
5981
|
]
|
|
4496
5982
|
});
|
|
4497
5983
|
console.log(` ${green}✓${reset} Configured update check hook`);
|
|
5984
|
+
} else if (!hasSddUpdateHook && !fs.existsSync(checkUpdateFile)) {
|
|
5985
|
+
console.warn(` ${yellow}⚠${reset} Skipped update check hook — sdd-check-update.js not found at target`);
|
|
4498
5986
|
}
|
|
4499
5987
|
|
|
4500
5988
|
// Configure post-tool hook for context window monitoring
|
|
@@ -4506,7 +5994,8 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
4506
5994
|
entry.hooks && entry.hooks.some(h => h.command && h.command.includes('sdd-context-monitor'))
|
|
4507
5995
|
);
|
|
4508
5996
|
|
|
4509
|
-
|
|
5997
|
+
const contextMonitorFile = path.join(targetDir, 'hooks', 'sdd-context-monitor.js');
|
|
5998
|
+
if (!hasContextMonitorHook && fs.existsSync(contextMonitorFile)) {
|
|
4510
5999
|
settings.hooks[postToolEvent].push({
|
|
4511
6000
|
matcher: 'Bash|Edit|Write|MultiEdit|Agent|Task',
|
|
4512
6001
|
hooks: [
|
|
@@ -4518,6 +6007,8 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
4518
6007
|
]
|
|
4519
6008
|
});
|
|
4520
6009
|
console.log(` ${green}✓${reset} Configured context window monitor hook`);
|
|
6010
|
+
} else if (!hasContextMonitorHook && !fs.existsSync(contextMonitorFile)) {
|
|
6011
|
+
console.warn(` ${yellow}⚠${reset} Skipped context monitor hook — sdd-context-monitor.js not found at target`);
|
|
4521
6012
|
} else {
|
|
4522
6013
|
// Migrate existing context monitor hooks: add matcher and timeout if missing
|
|
4523
6014
|
for (const entry of settings.hooks[postToolEvent]) {
|
|
@@ -4551,7 +6042,8 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
4551
6042
|
entry.hooks && entry.hooks.some(h => h.command && h.command.includes('sdd-prompt-guard'))
|
|
4552
6043
|
);
|
|
4553
6044
|
|
|
4554
|
-
|
|
6045
|
+
const promptGuardFile = path.join(targetDir, 'hooks', 'sdd-prompt-guard.js');
|
|
6046
|
+
if (!hasPromptGuardHook && fs.existsSync(promptGuardFile)) {
|
|
4555
6047
|
settings.hooks[preToolEvent].push({
|
|
4556
6048
|
matcher: 'Write|Edit',
|
|
4557
6049
|
hooks: [
|
|
@@ -4563,23 +6055,157 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
4563
6055
|
]
|
|
4564
6056
|
});
|
|
4565
6057
|
console.log(` ${green}✓${reset} Configured prompt injection guard hook`);
|
|
6058
|
+
} else if (!hasPromptGuardHook && !fs.existsSync(promptGuardFile)) {
|
|
6059
|
+
console.warn(` ${yellow}⚠${reset} Skipped prompt guard hook — sdd-prompt-guard.js not found at target`);
|
|
6060
|
+
}
|
|
6061
|
+
|
|
6062
|
+
// Configure PreToolUse hook for read-before-edit guidance (#1628)
|
|
6063
|
+
// Prevents infinite retry loops when non-Claude models attempt to edit
|
|
6064
|
+
// files without reading them first. Advisory-only — does not block.
|
|
6065
|
+
const hasReadGuardHook = settings.hooks[preToolEvent].some(entry =>
|
|
6066
|
+
entry.hooks && entry.hooks.some(h => h.command && h.command.includes('sdd-read-guard'))
|
|
6067
|
+
);
|
|
6068
|
+
|
|
6069
|
+
const readGuardFile = path.join(targetDir, 'hooks', 'sdd-read-guard.js');
|
|
6070
|
+
if (!hasReadGuardHook && fs.existsSync(readGuardFile)) {
|
|
6071
|
+
settings.hooks[preToolEvent].push({
|
|
6072
|
+
matcher: 'Write|Edit',
|
|
6073
|
+
hooks: [
|
|
6074
|
+
{
|
|
6075
|
+
type: 'command',
|
|
6076
|
+
command: readGuardCommand,
|
|
6077
|
+
timeout: 5
|
|
6078
|
+
}
|
|
6079
|
+
]
|
|
6080
|
+
});
|
|
6081
|
+
console.log(` ${green}✓${reset} Configured read-before-edit guard hook`);
|
|
6082
|
+
} else if (!hasReadGuardHook && !fs.existsSync(readGuardFile)) {
|
|
6083
|
+
console.warn(` ${yellow}⚠${reset} Skipped read guard hook — sdd-read-guard.js not found at target`);
|
|
6084
|
+
}
|
|
6085
|
+
|
|
6086
|
+
// Community hooks — registered on install but opt-in at runtime.
|
|
6087
|
+
// Each hook checks .planning/config.json for hooks.community: true
|
|
6088
|
+
// and exits silently (no-op) if not enabled. This lets users enable
|
|
6089
|
+
// them per-project by adding: "hooks": { "community": true }
|
|
6090
|
+
|
|
6091
|
+
// Configure workflow guard hook (opt-in via hooks.workflow_guard: true)
|
|
6092
|
+
// Detects file edits outside SDD workflow context and advises using
|
|
6093
|
+
// /sdd-quick or /sdd-fast for state-tracked changes. Advisory only.
|
|
6094
|
+
const workflowGuardCommand = isGlobal
|
|
6095
|
+
? buildHookCommand(targetDir, 'sdd-workflow-guard.js')
|
|
6096
|
+
: 'node ' + localPrefix + '/hooks/sdd-workflow-guard.js';
|
|
6097
|
+
const hasWorkflowGuardHook = settings.hooks[preToolEvent].some(entry =>
|
|
6098
|
+
entry.hooks && entry.hooks.some(h => h.command && h.command.includes('sdd-workflow-guard'))
|
|
6099
|
+
);
|
|
6100
|
+
|
|
6101
|
+
const workflowGuardFile = path.join(targetDir, 'hooks', 'sdd-workflow-guard.js');
|
|
6102
|
+
if (!hasWorkflowGuardHook && fs.existsSync(workflowGuardFile)) {
|
|
6103
|
+
settings.hooks[preToolEvent].push({
|
|
6104
|
+
matcher: 'Write|Edit',
|
|
6105
|
+
hooks: [
|
|
6106
|
+
{
|
|
6107
|
+
type: 'command',
|
|
6108
|
+
command: workflowGuardCommand,
|
|
6109
|
+
timeout: 5
|
|
6110
|
+
}
|
|
6111
|
+
]
|
|
6112
|
+
});
|
|
6113
|
+
console.log(` ${green}✓${reset} Configured workflow guard hook (opt-in via hooks.workflow_guard)`);
|
|
6114
|
+
} else if (!hasWorkflowGuardHook && !fs.existsSync(workflowGuardFile)) {
|
|
6115
|
+
console.warn(` ${yellow}⚠${reset} Skipped workflow guard hook — sdd-workflow-guard.js not found at target`);
|
|
6116
|
+
}
|
|
6117
|
+
|
|
6118
|
+
// Configure commit validation hook (Conventional Commits enforcement, opt-in)
|
|
6119
|
+
const validateCommitCommand = isGlobal
|
|
6120
|
+
? buildHookCommand(targetDir, 'sdd-validate-commit.sh')
|
|
6121
|
+
: 'bash ' + localPrefix + '/hooks/sdd-validate-commit.sh';
|
|
6122
|
+
const hasValidateCommitHook = settings.hooks[preToolEvent].some(entry =>
|
|
6123
|
+
entry.hooks && entry.hooks.some(h => h.command && h.command.includes('sdd-validate-commit'))
|
|
6124
|
+
);
|
|
6125
|
+
// Guard: only register if the .sh file was actually installed. If the npm package
|
|
6126
|
+
// omitted the file (as happened in v1.32.0, bug #1817), registering a missing hook
|
|
6127
|
+
// causes a hook error on every Bash tool invocation.
|
|
6128
|
+
const validateCommitFile = path.join(targetDir, 'hooks', 'sdd-validate-commit.sh');
|
|
6129
|
+
if (!hasValidateCommitHook && fs.existsSync(validateCommitFile)) {
|
|
6130
|
+
settings.hooks[preToolEvent].push({
|
|
6131
|
+
matcher: 'Bash',
|
|
6132
|
+
hooks: [
|
|
6133
|
+
{
|
|
6134
|
+
type: 'command',
|
|
6135
|
+
command: validateCommitCommand,
|
|
6136
|
+
timeout: 5
|
|
6137
|
+
}
|
|
6138
|
+
]
|
|
6139
|
+
});
|
|
6140
|
+
console.log(` ${green}✓${reset} Configured commit validation hook (opt-in via config)`);
|
|
6141
|
+
} else if (!hasValidateCommitHook && !fs.existsSync(validateCommitFile)) {
|
|
6142
|
+
console.warn(` ${yellow}⚠${reset} Skipped commit validation hook — sdd-validate-commit.sh not found at target`);
|
|
6143
|
+
}
|
|
6144
|
+
|
|
6145
|
+
// Configure session state orientation hook (opt-in)
|
|
6146
|
+
const sessionStateCommand = isGlobal
|
|
6147
|
+
? buildHookCommand(targetDir, 'sdd-session-state.sh')
|
|
6148
|
+
: 'bash ' + localPrefix + '/hooks/sdd-session-state.sh';
|
|
6149
|
+
const hasSessionStateHook = settings.hooks.SessionStart.some(entry =>
|
|
6150
|
+
entry.hooks && entry.hooks.some(h => h.command && h.command.includes('sdd-session-state'))
|
|
6151
|
+
);
|
|
6152
|
+
const sessionStateFile = path.join(targetDir, 'hooks', 'sdd-session-state.sh');
|
|
6153
|
+
if (!hasSessionStateHook && fs.existsSync(sessionStateFile)) {
|
|
6154
|
+
settings.hooks.SessionStart.push({
|
|
6155
|
+
hooks: [
|
|
6156
|
+
{
|
|
6157
|
+
type: 'command',
|
|
6158
|
+
command: sessionStateCommand
|
|
6159
|
+
}
|
|
6160
|
+
]
|
|
6161
|
+
});
|
|
6162
|
+
console.log(` ${green}✓${reset} Configured session state orientation hook (opt-in via config)`);
|
|
6163
|
+
} else if (!hasSessionStateHook && !fs.existsSync(sessionStateFile)) {
|
|
6164
|
+
console.warn(` ${yellow}⚠${reset} Skipped session state hook — sdd-session-state.sh not found at target`);
|
|
6165
|
+
}
|
|
6166
|
+
|
|
6167
|
+
// Configure phase boundary detection hook (opt-in)
|
|
6168
|
+
const phaseBoundaryCommand = isGlobal
|
|
6169
|
+
? buildHookCommand(targetDir, 'sdd-phase-boundary.sh')
|
|
6170
|
+
: 'bash ' + localPrefix + '/hooks/sdd-phase-boundary.sh';
|
|
6171
|
+
const hasPhaseBoundaryHook = settings.hooks[postToolEvent].some(entry =>
|
|
6172
|
+
entry.hooks && entry.hooks.some(h => h.command && h.command.includes('sdd-phase-boundary'))
|
|
6173
|
+
);
|
|
6174
|
+
const phaseBoundaryFile = path.join(targetDir, 'hooks', 'sdd-phase-boundary.sh');
|
|
6175
|
+
if (!hasPhaseBoundaryHook && fs.existsSync(phaseBoundaryFile)) {
|
|
6176
|
+
settings.hooks[postToolEvent].push({
|
|
6177
|
+
matcher: 'Write|Edit',
|
|
6178
|
+
hooks: [
|
|
6179
|
+
{
|
|
6180
|
+
type: 'command',
|
|
6181
|
+
command: phaseBoundaryCommand,
|
|
6182
|
+
timeout: 5
|
|
6183
|
+
}
|
|
6184
|
+
]
|
|
6185
|
+
});
|
|
6186
|
+
console.log(` ${green}✓${reset} Configured phase boundary detection hook (opt-in via config)`);
|
|
6187
|
+
} else if (!hasPhaseBoundaryHook && !fs.existsSync(phaseBoundaryFile)) {
|
|
6188
|
+
console.warn(` ${yellow}⚠${reset} Skipped phase boundary hook — sdd-phase-boundary.sh not found at target`);
|
|
4566
6189
|
}
|
|
4567
6190
|
}
|
|
4568
6191
|
|
|
4569
|
-
return { settingsPath, settings, statuslineCommand, runtime };
|
|
6192
|
+
return { settingsPath, settings, statuslineCommand, runtime, configDir: targetDir };
|
|
4570
6193
|
}
|
|
4571
6194
|
|
|
4572
6195
|
/**
|
|
4573
6196
|
* Apply statusline config, then print completion message
|
|
4574
6197
|
*/
|
|
4575
|
-
function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = 'claude', isGlobal = true) {
|
|
6198
|
+
function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = 'claude', isGlobal = true, configDir = null) {
|
|
4576
6199
|
const isOpencode = runtime === 'opencode';
|
|
6200
|
+
const isKilo = runtime === 'kilo';
|
|
4577
6201
|
const isCodex = runtime === 'codex';
|
|
4578
6202
|
const isCopilot = runtime === 'copilot';
|
|
4579
6203
|
const isCursor = runtime === 'cursor';
|
|
4580
6204
|
const isWindsurf = runtime === 'windsurf';
|
|
6205
|
+
const isTrae = runtime === 'trae';
|
|
6206
|
+
const isCline = runtime === 'cline';
|
|
4581
6207
|
|
|
4582
|
-
if (shouldInstallStatusline && !isOpencode && !isCodex && !isCopilot && !isCursor && !isWindsurf) {
|
|
6208
|
+
if (shouldInstallStatusline && !isOpencode && !isKilo && !isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae) {
|
|
4583
6209
|
settings.statusLine = {
|
|
4584
6210
|
type: 'command',
|
|
4585
6211
|
command: statuslineCommand
|
|
@@ -4588,13 +6214,18 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
4588
6214
|
}
|
|
4589
6215
|
|
|
4590
6216
|
// Write settings when runtime supports settings.json
|
|
4591
|
-
if (!isCodex && !isCopilot && !isCursor && !isWindsurf) {
|
|
6217
|
+
if (!isCodex && !isCopilot && !isKilo && !isCursor && !isWindsurf && !isTrae && !isCline) {
|
|
4592
6218
|
writeSettings(settingsPath, settings);
|
|
4593
6219
|
}
|
|
4594
6220
|
|
|
4595
6221
|
// Configure OpenCode permissions
|
|
4596
6222
|
if (isOpencode) {
|
|
4597
|
-
configureOpencodePermissions(isGlobal);
|
|
6223
|
+
configureOpencodePermissions(isGlobal, configDir);
|
|
6224
|
+
}
|
|
6225
|
+
|
|
6226
|
+
// Configure Kilo permissions
|
|
6227
|
+
if (isKilo) {
|
|
6228
|
+
configureKiloPermissions(isGlobal, configDir);
|
|
4598
6229
|
}
|
|
4599
6230
|
|
|
4600
6231
|
// For non-Claude runtimes, set resolve_model_ids: "omit" in ~/.sdd/defaults.json
|
|
@@ -4621,21 +6252,31 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
4621
6252
|
let program = 'Claude Code';
|
|
4622
6253
|
if (runtime === 'opencode') program = 'OpenCode';
|
|
4623
6254
|
if (runtime === 'gemini') program = 'Gemini';
|
|
6255
|
+
if (runtime === 'kilo') program = 'Kilo';
|
|
4624
6256
|
if (runtime === 'codex') program = 'Codex';
|
|
4625
6257
|
if (runtime === 'copilot') program = 'Copilot';
|
|
4626
6258
|
if (runtime === 'antigravity') program = 'Antigravity';
|
|
4627
6259
|
if (runtime === 'cursor') program = 'Cursor';
|
|
6260
|
+
if (runtime === 'windsurf') program = 'Windsurf';
|
|
6261
|
+
if (runtime === 'augment') program = 'Augment';
|
|
6262
|
+
if (runtime === 'trae') program = 'Trae';
|
|
6263
|
+
if (runtime === 'cline') program = 'Cline';
|
|
4628
6264
|
|
|
4629
|
-
let command = '/sdd
|
|
6265
|
+
let command = '/sdd-new-project';
|
|
4630
6266
|
if (runtime === 'opencode') command = '/sdd-new-project';
|
|
6267
|
+
if (runtime === 'kilo') command = '/sdd-new-project';
|
|
4631
6268
|
if (runtime === 'codex') command = '$sdd-new-project';
|
|
4632
6269
|
if (runtime === 'copilot') command = '/sdd-new-project';
|
|
4633
6270
|
if (runtime === 'antigravity') command = '/sdd-new-project';
|
|
4634
6271
|
if (runtime === 'cursor') command = 'sdd-new-project (mention the skill name)';
|
|
6272
|
+
if (runtime === 'windsurf') command = '/sdd-new-project';
|
|
6273
|
+
if (runtime === 'augment') command = '/sdd-new-project';
|
|
6274
|
+
if (runtime === 'trae') command = '/sdd-new-project';
|
|
6275
|
+
if (runtime === 'cline') command = '/sdd-new-project';
|
|
4635
6276
|
console.log(`
|
|
4636
6277
|
${green}Done!${reset} Open a blank directory in ${program} and run ${cyan}${command}${reset}.
|
|
4637
6278
|
|
|
4638
|
-
${cyan}Join the community:${reset} https://discord.gg/
|
|
6279
|
+
${cyan}Join the community:${reset} https://discord.gg/mYgfVNfA2r
|
|
4639
6280
|
`);
|
|
4640
6281
|
}
|
|
4641
6282
|
|
|
@@ -4690,71 +6331,6 @@ function handleStatusline(settings, isInteractive, callback) {
|
|
|
4690
6331
|
});
|
|
4691
6332
|
}
|
|
4692
6333
|
|
|
4693
|
-
/**
|
|
4694
|
-
* Install the SDD SDK globally via npm.
|
|
4695
|
-
* @returns {boolean} true if install succeeded
|
|
4696
|
-
*/
|
|
4697
|
-
function installSdk() {
|
|
4698
|
-
const sdkVersion = pkg.version;
|
|
4699
|
-
const sdkPkg = `@gsd-build/sdk@${sdkVersion}`;
|
|
4700
|
-
console.log(`\n ${cyan}Installing SDD SDK...${reset}`);
|
|
4701
|
-
console.log(` ${dim}npm install -g ${sdkPkg}${reset}\n`);
|
|
4702
|
-
try {
|
|
4703
|
-
require('child_process').execSync(`npm install -g ${sdkPkg}`, { stdio: 'inherit' });
|
|
4704
|
-
console.log(`\n ${green}✓${reset} SDD SDK installed (${cyan}sdd-sdk${reset} command available)`);
|
|
4705
|
-
return true;
|
|
4706
|
-
} catch (e) {
|
|
4707
|
-
console.log(`\n ${yellow}⚠${reset} SDK install failed: ${e.message}`);
|
|
4708
|
-
console.log(` ${dim}You can install it manually: npm install -g ${sdkPkg}${reset}`);
|
|
4709
|
-
return false;
|
|
4710
|
-
}
|
|
4711
|
-
}
|
|
4712
|
-
|
|
4713
|
-
/**
|
|
4714
|
-
* Prompt the user to optionally install the SDD SDK.
|
|
4715
|
-
* Called after runtime installation completes.
|
|
4716
|
-
* @param {Function} callback - called with true/false
|
|
4717
|
-
*/
|
|
4718
|
-
function promptSdk(callback) {
|
|
4719
|
-
if (!process.stdin.isTTY) {
|
|
4720
|
-
callback(false);
|
|
4721
|
-
return;
|
|
4722
|
-
}
|
|
4723
|
-
|
|
4724
|
-
const rl = readline.createInterface({
|
|
4725
|
-
input: process.stdin,
|
|
4726
|
-
output: process.stdout
|
|
4727
|
-
});
|
|
4728
|
-
|
|
4729
|
-
let answered = false;
|
|
4730
|
-
|
|
4731
|
-
rl.on('close', () => {
|
|
4732
|
-
if (!answered) {
|
|
4733
|
-
answered = true;
|
|
4734
|
-
callback(false);
|
|
4735
|
-
}
|
|
4736
|
-
});
|
|
4737
|
-
|
|
4738
|
-
console.log(`
|
|
4739
|
-
${yellow}Also install the SDD SDK?${reset}
|
|
4740
|
-
|
|
4741
|
-
The SDK provides a standalone CLI for autonomous execution:
|
|
4742
|
-
${dim}sdd-sdk init @prd.md${reset} Bootstrap a project from a PRD
|
|
4743
|
-
${dim}sdd-sdk auto${reset} Run full autonomous lifecycle
|
|
4744
|
-
${dim}sdd-sdk run "prompt"${reset} Execute a milestone from text
|
|
4745
|
-
|
|
4746
|
-
${cyan}1${reset}) No
|
|
4747
|
-
${cyan}2${reset}) Yes ${dim}(runs: npm install -g @gsd-build/sdk)${reset}
|
|
4748
|
-
`);
|
|
4749
|
-
|
|
4750
|
-
rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
|
|
4751
|
-
answered = true;
|
|
4752
|
-
rl.close();
|
|
4753
|
-
const choice = answer.trim() || '1';
|
|
4754
|
-
callback(choice === '2');
|
|
4755
|
-
});
|
|
4756
|
-
}
|
|
4757
|
-
|
|
4758
6334
|
/**
|
|
4759
6335
|
* Prompt for runtime selection
|
|
4760
6336
|
*/
|
|
@@ -4776,27 +6352,39 @@ function promptRuntime(callback) {
|
|
|
4776
6352
|
|
|
4777
6353
|
const runtimeMap = {
|
|
4778
6354
|
'1': 'claude',
|
|
4779
|
-
'2': '
|
|
4780
|
-
'3': '
|
|
4781
|
-
'4': '
|
|
4782
|
-
'5': '
|
|
4783
|
-
'6': '
|
|
4784
|
-
'7': '
|
|
4785
|
-
'8': '
|
|
6355
|
+
'2': 'antigravity',
|
|
6356
|
+
'3': 'augment',
|
|
6357
|
+
'4': 'cline',
|
|
6358
|
+
'5': 'codebuddy',
|
|
6359
|
+
'6': 'codex',
|
|
6360
|
+
'7': 'copilot',
|
|
6361
|
+
'8': 'cursor',
|
|
6362
|
+
'9': 'gemini',
|
|
6363
|
+
'10': 'kilo',
|
|
6364
|
+
'11': 'opencode',
|
|
6365
|
+
'12': 'qwen',
|
|
6366
|
+
'13': 'trae',
|
|
6367
|
+
'14': 'windsurf'
|
|
4786
6368
|
};
|
|
4787
|
-
const allRuntimes = ['claude', '
|
|
6369
|
+
const allRuntimes = ['claude', 'antigravity', 'augment', 'cline', 'codebuddy', 'codex', 'copilot', 'cursor', 'gemini', 'kilo', 'opencode', 'qwen', 'trae', 'windsurf'];
|
|
4788
6370
|
|
|
4789
6371
|
console.log(` ${yellow}Which runtime(s) would you like to install for?${reset}\n\n ${cyan}1${reset}) Claude Code ${dim}(~/.claude)${reset}
|
|
4790
|
-
${cyan}2${reset})
|
|
4791
|
-
${cyan}3${reset})
|
|
4792
|
-
${cyan}4${reset})
|
|
4793
|
-
${cyan}5${reset})
|
|
4794
|
-
${cyan}6${reset})
|
|
4795
|
-
${cyan}7${reset})
|
|
4796
|
-
${cyan}8${reset})
|
|
4797
|
-
${cyan}9${reset})
|
|
4798
|
-
|
|
4799
|
-
${
|
|
6372
|
+
${cyan}2${reset}) Antigravity ${dim}(~/.gemini/antigravity)${reset}
|
|
6373
|
+
${cyan}3${reset}) Augment ${dim}(~/.augment)${reset}
|
|
6374
|
+
${cyan}4${reset}) Cline ${dim}(.clinerules)${reset}
|
|
6375
|
+
${cyan}5${reset}) CodeBuddy ${dim}(~/.codebuddy)${reset}
|
|
6376
|
+
${cyan}6${reset}) Codex ${dim}(~/.codex)${reset}
|
|
6377
|
+
${cyan}7${reset}) Copilot ${dim}(~/.copilot)${reset}
|
|
6378
|
+
${cyan}8${reset}) Cursor ${dim}(~/.cursor)${reset}
|
|
6379
|
+
${cyan}9${reset}) Gemini ${dim}(~/.gemini)${reset}
|
|
6380
|
+
${cyan}10${reset}) Kilo ${dim}(~/.config/kilo)${reset}
|
|
6381
|
+
${cyan}11${reset}) OpenCode ${dim}(~/.config/opencode)${reset}
|
|
6382
|
+
${cyan}12${reset}) Qwen Code ${dim}(~/.qwen)${reset}
|
|
6383
|
+
${cyan}13${reset}) Trae ${dim}(~/.trae)${reset}
|
|
6384
|
+
${cyan}14${reset}) Windsurf ${dim}(~/.codeium/windsurf)${reset}
|
|
6385
|
+
${cyan}15${reset}) All
|
|
6386
|
+
|
|
6387
|
+
${dim}Select multiple: 1,2,6 or 1 2 6${reset}
|
|
4800
6388
|
`);
|
|
4801
6389
|
|
|
4802
6390
|
rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
|
|
@@ -4805,7 +6393,7 @@ function promptRuntime(callback) {
|
|
|
4805
6393
|
const input = answer.trim() || '1';
|
|
4806
6394
|
|
|
4807
6395
|
// "All" shortcut
|
|
4808
|
-
if (input === '
|
|
6396
|
+
if (input === '15') {
|
|
4809
6397
|
callback(allRuntimes);
|
|
4810
6398
|
return;
|
|
4811
6399
|
}
|
|
@@ -4894,23 +6482,13 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
|
|
|
4894
6482
|
result.statuslineCommand,
|
|
4895
6483
|
useStatusline,
|
|
4896
6484
|
result.runtime,
|
|
4897
|
-
isGlobal
|
|
6485
|
+
isGlobal,
|
|
6486
|
+
result.configDir
|
|
4898
6487
|
);
|
|
4899
6488
|
}
|
|
4900
6489
|
};
|
|
4901
6490
|
|
|
4902
|
-
|
|
4903
|
-
// --sdk flag: install without prompting
|
|
4904
|
-
installSdk();
|
|
4905
|
-
printSummaries();
|
|
4906
|
-
} else if (isInteractive) {
|
|
4907
|
-
promptSdk((wantsSdk) => {
|
|
4908
|
-
if (wantsSdk) installSdk();
|
|
4909
|
-
printSummaries();
|
|
4910
|
-
});
|
|
4911
|
-
} else {
|
|
4912
|
-
printSummaries();
|
|
4913
|
-
}
|
|
6491
|
+
printSummaries();
|
|
4914
6492
|
};
|
|
4915
6493
|
|
|
4916
6494
|
if (primaryStatuslineResult) {
|
|
@@ -4931,18 +6509,23 @@ if (process.env.SDD_TEST_MODE) {
|
|
|
4931
6509
|
convertClaudeAgentToCodexAgent,
|
|
4932
6510
|
generateCodexAgentToml,
|
|
4933
6511
|
generateCodexConfigBlock,
|
|
4934
|
-
|
|
6512
|
+
stripSddFromCodexConfig,
|
|
4935
6513
|
mergeCodexConfig,
|
|
4936
6514
|
installCodexConfig,
|
|
4937
6515
|
install,
|
|
6516
|
+
uninstall,
|
|
4938
6517
|
convertClaudeCommandToCodexSkill,
|
|
4939
6518
|
convertClaudeToOpencodeFrontmatter,
|
|
6519
|
+
convertClaudeToKiloFrontmatter,
|
|
6520
|
+
configureOpencodePermissions,
|
|
4940
6521
|
neutralizeAgentReferences,
|
|
4941
6522
|
SDD_CODEX_MARKER,
|
|
4942
6523
|
CODEX_AGENT_SANDBOX,
|
|
4943
6524
|
getDirName,
|
|
4944
6525
|
getGlobalDir,
|
|
4945
6526
|
getConfigDirFromHome,
|
|
6527
|
+
resolveKiloConfigPath,
|
|
6528
|
+
configureKiloPermissions,
|
|
4946
6529
|
claudeToCopilotTools,
|
|
4947
6530
|
convertCopilotToolName,
|
|
4948
6531
|
convertClaudeToCopilotContent,
|
|
@@ -4952,58 +6535,75 @@ if (process.env.SDD_TEST_MODE) {
|
|
|
4952
6535
|
SDD_COPILOT_INSTRUCTIONS_MARKER,
|
|
4953
6536
|
SDD_COPILOT_INSTRUCTIONS_CLOSE_MARKER,
|
|
4954
6537
|
mergeCopilotInstructions,
|
|
4955
|
-
|
|
6538
|
+
stripSddFromCopilotInstructions,
|
|
4956
6539
|
convertClaudeToAntigravityContent,
|
|
4957
6540
|
convertClaudeCommandToAntigravitySkill,
|
|
4958
6541
|
convertClaudeAgentToAntigravityAgent,
|
|
4959
6542
|
copyCommandsAsAntigravitySkills,
|
|
6543
|
+
convertClaudeCommandToClaudeSkill,
|
|
6544
|
+
copyCommandsAsClaudeSkills,
|
|
4960
6545
|
convertClaudeToWindsurfMarkdown,
|
|
4961
6546
|
convertClaudeCommandToWindsurfSkill,
|
|
4962
6547
|
convertClaudeAgentToWindsurfAgent,
|
|
4963
6548
|
copyCommandsAsWindsurfSkills,
|
|
6549
|
+
convertClaudeToAugmentMarkdown,
|
|
6550
|
+
convertClaudeCommandToAugmentSkill,
|
|
6551
|
+
convertClaudeAgentToAugmentAgent,
|
|
6552
|
+
copyCommandsAsAugmentSkills,
|
|
6553
|
+
convertClaudeToTraeMarkdown,
|
|
6554
|
+
convertClaudeCommandToTraeSkill,
|
|
6555
|
+
convertClaudeAgentToTraeAgent,
|
|
6556
|
+
copyCommandsAsTraeSkills,
|
|
6557
|
+
convertClaudeToCodebuddyMarkdown,
|
|
6558
|
+
convertClaudeCommandToCodebuddySkill,
|
|
6559
|
+
convertClaudeAgentToCodebuddyAgent,
|
|
6560
|
+
copyCommandsAsCodebuddySkills,
|
|
6561
|
+
convertClaudeToCliineMarkdown,
|
|
6562
|
+
convertClaudeAgentToClineAgent,
|
|
4964
6563
|
writeManifest,
|
|
4965
6564
|
reportLocalPatches,
|
|
4966
6565
|
validateHookFields,
|
|
4967
|
-
|
|
4968
|
-
|
|
6566
|
+
preserveUserArtifacts,
|
|
6567
|
+
restoreUserArtifacts,
|
|
6568
|
+
finishInstall,
|
|
4969
6569
|
};
|
|
4970
6570
|
} else {
|
|
4971
6571
|
|
|
4972
|
-
// Main logic
|
|
4973
|
-
if (hasGlobal && hasLocal) {
|
|
4974
|
-
|
|
4975
|
-
process.exit(1);
|
|
4976
|
-
} else if (explicitConfigDir && hasLocal) {
|
|
4977
|
-
console.error(` ${yellow}Cannot use --config-dir with --local${reset}`);
|
|
4978
|
-
process.exit(1);
|
|
4979
|
-
} else if (hasUninstall) {
|
|
4980
|
-
if (!hasGlobal && !hasLocal) {
|
|
4981
|
-
console.error(` ${yellow}--uninstall requires --global or --local${reset}`);
|
|
6572
|
+
// Main logic
|
|
6573
|
+
if (hasGlobal && hasLocal) {
|
|
6574
|
+
console.error(` ${yellow}Cannot specify both --global and --local${reset}`);
|
|
4982
6575
|
process.exit(1);
|
|
4983
|
-
}
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
}
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
}
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
6576
|
+
} else if (explicitConfigDir && hasLocal) {
|
|
6577
|
+
console.error(` ${yellow}Cannot use --config-dir with --local${reset}`);
|
|
6578
|
+
process.exit(1);
|
|
6579
|
+
} else if (hasUninstall) {
|
|
6580
|
+
if (!hasGlobal && !hasLocal) {
|
|
6581
|
+
console.error(` ${yellow}--uninstall requires --global or --local${reset}`);
|
|
6582
|
+
process.exit(1);
|
|
6583
|
+
}
|
|
6584
|
+
const runtimes = selectedRuntimes.length > 0 ? selectedRuntimes : ['claude'];
|
|
6585
|
+
for (const runtime of runtimes) {
|
|
6586
|
+
uninstall(hasGlobal, runtime);
|
|
6587
|
+
}
|
|
6588
|
+
} else if (selectedRuntimes.length > 0) {
|
|
6589
|
+
if (!hasGlobal && !hasLocal) {
|
|
6590
|
+
promptLocation(selectedRuntimes);
|
|
6591
|
+
} else {
|
|
6592
|
+
installAllRuntimes(selectedRuntimes, hasGlobal, false);
|
|
6593
|
+
}
|
|
6594
|
+
} else if (hasGlobal || hasLocal) {
|
|
6595
|
+
// Default to Claude if no runtime specified but location is
|
|
6596
|
+
installAllRuntimes(['claude'], hasGlobal, false);
|
|
5002
6597
|
} else {
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
6598
|
+
// Interactive
|
|
6599
|
+
if (!process.stdin.isTTY) {
|
|
6600
|
+
console.log(` ${yellow}Non-interactive terminal detected, defaulting to Claude Code global install${reset}\n`);
|
|
6601
|
+
installAllRuntimes(['claude'], true, false);
|
|
6602
|
+
} else {
|
|
6603
|
+
promptRuntime((runtimes) => {
|
|
6604
|
+
promptLocation(runtimes);
|
|
6605
|
+
});
|
|
6606
|
+
}
|
|
5006
6607
|
}
|
|
5007
|
-
}
|
|
5008
6608
|
|
|
5009
6609
|
} // end of else block for SDD_TEST_MODE
|