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.
Files changed (50) hide show
  1. package/README.md +111 -0
  2. package/agents/gsp-accessibility-auditor.md +52 -0
  3. package/agents/gsp-brand-strategist.md +45 -0
  4. package/agents/gsp-campaign-director.md +48 -0
  5. package/agents/gsp-critic.md +55 -0
  6. package/agents/gsp-design-engineer.md +53 -0
  7. package/agents/gsp-researcher.md +47 -0
  8. package/agents/gsp-spec-engineer.md +45 -0
  9. package/agents/gsp-system-architect.md +56 -0
  10. package/agents/gsp-ui-designer.md +51 -0
  11. package/bin/install.js +1084 -0
  12. package/commands/gsp/brand.md +68 -0
  13. package/commands/gsp/build.md +75 -0
  14. package/commands/gsp/design.md +71 -0
  15. package/commands/gsp/help.md +68 -0
  16. package/commands/gsp/launch.md +75 -0
  17. package/commands/gsp/new-project.md +91 -0
  18. package/commands/gsp/progress.md +61 -0
  19. package/commands/gsp/research.md +71 -0
  20. package/commands/gsp/review.md +99 -0
  21. package/commands/gsp/spec.md +68 -0
  22. package/commands/gsp/system.md +72 -0
  23. package/package.json +38 -0
  24. package/prompts/01-design-system-architect.md +29 -0
  25. package/prompts/02-brand-identity-creator.md +29 -0
  26. package/prompts/03-ui-ux-pattern-master.md +31 -0
  27. package/prompts/04-marketing-asset-factory.md +27 -0
  28. package/prompts/05-figma-auto-layout-expert.md +27 -0
  29. package/prompts/06-design-critique-partner.md +30 -0
  30. package/prompts/07-design-trend-synthesizer.md +30 -0
  31. package/prompts/08-accessibility-auditor.md +29 -0
  32. package/prompts/09-design-to-code-translator.md +31 -0
  33. package/references/apple-hig-patterns.md +141 -0
  34. package/references/design-tokens.md +182 -0
  35. package/references/nielsen-heuristics.md +141 -0
  36. package/references/questioning.md +73 -0
  37. package/references/wcag-checklist.md +111 -0
  38. package/scripts/gsp-statusline.js +132 -0
  39. package/templates/config.json +25 -0
  40. package/templates/phases/brand.md +60 -0
  41. package/templates/phases/build.md +59 -0
  42. package/templates/phases/design.md +48 -0
  43. package/templates/phases/launch.md +62 -0
  44. package/templates/phases/research.md +53 -0
  45. package/templates/phases/review.md +88 -0
  46. package/templates/phases/spec.md +60 -0
  47. package/templates/phases/system.md +84 -0
  48. package/templates/project.md +48 -0
  49. package/templates/roadmap.md +62 -0
  50. 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
+ }