get-shit-pretty 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -0
- package/agents/gsp-accessibility-auditor.md +52 -0
- package/agents/gsp-brand-strategist.md +45 -0
- package/agents/gsp-campaign-director.md +48 -0
- package/agents/gsp-critic.md +55 -0
- package/agents/gsp-design-engineer.md +53 -0
- package/agents/gsp-researcher.md +47 -0
- package/agents/gsp-spec-engineer.md +45 -0
- package/agents/gsp-system-architect.md +56 -0
- package/agents/gsp-ui-designer.md +51 -0
- package/bin/install.js +1084 -0
- package/commands/gsp/brand.md +68 -0
- package/commands/gsp/build.md +75 -0
- package/commands/gsp/design.md +71 -0
- package/commands/gsp/help.md +68 -0
- package/commands/gsp/launch.md +75 -0
- package/commands/gsp/new-project.md +91 -0
- package/commands/gsp/progress.md +61 -0
- package/commands/gsp/research.md +71 -0
- package/commands/gsp/review.md +99 -0
- package/commands/gsp/spec.md +68 -0
- package/commands/gsp/system.md +72 -0
- package/package.json +38 -0
- package/prompts/01-design-system-architect.md +29 -0
- package/prompts/02-brand-identity-creator.md +29 -0
- package/prompts/03-ui-ux-pattern-master.md +31 -0
- package/prompts/04-marketing-asset-factory.md +27 -0
- package/prompts/05-figma-auto-layout-expert.md +27 -0
- package/prompts/06-design-critique-partner.md +30 -0
- package/prompts/07-design-trend-synthesizer.md +30 -0
- package/prompts/08-accessibility-auditor.md +29 -0
- package/prompts/09-design-to-code-translator.md +31 -0
- package/references/apple-hig-patterns.md +141 -0
- package/references/design-tokens.md +182 -0
- package/references/nielsen-heuristics.md +141 -0
- package/references/questioning.md +73 -0
- package/references/wcag-checklist.md +111 -0
- package/scripts/gsp-statusline.js +132 -0
- package/templates/config.json +25 -0
- package/templates/phases/brand.md +60 -0
- package/templates/phases/build.md +59 -0
- package/templates/phases/design.md +48 -0
- package/templates/phases/launch.md +62 -0
- package/templates/phases/research.md +53 -0
- package/templates/phases/review.md +88 -0
- package/templates/phases/spec.md +60 -0
- package/templates/phases/system.md +84 -0
- package/templates/project.md +48 -0
- package/templates/roadmap.md +62 -0
- package/templates/state.md +33 -0
package/bin/install.js
ADDED
|
@@ -0,0 +1,1084 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const readline = require('readline');
|
|
7
|
+
|
|
8
|
+
// Colors
|
|
9
|
+
const cyan = '\x1b[36m';
|
|
10
|
+
const green = '\x1b[32m';
|
|
11
|
+
const yellow = '\x1b[33m';
|
|
12
|
+
const dim = '\x1b[2m';
|
|
13
|
+
const reset = '\x1b[0m';
|
|
14
|
+
|
|
15
|
+
// Get version from package.json
|
|
16
|
+
const pkg = require('../package.json');
|
|
17
|
+
|
|
18
|
+
// Parse args
|
|
19
|
+
const args = process.argv.slice(2);
|
|
20
|
+
const hasGlobal = args.includes('--global') || args.includes('-g');
|
|
21
|
+
const hasLocal = args.includes('--local') || args.includes('-l');
|
|
22
|
+
const hasClaude = args.includes('--claude');
|
|
23
|
+
const hasOpencode = args.includes('--opencode');
|
|
24
|
+
const hasGemini = args.includes('--gemini');
|
|
25
|
+
const hasCodex = args.includes('--codex');
|
|
26
|
+
const hasAll = args.includes('--all');
|
|
27
|
+
const hasUninstall = args.includes('--uninstall') || args.includes('-u');
|
|
28
|
+
const hasHelp = args.includes('--help') || args.includes('-h');
|
|
29
|
+
const forceStatusline = args.includes('--force-statusline');
|
|
30
|
+
|
|
31
|
+
// Runtime selection
|
|
32
|
+
let selectedRuntimes = [];
|
|
33
|
+
if (hasAll) {
|
|
34
|
+
selectedRuntimes = ['claude', 'opencode', 'gemini', 'codex'];
|
|
35
|
+
} else {
|
|
36
|
+
if (hasClaude) selectedRuntimes.push('claude');
|
|
37
|
+
if (hasOpencode) selectedRuntimes.push('opencode');
|
|
38
|
+
if (hasGemini) selectedRuntimes.push('gemini');
|
|
39
|
+
if (hasCodex) selectedRuntimes.push('codex');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Parse --config-dir argument
|
|
43
|
+
function parseConfigDirArg() {
|
|
44
|
+
const idx = args.findIndex(a => a === '--config-dir' || a === '-c');
|
|
45
|
+
if (idx !== -1) {
|
|
46
|
+
const next = args[idx + 1];
|
|
47
|
+
if (!next || next.startsWith('-')) {
|
|
48
|
+
console.error(` ${yellow}--config-dir requires a path argument${reset}`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
return next;
|
|
52
|
+
}
|
|
53
|
+
const eqArg = args.find(a => a.startsWith('--config-dir=') || a.startsWith('-c='));
|
|
54
|
+
if (eqArg) {
|
|
55
|
+
const val = eqArg.split('=')[1];
|
|
56
|
+
if (!val) {
|
|
57
|
+
console.error(` ${yellow}--config-dir requires a non-empty path${reset}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
return val;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const explicitConfigDir = parseConfigDirArg();
|
|
65
|
+
|
|
66
|
+
// Banner
|
|
67
|
+
const banner = '\n' +
|
|
68
|
+
cyan + ' ██████╗ ███████╗██████╗\n' +
|
|
69
|
+
' ██╔════╝ ██╔════╝██╔══██╗\n' +
|
|
70
|
+
' ██║ ███╗███████╗██████╔╝\n' +
|
|
71
|
+
' ██║ ██║╚════██║██╔═══╝\n' +
|
|
72
|
+
' ╚██████╔╝███████║██║\n' +
|
|
73
|
+
' ╚═════╝ ╚══════╝╚═╝' + reset + '\n' +
|
|
74
|
+
'\n' +
|
|
75
|
+
' Get Shit Pretty ' + dim + 'v' + pkg.version + reset + '\n' +
|
|
76
|
+
' A design engineering system for Claude Code,\n' +
|
|
77
|
+
' OpenCode, Gemini, and Codex.\n';
|
|
78
|
+
|
|
79
|
+
console.log(banner);
|
|
80
|
+
|
|
81
|
+
// Help
|
|
82
|
+
if (hasHelp) {
|
|
83
|
+
console.log(` ${yellow}Usage:${reset} npx get-shit-pretty [options]\n
|
|
84
|
+
${yellow}Options:${reset}
|
|
85
|
+
${cyan}-g, --global${reset} Install globally (to config directory)
|
|
86
|
+
${cyan}-l, --local${reset} Install locally (to current directory)
|
|
87
|
+
${cyan}--claude${reset} Install for Claude Code only
|
|
88
|
+
${cyan}--opencode${reset} Install for OpenCode only
|
|
89
|
+
${cyan}--gemini${reset} Install for Gemini only
|
|
90
|
+
${cyan}--codex${reset} Install for Codex CLI only
|
|
91
|
+
${cyan}--all${reset} Install for all runtimes
|
|
92
|
+
${cyan}-u, --uninstall${reset} Uninstall GSP (remove GSP files only)
|
|
93
|
+
${cyan}-c, --config-dir <path>${reset} Specify custom config directory
|
|
94
|
+
${cyan}--force-statusline${reset} Replace existing statusline config
|
|
95
|
+
${cyan}-h, --help${reset} Show this help message
|
|
96
|
+
|
|
97
|
+
${yellow}Examples:${reset}
|
|
98
|
+
${dim}# Interactive install (prompts for runtime and location)${reset}
|
|
99
|
+
npx get-shit-pretty
|
|
100
|
+
|
|
101
|
+
${dim}# Install for Claude Code globally${reset}
|
|
102
|
+
npx get-shit-pretty --claude --global
|
|
103
|
+
|
|
104
|
+
${dim}# Install for all runtimes globally${reset}
|
|
105
|
+
npx get-shit-pretty --all --global
|
|
106
|
+
|
|
107
|
+
${dim}# Install to current project only${reset}
|
|
108
|
+
npx get-shit-pretty --claude --local
|
|
109
|
+
|
|
110
|
+
${dim}# Uninstall GSP from Claude Code globally${reset}
|
|
111
|
+
npx get-shit-pretty --claude --global --uninstall
|
|
112
|
+
`);
|
|
113
|
+
process.exit(0);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ──────────────────────────────────────────────────────
|
|
117
|
+
// Utility functions
|
|
118
|
+
// ──────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
function expandTilde(filePath) {
|
|
121
|
+
if (filePath && filePath.startsWith('~/')) {
|
|
122
|
+
return path.join(os.homedir(), filePath.slice(2));
|
|
123
|
+
}
|
|
124
|
+
return filePath;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function getDirName(runtime) {
|
|
128
|
+
if (runtime === 'opencode') return '.opencode';
|
|
129
|
+
if (runtime === 'gemini') return '.gemini';
|
|
130
|
+
if (runtime === 'codex') return '.codex';
|
|
131
|
+
return '.claude';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function getGlobalDir(runtime, explicitDir = null) {
|
|
135
|
+
if (explicitDir) return expandTilde(explicitDir);
|
|
136
|
+
|
|
137
|
+
if (runtime === 'opencode') {
|
|
138
|
+
if (process.env.OPENCODE_CONFIG_DIR) return expandTilde(process.env.OPENCODE_CONFIG_DIR);
|
|
139
|
+
if (process.env.OPENCODE_CONFIG) return path.dirname(expandTilde(process.env.OPENCODE_CONFIG));
|
|
140
|
+
if (process.env.XDG_CONFIG_HOME) return path.join(expandTilde(process.env.XDG_CONFIG_HOME), 'opencode');
|
|
141
|
+
return path.join(os.homedir(), '.config', 'opencode');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (runtime === 'gemini') {
|
|
145
|
+
if (process.env.GEMINI_CONFIG_DIR) return expandTilde(process.env.GEMINI_CONFIG_DIR);
|
|
146
|
+
return path.join(os.homedir(), '.gemini');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (runtime === 'codex') {
|
|
150
|
+
if (process.env.CODEX_CONFIG_DIR) return expandTilde(process.env.CODEX_CONFIG_DIR);
|
|
151
|
+
return path.join(os.homedir(), '.codex');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Claude
|
|
155
|
+
if (process.env.CLAUDE_CONFIG_DIR) return expandTilde(process.env.CLAUDE_CONFIG_DIR);
|
|
156
|
+
return path.join(os.homedir(), '.claude');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function getConfigDirFromHome(runtime, isGlobal) {
|
|
160
|
+
if (!isGlobal) return `'${getDirName(runtime)}'`;
|
|
161
|
+
if (runtime === 'opencode') return "'.config', 'opencode'";
|
|
162
|
+
if (runtime === 'gemini') return "'.gemini'";
|
|
163
|
+
if (runtime === 'codex') return "'.codex'";
|
|
164
|
+
return "'.claude'";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function getRuntimeLabel(runtime) {
|
|
168
|
+
if (runtime === 'opencode') return 'OpenCode';
|
|
169
|
+
if (runtime === 'gemini') return 'Gemini';
|
|
170
|
+
if (runtime === 'codex') return 'Codex';
|
|
171
|
+
return 'Claude Code';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function readSettings(settingsPath) {
|
|
175
|
+
if (fs.existsSync(settingsPath)) {
|
|
176
|
+
try { return JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch (e) { return {}; }
|
|
177
|
+
}
|
|
178
|
+
return {};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function writeSettings(settingsPath, settings) {
|
|
182
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ──────────────────────────────────────────────────────
|
|
186
|
+
// Tool name mappings
|
|
187
|
+
// ──────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
const claudeToOpencodeTools = {
|
|
190
|
+
AskUserQuestion: 'question',
|
|
191
|
+
SlashCommand: 'skill',
|
|
192
|
+
TodoWrite: 'todowrite',
|
|
193
|
+
WebFetch: 'webfetch',
|
|
194
|
+
WebSearch: 'websearch',
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const claudeToGeminiTools = {
|
|
198
|
+
Read: 'read_file',
|
|
199
|
+
Write: 'write_file',
|
|
200
|
+
Edit: 'replace',
|
|
201
|
+
Bash: 'run_shell_command',
|
|
202
|
+
Glob: 'glob',
|
|
203
|
+
Grep: 'search_file_content',
|
|
204
|
+
WebSearch: 'google_web_search',
|
|
205
|
+
WebFetch: 'web_fetch',
|
|
206
|
+
TodoWrite: 'write_todos',
|
|
207
|
+
AskUserQuestion: 'ask_user',
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const claudeToCodexTools = {
|
|
211
|
+
Read: 'read',
|
|
212
|
+
Write: 'write',
|
|
213
|
+
Edit: 'edit',
|
|
214
|
+
Bash: 'shell',
|
|
215
|
+
Glob: 'glob',
|
|
216
|
+
Grep: 'grep',
|
|
217
|
+
WebSearch: 'web_search',
|
|
218
|
+
WebFetch: 'web_fetch',
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const colorNameToHex = {
|
|
222
|
+
cyan: '#00FFFF', red: '#FF0000', green: '#00FF00', blue: '#0000FF',
|
|
223
|
+
yellow: '#FFFF00', magenta: '#FF00FF', orange: '#FFA500', purple: '#800080',
|
|
224
|
+
pink: '#FFC0CB', white: '#FFFFFF', black: '#000000', gray: '#808080', grey: '#808080',
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// ──────────────────────────────────────────────────────
|
|
228
|
+
// Conversion functions
|
|
229
|
+
// ──────────────────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
function convertToolName(claudeTool) {
|
|
232
|
+
if (claudeToOpencodeTools[claudeTool]) return claudeToOpencodeTools[claudeTool];
|
|
233
|
+
if (claudeTool.startsWith('mcp__')) return claudeTool;
|
|
234
|
+
return claudeTool.toLowerCase();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function convertGeminiToolName(claudeTool) {
|
|
238
|
+
if (claudeTool.startsWith('mcp__')) return null;
|
|
239
|
+
if (claudeTool === 'Task') return null;
|
|
240
|
+
if (claudeToGeminiTools[claudeTool]) return claudeToGeminiTools[claudeTool];
|
|
241
|
+
return claudeTool.toLowerCase();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function convertCodexToolName(claudeTool) {
|
|
245
|
+
if (claudeTool.startsWith('mcp__')) return null;
|
|
246
|
+
if (claudeTool === 'Task') return null;
|
|
247
|
+
if (claudeToCodexTools[claudeTool]) return claudeToCodexTools[claudeTool];
|
|
248
|
+
return claudeTool.toLowerCase();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function stripSubTags(content) {
|
|
252
|
+
return content.replace(/<sub>(.*?)<\/sub>/g, '*($1)*');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Convert Claude frontmatter to OpenCode format
|
|
257
|
+
*/
|
|
258
|
+
function convertClaudeToOpencodeFrontmatter(content) {
|
|
259
|
+
let converted = content;
|
|
260
|
+
converted = converted.replace(/\bAskUserQuestion\b/g, 'question');
|
|
261
|
+
converted = converted.replace(/\bSlashCommand\b/g, 'skill');
|
|
262
|
+
converted = converted.replace(/\bTodoWrite\b/g, 'todowrite');
|
|
263
|
+
converted = converted.replace(/\/gsp:/g, '/gsp-');
|
|
264
|
+
converted = converted.replace(/~\/\.claude\b/g, '~/.config/opencode');
|
|
265
|
+
converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="general"');
|
|
266
|
+
|
|
267
|
+
if (!converted.startsWith('---')) return converted;
|
|
268
|
+
const endIndex = converted.indexOf('---', 3);
|
|
269
|
+
if (endIndex === -1) return converted;
|
|
270
|
+
|
|
271
|
+
const frontmatter = converted.substring(3, endIndex).trim();
|
|
272
|
+
const body = converted.substring(endIndex + 3);
|
|
273
|
+
const lines = frontmatter.split('\n');
|
|
274
|
+
const newLines = [];
|
|
275
|
+
let inAllowedTools = false;
|
|
276
|
+
const allowedTools = [];
|
|
277
|
+
|
|
278
|
+
for (const line of lines) {
|
|
279
|
+
const trimmed = line.trim();
|
|
280
|
+
|
|
281
|
+
if (trimmed.startsWith('allowed-tools:')) { inAllowedTools = true; continue; }
|
|
282
|
+
|
|
283
|
+
if (trimmed.startsWith('tools:')) {
|
|
284
|
+
const val = trimmed.substring(6).trim();
|
|
285
|
+
if (val) {
|
|
286
|
+
allowedTools.push(...val.split(',').map(t => t.trim()).filter(Boolean));
|
|
287
|
+
} else {
|
|
288
|
+
inAllowedTools = true;
|
|
289
|
+
}
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (trimmed.startsWith('name:')) continue;
|
|
294
|
+
|
|
295
|
+
if (trimmed.startsWith('color:')) {
|
|
296
|
+
const colorValue = trimmed.substring(6).trim().toLowerCase();
|
|
297
|
+
const hex = colorNameToHex[colorValue];
|
|
298
|
+
if (hex) newLines.push(`color: "${hex}"`);
|
|
299
|
+
else if (colorValue.startsWith('#') && /^#[0-9a-f]{3}$|^#[0-9a-f]{6}$/i.test(colorValue)) {
|
|
300
|
+
newLines.push(line);
|
|
301
|
+
}
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (inAllowedTools) {
|
|
306
|
+
if (trimmed.startsWith('- ')) { allowedTools.push(trimmed.substring(2).trim()); continue; }
|
|
307
|
+
else if (trimmed && !trimmed.startsWith('-')) { inAllowedTools = false; }
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (!inAllowedTools) newLines.push(line);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (allowedTools.length > 0) {
|
|
314
|
+
newLines.push('tools:');
|
|
315
|
+
for (const tool of allowedTools) {
|
|
316
|
+
newLines.push(` ${convertToolName(tool)}: true`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return `---\n${newLines.join('\n').trim()}\n---${body}`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Convert Claude command to Gemini TOML format
|
|
325
|
+
*/
|
|
326
|
+
function convertClaudeToGeminiToml(content) {
|
|
327
|
+
if (!content.startsWith('---')) return `prompt = ${JSON.stringify(content)}\n`;
|
|
328
|
+
const endIndex = content.indexOf('---', 3);
|
|
329
|
+
if (endIndex === -1) return `prompt = ${JSON.stringify(content)}\n`;
|
|
330
|
+
|
|
331
|
+
const frontmatter = content.substring(3, endIndex).trim();
|
|
332
|
+
const body = content.substring(endIndex + 3).trim();
|
|
333
|
+
|
|
334
|
+
let description = '';
|
|
335
|
+
for (const line of frontmatter.split('\n')) {
|
|
336
|
+
const trimmed = line.trim();
|
|
337
|
+
if (trimmed.startsWith('description:')) {
|
|
338
|
+
description = trimmed.substring(12).trim();
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
let toml = '';
|
|
344
|
+
if (description) toml += `description = ${JSON.stringify(description)}\n`;
|
|
345
|
+
toml += `prompt = ${JSON.stringify(body)}\n`;
|
|
346
|
+
return toml;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Convert Claude agent to Gemini agent format
|
|
351
|
+
*/
|
|
352
|
+
function convertClaudeToGeminiAgent(content) {
|
|
353
|
+
if (!content.startsWith('---')) return content;
|
|
354
|
+
const endIndex = content.indexOf('---', 3);
|
|
355
|
+
if (endIndex === -1) return content;
|
|
356
|
+
|
|
357
|
+
const frontmatter = content.substring(3, endIndex).trim();
|
|
358
|
+
const body = content.substring(endIndex + 3);
|
|
359
|
+
const lines = frontmatter.split('\n');
|
|
360
|
+
const newLines = [];
|
|
361
|
+
let inAllowedTools = false;
|
|
362
|
+
const tools = [];
|
|
363
|
+
|
|
364
|
+
for (const line of lines) {
|
|
365
|
+
const trimmed = line.trim();
|
|
366
|
+
if (trimmed.startsWith('allowed-tools:')) { inAllowedTools = true; continue; }
|
|
367
|
+
if (trimmed.startsWith('tools:')) {
|
|
368
|
+
const val = trimmed.substring(6).trim();
|
|
369
|
+
if (val) {
|
|
370
|
+
for (const t of val.split(',').map(s => s.trim()).filter(Boolean)) {
|
|
371
|
+
const mapped = convertGeminiToolName(t);
|
|
372
|
+
if (mapped) tools.push(mapped);
|
|
373
|
+
}
|
|
374
|
+
} else { inAllowedTools = true; }
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (trimmed.startsWith('color:')) continue;
|
|
378
|
+
if (inAllowedTools) {
|
|
379
|
+
if (trimmed.startsWith('- ')) {
|
|
380
|
+
const mapped = convertGeminiToolName(trimmed.substring(2).trim());
|
|
381
|
+
if (mapped) tools.push(mapped);
|
|
382
|
+
continue;
|
|
383
|
+
} else if (trimmed && !trimmed.startsWith('-')) { inAllowedTools = false; }
|
|
384
|
+
}
|
|
385
|
+
if (!inAllowedTools) newLines.push(line);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (tools.length > 0) {
|
|
389
|
+
newLines.push('tools:');
|
|
390
|
+
for (const tool of tools) newLines.push(` - ${tool}`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const escapedBody = body.replace(/\$\{(\w+)\}/g, '$$$1');
|
|
394
|
+
return `---\n${newLines.join('\n').trim()}\n---${stripSubTags(escapedBody)}`;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Convert Claude command to Codex SKILL.md format
|
|
399
|
+
* Codex expects: ~/.codex/skills/gsp-help/SKILL.md
|
|
400
|
+
*/
|
|
401
|
+
function convertClaudeCommandToCodexSkill(content) {
|
|
402
|
+
if (!content.startsWith('---')) return content;
|
|
403
|
+
const endIndex = content.indexOf('---', 3);
|
|
404
|
+
if (endIndex === -1) return content;
|
|
405
|
+
|
|
406
|
+
const frontmatter = content.substring(3, endIndex).trim();
|
|
407
|
+
let body = content.substring(endIndex + 3).trim();
|
|
408
|
+
|
|
409
|
+
// Extract metadata from frontmatter
|
|
410
|
+
let description = '';
|
|
411
|
+
const tools = [];
|
|
412
|
+
let inTools = false;
|
|
413
|
+
|
|
414
|
+
for (const line of frontmatter.split('\n')) {
|
|
415
|
+
const trimmed = line.trim();
|
|
416
|
+
if (trimmed.startsWith('description:')) {
|
|
417
|
+
description = trimmed.substring(12).trim();
|
|
418
|
+
inTools = false;
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
if (trimmed.startsWith('allowed-tools:') || trimmed.startsWith('tools:')) {
|
|
422
|
+
const val = trimmed.includes(':') ? trimmed.split(':').slice(1).join(':').trim() : '';
|
|
423
|
+
if (val) {
|
|
424
|
+
for (const t of val.split(',').map(s => s.trim()).filter(Boolean)) {
|
|
425
|
+
const mapped = convertCodexToolName(t);
|
|
426
|
+
if (mapped) tools.push(mapped);
|
|
427
|
+
}
|
|
428
|
+
} else {
|
|
429
|
+
inTools = true;
|
|
430
|
+
}
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (inTools) {
|
|
434
|
+
if (trimmed.startsWith('- ')) {
|
|
435
|
+
const mapped = convertCodexToolName(trimmed.substring(2).trim());
|
|
436
|
+
if (mapped) tools.push(mapped);
|
|
437
|
+
continue;
|
|
438
|
+
} else if (trimmed && !trimmed.startsWith('-')) {
|
|
439
|
+
inTools = false;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Replace slash command references: /gsp: → $gsp-
|
|
445
|
+
body = body.replace(/\/gsp:/g, '$gsp-');
|
|
446
|
+
// Replace ~/.claude references
|
|
447
|
+
body = body.replace(/~\/\.claude\b/g, '~/.codex');
|
|
448
|
+
|
|
449
|
+
// Build SKILL.md
|
|
450
|
+
let skill = '';
|
|
451
|
+
if (description) skill += `# ${description}\n\n`;
|
|
452
|
+
if (tools.length > 0) skill += `Tools: ${tools.join(', ')}\n\n`;
|
|
453
|
+
skill += body;
|
|
454
|
+
return skill;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Convert Claude agent to Codex agent format
|
|
459
|
+
*/
|
|
460
|
+
function convertClaudeToCodexAgent(content) {
|
|
461
|
+
let converted = content;
|
|
462
|
+
converted = converted.replace(/\/gsp:/g, '$gsp-');
|
|
463
|
+
converted = converted.replace(/~\/\.claude\b/g, '~/.codex');
|
|
464
|
+
|
|
465
|
+
if (!converted.startsWith('---')) return converted;
|
|
466
|
+
const endIndex = converted.indexOf('---', 3);
|
|
467
|
+
if (endIndex === -1) return converted;
|
|
468
|
+
|
|
469
|
+
const frontmatter = converted.substring(3, endIndex).trim();
|
|
470
|
+
const body = converted.substring(endIndex + 3);
|
|
471
|
+
const lines = frontmatter.split('\n');
|
|
472
|
+
const newLines = [];
|
|
473
|
+
let inTools = false;
|
|
474
|
+
const tools = [];
|
|
475
|
+
|
|
476
|
+
for (const line of lines) {
|
|
477
|
+
const trimmed = line.trim();
|
|
478
|
+
if (trimmed.startsWith('allowed-tools:') || trimmed.startsWith('tools:')) {
|
|
479
|
+
const val = trimmed.includes(':') ? trimmed.split(':').slice(1).join(':').trim() : '';
|
|
480
|
+
if (val) {
|
|
481
|
+
for (const t of val.split(',').map(s => s.trim()).filter(Boolean)) {
|
|
482
|
+
const mapped = convertCodexToolName(t);
|
|
483
|
+
if (mapped) tools.push(mapped);
|
|
484
|
+
}
|
|
485
|
+
} else { inTools = true; }
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
if (trimmed.startsWith('color:')) continue;
|
|
489
|
+
if (inTools) {
|
|
490
|
+
if (trimmed.startsWith('- ')) {
|
|
491
|
+
const mapped = convertCodexToolName(trimmed.substring(2).trim());
|
|
492
|
+
if (mapped) tools.push(mapped);
|
|
493
|
+
continue;
|
|
494
|
+
} else if (trimmed && !trimmed.startsWith('-')) { inTools = false; }
|
|
495
|
+
}
|
|
496
|
+
if (!inTools) newLines.push(line);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (tools.length > 0) {
|
|
500
|
+
newLines.push('tools: ' + tools.join(', '));
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return `---\n${newLines.join('\n').trim()}\n---${body}`;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// ──────────────────────────────────────────────────────
|
|
507
|
+
// File copy helpers
|
|
508
|
+
// ──────────────────────────────────────────────────────
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Copy commands to flat structure for OpenCode
|
|
512
|
+
* commands/gsp/help.md → command/gsp-help.md
|
|
513
|
+
*/
|
|
514
|
+
function copyFlattenedCommands(srcDir, destDir, prefix, pathPrefix, runtime) {
|
|
515
|
+
if (!fs.existsSync(srcDir)) return;
|
|
516
|
+
|
|
517
|
+
if (fs.existsSync(destDir)) {
|
|
518
|
+
for (const file of fs.readdirSync(destDir)) {
|
|
519
|
+
if (file.startsWith(`${prefix}-`) && file.endsWith('.md')) {
|
|
520
|
+
fs.unlinkSync(path.join(destDir, file));
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
} else {
|
|
524
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
528
|
+
for (const entry of entries) {
|
|
529
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
530
|
+
if (entry.isDirectory()) {
|
|
531
|
+
copyFlattenedCommands(srcPath, destDir, `${prefix}-${entry.name}`, pathPrefix, runtime);
|
|
532
|
+
} else if (entry.name.endsWith('.md')) {
|
|
533
|
+
const baseName = entry.name.replace('.md', '');
|
|
534
|
+
const destPath = path.join(destDir, `${prefix}-${baseName}.md`);
|
|
535
|
+
let content = fs.readFileSync(srcPath, 'utf8');
|
|
536
|
+
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
537
|
+
content = content.replace(/\.\/\.claude\//g, `./${getDirName(runtime)}/`);
|
|
538
|
+
content = convertClaudeToOpencodeFrontmatter(content);
|
|
539
|
+
fs.writeFileSync(destPath, content);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Copy commands to Codex skill structure
|
|
546
|
+
* commands/gsp/help.md → skills/gsp-help/SKILL.md
|
|
547
|
+
*/
|
|
548
|
+
function copyCodexSkills(srcDir, destDir, prefix, pathPrefix) {
|
|
549
|
+
if (!fs.existsSync(srcDir)) return;
|
|
550
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
551
|
+
|
|
552
|
+
// Clean old gsp-* skill dirs
|
|
553
|
+
if (fs.existsSync(destDir)) {
|
|
554
|
+
for (const entry of fs.readdirSync(destDir, { withFileTypes: true })) {
|
|
555
|
+
if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
|
|
556
|
+
fs.rmSync(path.join(destDir, entry.name), { recursive: true });
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
562
|
+
for (const entry of entries) {
|
|
563
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
564
|
+
if (entry.isDirectory()) {
|
|
565
|
+
copyCodexSkills(srcPath, destDir, `${prefix}-${entry.name}`, pathPrefix);
|
|
566
|
+
} else if (entry.name.endsWith('.md')) {
|
|
567
|
+
const baseName = entry.name.replace('.md', '');
|
|
568
|
+
const skillDir = path.join(destDir, `${prefix}-${baseName}`);
|
|
569
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
570
|
+
let content = fs.readFileSync(srcPath, 'utf8');
|
|
571
|
+
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
572
|
+
content = convertClaudeCommandToCodexSkill(content);
|
|
573
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Recursively copy directory with path replacement and runtime conversion
|
|
580
|
+
*/
|
|
581
|
+
function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand = false) {
|
|
582
|
+
const dirName = getDirName(runtime);
|
|
583
|
+
|
|
584
|
+
if (fs.existsSync(destDir)) {
|
|
585
|
+
fs.rmSync(destDir, { recursive: true });
|
|
586
|
+
}
|
|
587
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
588
|
+
|
|
589
|
+
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
590
|
+
for (const entry of entries) {
|
|
591
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
592
|
+
const destPath = path.join(destDir, entry.name);
|
|
593
|
+
|
|
594
|
+
if (entry.isDirectory()) {
|
|
595
|
+
copyWithPathReplacement(srcPath, destPath, pathPrefix, runtime, isCommand);
|
|
596
|
+
} else if (entry.name.endsWith('.md')) {
|
|
597
|
+
let content = fs.readFileSync(srcPath, 'utf8');
|
|
598
|
+
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
599
|
+
content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
|
|
600
|
+
|
|
601
|
+
if (runtime === 'opencode') {
|
|
602
|
+
content = convertClaudeToOpencodeFrontmatter(content);
|
|
603
|
+
fs.writeFileSync(destPath, content);
|
|
604
|
+
} else if (runtime === 'gemini') {
|
|
605
|
+
if (isCommand) {
|
|
606
|
+
content = stripSubTags(content);
|
|
607
|
+
const tomlContent = convertClaudeToGeminiToml(content);
|
|
608
|
+
fs.writeFileSync(destPath.replace(/\.md$/, '.toml'), tomlContent);
|
|
609
|
+
} else {
|
|
610
|
+
fs.writeFileSync(destPath, content);
|
|
611
|
+
}
|
|
612
|
+
} else if (runtime === 'codex') {
|
|
613
|
+
content = content.replace(/\/gsp:/g, '$gsp-');
|
|
614
|
+
fs.writeFileSync(destPath, content);
|
|
615
|
+
} else {
|
|
616
|
+
fs.writeFileSync(destPath, content);
|
|
617
|
+
}
|
|
618
|
+
} else {
|
|
619
|
+
fs.copyFileSync(srcPath, destPath);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function verifyInstalled(dirPath, description) {
|
|
625
|
+
if (!fs.existsSync(dirPath)) {
|
|
626
|
+
console.error(` ${yellow}!${reset} Failed to install ${description}: directory not created`);
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
try {
|
|
630
|
+
if (fs.readdirSync(dirPath).length === 0) {
|
|
631
|
+
console.error(` ${yellow}!${reset} Failed to install ${description}: directory is empty`);
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
} catch (e) {
|
|
635
|
+
console.error(` ${yellow}!${reset} Failed to install ${description}: ${e.message}`);
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
return true;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// ──────────────────────────────────────────────────────
|
|
642
|
+
// Install
|
|
643
|
+
// ──────────────────────────────────────────────────────
|
|
644
|
+
|
|
645
|
+
function install(isGlobal, runtime = 'claude') {
|
|
646
|
+
const isOpencode = runtime === 'opencode';
|
|
647
|
+
const isGemini = runtime === 'gemini';
|
|
648
|
+
const isCodex = runtime === 'codex';
|
|
649
|
+
const dirName = getDirName(runtime);
|
|
650
|
+
const src = path.join(__dirname, '..');
|
|
651
|
+
|
|
652
|
+
const targetDir = isGlobal
|
|
653
|
+
? getGlobalDir(runtime, explicitConfigDir)
|
|
654
|
+
: path.join(process.cwd(), dirName);
|
|
655
|
+
|
|
656
|
+
const locationLabel = isGlobal
|
|
657
|
+
? targetDir.replace(os.homedir(), '~')
|
|
658
|
+
: targetDir.replace(process.cwd(), '.');
|
|
659
|
+
|
|
660
|
+
const pathPrefix = isGlobal
|
|
661
|
+
? `${targetDir.replace(/\\/g, '/')}/`
|
|
662
|
+
: `./${dirName}/`;
|
|
663
|
+
|
|
664
|
+
const runtimeLabel = getRuntimeLabel(runtime);
|
|
665
|
+
console.log(` Installing for ${cyan}${runtimeLabel}${reset} to ${cyan}${locationLabel}${reset}\n`);
|
|
666
|
+
|
|
667
|
+
const failures = [];
|
|
668
|
+
|
|
669
|
+
// ── Commands ──
|
|
670
|
+
if (isOpencode) {
|
|
671
|
+
const commandDir = path.join(targetDir, 'command');
|
|
672
|
+
fs.mkdirSync(commandDir, { recursive: true });
|
|
673
|
+
copyFlattenedCommands(path.join(src, 'commands', 'gsp'), commandDir, 'gsp', pathPrefix, runtime);
|
|
674
|
+
if (verifyInstalled(commandDir, 'command/gsp-*')) {
|
|
675
|
+
const count = fs.readdirSync(commandDir).filter(f => f.startsWith('gsp-')).length;
|
|
676
|
+
console.log(` ${green}+${reset} Installed ${count} commands to command/`);
|
|
677
|
+
} else { failures.push('commands'); }
|
|
678
|
+
} else if (isCodex) {
|
|
679
|
+
const skillsDir = path.join(targetDir, 'skills');
|
|
680
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
681
|
+
copyCodexSkills(path.join(src, 'commands', 'gsp'), skillsDir, 'gsp', pathPrefix);
|
|
682
|
+
if (verifyInstalled(skillsDir, 'skills/gsp-*')) {
|
|
683
|
+
const count = fs.readdirSync(skillsDir).filter(f => f.startsWith('gsp-')).length;
|
|
684
|
+
console.log(` ${green}+${reset} Installed ${count} skills to skills/`);
|
|
685
|
+
} else { failures.push('skills'); }
|
|
686
|
+
} else {
|
|
687
|
+
const commandsDir = path.join(targetDir, 'commands');
|
|
688
|
+
fs.mkdirSync(commandsDir, { recursive: true });
|
|
689
|
+
const gspDest = path.join(commandsDir, 'gsp');
|
|
690
|
+
copyWithPathReplacement(path.join(src, 'commands', 'gsp'), gspDest, pathPrefix, runtime, true);
|
|
691
|
+
if (verifyInstalled(gspDest, 'commands/gsp')) {
|
|
692
|
+
console.log(` ${green}+${reset} Installed commands/gsp`);
|
|
693
|
+
} else { failures.push('commands'); }
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// ── Agents ──
|
|
697
|
+
const agentsSrc = path.join(src, 'agents');
|
|
698
|
+
if (fs.existsSync(agentsSrc)) {
|
|
699
|
+
const agentsDest = path.join(targetDir, 'agents');
|
|
700
|
+
fs.mkdirSync(agentsDest, { recursive: true });
|
|
701
|
+
|
|
702
|
+
// Remove old GSP agents before copying new ones
|
|
703
|
+
if (fs.existsSync(agentsDest)) {
|
|
704
|
+
for (const file of fs.readdirSync(agentsDest)) {
|
|
705
|
+
if (file.startsWith('gsp-') && file.endsWith('.md')) {
|
|
706
|
+
fs.unlinkSync(path.join(agentsDest, file));
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const agentEntries = fs.readdirSync(agentsSrc, { withFileTypes: true });
|
|
712
|
+
for (const entry of agentEntries) {
|
|
713
|
+
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
714
|
+
let content = fs.readFileSync(path.join(agentsSrc, entry.name), 'utf8');
|
|
715
|
+
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
716
|
+
|
|
717
|
+
if (isOpencode) {
|
|
718
|
+
content = convertClaudeToOpencodeFrontmatter(content);
|
|
719
|
+
} else if (isGemini) {
|
|
720
|
+
content = convertClaudeToGeminiAgent(content);
|
|
721
|
+
} else if (isCodex) {
|
|
722
|
+
content = convertClaudeToCodexAgent(content);
|
|
723
|
+
}
|
|
724
|
+
fs.writeFileSync(path.join(agentsDest, entry.name), content);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
if (verifyInstalled(agentsDest, 'agents')) {
|
|
728
|
+
const count = fs.readdirSync(agentsDest).filter(f => f.startsWith('gsp-')).length;
|
|
729
|
+
console.log(` ${green}+${reset} Installed ${count} agents`);
|
|
730
|
+
} else { failures.push('agents'); }
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// ── Bundle: prompts, templates, references → get-shit-pretty/ ──
|
|
734
|
+
const bundleDest = path.join(targetDir, 'get-shit-pretty');
|
|
735
|
+
// Clean install: remove old bundle dir to prevent stale files from previous installs
|
|
736
|
+
if (fs.existsSync(bundleDest)) {
|
|
737
|
+
fs.rmSync(bundleDest, { recursive: true });
|
|
738
|
+
}
|
|
739
|
+
fs.mkdirSync(bundleDest, { recursive: true });
|
|
740
|
+
|
|
741
|
+
const bundleDirs = ['prompts', 'templates', 'references'];
|
|
742
|
+
for (const dir of bundleDirs) {
|
|
743
|
+
const dirSrc = path.join(src, dir);
|
|
744
|
+
if (fs.existsSync(dirSrc)) {
|
|
745
|
+
copyWithPathReplacement(dirSrc, path.join(bundleDest, dir), pathPrefix, runtime);
|
|
746
|
+
if (verifyInstalled(path.join(bundleDest, dir), `get-shit-pretty/${dir}`)) {
|
|
747
|
+
console.log(` ${green}+${reset} Installed get-shit-pretty/${dir}`);
|
|
748
|
+
} else { failures.push(dir); }
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Write VERSION file
|
|
753
|
+
fs.writeFileSync(path.join(bundleDest, 'VERSION'), pkg.version);
|
|
754
|
+
console.log(` ${green}+${reset} Wrote VERSION (${pkg.version})`);
|
|
755
|
+
|
|
756
|
+
// ── Statusline (Claude Code only) ──
|
|
757
|
+
if (runtime === 'claude') {
|
|
758
|
+
const statuslineSrc = path.join(src, 'scripts', 'gsp-statusline.js');
|
|
759
|
+
if (fs.existsSync(statuslineSrc)) {
|
|
760
|
+
const hooksDest = path.join(targetDir, 'hooks');
|
|
761
|
+
fs.mkdirSync(hooksDest, { recursive: true });
|
|
762
|
+
let content = fs.readFileSync(statuslineSrc, 'utf8');
|
|
763
|
+
content = content.replace(/'\.claude'/g, getConfigDirFromHome(runtime, isGlobal));
|
|
764
|
+
fs.writeFileSync(path.join(hooksDest, 'gsp-statusline.js'), content);
|
|
765
|
+
console.log(` ${green}+${reset} Installed statusline hook`);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (failures.length > 0) {
|
|
770
|
+
console.error(`\n ${yellow}Installation incomplete!${reset} Failed: ${failures.join(', ')}`);
|
|
771
|
+
process.exit(1);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// ── Settings (Claude Code & Gemini only) ──
|
|
775
|
+
const settingsPath = path.join(targetDir, 'settings.json');
|
|
776
|
+
const settings = readSettings(settingsPath);
|
|
777
|
+
|
|
778
|
+
const statuslineCommand = isGlobal
|
|
779
|
+
? `node "${targetDir.replace(/\\/g, '/')}/hooks/gsp-statusline.js"`
|
|
780
|
+
: `node ${dirName}/hooks/gsp-statusline.js`;
|
|
781
|
+
|
|
782
|
+
return { settingsPath, settings, statuslineCommand, runtime };
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// ──────────────────────────────────────────────────────
|
|
786
|
+
// Uninstall
|
|
787
|
+
// ──────────────────────────────────────────────────────
|
|
788
|
+
|
|
789
|
+
function uninstall(isGlobal, runtime = 'claude') {
|
|
790
|
+
const isOpencode = runtime === 'opencode';
|
|
791
|
+
const isCodex = runtime === 'codex';
|
|
792
|
+
const dirName = getDirName(runtime);
|
|
793
|
+
|
|
794
|
+
const targetDir = isGlobal
|
|
795
|
+
? getGlobalDir(runtime, explicitConfigDir)
|
|
796
|
+
: path.join(process.cwd(), dirName);
|
|
797
|
+
|
|
798
|
+
const locationLabel = isGlobal
|
|
799
|
+
? targetDir.replace(os.homedir(), '~')
|
|
800
|
+
: targetDir.replace(process.cwd(), '.');
|
|
801
|
+
|
|
802
|
+
const runtimeLabel = getRuntimeLabel(runtime);
|
|
803
|
+
console.log(` Uninstalling GSP from ${cyan}${runtimeLabel}${reset} at ${cyan}${locationLabel}${reset}\n`);
|
|
804
|
+
|
|
805
|
+
if (!fs.existsSync(targetDir)) {
|
|
806
|
+
console.log(` ${yellow}!${reset} Directory does not exist: ${locationLabel}`);
|
|
807
|
+
console.log(` Nothing to uninstall.\n`);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
let removedCount = 0;
|
|
812
|
+
|
|
813
|
+
// Remove commands
|
|
814
|
+
if (isOpencode) {
|
|
815
|
+
const commandDir = path.join(targetDir, 'command');
|
|
816
|
+
if (fs.existsSync(commandDir)) {
|
|
817
|
+
for (const file of fs.readdirSync(commandDir)) {
|
|
818
|
+
if (file.startsWith('gsp-') && file.endsWith('.md')) {
|
|
819
|
+
fs.unlinkSync(path.join(commandDir, file));
|
|
820
|
+
removedCount++;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
if (removedCount > 0) console.log(` ${green}+${reset} Removed GSP commands from command/`);
|
|
824
|
+
}
|
|
825
|
+
} else if (isCodex) {
|
|
826
|
+
const skillsDir = path.join(targetDir, 'skills');
|
|
827
|
+
if (fs.existsSync(skillsDir)) {
|
|
828
|
+
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
829
|
+
if (entry.isDirectory() && entry.name.startsWith('gsp-')) {
|
|
830
|
+
fs.rmSync(path.join(skillsDir, entry.name), { recursive: true });
|
|
831
|
+
removedCount++;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
if (removedCount > 0) console.log(` ${green}+${reset} Removed GSP skills from skills/`);
|
|
835
|
+
}
|
|
836
|
+
} else {
|
|
837
|
+
const gspCommandsDir = path.join(targetDir, 'commands', 'gsp');
|
|
838
|
+
if (fs.existsSync(gspCommandsDir)) {
|
|
839
|
+
fs.rmSync(gspCommandsDir, { recursive: true });
|
|
840
|
+
removedCount++;
|
|
841
|
+
console.log(` ${green}+${reset} Removed commands/gsp/`);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Remove get-shit-pretty bundle dir
|
|
846
|
+
const gspDir = path.join(targetDir, 'get-shit-pretty');
|
|
847
|
+
if (fs.existsSync(gspDir)) {
|
|
848
|
+
fs.rmSync(gspDir, { recursive: true });
|
|
849
|
+
removedCount++;
|
|
850
|
+
console.log(` ${green}+${reset} Removed get-shit-pretty/`);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// Remove GSP agents
|
|
854
|
+
const agentsDir = path.join(targetDir, 'agents');
|
|
855
|
+
if (fs.existsSync(agentsDir)) {
|
|
856
|
+
let agentCount = 0;
|
|
857
|
+
for (const file of fs.readdirSync(agentsDir)) {
|
|
858
|
+
if (file.startsWith('gsp-') && file.endsWith('.md')) {
|
|
859
|
+
fs.unlinkSync(path.join(agentsDir, file));
|
|
860
|
+
agentCount++;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
if (agentCount > 0) {
|
|
864
|
+
removedCount++;
|
|
865
|
+
console.log(` ${green}+${reset} Removed ${agentCount} GSP agents`);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Remove statusline hook
|
|
870
|
+
const statuslineHook = path.join(targetDir, 'hooks', 'gsp-statusline.js');
|
|
871
|
+
if (fs.existsSync(statuslineHook)) {
|
|
872
|
+
fs.unlinkSync(statuslineHook);
|
|
873
|
+
removedCount++;
|
|
874
|
+
console.log(` ${green}+${reset} Removed statusline hook`);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Clean up settings.json (statusline only — no hooks to clean)
|
|
878
|
+
const settingsPath = path.join(targetDir, 'settings.json');
|
|
879
|
+
if (fs.existsSync(settingsPath)) {
|
|
880
|
+
const settings = readSettings(settingsPath);
|
|
881
|
+
let modified = false;
|
|
882
|
+
|
|
883
|
+
if (settings.statusLine && settings.statusLine.command &&
|
|
884
|
+
settings.statusLine.command.includes('gsp-statusline')) {
|
|
885
|
+
delete settings.statusLine;
|
|
886
|
+
modified = true;
|
|
887
|
+
console.log(` ${green}+${reset} Removed GSP statusline from settings`);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
if (modified) {
|
|
891
|
+
writeSettings(settingsPath, settings);
|
|
892
|
+
removedCount++;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
if (removedCount === 0) {
|
|
897
|
+
console.log(` ${yellow}!${reset} No GSP files found to remove.`);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
console.log(`\n ${green}Done!${reset} GSP has been uninstalled from ${runtimeLabel}.\n Your other files and settings have been preserved.\n`);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// ──────────────────────────────────────────────────────
|
|
904
|
+
// Statusline handling
|
|
905
|
+
// ──────────────────────────────────────────────────────
|
|
906
|
+
|
|
907
|
+
function handleStatusline(settings, isInteractive, callback) {
|
|
908
|
+
const hasExisting = settings.statusLine != null;
|
|
909
|
+
|
|
910
|
+
if (!hasExisting) { callback(true); return; }
|
|
911
|
+
if (forceStatusline) { callback(true); return; }
|
|
912
|
+
|
|
913
|
+
if (!isInteractive) {
|
|
914
|
+
console.log(` ${yellow}!${reset} Skipping statusline (already configured)`);
|
|
915
|
+
console.log(` Use ${cyan}--force-statusline${reset} to replace\n`);
|
|
916
|
+
callback(false);
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
const existingCmd = settings.statusLine.command || settings.statusLine.url || '(custom)';
|
|
921
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
922
|
+
|
|
923
|
+
console.log(`\n ${yellow}!${reset} Existing statusline detected\n
|
|
924
|
+
Your current statusline:
|
|
925
|
+
${dim}command: ${existingCmd}${reset}
|
|
926
|
+
|
|
927
|
+
GSP includes a statusline showing:
|
|
928
|
+
* Model name
|
|
929
|
+
* Current design phase + prettiness meter
|
|
930
|
+
* Context window usage (color-coded)
|
|
931
|
+
|
|
932
|
+
${cyan}1${reset}) Keep existing
|
|
933
|
+
${cyan}2${reset}) Replace with GSP statusline\n`);
|
|
934
|
+
|
|
935
|
+
rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
|
|
936
|
+
rl.close();
|
|
937
|
+
callback((answer.trim() || '1') === '2');
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = 'claude', isGlobal = true) {
|
|
942
|
+
const isOpencode = runtime === 'opencode';
|
|
943
|
+
const isCodex = runtime === 'codex';
|
|
944
|
+
|
|
945
|
+
// Only Claude Code and Gemini support statusline
|
|
946
|
+
if (shouldInstallStatusline && !isOpencode && !isCodex) {
|
|
947
|
+
settings.statusLine = {
|
|
948
|
+
type: 'command',
|
|
949
|
+
command: statuslineCommand
|
|
950
|
+
};
|
|
951
|
+
console.log(` ${green}+${reset} Configured statusline`);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Write settings for Claude/Gemini (they use settings.json)
|
|
955
|
+
if (!isOpencode && !isCodex) {
|
|
956
|
+
writeSettings(settingsPath, settings);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
const runtimeLabel = getRuntimeLabel(runtime);
|
|
960
|
+
const command = isOpencode ? '/gsp-help' : isCodex ? '$gsp-help' : '/gsp:help';
|
|
961
|
+
console.log(`\n ${green}Done!${reset} Launch ${runtimeLabel} and run ${cyan}${command}${reset}.\n`);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// ──────────────────────────────────────────────────────
|
|
965
|
+
// Interactive prompts
|
|
966
|
+
// ──────────────────────────────────────────────────────
|
|
967
|
+
|
|
968
|
+
function promptRuntime(callback) {
|
|
969
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
970
|
+
let answered = false;
|
|
971
|
+
|
|
972
|
+
rl.on('close', () => {
|
|
973
|
+
if (!answered) { answered = true; console.log(`\n ${yellow}Installation cancelled${reset}\n`); process.exit(0); }
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
console.log(` ${yellow}Which runtime(s) would you like to install for?${reset}\n
|
|
977
|
+
${cyan}1${reset}) Claude Code ${dim}(~/.claude)${reset}
|
|
978
|
+
${cyan}2${reset}) OpenCode ${dim}(~/.config/opencode)${reset}
|
|
979
|
+
${cyan}3${reset}) Gemini ${dim}(~/.gemini)${reset}
|
|
980
|
+
${cyan}4${reset}) Codex ${dim}(~/.codex)${reset}
|
|
981
|
+
${cyan}5${reset}) All\n`);
|
|
982
|
+
|
|
983
|
+
rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
|
|
984
|
+
answered = true;
|
|
985
|
+
rl.close();
|
|
986
|
+
const choice = answer.trim() || '1';
|
|
987
|
+
if (choice === '5') callback(['claude', 'opencode', 'gemini', 'codex']);
|
|
988
|
+
else if (choice === '4') callback(['codex']);
|
|
989
|
+
else if (choice === '3') callback(['gemini']);
|
|
990
|
+
else if (choice === '2') callback(['opencode']);
|
|
991
|
+
else callback(['claude']);
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function promptLocation(runtimes) {
|
|
996
|
+
if (!process.stdin.isTTY) {
|
|
997
|
+
console.log(` ${yellow}Non-interactive terminal detected, defaulting to global install${reset}\n`);
|
|
998
|
+
installAllRuntimes(runtimes, true, false);
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1003
|
+
let answered = false;
|
|
1004
|
+
|
|
1005
|
+
rl.on('close', () => {
|
|
1006
|
+
if (!answered) { answered = true; console.log(`\n ${yellow}Installation cancelled${reset}\n`); process.exit(0); }
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
const pathExamples = runtimes.map(r => getGlobalDir(r, explicitConfigDir).replace(os.homedir(), '~')).join(', ');
|
|
1010
|
+
const localExamples = runtimes.map(r => `./${getDirName(r)}`).join(', ');
|
|
1011
|
+
|
|
1012
|
+
console.log(` ${yellow}Where would you like to install?${reset}\n
|
|
1013
|
+
${cyan}1${reset}) Global ${dim}(${pathExamples})${reset} - available in all projects
|
|
1014
|
+
${cyan}2${reset}) Local ${dim}(${localExamples})${reset} - this project only\n`);
|
|
1015
|
+
|
|
1016
|
+
rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
|
|
1017
|
+
answered = true;
|
|
1018
|
+
rl.close();
|
|
1019
|
+
const isGlobal = (answer.trim() || '1') !== '2';
|
|
1020
|
+
installAllRuntimes(runtimes, isGlobal, true);
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function installAllRuntimes(runtimes, isGlobal, isInteractive) {
|
|
1025
|
+
const results = [];
|
|
1026
|
+
for (const runtime of runtimes) {
|
|
1027
|
+
results.push(install(isGlobal, runtime));
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// Handle statusline for Claude & Gemini
|
|
1031
|
+
const claudeResult = results.find(r => r.runtime === 'claude');
|
|
1032
|
+
const geminiResult = results.find(r => r.runtime === 'gemini');
|
|
1033
|
+
|
|
1034
|
+
if (claudeResult || geminiResult) {
|
|
1035
|
+
const primaryResult = claudeResult || geminiResult;
|
|
1036
|
+
handleStatusline(primaryResult.settings, isInteractive, (shouldInstallStatusline) => {
|
|
1037
|
+
for (const result of results) {
|
|
1038
|
+
finishInstall(result.settingsPath, result.settings, result.statuslineCommand, shouldInstallStatusline, result.runtime, isGlobal);
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
} else {
|
|
1042
|
+
for (const result of results) {
|
|
1043
|
+
finishInstall(result.settingsPath, result.settings, result.statuslineCommand, false, result.runtime, isGlobal);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// ──────────────────────────────────────────────────────
|
|
1049
|
+
// Main
|
|
1050
|
+
// ──────────────────────────────────────────────────────
|
|
1051
|
+
|
|
1052
|
+
if (hasGlobal && hasLocal) {
|
|
1053
|
+
console.error(` ${yellow}Cannot specify both --global and --local${reset}`);
|
|
1054
|
+
process.exit(1);
|
|
1055
|
+
} else if (explicitConfigDir && hasLocal) {
|
|
1056
|
+
console.error(` ${yellow}Cannot use --config-dir with --local${reset}`);
|
|
1057
|
+
process.exit(1);
|
|
1058
|
+
} else if (hasUninstall) {
|
|
1059
|
+
if (!hasGlobal && !hasLocal) {
|
|
1060
|
+
console.error(` ${yellow}--uninstall requires --global or --local${reset}`);
|
|
1061
|
+
process.exit(1);
|
|
1062
|
+
}
|
|
1063
|
+
const runtimes = selectedRuntimes.length > 0 ? selectedRuntimes : ['claude'];
|
|
1064
|
+
for (const runtime of runtimes) {
|
|
1065
|
+
uninstall(hasGlobal, runtime);
|
|
1066
|
+
}
|
|
1067
|
+
} else if (selectedRuntimes.length > 0) {
|
|
1068
|
+
if (!hasGlobal && !hasLocal) {
|
|
1069
|
+
promptLocation(selectedRuntimes);
|
|
1070
|
+
} else {
|
|
1071
|
+
installAllRuntimes(selectedRuntimes, hasGlobal, false);
|
|
1072
|
+
}
|
|
1073
|
+
} else if (hasGlobal || hasLocal) {
|
|
1074
|
+
installAllRuntimes(['claude'], hasGlobal, false);
|
|
1075
|
+
} else {
|
|
1076
|
+
if (!process.stdin.isTTY) {
|
|
1077
|
+
console.log(` ${yellow}Non-interactive terminal detected, defaulting to Claude Code global install${reset}\n`);
|
|
1078
|
+
installAllRuntimes(['claude'], true, false);
|
|
1079
|
+
} else {
|
|
1080
|
+
promptRuntime((runtimes) => {
|
|
1081
|
+
promptLocation(runtimes);
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
}
|