@undeemed/get-shit-done-codex 1.20.8 → 1.20.10

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/bin/install.js CHANGED
@@ -36,6 +36,45 @@ const args = process.argv.slice(2);
36
36
  const hasGlobal = args.includes('--global') || args.includes('-g');
37
37
  const hasLocal = args.includes('--local') || args.includes('-l');
38
38
  const hasHelp = args.includes('--help') || args.includes('-h');
39
+ const hasVerify = args.includes('--verify');
40
+ const hasRepair = args.includes('--repair');
41
+ const hasNoVersionCheck = args.includes('--no-version-check') || process.env.GSD_SKIP_VERSION_CHECK === '1';
42
+ const hasMigrate = args.includes('--migrate');
43
+ const hasSkipMigrate = args.includes('--skip-migrate') || args.includes('--no-migrate');
44
+ const isInteractiveTerminal = Boolean(process.stdin.isTTY && process.stdout.isTTY);
45
+ const CODEX_MODES = new Set(['prompts', 'skills']);
46
+ const hasCodexModeArg = args.some((arg) =>
47
+ arg.startsWith('--codex-mode=') || arg === '--codex-mode' || arg === '-m'
48
+ );
49
+
50
+ function parseCodexModeArg(argv) {
51
+ const explicit = argv.find((arg) => arg.startsWith('--codex-mode='));
52
+ const flagIndex = argv.findIndex((arg) => arg === '--codex-mode' || arg === '-m');
53
+ let mode = 'skills';
54
+
55
+ if (explicit) {
56
+ mode = explicit.split('=')[1] || '';
57
+ }
58
+
59
+ if (flagIndex !== -1) {
60
+ const next = argv[flagIndex + 1];
61
+ if (!next || next.startsWith('-')) {
62
+ console.error(` ${yellow}--codex-mode requires one of: prompts, skills${reset}`);
63
+ process.exit(1);
64
+ }
65
+ mode = next;
66
+ }
67
+
68
+ mode = String(mode).toLowerCase().trim();
69
+ if (!CODEX_MODES.has(mode)) {
70
+ console.error(` ${yellow}Invalid --codex-mode '${mode}'. Expected prompts or skills.${reset}`);
71
+ process.exit(1);
72
+ }
73
+
74
+ return mode;
75
+ }
76
+
77
+ const codexMode = hasHelp ? 'skills' : parseCodexModeArg(args);
39
78
 
40
79
  console.log(banner);
41
80
 
@@ -45,6 +84,12 @@ if (hasHelp) {
45
84
  ${yellow}Options:${reset}
46
85
  ${cyan}-g, --global${reset} Install globally (to ~/.codex/)
47
86
  ${cyan}-l, --local${reset} Install locally (to current directory)
87
+ ${cyan}-m, --codex-mode <mode>${reset} Command mode: skills or prompts
88
+ ${cyan}--migrate${reset} Apply detected legacy surface cleanup without prompting
89
+ ${cyan}--skip-migrate${reset} Keep legacy surface files when migration is detected
90
+ ${cyan}--verify${reset} Verify current install integrity
91
+ ${cyan}--repair${reset} Repair failed verification checks
92
+ ${cyan}--no-version-check${reset} Skip npm version lookup
48
93
  ${cyan}-h, --help${reset} Show this help message
49
94
 
50
95
  ${yellow}Examples:${reset}
@@ -54,24 +99,45 @@ if (hasHelp) {
54
99
  ${dim}# Install to current project only${reset}
55
100
  npx ${NPM_PACKAGE} --local
56
101
 
102
+ ${dim}# Install native Codex skills only${reset}
103
+ npx ${NPM_PACKAGE} --global --codex-mode skills
104
+
105
+ ${dim}# Install prompt aliases only${reset}
106
+ npx ${NPM_PACKAGE} --global --codex-mode prompts
107
+
108
+ ${dim}# Verify global installation${reset}
109
+ npx ${NPM_PACKAGE} --verify --global
110
+
111
+ ${dim}# Verify and auto-repair local installation${reset}
112
+ npx ${NPM_PACKAGE} --verify --repair --local
113
+
114
+ ${dim}# Force migration cleanup in non-interactive runs${reset}
115
+ npx ${NPM_PACKAGE} --global --migrate
116
+
57
117
  ${yellow}Notes:${reset}
58
118
  - Installs AGENTS.md into the target directory
59
- - Installs GSD prompt commands into prompts/
119
+ - If --codex-mode is omitted in interactive mode, installer prompts to choose
120
+ - Non-interactive runs default to skills mode
121
+ - If legacy surface files are detected, installer asks before removing them
60
122
  - Installs get-shit-done/ workflow files
61
123
  `);
62
124
  process.exit(0);
63
125
  }
64
126
 
65
- function applyReplacements(content, pathPrefix) {
66
- const runtimeRelativePrefix = pathPrefix.replace('~/', '');
67
- const explicitRelativePrefix = runtimeRelativePrefix === './'
68
- ? './'
69
- : `./${runtimeRelativePrefix}`;
127
+ function applyPathPrefixReplacements(content, pathPrefix) {
128
+ if (pathPrefix === '~/.codex/') {
129
+ return content
130
+ .replace(/\.\/\.codex\//g, '~/.codex/')
131
+ .replace(/~\/\.codex\//g, '~/.codex/');
132
+ }
70
133
 
71
- // Rewrite codex config roots for global vs local installs.
72
- // Global install keeps ~/.codex/* references; local rewrites to ./*
73
- content = content.replace(/\.\/\.codex\//g, explicitRelativePrefix);
74
- content = content.replace(/~\/\.codex\//g, '~/.codex/');
134
+ return content
135
+ .replace(/\.\/\.codex\//g, pathPrefix)
136
+ .replace(/~\/\.codex\//g, pathPrefix);
137
+ }
138
+
139
+ function applyReplacements(content, pathPrefix) {
140
+ content = applyPathPrefixReplacements(content, pathPrefix);
75
141
 
76
142
  content = content.replace(/Codex CLI/g, 'Codex CLI');
77
143
  content = content.replace(/Codex/g, 'Codex');
@@ -86,6 +152,208 @@ function applyReplacements(content, pathPrefix) {
86
152
  return content;
87
153
  }
88
154
 
155
+ function convertPromptRefsToSkillRefs(content) {
156
+ let converted = content.replace(/\/prompts:gsd-([a-z0-9-]+)/gi, (_, commandName) => `$gsd-${String(commandName).toLowerCase()}`);
157
+ converted = converted.replace(/\/gsd:([a-z0-9-]+)/gi, (_, commandName) => `$gsd-${String(commandName).toLowerCase()}`);
158
+ converted = converted.replace(/\/gsd-help\b/gi, '$gsd-help');
159
+ return converted;
160
+ }
161
+
162
+ function convertSkillRefsToPromptRefs(content) {
163
+ return content.replace(/\$gsd-([a-z0-9-]+)/gi, (_, commandName) => `/prompts:gsd-${String(commandName).toLowerCase()}`);
164
+ }
165
+
166
+ function convertCommandRefsToSkillMentions(content) {
167
+ return convertPromptRefsToSkillRefs(content).replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
168
+ }
169
+
170
+ function rewriteAgentInvocationLine(content, replacement) {
171
+ return content
172
+ .replace('Invoke them with `/gsd:command-name`:', replacement)
173
+ .replace('Invoke them with `/prompts:gsd-command-name`:', replacement)
174
+ .replace('Invoke them with `$gsd-command-name`:', replacement);
175
+ }
176
+
177
+ function adaptAgentsForCodexMode(content, mode) {
178
+ if (mode === 'skills') {
179
+ let adapted = convertPromptRefsToSkillRefs(content);
180
+ adapted = rewriteAgentInvocationLine(adapted, 'Invoke them with `$gsd-command-name`:');
181
+ return adapted;
182
+ }
183
+
184
+ if (mode === 'prompts') {
185
+ let adapted = convertSkillRefsToPromptRefs(content);
186
+ adapted = rewriteAgentInvocationLine(adapted, 'Invoke them with `/prompts:gsd-command-name`:');
187
+ return adapted;
188
+ }
189
+
190
+ return content;
191
+ }
192
+
193
+ function extractFrontmatterAndBody(content) {
194
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
195
+ if (!match) {
196
+ return { frontmatter: null, body: content };
197
+ }
198
+ return { frontmatter: match[1], body: content.slice(match[0].length) };
199
+ }
200
+
201
+ function extractFrontmatterField(frontmatter, fieldName) {
202
+ if (!frontmatter) return null;
203
+ const match = frontmatter.match(new RegExp(`^${fieldName}:\\s*(.+)$`, 'mi'));
204
+ if (!match) return null;
205
+ return match[1].trim().replace(/^['"]|['"]$/g, '');
206
+ }
207
+
208
+ function yamlQuote(value) {
209
+ return JSON.stringify(String(value));
210
+ }
211
+
212
+ function toSingleLine(value) {
213
+ return String(value).replace(/\s+/g, ' ').trim();
214
+ }
215
+
216
+ function getCodexSkillAdapterHeader(skillName) {
217
+ return `<codex_skill_adapter>
218
+ Codex native skill mode:
219
+ - AGENTS-first: treat AGENTS.md as the persistent source of truth for behavior and constraints.
220
+ - Invoke this workflow with \`$${skillName}\`.
221
+ - Treat user text after \`$${skillName}\` as \`{{GSD_ARGS}}\` (empty when omitted).
222
+ - Any legacy \`Task(...)\` notation in referenced docs means "spawn a specialized subagent" in Codex.
223
+ </codex_skill_adapter>`;
224
+ }
225
+
226
+ function convertCommandToCodexSkill(commandContent, skillName, pathPrefix) {
227
+ const replaced = applyReplacements(commandContent, pathPrefix);
228
+ const converted = convertCommandRefsToSkillMentions(replaced);
229
+ const { frontmatter, body } = extractFrontmatterAndBody(converted);
230
+ const description = toSingleLine(
231
+ extractFrontmatterField(frontmatter, 'description') || `Run GSD workflow ${skillName}.`
232
+ );
233
+ const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
234
+
235
+ return `---
236
+ name: ${yamlQuote(skillName)}
237
+ description: ${yamlQuote(description)}
238
+ metadata:
239
+ short-description: ${yamlQuote(shortDescription)}
240
+ ---
241
+
242
+ ${getCodexSkillAdapterHeader(skillName)}
243
+
244
+ ${body.trimStart()}`;
245
+ }
246
+
247
+ function installPrompts(commandsDir, promptsDir, markdownEntries, pathPrefix) {
248
+ fs.mkdirSync(promptsDir, { recursive: true });
249
+ for (const entry of markdownEntries) {
250
+ const srcPath = path.join(commandsDir, entry);
251
+ const destPath = path.join(promptsDir, `gsd-${entry}`);
252
+ const content = applyReplacements(fs.readFileSync(srcPath, 'utf8'), pathPrefix);
253
+ fs.writeFileSync(destPath, content, 'utf8');
254
+ }
255
+ }
256
+
257
+ function installCodexSkills(commandsDir, skillsDir, markdownEntries, pathPrefix) {
258
+ fs.mkdirSync(skillsDir, { recursive: true });
259
+
260
+ const existingSkills = fs.readdirSync(skillsDir, { withFileTypes: true });
261
+ for (const entry of existingSkills) {
262
+ if (entry.isDirectory() && entry.name.startsWith('gsd-')) {
263
+ fs.rmSync(path.join(skillsDir, entry.name), { recursive: true, force: true });
264
+ }
265
+ }
266
+
267
+ for (const entry of markdownEntries) {
268
+ const commandPath = path.join(commandsDir, entry);
269
+ const commandContent = fs.readFileSync(commandPath, 'utf8');
270
+ const commandName = entry.replace(/\.md$/i, '');
271
+ const skillName = `gsd-${commandName}`;
272
+ const skillDir = path.join(skillsDir, skillName);
273
+ fs.mkdirSync(skillDir, { recursive: true });
274
+ fs.writeFileSync(
275
+ path.join(skillDir, 'SKILL.md'),
276
+ convertCommandToCodexSkill(commandContent, skillName, pathPrefix),
277
+ 'utf8'
278
+ );
279
+ }
280
+ }
281
+
282
+ function removePromptAliases(promptsDir) {
283
+ if (!fs.existsSync(promptsDir)) return 0;
284
+ const entries = fs.readdirSync(promptsDir);
285
+ let removed = 0;
286
+ for (const entry of entries) {
287
+ if (/^gsd-.*\.md$/i.test(entry)) {
288
+ fs.rmSync(path.join(promptsDir, entry), { force: true });
289
+ removed++;
290
+ }
291
+ }
292
+ return removed;
293
+ }
294
+
295
+ function removeSkillAliases(skillsDir) {
296
+ if (!fs.existsSync(skillsDir)) return 0;
297
+ const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
298
+ let removed = 0;
299
+ for (const entry of entries) {
300
+ if (entry.isDirectory() && entry.name.startsWith('gsd-')) {
301
+ fs.rmSync(path.join(skillsDir, entry.name), { recursive: true, force: true });
302
+ removed++;
303
+ }
304
+ }
305
+ return removed;
306
+ }
307
+
308
+ function detectMigrationPlan(codexDir, mode) {
309
+ const promptsDir = path.join(codexDir, 'prompts');
310
+ const skillsDir = path.join(codexDir, 'skills');
311
+ const promptFiles = listPromptCommandFiles(promptsDir);
312
+ const skillNames = listSkillNames(skillsDir);
313
+
314
+ const promptCountToRemove = mode === 'skills' ? promptFiles.length : 0;
315
+ const skillCountToRemove = mode === 'prompts' ? skillNames.length : 0;
316
+
317
+ return {
318
+ mode,
319
+ promptsDir,
320
+ skillsDir,
321
+ promptCountToRemove,
322
+ skillCountToRemove,
323
+ hasChanges: promptCountToRemove > 0 || skillCountToRemove > 0,
324
+ };
325
+ }
326
+
327
+ function describeMigrationPlan(plan) {
328
+ const items = [];
329
+ if (plan.promptCountToRemove > 0) {
330
+ items.push(`${plan.promptCountToRemove} legacy prompt alias file(s) in prompts/`);
331
+ }
332
+ if (plan.skillCountToRemove > 0) {
333
+ items.push(`${plan.skillCountToRemove} legacy skill director${plan.skillCountToRemove === 1 ? 'y' : 'ies'} in skills/`);
334
+ }
335
+ return items.join(' and ');
336
+ }
337
+
338
+ function applyMigrationPlan(plan) {
339
+ let removedPrompts = 0;
340
+ let removedSkills = 0;
341
+
342
+ if (plan.promptCountToRemove > 0) {
343
+ removedPrompts = removePromptAliases(plan.promptsDir);
344
+ }
345
+ if (plan.skillCountToRemove > 0) {
346
+ removedSkills = removeSkillAliases(plan.skillsDir);
347
+ }
348
+
349
+ if (removedPrompts > 0 || removedSkills > 0) {
350
+ const changes = [];
351
+ if (removedPrompts > 0) changes.push(`${removedPrompts} prompt alias file(s)`);
352
+ if (removedSkills > 0) changes.push(`${removedSkills} skill director${removedSkills === 1 ? 'y' : 'ies'}`);
353
+ console.log(` ${green}✓${reset} Migration applied: removed ${changes.join(', ')}`);
354
+ }
355
+ }
356
+
89
357
  function copyWithPathReplacement(srcDir, destDir, pathPrefix) {
90
358
  fs.mkdirSync(destDir, { recursive: true });
91
359
  const entries = fs.readdirSync(srcDir, { withFileTypes: true });
@@ -131,34 +399,137 @@ function showCachedVersionWarning() {
131
399
  }
132
400
  }
133
401
 
134
- function install(isGlobal) {
135
- const src = path.join(__dirname, '..');
402
+ function getInstallContext(isGlobal) {
136
403
  const codexDir = isGlobal ? path.join(os.homedir(), '.codex') : process.cwd();
137
- const locationLabel = isGlobal ? '~/.codex' : '.';
138
- const pathPrefix = isGlobal ? '~/.codex/' : './';
404
+ return {
405
+ codexDir,
406
+ locationLabel: isGlobal ? '~/.codex' : '.',
407
+ pathPrefix: isGlobal ? '~/.codex/' : './',
408
+ };
409
+ }
410
+
411
+ function listPromptCommandFiles(promptsDir) {
412
+ if (!fs.existsSync(promptsDir)) return [];
413
+ return fs.readdirSync(promptsDir)
414
+ .filter((name) => /^gsd-.*\.md$/i.test(name))
415
+ .sort();
416
+ }
417
+
418
+ function listSkillNames(skillsDir) {
419
+ if (!fs.existsSync(skillsDir)) return [];
420
+ return fs.readdirSync(skillsDir, { withFileTypes: true })
421
+ .filter((entry) => entry.isDirectory() && entry.name.startsWith('gsd-'))
422
+ .filter((entry) => fs.existsSync(path.join(skillsDir, entry.name, 'SKILL.md')))
423
+ .map((entry) => entry.name)
424
+ .sort();
425
+ }
426
+
427
+ function detectInstalledMode(promptCount, skillCount) {
428
+ if (promptCount > 0 && skillCount > 0) return 'mixed';
429
+ if (promptCount > 0) return 'prompts';
430
+ if (skillCount > 0) return 'skills';
431
+ return 'none';
432
+ }
433
+
434
+ function verifyInstall(isGlobal, expectedMode, strictMode = false) {
435
+ const src = path.join(__dirname, '..');
436
+ const { codexDir, locationLabel } = getInstallContext(isGlobal);
437
+ const promptsDir = path.join(codexDir, 'prompts');
438
+ const skillsDir = path.join(codexDir, 'skills');
439
+ const workflowRoot = path.join(codexDir, 'get-shit-done');
440
+ const versionFile = path.join(workflowRoot, 'VERSION');
441
+ const expectedCount = fs.readdirSync(path.join(src, 'commands', 'gsd'))
442
+ .filter((entry) => entry.endsWith('.md'))
443
+ .length;
444
+
445
+ const promptFiles = listPromptCommandFiles(promptsDir);
446
+ const skillNames = listSkillNames(skillsDir);
447
+ const detectedMode = detectInstalledMode(promptFiles.length, skillNames.length);
448
+ const modeToCheck = strictMode ? expectedMode : (detectedMode === 'none' ? expectedMode : detectedMode);
449
+
450
+ const checks = [];
451
+ const addCheck = (ok, label, detail) => checks.push({ ok, label, detail });
452
+
453
+ addCheck(fs.existsSync(codexDir), 'Config directory exists', codexDir);
454
+ addCheck(fs.existsSync(path.join(codexDir, 'AGENTS.md')), 'AGENTS.md installed', path.join(codexDir, 'AGENTS.md'));
455
+ addCheck(fs.existsSync(workflowRoot), 'get-shit-done assets installed', workflowRoot);
456
+ addCheck(fs.existsSync(path.join(workflowRoot, 'workflows')), 'Workflow directory installed', path.join(workflowRoot, 'workflows'));
457
+ addCheck(fs.existsSync(path.join(workflowRoot, 'templates')), 'Template directory installed', path.join(workflowRoot, 'templates'));
458
+ addCheck(fs.existsSync(versionFile), 'VERSION file installed', versionFile);
459
+
460
+ if (fs.existsSync(versionFile)) {
461
+ const version = fs.readFileSync(versionFile, 'utf8').split(/\r?\n/)[0].trim();
462
+ addCheck(version === pkg.version, 'VERSION matches installer package', `${version || '(empty)'} vs ${pkg.version}`);
463
+ }
464
+
465
+ if (modeToCheck === 'prompts') {
466
+ addCheck(promptFiles.length === expectedCount, 'Prompt aliases complete', `${promptFiles.length}/${expectedCount}`);
467
+ }
468
+ if (modeToCheck === 'skills') {
469
+ addCheck(skillNames.length === expectedCount, 'Native skills complete', `${skillNames.length}/${expectedCount}`);
470
+ }
471
+ if (modeToCheck === 'mixed') {
472
+ addCheck(
473
+ false,
474
+ 'Single command surface required',
475
+ 'Both skills and prompt aliases found. Choose one mode and run install with --migrate.'
476
+ );
477
+ }
478
+ if (modeToCheck === 'none') {
479
+ addCheck(false, 'At least one command surface installed', 'No skills/ or prompts/ entries found');
480
+ }
481
+
482
+ console.log(` Verifying ${cyan}${locationLabel}${reset} (detected mode: ${cyan}${detectedMode}${reset}, check mode: ${cyan}${modeToCheck}${reset})`);
483
+ for (const check of checks) {
484
+ const icon = check.ok ? `${green}✓${reset}` : `${yellow}✗${reset}`;
485
+ const detail = check.detail ? ` ${dim}(${check.detail})${reset}` : '';
486
+ console.log(` ${icon} ${check.label}${detail}`);
487
+ }
488
+
489
+ const ok = checks.every((check) => check.ok);
490
+ if (ok) {
491
+ console.log(`\n ${green}Integrity check passed.${reset}`);
492
+ } else {
493
+ console.log(`\n ${yellow}Integrity check failed.${reset}`);
494
+ }
495
+
496
+ return { ok, detectedMode, checkedMode: modeToCheck };
497
+ }
498
+
499
+ function installCore(isGlobal, mode, migrationPlan, applyMigration) {
500
+ const src = path.join(__dirname, '..');
501
+ const { codexDir, locationLabel, pathPrefix } = getInstallContext(isGlobal);
502
+ const installPromptsEnabled = mode === 'prompts';
503
+ const installSkillsEnabled = mode === 'skills';
139
504
 
140
505
  console.log(` Installing to ${cyan}${locationLabel}${reset}\n`);
141
506
  fs.mkdirSync(codexDir, { recursive: true });
507
+ if (applyMigration && migrationPlan && migrationPlan.hasChanges) {
508
+ applyMigrationPlan(migrationPlan);
509
+ }
142
510
 
143
511
  const agentsSrc = path.join(src, 'AGENTS.md');
144
512
  const agentsDest = path.join(codexDir, 'AGENTS.md');
145
- const agentsContent = applyReplacements(fs.readFileSync(agentsSrc, 'utf8'), pathPrefix);
513
+ let agentsContent = applyReplacements(fs.readFileSync(agentsSrc, 'utf8'), pathPrefix);
514
+ agentsContent = adaptAgentsForCodexMode(agentsContent, mode);
146
515
  fs.writeFileSync(agentsDest, agentsContent, 'utf8');
147
516
  console.log(` ${green}✓${reset} Installed AGENTS.md`);
148
517
 
149
- const promptsDir = path.join(codexDir, 'prompts');
150
- fs.mkdirSync(promptsDir, { recursive: true });
151
-
152
518
  const gsdSrc = path.join(src, 'commands', 'gsd');
153
519
  const entries = fs.readdirSync(gsdSrc);
154
520
  const markdownEntries = entries.filter((entry) => entry.endsWith('.md'));
155
- for (const entry of markdownEntries) {
156
- const srcPath = path.join(gsdSrc, entry);
157
- const destPath = path.join(promptsDir, `gsd-${entry}`);
158
- const content = applyReplacements(fs.readFileSync(srcPath, 'utf8'), pathPrefix);
159
- fs.writeFileSync(destPath, content, 'utf8');
521
+
522
+ if (installPromptsEnabled) {
523
+ const promptsDir = path.join(codexDir, 'prompts');
524
+ installPrompts(gsdSrc, promptsDir, markdownEntries, pathPrefix);
525
+ console.log(` ${green}✓${reset} Installed prompts/gsd-*.md (${markdownEntries.length} commands)`);
526
+ }
527
+
528
+ if (installSkillsEnabled) {
529
+ const skillsDir = path.join(codexDir, 'skills');
530
+ installCodexSkills(gsdSrc, skillsDir, markdownEntries, pathPrefix);
531
+ console.log(` ${green}✓${reset} Installed skills/gsd-*/SKILL.md (${markdownEntries.length} skills)`);
160
532
  }
161
- console.log(` ${green}✓${reset} Installed prompts/gsd-*.md (${markdownEntries.length} commands)`);
162
533
 
163
534
  const skillSrc = path.join(src, 'get-shit-done');
164
535
  const skillDest = path.join(codexDir, 'get-shit-done');
@@ -172,20 +543,100 @@ function install(isGlobal) {
172
543
 
173
544
  ${yellow}For Codex (CLI + Desktop):${reset}
174
545
  - AGENTS.md: ${cyan}${codexDir}/AGENTS.md${reset}
175
- - Prompt commands: ${cyan}${codexDir}/prompts/${reset}
546
+ ${installPromptsEnabled ? `- Prompt commands: ${cyan}${codexDir}/prompts/${reset}` : ''}
547
+ ${installSkillsEnabled ? `- Native skills: ${cyan}${codexDir}/skills/gsd-*/SKILL.md${reset}` : ''}
176
548
 
177
549
  ${yellow}Getting Started:${reset}
178
550
  1. Run ${cyan}codex${reset} (CLI) or ${cyan}codex app${reset} (Desktop)
179
- 2. Use ${cyan}/prompts:gsd-help${reset} to list commands
180
- 3. Start with ${cyan}/prompts:gsd-new-project${reset}
551
+ 2. Use ${cyan}${installSkillsEnabled ? '$gsd-help' : '/prompts:gsd-help'}${reset} to list commands
552
+ 3. Start with ${cyan}${installSkillsEnabled ? '$gsd-new-project' : '/prompts:gsd-new-project'}${reset}
181
553
 
182
554
  ${yellow}Staying Updated:${reset}
183
- - In Codex: ${cyan}/prompts:gsd-update${reset}
555
+ - In Codex: ${cyan}${installSkillsEnabled ? '$gsd-update' : '/prompts:gsd-update'}${reset}
184
556
  - In terminal: ${cyan}npx ${NPM_PACKAGE_LATEST}${reset}
185
557
  `);
186
558
  }
187
559
 
188
- function promptLocation() {
560
+ function install(isGlobal, mode = codexMode, done = () => {}) {
561
+ const { codexDir } = getInstallContext(isGlobal);
562
+ const migrationPlan = detectMigrationPlan(codexDir, mode);
563
+
564
+ if (!migrationPlan.hasChanges) {
565
+ installCore(isGlobal, mode, migrationPlan, false);
566
+ done();
567
+ return;
568
+ }
569
+
570
+ const summary = describeMigrationPlan(migrationPlan);
571
+ console.log(` ${yellow}Migration detected:${reset} ${summary}`);
572
+
573
+ if (hasMigrate) {
574
+ console.log(` ${green}✓${reset} Migration approved by --migrate`);
575
+ installCore(isGlobal, mode, migrationPlan, true);
576
+ done();
577
+ return;
578
+ }
579
+
580
+ if (hasSkipMigrate) {
581
+ console.log(` ${yellow}Skipping migration due to --skip-migrate.${reset}`);
582
+ installCore(isGlobal, mode, migrationPlan, false);
583
+ done();
584
+ return;
585
+ }
586
+
587
+ if (!isInteractiveTerminal) {
588
+ console.log(` ${yellow}Skipping migration in non-interactive mode.${reset}`);
589
+ console.log(` ${dim}Re-run with --migrate to apply cleanup or --skip-migrate to keep legacy files explicitly.${reset}`);
590
+ installCore(isGlobal, mode, migrationPlan, false);
591
+ done();
592
+ return;
593
+ }
594
+
595
+ const rl = readline.createInterface({
596
+ input: process.stdin,
597
+ output: process.stdout
598
+ });
599
+
600
+ rl.question(` Remove legacy files now? ${dim}[Y/n]${reset}: `, (answer) => {
601
+ rl.close();
602
+ const normalized = answer.trim().toLowerCase();
603
+ const applyMigration = normalized === '' || normalized === 'y' || normalized === 'yes';
604
+ if (!applyMigration) {
605
+ console.log(` ${yellow}Keeping legacy files.${reset}`);
606
+ }
607
+ installCore(isGlobal, mode, migrationPlan, applyMigration);
608
+ done();
609
+ });
610
+ }
611
+
612
+ function promptCodexMode(done) {
613
+ if (hasCodexModeArg || !isInteractiveTerminal) {
614
+ done(codexMode);
615
+ return;
616
+ }
617
+
618
+ const rl = readline.createInterface({
619
+ input: process.stdin,
620
+ output: process.stdout
621
+ });
622
+
623
+ console.log(` ${yellow}Which command surface do you want?${reset}
624
+
625
+ ${cyan}1${reset}) Skills ${dim}($gsd-*, recommended)${reset}
626
+ ${cyan}2${reset}) Prompts ${dim}(/prompts:gsd-*)${reset}
627
+ `);
628
+
629
+ rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
630
+ rl.close();
631
+ const normalized = answer.trim().toLowerCase();
632
+ const mode = normalized === '2' || normalized === 'prompt' || normalized === 'prompts'
633
+ ? 'prompts'
634
+ : 'skills';
635
+ done(mode);
636
+ });
637
+ }
638
+
639
+ function promptLocation(done) {
189
640
  const rl = readline.createInterface({
190
641
  input: process.stdin,
191
642
  output: process.stdout
@@ -200,21 +651,57 @@ function promptLocation() {
200
651
  rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
201
652
  rl.close();
202
653
  const choice = answer.trim() || '1';
203
- install(choice !== '2');
654
+ done(choice !== '2');
204
655
  });
205
656
  }
206
657
 
207
- showCachedVersionWarning();
208
-
209
658
  if (hasGlobal && hasLocal) {
210
659
  console.error(` ${yellow}Cannot specify both --global and --local${reset}`);
211
660
  process.exit(1);
212
661
  }
213
662
 
214
- if (hasGlobal) {
215
- install(true);
216
- } else if (hasLocal) {
217
- install(false);
663
+ if (hasRepair && !hasVerify) {
664
+ console.error(` ${yellow}--repair requires --verify${reset}`);
665
+ process.exit(1);
666
+ }
667
+
668
+ if (hasMigrate && hasSkipMigrate) {
669
+ console.error(` ${yellow}Cannot combine --migrate and --skip-migrate${reset}`);
670
+ process.exit(1);
671
+ }
672
+
673
+ if (!hasNoVersionCheck && !hasVerify) {
674
+ showCachedVersionWarning();
675
+ }
676
+
677
+ if (hasVerify) {
678
+ const isGlobal = hasLocal ? false : true;
679
+ const strictMode = hasCodexModeArg;
680
+ let result = verifyInstall(isGlobal, codexMode, strictMode);
681
+
682
+ if (!result.ok && hasRepair) {
683
+ const repairMode = codexMode;
684
+ console.log(`\n ${yellow}Repairing install using mode '${repairMode}'...${reset}\n`);
685
+ install(isGlobal, repairMode, () => {
686
+ console.log('');
687
+ const repaired = verifyInstall(isGlobal, repairMode, true);
688
+ process.exit(repaired.ok ? 0 : 1);
689
+ });
690
+ } else {
691
+ process.exit(result.ok ? 0 : 1);
692
+ }
218
693
  } else {
219
- promptLocation();
694
+ const installWithSelectedMode = (isGlobal) => {
695
+ promptCodexMode((selectedMode) => {
696
+ install(isGlobal, selectedMode);
697
+ });
698
+ };
699
+
700
+ if (hasGlobal) {
701
+ installWithSelectedMode(true);
702
+ } else if (hasLocal) {
703
+ installWithSelectedMode(false);
704
+ } else {
705
+ promptLocation(installWithSelectedMode);
706
+ }
220
707
  }
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: gsd:add-tests
3
+ description: Generate tests for a completed phase based on UAT criteria and implementation
4
+ argument-hint: "<phase> [additional instructions]"
5
+ allowed-tools:
6
+ - Read
7
+ - Write
8
+ - Edit
9
+ - Bash
10
+ - Glob
11
+ - Grep
12
+ - Task
13
+ - AskUserQuestion
14
+ argument-instructions: |
15
+ Parse the argument as a phase number (integer, decimal, or letter-suffix), plus optional free-text instructions.
16
+ Example: /gsd:add-tests 12
17
+ Example: /gsd:add-tests 12 focus on edge cases in the pricing module
18
+ ---
19
+ <objective>
20
+ Generate unit and E2E tests for a completed phase, using its SUMMARY.md, CONTEXT.md, and VERIFICATION.md as specifications.
21
+
22
+ Analyzes implementation files, classifies them into TDD (unit), E2E (browser), or Skip categories, presents a test plan for user approval, then generates tests following RED-GREEN conventions.
23
+
24
+ Output: Test files committed with message `test(phase-{N}): add unit and E2E tests from add-tests command`
25
+ </objective>
26
+
27
+ <execution_context>
28
+ @~/.claude/get-shit-done/workflows/add-tests.md
29
+ </execution_context>
30
+
31
+ <context>
32
+ Phase: $ARGUMENTS
33
+
34
+ @.planning/STATE.md
35
+ @.planning/ROADMAP.md
36
+ </context>
37
+
38
+ <process>
39
+ Execute the add-tests workflow from @~/.claude/get-shit-done/workflows/add-tests.md end-to-end.
40
+ Preserve all workflow gates (classification approval, test plan approval, RED-GREEN verification, gap reporting).
41
+ </process>