@hustle-together/api-dev-tools 3.11.1 → 3.12.1

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 (139) hide show
  1. package/.claude/agents/code-reviewer.md +170 -0
  2. package/.claude/agents/docs-generator.md +80 -0
  3. package/.claude/agents/implementation-reviewer.md +119 -0
  4. package/.claude/agents/parallel-researcher.md +52 -0
  5. package/.claude/agents/research-validator.md +116 -0
  6. package/.claude/agents/schema-generator.md +70 -0
  7. package/.claude/agents/test-writer.md +104 -0
  8. package/.claude/api-dev-state.json +228 -56
  9. package/.claude/commands/README.md +21 -10
  10. package/.claude/commands/add-command.md +8 -5
  11. package/.claude/commands/api-create.md +36 -25
  12. package/.claude/commands/api-env.md +1 -0
  13. package/.claude/commands/api-interview.md +32 -19
  14. package/.claude/commands/api-research.md +47 -21
  15. package/.claude/commands/api-status.md +21 -1
  16. package/.claude/commands/api-verify.md +14 -13
  17. package/.claude/commands/beepboop.md +4 -5
  18. package/.claude/commands/busycommit.md +2 -3
  19. package/.claude/commands/commit.md +2 -3
  20. package/.claude/commands/cycle.md +2 -7
  21. package/.claude/commands/gap.md +2 -3
  22. package/.claude/commands/green.md +2 -7
  23. package/.claude/commands/issue.md +3 -8
  24. package/.claude/commands/ntfy-setup.md +91 -0
  25. package/.claude/commands/ntfy-test.md +74 -0
  26. package/.claude/commands/plan.md +2 -3
  27. package/.claude/commands/pr.md +2 -3
  28. package/.claude/commands/publish.md +40 -0
  29. package/.claude/commands/red.md +2 -7
  30. package/.claude/commands/refactor.md +2 -7
  31. package/.claude/commands/spike.md +2 -7
  32. package/.claude/commands/summarize.md +2 -3
  33. package/.claude/commands/tdd.md +2 -7
  34. package/.claude/commands/worktree-add.md +208 -216
  35. package/.claude/commands/worktree-cleanup.md +172 -178
  36. package/.claude/settings.json +63 -12
  37. package/.claude/settings.local.json +2 -1
  38. package/.claude-plugin/marketplace.json +2 -11
  39. package/.skills/README.md +55 -53
  40. package/.skills/_shared/settings.json +1 -1
  41. package/.skills/add-command/SKILL.md +10 -5
  42. package/.skills/api-create/SKILL.md +146 -35
  43. package/.skills/api-env/SKILL.md +1 -0
  44. package/.skills/api-interview/SKILL.md +32 -19
  45. package/.skills/api-research/SKILL.md +47 -21
  46. package/.skills/api-status/SKILL.md +21 -1
  47. package/.skills/api-verify/SKILL.md +14 -13
  48. package/.skills/beepboop/SKILL.md +6 -5
  49. package/.skills/busycommit/SKILL.md +4 -3
  50. package/.skills/commit/SKILL.md +4 -3
  51. package/.skills/cycle/SKILL.md +4 -7
  52. package/.skills/gap/SKILL.md +4 -3
  53. package/.skills/green/SKILL.md +4 -7
  54. package/.skills/issue/SKILL.md +5 -8
  55. package/.skills/plan/SKILL.md +4 -3
  56. package/.skills/pr/SKILL.md +4 -3
  57. package/.skills/publish/SKILL.md +160 -0
  58. package/.skills/red/SKILL.md +4 -7
  59. package/.skills/refactor/SKILL.md +4 -7
  60. package/.skills/spike/SKILL.md +4 -7
  61. package/.skills/summarize/SKILL.md +4 -3
  62. package/.skills/tdd/SKILL.md +4 -7
  63. package/.skills/update-todos/SKILL.md +22 -0
  64. package/.skills/worktree-add/SKILL.md +210 -216
  65. package/.skills/worktree-cleanup/SKILL.md +183 -187
  66. package/CHANGELOG.md +97 -79
  67. package/README.md +161 -7142
  68. package/bin/cli.js +448 -805
  69. package/commands/README.md +66 -31
  70. package/commands/add-command.md +8 -5
  71. package/commands/beepboop.md +4 -5
  72. package/commands/busycommit.md +2 -3
  73. package/commands/commit.md +2 -3
  74. package/commands/cycle.md +2 -7
  75. package/commands/gap.md +2 -3
  76. package/commands/green.md +2 -7
  77. package/commands/hustle-api-continue.md +8 -5
  78. package/commands/hustle-api-create.md +70 -29
  79. package/commands/hustle-api-env.md +1 -0
  80. package/commands/hustle-api-interview.md +32 -19
  81. package/commands/hustle-api-research.md +47 -21
  82. package/commands/hustle-api-sessions.md +8 -7
  83. package/commands/hustle-api-status.md +21 -1
  84. package/commands/hustle-api-verify.md +14 -13
  85. package/commands/hustle-combine.md +488 -241
  86. package/commands/hustle-ui-create-page.md +113 -50
  87. package/commands/hustle-ui-create.md +179 -26
  88. package/commands/issue.md +3 -8
  89. package/commands/plan.md +2 -3
  90. package/commands/pr.md +2 -3
  91. package/commands/red.md +2 -7
  92. package/commands/refactor.md +2 -7
  93. package/commands/spike.md +2 -7
  94. package/commands/summarize.md +2 -3
  95. package/commands/tdd.md +2 -7
  96. package/commands/worktree-add.md +208 -216
  97. package/commands/worktree-cleanup.md +172 -178
  98. package/hooks/api-workflow-check.py +5 -3
  99. package/hooks/enforce-component-type-confirm.py +97 -0
  100. package/hooks/lib/__init__.py +1 -0
  101. package/hooks/lib/greptile.py +355 -0
  102. package/hooks/lib/ntfy.py +209 -0
  103. package/hooks/notify-input-needed.py +73 -0
  104. package/hooks/notify-phase-complete.py +90 -0
  105. package/hooks/run-code-review.py +246 -0
  106. package/hooks/track-token-usage.py +121 -0
  107. package/package.json +13 -3
  108. package/scripts/collect-test-results.ts +102 -77
  109. package/scripts/extract-parameters.ts +112 -70
  110. package/scripts/generate-test-manifest.ts +118 -77
  111. package/templates/.env.example +57 -0
  112. package/templates/BRAND_GUIDE.md +92 -52
  113. package/templates/CLAUDE-SECTION.md +40 -37
  114. package/templates/SPEC.json +186 -38
  115. package/templates/api-dev-state.json +33 -4
  116. package/templates/api-showcase/_components/APICard.tsx +22 -18
  117. package/templates/api-showcase/_components/APIModal.tsx +110 -64
  118. package/templates/api-showcase/_components/APIShowcase.tsx +53 -35
  119. package/templates/api-showcase/_components/APITester.tsx +128 -67
  120. package/templates/api-showcase/page.tsx +4 -4
  121. package/templates/api-test/page.tsx +51 -30
  122. package/templates/api-test/test-structure/route.ts +43 -34
  123. package/templates/component/Component.stories.tsx +41 -39
  124. package/templates/component/Component.test.tsx +96 -78
  125. package/templates/component/Component.tsx +63 -52
  126. package/templates/component/Component.types.ts +10 -6
  127. package/templates/component/Component.visual.spec.ts +170 -0
  128. package/templates/component/index.ts +2 -2
  129. package/templates/dev-tools/_components/DevToolsLanding.tsx +8 -8
  130. package/templates/dev-tools/page.tsx +4 -3
  131. package/templates/mcp-servers.json +30 -2
  132. package/templates/page/page.e2e.test.ts +56 -48
  133. package/templates/page/page.tsx +3 -3
  134. package/templates/shared/HeroHeader.tsx +16 -15
  135. package/templates/shared/index.ts +1 -1
  136. package/templates/ui-showcase/_components/PreviewCard.tsx +20 -20
  137. package/templates/ui-showcase/_components/PreviewModal.tsx +149 -108
  138. package/templates/ui-showcase/_components/UIShowcase.tsx +43 -35
  139. package/templates/ui-showcase/page.tsx +4 -4
package/bin/cli.js CHANGED
@@ -1,19 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const fs = require('fs');
4
- const path = require('path');
5
- const { execSync } = require('child_process');
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { execSync } = require("child_process");
6
+ const readline = require("readline");
6
7
 
7
8
  /**
8
- * API Development Tools Installer
9
+ * API Development Tools Installer v1.0.0
9
10
  *
10
- * Installs slash commands AND enforcement hooks for interview-driven API development.
11
- *
12
- * Features:
13
- * - Slash commands in .claude/commands/
14
- * - Python hooks for programmatic enforcement
15
- * - Settings configuration for hook registration
16
- * - State file template for progress tracking
11
+ * Interactive CLI installer with:
12
+ * - ASCII art branding
13
+ * - Progress indicators
14
+ * - Comprehensive file installation
17
15
  *
18
16
  * Usage: npx @hustle-together/api-dev-tools --scope=project
19
17
  *
@@ -21,916 +19,549 @@ const { execSync } = require('child_process');
21
19
  * --with-storybook Auto-initialize Storybook for component development
22
20
  * --with-playwright Auto-initialize Playwright for E2E testing
23
21
  * --with-sandpack Auto-install Sandpack for live UI previews
22
+ * --silent Skip banner and reduce output
24
23
  */
25
24
 
26
- // Parse command-line arguments
27
- const args = process.argv.slice(2);
28
- const scope = args.find(arg => arg.startsWith('--scope='))?.split('=')[1] || 'project';
29
- const withStorybook = args.includes('--with-storybook');
30
- const withPlaywright = args.includes('--with-playwright');
31
- const withSandpack = args.includes('--with-sandpack');
32
-
33
- // Colors for terminal output
34
- const colors = {
35
- reset: '\x1b[0m',
36
- bright: '\x1b[1m',
37
- green: '\x1b[32m',
38
- blue: '\x1b[34m',
39
- yellow: '\x1b[33m',
40
- red: '\x1b[31m',
41
- cyan: '\x1b[36m',
25
+ // ═══════════════════════════════════════════════════════════════════════════
26
+ // ANSI Colors (red/black/white branding)
27
+ // ═══════════════════════════════════════════════════════════════════════════
28
+
29
+ const c = {
30
+ reset: "\x1b[0m",
31
+ bold: "\x1b[1m",
32
+ dim: "\x1b[2m",
33
+ red: "\x1b[31m",
34
+ white: "\x1b[37m",
35
+ gray: "\x1b[90m",
42
36
  };
43
37
 
44
- function log(message, color = 'reset') {
45
- console.log(`${colors[color]}${message}${colors.reset}`);
38
+ // ═══════════════════════════════════════════════════════════════════════════
39
+ // ASCII Art Banner
40
+ // ═══════════════════════════════════════════════════════════════════════════
41
+
42
+ const BANNER = `
43
+ ${c.red} ╔═══════════════════════════════════════════════════════════════╗
44
+ ║ ║
45
+ ║ ██╗ ██╗██╗ ██╗███████╗████████╗██╗ ███████╗ ║
46
+ ║ ██║ ██║██║ ██║██╔════╝╚══██╔══╝██║ ██╔════╝ ║
47
+ ║ ███████║██║ ██║███████╗ ██║ ██║ █████╗ ║
48
+ ║ ██╔══██║██║ ██║╚════██║ ██║ ██║ ██╔══╝ ║
49
+ ║ ██║ ██║╚██████╔╝███████║ ██║ ███████╗███████╗ ║
50
+ ║ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚══════╝╚══════╝ ║
51
+ ║ ║
52
+ ╚═══════════════════════════════════════════════════════════════╝${c.reset}
53
+
54
+ ${c.bold} API Development Tools for Claude Code${c.reset}
55
+ ${c.dim} Interview-driven, research-first API development${c.reset}
56
+ ${c.gray}v1.0.0${c.reset}
57
+ `;
58
+
59
+ // ═══════════════════════════════════════════════════════════════════════════
60
+ // Spinner Animation
61
+ // ═══════════════════════════════════════════════════════════════════════════
62
+
63
+ const spinnerFrames = ["◐", "◓", "◑", "◒"];
64
+ let spinnerIndex = 0;
65
+ let spinnerInterval = null;
66
+
67
+ function startSpinner(message) {
68
+ process.stdout.write(`\r${c.gray}${spinnerFrames[0]}${c.reset} ${message}`);
69
+ spinnerInterval = setInterval(() => {
70
+ spinnerIndex = (spinnerIndex + 1) % spinnerFrames.length;
71
+ process.stdout.write(
72
+ `\r${c.gray}${spinnerFrames[spinnerIndex]}${c.reset} ${message}`,
73
+ );
74
+ }, 100);
46
75
  }
47
76
 
48
- /**
49
- * Check if Python 3 is available
50
- */
51
- function checkPython() {
52
- const pythonCommands = ['python3', 'python'];
53
-
54
- for (const cmd of pythonCommands) {
55
- try {
56
- const version = execSync(`${cmd} --version 2>&1`, { encoding: 'utf8' }).trim();
57
- if (version.includes('Python 3')) {
58
- return { available: true, command: cmd, version };
59
- }
60
- } catch (e) {
61
- // Command not found, try next
62
- }
77
+ function stopSpinner(success, message) {
78
+ if (spinnerInterval) {
79
+ clearInterval(spinnerInterval);
80
+ spinnerInterval = null;
63
81
  }
82
+ const icon = success ? `${c.white}●${c.reset}` : `${c.red}○${c.reset}`;
83
+ process.stdout.write(`\r${icon} ${message}\n`);
84
+ }
85
+
86
+ // ═══════════════════════════════════════════════════════════════════════════
87
+ // Logging Utilities
88
+ // ═══════════════════════════════════════════════════════════════════════════
64
89
 
65
- return { available: false, command: null, version: null };
90
+ function log(message) {
91
+ console.log(message);
66
92
  }
67
93
 
68
- /**
69
- * Verify installation was successful
70
- */
71
- function verifyInstallation(claudeDir, hooksDir) {
72
- const checks = [
73
- { path: path.join(claudeDir, 'commands'), name: 'Commands directory' },
74
- { path: path.join(claudeDir, 'settings.json'), name: 'Settings file' },
75
- { path: path.join(claudeDir, 'api-dev-state.json'), name: 'State file' },
76
- { path: path.join(claudeDir, 'registry.json'), name: 'Registry file' },
77
- { path: path.join(claudeDir, 'performance-budgets.json'), name: 'Performance budgets' },
78
- { path: path.join(claudeDir, 'research'), name: 'Research cache directory' },
79
- { path: path.join(claudeDir, 'research', 'index.json'), name: 'Research index' },
80
- ];
94
+ function logStep(step, total, message) {
95
+ const progress = `${c.dim}[${step}/${total}]${c.reset}`;
96
+ console.log(`\n${progress} ${c.bold}${message}${c.reset}`);
97
+ }
81
98
 
82
- // Add hook checks if hooks directory exists (v3.0 has 18 hooks for 100% phase enforcement with user checkpoints)
83
- if (fs.existsSync(hooksDir)) {
84
- checks.push(
85
- // Core utility hooks (5)
86
- { path: path.join(hooksDir, 'session-startup.py'), name: 'session-startup.py' },
87
- { path: path.join(hooksDir, 'enforce-external-research.py'), name: 'enforce-external-research.py' },
88
- { path: path.join(hooksDir, 'track-tool-use.py'), name: 'track-tool-use.py' },
89
- { path: path.join(hooksDir, 'periodic-reground.py'), name: 'periodic-reground.py' },
90
- { path: path.join(hooksDir, 'api-workflow-check.py'), name: 'api-workflow-check.py' },
91
- // Phase enforcement hooks with user checkpoints (12 - one per phase)
92
- { path: path.join(hooksDir, 'enforce-disambiguation.py'), name: 'enforce-disambiguation.py' },
93
- { path: path.join(hooksDir, 'enforce-scope.py'), name: 'enforce-scope.py' },
94
- { path: path.join(hooksDir, 'enforce-research.py'), name: 'enforce-research.py' },
95
- { path: path.join(hooksDir, 'enforce-interview.py'), name: 'enforce-interview.py' },
96
- { path: path.join(hooksDir, 'enforce-deep-research.py'), name: 'enforce-deep-research.py' },
97
- { path: path.join(hooksDir, 'enforce-schema.py'), name: 'enforce-schema.py' },
98
- { path: path.join(hooksDir, 'enforce-environment.py'), name: 'enforce-environment.py' },
99
- { path: path.join(hooksDir, 'enforce-tdd-red.py'), name: 'enforce-tdd-red.py' },
100
- { path: path.join(hooksDir, 'verify-implementation.py'), name: 'verify-implementation.py' },
101
- { path: path.join(hooksDir, 'verify-after-green.py'), name: 'verify-after-green.py' },
102
- { path: path.join(hooksDir, 'enforce-verify.py'), name: 'enforce-verify.py' },
103
- { path: path.join(hooksDir, 'enforce-refactor.py'), name: 'enforce-refactor.py' },
104
- { path: path.join(hooksDir, 'enforce-documentation.py'), name: 'enforce-documentation.py' },
105
- { path: path.join(hooksDir, 'update-registry.py'), name: 'update-registry.py' }
106
- );
107
- }
99
+ function logSuccess(message) {
100
+ console.log(` ${c.white}●${c.reset} ${message}`);
101
+ }
108
102
 
109
- const failures = [];
110
- for (const check of checks) {
111
- if (!fs.existsSync(check.path)) {
112
- failures.push(check.name);
113
- }
114
- }
103
+ function logInfo(message) {
104
+ console.log(` ${c.gray}○${c.reset} ${message}`);
105
+ }
106
+
107
+ function logWarn(message) {
108
+ console.log(` ${c.red}◆${c.reset} ${message}`);
109
+ }
115
110
 
116
- return { success: failures.length === 0, failures };
111
+ function logError(message) {
112
+ console.log(` ${c.red}✗${c.reset} ${message}`);
117
113
  }
118
114
 
119
- function main() {
120
- log('\n🚀 Installing API Development Tools for Claude Code...\n', 'bright');
115
+ // ═══════════════════════════════════════════════════════════════════════════
116
+ // File Copy Utilities
117
+ // ═══════════════════════════════════════════════════════════════════════════
121
118
 
122
- // Check Python availability first
123
- const python = checkPython();
124
- if (!python.available) {
125
- log('⚠️ Warning: Python 3 not found', 'yellow');
126
- log(' Enforcement hooks require Python 3 to run.', 'yellow');
127
- log(' Hooks will be installed but may not work until Python 3 is available.', 'yellow');
128
- log(' Install Python 3: https://www.python.org/downloads/\n', 'yellow');
129
- } else {
130
- log(`✅ Found ${python.version}`, 'green');
131
- }
119
+ function copyDir(src, dest, options = {}) {
120
+ const { filter = () => true, overwrite = false } = options;
121
+ let copied = 0;
132
122
 
133
- // Get target directory (where user ran the command)
134
- const targetDir = process.cwd();
135
- const claudeDir = path.join(targetDir, '.claude');
136
- const commandsDir = path.join(claudeDir, 'commands');
137
- const hooksDir = path.join(claudeDir, 'hooks');
123
+ if (!fs.existsSync(src)) return copied;
138
124
 
139
- // Get source directories from this package
140
- const packageDir = path.dirname(__dirname);
141
- const sourceCommandsDir = path.join(packageDir, 'commands');
142
- const sourceHooksDir = path.join(packageDir, 'hooks');
143
- const sourceTemplatesDir = path.join(packageDir, 'templates');
144
-
145
- // Verify source commands exist
146
- if (!fs.existsSync(sourceCommandsDir)) {
147
- log('❌ Error: Commands directory not found in package', 'red');
148
- log(` Looking for: ${sourceCommandsDir}`, 'red');
149
- process.exit(1);
125
+ if (!fs.existsSync(dest)) {
126
+ fs.mkdirSync(dest, { recursive: true });
150
127
  }
151
128
 
152
- // ========================================
153
- // 1. Install Commands
154
- // ========================================
155
- if (!fs.existsSync(commandsDir)) {
156
- log(`📁 Creating directory: ${commandsDir}`, 'blue');
157
- fs.mkdirSync(commandsDir, { recursive: true });
158
- }
129
+ const entries = fs.readdirSync(src, { withFileTypes: true });
159
130
 
160
- const commandFiles = fs.readdirSync(sourceCommandsDir).filter(file =>
161
- file.endsWith('.md')
162
- );
131
+ for (const entry of entries) {
132
+ const srcPath = path.join(src, entry.name);
133
+ const destPath = path.join(dest, entry.name);
163
134
 
164
- if (commandFiles.length === 0) {
165
- log('⚠️ Warning: No command files found to install', 'yellow');
166
- } else {
167
- log('📦 Installing commands:', 'blue');
168
- commandFiles.forEach(file => {
169
- const source = path.join(sourceCommandsDir, file);
170
- const dest = path.join(commandsDir, file);
135
+ if (!filter(entry.name, srcPath)) continue;
171
136
 
172
- try {
173
- fs.copyFileSync(source, dest);
174
- log(` ✅ ${file}`, 'green');
175
- } catch (error) {
176
- log(` ❌ Failed to copy ${file}: ${error.message}`, 'red');
137
+ if (entry.isDirectory()) {
138
+ copied += copyDir(srcPath, destPath, options);
139
+ } else {
140
+ if (!fs.existsSync(destPath) || overwrite) {
141
+ fs.copyFileSync(srcPath, destPath);
142
+ copied++;
177
143
  }
178
- });
144
+ }
179
145
  }
180
146
 
181
- // ========================================
182
- // 2. Install Hooks (Programmatic Enforcement)
183
- // ========================================
184
- if (fs.existsSync(sourceHooksDir)) {
185
- if (!fs.existsSync(hooksDir)) {
186
- log(`\n📁 Creating directory: ${hooksDir}`, 'blue');
187
- fs.mkdirSync(hooksDir, { recursive: true });
188
- }
147
+ return copied;
148
+ }
189
149
 
190
- const hookFiles = fs.readdirSync(sourceHooksDir).filter(file =>
191
- file.endsWith('.py')
192
- );
150
+ function copyFile(src, dest, options = {}) {
151
+ const { overwrite = false, executable = false } = options;
193
152
 
194
- if (hookFiles.length > 0) {
195
- log('\n🔒 Installing enforcement hooks:', 'cyan');
196
- hookFiles.forEach(file => {
197
- const source = path.join(sourceHooksDir, file);
198
- const dest = path.join(hooksDir, file);
199
-
200
- try {
201
- fs.copyFileSync(source, dest);
202
- // Make executable
203
- fs.chmodSync(dest, '755');
204
- log(` ✅ ${file}`, 'green');
205
- } catch (error) {
206
- log(` ❌ Failed to copy ${file}: ${error.message}`, 'red');
207
- }
208
- });
153
+ if (!fs.existsSync(src)) return false;
209
154
 
210
- log('\n Hook purposes:', 'blue');
211
- log(' • enforce-research.py - Blocks code writing without research', 'blue');
212
- log(' • track-tool-use.py - Logs all research activity', 'blue');
213
- log(' • api-workflow-check.py - Prevents stopping until complete', 'blue');
214
- }
155
+ const destDir = path.dirname(dest);
156
+ if (!fs.existsSync(destDir)) {
157
+ fs.mkdirSync(destDir, { recursive: true });
215
158
  }
216
159
 
217
- // ========================================
218
- // 3. Install/Merge Settings (Hook Registration)
219
- // ========================================
220
- const settingsSource = path.join(sourceTemplatesDir, 'settings.json');
221
- const settingsDest = path.join(claudeDir, 'settings.json');
222
-
223
- if (fs.existsSync(settingsSource)) {
224
- log('\n⚙️ Configuring hook settings:', 'cyan');
225
-
226
- try {
227
- const newSettings = JSON.parse(fs.readFileSync(settingsSource, 'utf8'));
228
-
229
- if (fs.existsSync(settingsDest)) {
230
- // Merge with existing settings
231
- const existingSettings = JSON.parse(fs.readFileSync(settingsDest, 'utf8'));
232
- const mergedSettings = mergeSettings(existingSettings, newSettings);
233
- fs.writeFileSync(settingsDest, JSON.stringify(mergedSettings, null, 2));
234
- log(' ✅ Merged with existing settings.json', 'green');
235
- } else {
236
- // Create new settings file
237
- fs.writeFileSync(settingsDest, JSON.stringify(newSettings, null, 2));
238
- log(' ✅ Created settings.json with hook configuration', 'green');
239
- }
240
- } catch (error) {
241
- log(` ❌ Failed to configure settings: ${error.message}`, 'red');
160
+ if (!fs.existsSync(dest) || overwrite) {
161
+ fs.copyFileSync(src, dest);
162
+ if (executable) {
163
+ fs.chmodSync(dest, "755");
242
164
  }
165
+ return true;
243
166
  }
167
+ return false;
168
+ }
244
169
 
245
- // ========================================
246
- // 4. Install State File Template
247
- // ========================================
248
- const stateSource = path.join(sourceTemplatesDir, 'api-dev-state.json');
249
- const stateDest = path.join(claudeDir, 'api-dev-state.json');
250
-
251
- if (fs.existsSync(stateSource)) {
252
- log('\n📊 Setting up state tracking:', 'cyan');
170
+ // ═══════════════════════════════════════════════════════════════════════════
171
+ // Check Prerequisites
172
+ // ═══════════════════════════════════════════════════════════════════════════
253
173
 
254
- if (!fs.existsSync(stateDest)) {
255
- try {
256
- fs.copyFileSync(stateSource, stateDest);
257
- log(' ✅ Created api-dev-state.json template', 'green');
258
- } catch (error) {
259
- log(` ❌ Failed to create state file: ${error.message}`, 'red');
174
+ function checkPython() {
175
+ const commands = ["python3", "python"];
176
+ for (const cmd of commands) {
177
+ try {
178
+ const version = execSync(`${cmd} --version 2>&1`, {
179
+ encoding: "utf8",
180
+ }).trim();
181
+ if (version.includes("Python 3")) {
182
+ return { ok: true, cmd, version };
260
183
  }
261
- } else {
262
- log(' ℹ️ State file already exists (preserved)', 'blue');
184
+ } catch (e) {
185
+ // Continue to next command
263
186
  }
264
187
  }
188
+ return { ok: false };
189
+ }
265
190
 
266
- // ========================================
267
- // 4b. Install Registry (v3.8.0)
268
- // ========================================
269
- const registrySource = path.join(sourceTemplatesDir, 'registry.json');
270
- const registryDest = path.join(claudeDir, 'registry.json');
271
-
272
- if (fs.existsSync(registrySource)) {
273
- log('\n📋 Setting up central registry:', 'cyan');
191
+ function checkNode() {
192
+ try {
193
+ const version = process.version;
194
+ const major = parseInt(version.slice(1).split(".")[0], 10);
195
+ return { ok: major >= 18, version };
196
+ } catch (e) {
197
+ return { ok: false };
198
+ }
199
+ }
274
200
 
275
- if (!fs.existsSync(registryDest)) {
276
- try {
277
- fs.copyFileSync(registrySource, registryDest);
278
- log(' ✅ Created registry.json (tracks APIs, components, pages)', 'green');
279
- } catch (error) {
280
- log(` ❌ Failed to create registry: ${error.message}`, 'red');
281
- }
282
- } else {
283
- log(' ℹ️ Registry already exists (preserved)', 'blue');
284
- }
201
+ // ═══════════════════════════════════════════════════════════════════════════
202
+ // Main Installation
203
+ // ═══════════════════════════════════════════════════════════════════════════
204
+
205
+ async function main() {
206
+ // Parse arguments
207
+ const args = process.argv.slice(2);
208
+ const withStorybook = args.includes("--with-storybook");
209
+ const withPlaywright = args.includes("--with-playwright");
210
+ const withSandpack = args.includes("--with-sandpack");
211
+ const silent = args.includes("--silent") || args.includes("-s");
212
+
213
+ // Show banner
214
+ if (!silent) {
215
+ console.clear();
216
+ log(BANNER);
285
217
  }
286
218
 
287
- // ========================================
288
- // 4c. Install Brand Guide Template (v3.9.0)
289
- // ========================================
290
- const brandGuideSource = path.join(sourceTemplatesDir, 'BRAND_GUIDE.md');
291
- const brandGuideDest = path.join(claudeDir, 'BRAND_GUIDE.md');
219
+ // Directory setup
220
+ const targetDir = process.cwd();
221
+ const packageDir = path.dirname(__dirname);
222
+ const claudeDir = path.join(targetDir, ".claude");
223
+ const hooksDir = path.join(targetDir, "hooks");
292
224
 
293
- if (fs.existsSync(brandGuideSource)) {
294
- log('\n🎨 Setting up brand guide:', 'cyan');
225
+ const totalSteps = 8;
226
+ let currentStep = 0;
295
227
 
296
- if (!fs.existsSync(brandGuideDest)) {
297
- try {
298
- fs.copyFileSync(brandGuideSource, brandGuideDest);
299
- log(' ✅ Created BRAND_GUIDE.md (customize for your project branding)', 'green');
300
- } catch (error) {
301
- log(` ❌ Failed to create brand guide: ${error.message}`, 'red');
302
- }
303
- } else {
304
- log(' ℹ️ Brand guide already exists (preserved)', 'blue');
305
- }
306
- }
228
+ // ─────────────────────────────────────────────────────────────────────────
229
+ // Step 1: Check Prerequisites
230
+ // ─────────────────────────────────────────────────────────────────────────
307
231
 
308
- // ========================================
309
- // 4d. Install Performance Budgets (v3.9.0)
310
- // ========================================
311
- const perfBudgetsSource = path.join(sourceTemplatesDir, 'performance-budgets.json');
312
- const perfBudgetsDest = path.join(claudeDir, 'performance-budgets.json');
232
+ logStep(++currentStep, totalSteps, "Checking prerequisites");
313
233
 
314
- if (fs.existsSync(perfBudgetsSource)) {
315
- log('\n📊 Setting up performance budgets:', 'cyan');
234
+ const node = checkNode();
235
+ if (node.ok) {
236
+ logSuccess(`Node.js ${node.version}`);
237
+ } else {
238
+ logError(`Node.js 18+ required (found ${node.version || "none"})`);
239
+ process.exit(1);
240
+ }
316
241
 
317
- if (!fs.existsSync(perfBudgetsDest)) {
318
- try {
319
- fs.copyFileSync(perfBudgetsSource, perfBudgetsDest);
320
- log(' ✅ Created performance-budgets.json (thresholds for TDD gates)', 'green');
321
- } catch (error) {
322
- log(` ❌ Failed to create performance budgets: ${error.message}`, 'red');
323
- }
324
- } else {
325
- log(' ℹ️ Performance budgets already exist (preserved)', 'blue');
326
- }
242
+ const python = checkPython();
243
+ if (python.ok) {
244
+ logSuccess(`${python.version}`);
245
+ } else {
246
+ logWarn("Python 3 not found - hooks may not work");
247
+ logInfo("Install: https://www.python.org/downloads/");
327
248
  }
328
249
 
329
- // ========================================
330
- // 4e. Install Research Cache Structure (v3.0)
331
- // ========================================
332
- const researchDir = path.join(claudeDir, 'research');
333
- const researchIndexSource = path.join(sourceTemplatesDir, 'research-index.json');
334
- const researchIndexDest = path.join(researchDir, 'index.json');
250
+ // ─────────────────────────────────────────────────────────────────────────
251
+ // Step 2: Install Commands
252
+ // ─────────────────────────────────────────────────────────────────────────
335
253
 
336
- log('\n📚 Setting up research cache:', 'cyan');
254
+ logStep(++currentStep, totalSteps, "Installing slash commands");
337
255
 
338
- if (!fs.existsSync(researchDir)) {
339
- try {
340
- fs.mkdirSync(researchDir, { recursive: true });
341
- log(' ✅ Created .claude/research/ directory', 'green');
342
- } catch (error) {
343
- log(` ❌ Failed to create research directory: ${error.message}`, 'red');
344
- }
345
- }
256
+ const commandsDir = path.join(claudeDir, "commands");
257
+ const sourceCommandsDir = path.join(packageDir, "commands");
346
258
 
347
- if (fs.existsSync(researchIndexSource) && !fs.existsSync(researchIndexDest)) {
348
- try {
349
- fs.copyFileSync(researchIndexSource, researchIndexDest);
350
- log(' ✅ Created research/index.json for freshness tracking', 'green');
351
- } catch (error) {
352
- log(` ❌ Failed to create research index: ${error.message}`, 'red');
353
- }
354
- } else if (fs.existsSync(researchIndexDest)) {
355
- log(' ℹ️ Research index already exists (preserved)', 'blue');
259
+ if (!fs.existsSync(commandsDir)) {
260
+ fs.mkdirSync(commandsDir, { recursive: true });
356
261
  }
357
262
 
358
- // ========================================
359
- // 4c. Install Test UI (Parser API + Page)
360
- // ========================================
361
- log('\n🧪 Setting up Test UI:', 'cyan');
362
-
363
- const testUiSourceDir = path.join(sourceTemplatesDir, 'api-test');
364
- const hasNextJs = fs.existsSync(path.join(targetDir, 'next.config.js')) ||
365
- fs.existsSync(path.join(targetDir, 'next.config.mjs')) ||
366
- fs.existsSync(path.join(targetDir, 'next.config.ts'));
367
-
368
- // Detect App Router structure (used by Test UI and Showcase Pages)
369
- const appDir = fs.existsSync(path.join(targetDir, 'src', 'app'))
370
- ? path.join(targetDir, 'src', 'app')
371
- : fs.existsSync(path.join(targetDir, 'app'))
372
- ? path.join(targetDir, 'app')
373
- : null;
374
-
375
- if (!hasNextJs) {
376
- log(' ⚠️ Next.js not detected - skipping Test UI installation', 'yellow');
377
- log(' 💡 Test UI requires Next.js App Router', 'yellow');
378
- } else if (fs.existsSync(testUiSourceDir)) {
379
-
380
- if (!appDir) {
381
- log(' ⚠️ App Router not detected - skipping Test UI installation', 'yellow');
382
- log(' 💡 Test UI requires Next.js App Router (app/ or src/app/)', 'yellow');
383
- } else {
384
- // Install test-structure API route
385
- const apiTestStructureDir = path.join(appDir, 'api', 'test-structure');
386
- const apiTestStructureSource = path.join(testUiSourceDir, 'test-structure', 'route.ts');
387
- const apiTestStructureDest = path.join(apiTestStructureDir, 'route.ts');
263
+ const commandsCopied = copyDir(sourceCommandsDir, commandsDir, {
264
+ filter: (name) => name.endsWith(".md"),
265
+ });
388
266
 
389
- if (!fs.existsSync(apiTestStructureDir)) {
390
- fs.mkdirSync(apiTestStructureDir, { recursive: true });
391
- }
267
+ logSuccess(`${commandsCopied} commands installed to .claude/commands/`);
392
268
 
393
- if (!fs.existsSync(apiTestStructureDest)) {
394
- try {
395
- fs.copyFileSync(apiTestStructureSource, apiTestStructureDest);
396
- log(' ✅ Created /api/test-structure route (parses Vitest files)', 'green');
397
- } catch (error) {
398
- log(` ❌ Failed to create test-structure API: ${error.message}`, 'red');
399
- }
400
- } else {
401
- log(' ℹ️ /api/test-structure already exists (preserved)', 'blue');
402
- }
403
-
404
- // Install test UI page
405
- const apiTestPageDir = path.join(appDir, 'api-test');
406
- const apiTestPageSource = path.join(testUiSourceDir, 'page.tsx');
407
- const apiTestPageDest = path.join(apiTestPageDir, 'page.tsx');
269
+ // ─────────────────────────────────────────────────────────────────────────
270
+ // Step 3: Install Hooks
271
+ // ─────────────────────────────────────────────────────────────────────────
408
272
 
409
- if (!fs.existsSync(apiTestPageDir)) {
410
- fs.mkdirSync(apiTestPageDir, { recursive: true });
411
- }
273
+ logStep(++currentStep, totalSteps, "Installing enforcement hooks");
412
274
 
413
- if (!fs.existsSync(apiTestPageDest)) {
414
- try {
415
- fs.copyFileSync(apiTestPageSource, apiTestPageDest);
416
- log(' ✅ Created /api-test page (displays test structure)', 'green');
417
- } catch (error) {
418
- log(` ❌ Failed to create test UI page: ${error.message}`, 'red');
419
- }
420
- } else {
421
- log(' ℹ️ /api-test page already exists (preserved)', 'blue');
422
- }
275
+ const sourceHooksDir = path.join(packageDir, "hooks");
423
276
 
424
- log(' 💡 Test UI available at http://localhost:3000/api-test', 'yellow');
425
- }
426
- } else {
427
- log(' ⚠️ Test UI templates not found in package', 'yellow');
277
+ if (!fs.existsSync(hooksDir)) {
278
+ fs.mkdirSync(hooksDir, { recursive: true });
428
279
  }
429
280
 
430
- // ========================================
431
- // 4f. Install Showcase Pages (v3.9.2)
432
- // ========================================
433
- log('\n🎨 Setting up showcase pages:', 'cyan');
434
-
435
- if (hasNextJs && appDir) {
436
- const showcaseTemplates = [
437
- // Shared components first (required by other pages)
438
- {
439
- source: 'shared',
440
- dest: path.join(appDir, 'shared'),
441
- files: ['HeroHeader.tsx', 'index.ts'],
442
- name: 'Shared components (HeroHeader)'
443
- },
444
- // API Showcase
445
- {
446
- source: 'api-showcase',
447
- dest: path.join(appDir, 'api-showcase'),
448
- files: ['page.tsx'],
449
- componentsDir: '_components',
450
- componentFiles: ['APIShowcase.tsx', 'APICard.tsx', 'APIModal.tsx', 'APITester.tsx'],
451
- name: 'API Showcase'
452
- },
453
- // UI Showcase
454
- {
455
- source: 'ui-showcase',
456
- dest: path.join(appDir, 'ui-showcase'),
457
- files: ['page.tsx'],
458
- componentsDir: '_components',
459
- componentFiles: ['UIShowcase.tsx', 'PreviewCard.tsx', 'PreviewModal.tsx'],
460
- name: 'UI Showcase'
461
- },
462
- // Dev Tools Landing
463
- {
464
- source: 'dev-tools',
465
- dest: path.join(appDir, 'dev-tools'),
466
- files: ['page.tsx'],
467
- componentsDir: '_components',
468
- componentFiles: ['DevToolsLanding.tsx'],
469
- name: 'Dev Tools Landing'
470
- }
471
- ];
472
-
473
- for (const template of showcaseTemplates) {
474
- const sourceDir = path.join(sourceTemplatesDir, template.source);
281
+ // Copy hooks lib directory first
282
+ const sourceHooksLibDir = path.join(sourceHooksDir, "lib");
283
+ const destHooksLibDir = path.join(hooksDir, "lib");
475
284
 
476
- if (!fs.existsSync(sourceDir)) {
477
- log(` ⚠️ ${template.name} template not found`, 'yellow');
478
- continue;
479
- }
285
+ if (fs.existsSync(sourceHooksLibDir)) {
286
+ if (!fs.existsSync(destHooksLibDir)) {
287
+ fs.mkdirSync(destHooksLibDir, { recursive: true });
288
+ }
289
+ copyDir(sourceHooksLibDir, destHooksLibDir);
290
+ }
480
291
 
481
- // Create destination directory
482
- if (!fs.existsSync(template.dest)) {
483
- fs.mkdirSync(template.dest, { recursive: true });
292
+ // Copy Python hook files
293
+ let hooksCopied = 0;
294
+ if (fs.existsSync(sourceHooksDir)) {
295
+ const hookFiles = fs
296
+ .readdirSync(sourceHooksDir)
297
+ .filter((f) => f.endsWith(".py"));
298
+
299
+ for (const file of hookFiles) {
300
+ const src = path.join(sourceHooksDir, file);
301
+ const dest = path.join(hooksDir, file);
302
+
303
+ if (!fs.existsSync(dest)) {
304
+ fs.copyFileSync(src, dest);
305
+ fs.chmodSync(dest, "755");
306
+ hooksCopied++;
484
307
  }
308
+ }
309
+ }
485
310
 
486
- let installedFiles = [];
311
+ logSuccess(`${hooksCopied} hooks installed to hooks/`);
312
+ logInfo("Includes: enforce-*, notify-*, track-token-usage.py");
487
313
 
488
- // Copy main files (page.tsx, etc.)
489
- for (const file of template.files) {
490
- const srcFile = path.join(sourceDir, file);
491
- const destFile = path.join(template.dest, file);
314
+ // ─────────────────────────────────────────────────────────────────────────
315
+ // Step 4: Install Subagents
316
+ // ─────────────────────────────────────────────────────────────────────────
492
317
 
493
- if (fs.existsSync(srcFile) && !fs.existsSync(destFile)) {
494
- try {
495
- fs.copyFileSync(srcFile, destFile);
496
- installedFiles.push(file);
497
- } catch (error) {
498
- log(` ❌ Failed to copy ${file}: ${error.message}`, 'red');
499
- }
500
- }
501
- }
318
+ logStep(++currentStep, totalSteps, "Installing subagents");
502
319
 
503
- // Copy _components directory if exists
504
- if (template.componentsDir && template.componentFiles) {
505
- const srcComponentsDir = path.join(sourceDir, template.componentsDir);
506
- const destComponentsDir = path.join(template.dest, template.componentsDir);
507
-
508
- if (fs.existsSync(srcComponentsDir)) {
509
- if (!fs.existsSync(destComponentsDir)) {
510
- fs.mkdirSync(destComponentsDir, { recursive: true });
511
- }
512
-
513
- for (const file of template.componentFiles) {
514
- const srcFile = path.join(srcComponentsDir, file);
515
- const destFile = path.join(destComponentsDir, file);
516
-
517
- if (fs.existsSync(srcFile) && !fs.existsSync(destFile)) {
518
- try {
519
- fs.copyFileSync(srcFile, destFile);
520
- installedFiles.push(`_components/${file}`);
521
- } catch (error) {
522
- log(` ❌ Failed to copy ${file}: ${error.message}`, 'red');
523
- }
524
- }
525
- }
526
- }
527
- }
320
+ const agentsDir = path.join(claudeDir, "agents");
321
+ const sourceAgentsDir = path.join(packageDir, ".claude", "agents");
528
322
 
529
- if (installedFiles.length > 0) {
530
- log(` ✅ ${template.name} (${installedFiles.length} files)`, 'green');
531
- } else {
532
- log(` ℹ️ ${template.name} already exists (preserved)`, 'blue');
533
- }
534
- }
535
-
536
- log('\n 💡 Showcase pages available at:', 'yellow');
537
- log(' /dev-tools - Landing page with all dev tools', 'yellow');
538
- log(' /api-showcase - Interactive API testing', 'yellow');
539
- log(' /ui-showcase - Live component previews', 'yellow');
540
- } else {
541
- log(' ⚠️ Next.js App Router not detected - skipping showcase pages', 'yellow');
323
+ if (!fs.existsSync(agentsDir)) {
324
+ fs.mkdirSync(agentsDir, { recursive: true });
542
325
  }
543
326
 
544
- // ========================================
545
- // 4g. Install Component/Page Templates (v3.9.0)
546
- // ========================================
547
- log('\n📦 Setting up component/page templates:', 'cyan');
327
+ const agentsCopied = copyDir(sourceAgentsDir, agentsDir, {
328
+ filter: (name) => name.endsWith(".md"),
329
+ });
548
330
 
549
- const componentPageTemplates = [
550
- {
551
- source: 'component',
552
- dest: path.join(claudeDir, 'templates', 'component'),
553
- name: 'Component templates'
554
- },
555
- {
556
- source: 'page',
557
- dest: path.join(claudeDir, 'templates', 'page'),
558
- name: 'Page templates'
559
- }
560
- ];
331
+ logSuccess(`${agentsCopied} subagents installed to .claude/agents/`);
332
+ logInfo("Haiku: parallel-researcher, research-validator, docs-generator");
333
+ logInfo(
334
+ "Sonnet: schema-generator, test-writer, implementation-reviewer, code-reviewer",
335
+ );
561
336
 
562
- for (const template of componentPageTemplates) {
563
- const sourceDir = path.join(sourceTemplatesDir, template.source);
337
+ // ─────────────────────────────────────────────────────────────────────────
338
+ // Step 5: Install Configuration Files
339
+ // ─────────────────────────────────────────────────────────────────────────
564
340
 
565
- if (!fs.existsSync(sourceDir)) {
566
- log(` ⚠️ ${template.name} not found`, 'yellow');
567
- continue;
568
- }
569
-
570
- if (!fs.existsSync(template.dest)) {
571
- fs.mkdirSync(template.dest, { recursive: true });
572
- }
341
+ logStep(++currentStep, totalSteps, "Setting up configuration");
573
342
 
574
- const files = fs.readdirSync(sourceDir);
575
- let copiedCount = 0;
343
+ const sourceTemplatesDir = path.join(packageDir, "templates");
576
344
 
577
- for (const file of files) {
578
- const srcFile = path.join(sourceDir, file);
579
- const destFile = path.join(template.dest, file);
345
+ // Settings
346
+ const settingsSource = path.join(sourceTemplatesDir, "settings.json");
347
+ const settingsDest = path.join(claudeDir, "settings.json");
580
348
 
581
- if (fs.statSync(srcFile).isFile() && !fs.existsSync(destFile)) {
582
- try {
583
- fs.copyFileSync(srcFile, destFile);
584
- copiedCount++;
585
- } catch (error) {
586
- log(` ❌ Failed to copy ${file}: ${error.message}`, 'red');
587
- }
349
+ if (fs.existsSync(settingsSource)) {
350
+ if (fs.existsSync(settingsDest)) {
351
+ // Merge settings
352
+ try {
353
+ const existing = JSON.parse(fs.readFileSync(settingsDest, "utf8"));
354
+ const newSettings = JSON.parse(fs.readFileSync(settingsSource, "utf8"));
355
+ const merged = mergeSettings(existing, newSettings);
356
+ fs.writeFileSync(settingsDest, JSON.stringify(merged, null, 2));
357
+ logSuccess("Merged settings.json");
358
+ } catch (e) {
359
+ logWarn("Could not merge settings.json");
588
360
  }
589
- }
590
-
591
- if (copiedCount > 0) {
592
- log(` ✅ ${template.name} (${copiedCount} files)`, 'green');
593
361
  } else {
594
- log(` ℹ️ ${template.name} already exists (preserved)`, 'blue');
362
+ copyFile(settingsSource, settingsDest);
363
+ logSuccess("Created settings.json");
595
364
  }
596
365
  }
597
366
 
598
- // ========================================
599
- // 4h. Install Manifest Generation Scripts
600
- // ========================================
601
- log('\n📊 Setting up manifest generation scripts:', 'cyan');
367
+ // State file
368
+ const stateSource = path.join(sourceTemplatesDir, "api-dev-state.json");
369
+ const stateDest = path.join(claudeDir, "api-dev-state.json");
602
370
 
603
- const sourceScriptsDir = path.join(packageDir, 'scripts');
604
- const targetScriptsDir = path.join(targetDir, 'scripts', 'api-dev-tools');
371
+ if (!fs.existsSync(stateDest) && fs.existsSync(stateSource)) {
372
+ copyFile(stateSource, stateDest);
373
+ logSuccess("Created api-dev-state.json");
374
+ } else if (fs.existsSync(stateDest)) {
375
+ logInfo("State file preserved");
376
+ }
605
377
 
606
- if (fs.existsSync(sourceScriptsDir)) {
607
- if (!fs.existsSync(targetScriptsDir)) {
608
- fs.mkdirSync(targetScriptsDir, { recursive: true });
378
+ // Registry
379
+ const registrySource = path.join(sourceTemplatesDir, "registry.json");
380
+ const registryDest = path.join(claudeDir, "registry.json");
381
+
382
+ if (!fs.existsSync(registryDest) && fs.existsSync(registrySource)) {
383
+ copyFile(registrySource, registryDest);
384
+ logSuccess("Created registry.json");
385
+ }
386
+
387
+ // Research cache
388
+ const researchDir = path.join(claudeDir, "research");
389
+ if (!fs.existsSync(researchDir)) {
390
+ fs.mkdirSync(researchDir, { recursive: true });
391
+ const indexSource = path.join(sourceTemplatesDir, "research-index.json");
392
+ if (fs.existsSync(indexSource)) {
393
+ copyFile(indexSource, path.join(researchDir, "index.json"));
609
394
  }
395
+ logSuccess("Created research cache");
396
+ }
610
397
 
611
- const scriptFiles = fs.readdirSync(sourceScriptsDir).filter(file =>
612
- file.endsWith('.ts')
613
- );
398
+ // ─────────────────────────────────────────────────────────────────────────
399
+ // Step 6: Install Environment Template
400
+ // ─────────────────────────────────────────────────────────────────────────
614
401
 
615
- if (scriptFiles.length > 0) {
616
- scriptFiles.forEach(file => {
617
- const source = path.join(sourceScriptsDir, file);
618
- const dest = path.join(targetScriptsDir, file);
402
+ logStep(++currentStep, totalSteps, "Setting up environment");
619
403
 
620
- try {
621
- fs.copyFileSync(source, dest);
622
- log(` ✅ ${file}`, 'green');
623
- } catch (error) {
624
- log(` ❌ Failed to copy ${file}: ${error.message}`, 'red');
625
- }
626
- });
404
+ const templatesDestDir = path.join(targetDir, "templates");
405
+ if (!fs.existsSync(templatesDestDir)) {
406
+ fs.mkdirSync(templatesDestDir, { recursive: true });
407
+ }
627
408
 
628
- log('\n Script purposes:', 'blue');
629
- log(' • generate-test-manifest.ts - Parses tests → manifest (NO LLM)', 'blue');
630
- log(' • extract-parameters.ts - Extracts Zod params → matrix', 'blue');
631
- log(' • collect-test-results.ts - Runs Vitest → results JSON', 'blue');
632
- log('\n 💡 Scripts run automatically after tests pass (Phase 8 → 9)', 'yellow');
633
- log(' 💡 Manual: npx tsx scripts/api-dev-tools/generate-test-manifest.ts', 'yellow');
634
- }
635
- } else {
636
- log(' ⚠️ Scripts directory not found in package', 'yellow');
409
+ const envSource = path.join(sourceTemplatesDir, ".env.example");
410
+ const envDest = path.join(templatesDestDir, ".env.example");
411
+
412
+ if (fs.existsSync(envSource)) {
413
+ copyFile(envSource, envDest, { overwrite: true });
414
+ logSuccess("Created templates/.env.example");
415
+ logInfo("Copy to .env and configure your API keys");
637
416
  }
638
417
 
639
- // ========================================
640
- // 5. Install MCP Servers via CLI (Context7, GitHub)
641
- // ========================================
642
- // NOTE: We use `claude mcp add` directly because .mcp.json requires manual approval
643
- // and doesn't auto-load. Using the CLI ensures servers are immediately available.
644
- log('\n🔌 Configuring MCP servers:', 'cyan');
418
+ // ─────────────────────────────────────────────────────────────────────────
419
+ // Step 7: Configure MCP Servers
420
+ // ─────────────────────────────────────────────────────────────────────────
645
421
 
646
- const { execSync } = require('child_process');
422
+ logStep(++currentStep, totalSteps, "Configuring MCP servers");
647
423
 
648
424
  const mcpServers = [
649
- { name: 'context7', command: 'npx -y @upstash/context7-mcp', description: 'Live documentation from library source code' },
650
- { name: 'github', command: 'npx -y @modelcontextprotocol/server-github', description: 'GitHub issues, PRs, and repository access' }
425
+ { name: "context7", cmd: "npx -y @upstash/context7-mcp" },
426
+ { name: "github", cmd: "npx -y @modelcontextprotocol/server-github" },
427
+ {
428
+ name: "greptile",
429
+ cmd: "npx -y @anthropics/mcp-greptile",
430
+ optional: true,
431
+ },
651
432
  ];
652
433
 
653
434
  for (const server of mcpServers) {
654
435
  try {
655
- // Check if server already exists
656
- const checkResult = execSync(`claude mcp get ${server.name} 2>&1`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
657
- if (checkResult.includes('Connected') || checkResult.includes('Scope:')) {
658
- log(` ✓ ${server.name} - already configured`, 'blue');
659
- }
660
- } catch (checkError) {
661
- // Server doesn't exist, add it
436
+ execSync(`claude mcp get ${server.name} 2>&1`, {
437
+ encoding: "utf8",
438
+ stdio: ["pipe", "pipe", "pipe"],
439
+ });
440
+ logInfo(`${server.name} already configured`);
441
+ } catch (e) {
662
442
  try {
663
- execSync(`claude mcp add ${server.name} -- ${server.command}`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
664
- log(` ✅ ${server.name} - ${server.description}`, 'green');
665
- } catch (addError) {
666
- log(` ⚠️ ${server.name} - Could not add (run manually: claude mcp add ${server.name} -- ${server.command})`, 'yellow');
443
+ execSync(`claude mcp add ${server.name} -- ${server.cmd}`, {
444
+ encoding: "utf8",
445
+ stdio: ["pipe", "pipe", "pipe"],
446
+ });
447
+ logSuccess(`Added ${server.name}`);
448
+ } catch (addErr) {
449
+ if (server.optional) {
450
+ logInfo(
451
+ `${server.name} (optional) - configure with GREPTILE_API_KEY`,
452
+ );
453
+ } else {
454
+ logWarn(`Could not add ${server.name} - add manually`);
455
+ }
667
456
  }
668
457
  }
669
458
  }
670
459
 
671
- log('\n ⚠️ GitHub MCP requires GITHUB_PERSONAL_ACCESS_TOKEN in env', 'yellow');
672
- log(' 💡 Restart Claude Code for MCP tools to be available', 'yellow');
673
-
674
- // ========================================
675
- // 6. Update CLAUDE.md with workflow documentation
676
- // ========================================
677
- const claudeMdSection = path.join(sourceTemplatesDir, 'CLAUDE-SECTION.md');
678
- const projectClaudeMd = path.join(targetDir, 'CLAUDE.md');
679
-
680
- if (fs.existsSync(claudeMdSection)) {
681
- log('\n📝 CLAUDE.md workflow documentation:', 'cyan');
682
-
683
- const sectionContent = fs.readFileSync(claudeMdSection, 'utf8');
684
- const sectionMarker = '## API Development Workflow (v3.0)';
685
-
686
- if (fs.existsSync(projectClaudeMd)) {
687
- const existingContent = fs.readFileSync(projectClaudeMd, 'utf8');
688
-
689
- if (existingContent.includes(sectionMarker)) {
690
- // Update existing section
691
- const beforeSection = existingContent.split(sectionMarker)[0];
692
- // Find the next ## heading or end of file
693
- const afterMatch = existingContent.match(/## API Development Workflow[\s\S]*?((?=\n## )|$)/);
694
- const afterSection = afterMatch ? existingContent.substring(existingContent.indexOf(afterMatch[0]) + afterMatch[0].length) : '';
695
-
696
- fs.writeFileSync(projectClaudeMd, beforeSection + sectionContent + afterSection);
697
- log(' ✅ Updated API Development Workflow section in CLAUDE.md', 'green');
698
- } else {
699
- // Append section
700
- fs.appendFileSync(projectClaudeMd, '\n\n' + sectionContent);
701
- log(' ✅ Added API Development Workflow section to CLAUDE.md', 'green');
702
- }
703
- } else {
704
- // Create new CLAUDE.md with section
705
- fs.writeFileSync(projectClaudeMd, '# Project Instructions\n\n' + sectionContent);
706
- log(' ✅ Created CLAUDE.md with API Development Workflow section', 'green');
707
- }
708
- }
460
+ logInfo("Greptile requires GREPTILE_API_KEY + GITHUB_TOKEN for Phase 14");
709
461
 
710
- // ========================================
711
- // 7. Install Optional Development Tools
712
- // ========================================
713
- if (withStorybook || withPlaywright || withSandpack) {
714
- log('\n🔧 Installing optional development tools:', 'cyan');
462
+ // ─────────────────────────────────────────────────────────────────────────
463
+ // Step 8: Optional Tools
464
+ // ─────────────────────────────────────────────────────────────────────────
465
+
466
+ logStep(++currentStep, totalSteps, "Optional tools");
715
467
 
468
+ if (withStorybook || withPlaywright || withSandpack) {
716
469
  if (withSandpack) {
470
+ startSpinner("Installing Sandpack...");
717
471
  try {
718
- log(' 📦 Installing Sandpack for live component previews...', 'blue');
719
- execSync('pnpm add @codesandbox/sandpack-react 2>&1', { cwd: targetDir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
720
- log(' ✅ Sandpack installed successfully', 'green');
721
- } catch (error) {
722
- // Try npm if pnpm fails
723
- try {
724
- execSync('npm install @codesandbox/sandpack-react 2>&1', { cwd: targetDir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
725
- log(' ✅ Sandpack installed successfully (via npm)', 'green');
726
- } catch (npmError) {
727
- log(' ⚠️ Could not install Sandpack automatically. Run manually:', 'yellow');
728
- log(' pnpm add @codesandbox/sandpack-react', 'yellow');
729
- }
472
+ execSync("npm install @codesandbox/sandpack-react 2>&1", {
473
+ cwd: targetDir,
474
+ stdio: ["pipe", "pipe", "pipe"],
475
+ });
476
+ stopSpinner(true, "Sandpack installed");
477
+ } catch (e) {
478
+ stopSpinner(
479
+ false,
480
+ "Sandpack install failed - run: npm install @codesandbox/sandpack-react",
481
+ );
730
482
  }
731
483
  }
732
484
 
733
485
  if (withStorybook) {
486
+ startSpinner("Initializing Storybook (this takes a moment)...");
734
487
  try {
735
- log(' 📖 Initializing Storybook (this may take a moment)...', 'blue');
736
- execSync('npx storybook@latest init --yes 2>&1', { cwd: targetDir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 300000 });
737
- log(' ✅ Storybook initialized successfully', 'green');
738
- log(' 💡 Run with: pnpm storybook', 'yellow');
739
- } catch (error) {
740
- log(' ⚠️ Storybook init failed. Run manually:', 'yellow');
741
- log(' npx storybook@latest init', 'yellow');
488
+ execSync("npx storybook@latest init --yes 2>&1", {
489
+ cwd: targetDir,
490
+ stdio: ["pipe", "pipe", "pipe"],
491
+ timeout: 300000,
492
+ });
493
+ stopSpinner(true, "Storybook initialized");
494
+ } catch (e) {
495
+ stopSpinner(
496
+ false,
497
+ "Storybook init failed - run: npx storybook@latest init",
498
+ );
742
499
  }
743
500
  }
744
501
 
745
502
  if (withPlaywright) {
503
+ startSpinner("Initializing Playwright (this takes a moment)...");
746
504
  try {
747
- log(' 🎭 Initializing Playwright (this may take a moment)...', 'blue');
748
- execSync('npm init playwright@latest -- --yes 2>&1', { cwd: targetDir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 300000 });
749
- log(' ✅ Playwright initialized successfully', 'green');
750
- log(' 💡 Run tests with: npx playwright test', 'yellow');
751
- } catch (error) {
752
- log(' ⚠️ Playwright init failed. Run manually:', 'yellow');
753
- log(' npm init playwright@latest', 'yellow');
505
+ execSync("npm init playwright@latest -- --yes 2>&1", {
506
+ cwd: targetDir,
507
+ stdio: ["pipe", "pipe", "pipe"],
508
+ timeout: 300000,
509
+ });
510
+ stopSpinner(true, "Playwright initialized");
511
+ } catch (e) {
512
+ stopSpinner(
513
+ false,
514
+ "Playwright init failed - run: npm init playwright@latest",
515
+ );
754
516
  }
755
517
  }
518
+ } else {
519
+ logInfo("None selected");
520
+ logInfo(
521
+ "Add later with: --with-storybook --with-playwright --with-sandpack",
522
+ );
756
523
  }
757
524
 
758
- // ========================================
759
- // Success Summary
760
- // ========================================
761
- log('\n' + '═'.repeat(60), 'green');
762
- log('🎉 API Development Tools v3.10.0 installed successfully!', 'green');
763
- log('═'.repeat(60) + '\n', 'green');
764
-
765
- log('📋 What was installed:', 'bright');
766
- log(' Commands: .claude/commands/*.md', 'blue');
767
- log(' Hooks: .claude/hooks/*.py (enforcement + user checkpoints)', 'blue');
768
- log(' Settings: .claude/settings.json', 'blue');
769
- log(' State: .claude/api-dev-state.json', 'blue');
770
- log(' Registry: .claude/registry.json (tracks APIs, components, pages)', 'blue');
771
- log(' Brand: .claude/BRAND_GUIDE.md (customize for your branding)', 'blue');
772
- log(' Budgets: .claude/performance-budgets.json (TDD gate thresholds)', 'blue');
773
- log(' Research: .claude/research/ (with freshness tracking)', 'blue');
774
- log(' Scripts: scripts/api-dev-tools/*.ts (manifest generation)', 'blue');
775
- log(' MCP: context7, github (via claude mcp add)', 'blue');
776
- log(' Test UI: /api-test page + /api/test-structure API (if Next.js)', 'blue');
777
-
778
- log('\n🆕 New in v3.10.0:', 'bright');
779
- log(' • Enhanced installer with detailed optional tools info', 'cyan');
780
- log(' • UI workflow auto-prompts for Storybook/Playwright installation', 'cyan');
781
- log(' • Demo JSONs updated for 13-phase workflow accuracy', 'cyan');
782
- log(' • Animated 3D grid hero header on showcase pages', 'cyan');
783
- log(' • Dev Tools landing page at /dev-tools', 'cyan');
784
- log(' • Multi-endpoint API selector (e.g., /tts, /voices, /models)', 'cyan');
785
- log(' • Audio playback for TTS/voice API responses', 'cyan');
786
- log(' • Enhanced Hustle branding (#BA0C2F)', 'cyan');
787
- log(' • Dark mode support throughout', 'cyan');
788
-
789
- log('\n📦 v3.9.0 Features (included):', 'bright');
790
- log(' • /hustle-ui-create command for components and pages', 'cyan');
791
- log(' • UI Showcase page (grid + modal preview at /ui-showcase)', 'cyan');
792
- log(' • API Showcase page (interactive testing at /api-showcase)', 'cyan');
793
- log(' • Brand guide integration (.claude/BRAND_GUIDE.md)', 'cyan');
794
- log(' • Performance budgets as TDD gates (memory, re-renders, timing)', 'cyan');
795
- log(' • ShadCN component detection in src/components/ui/', 'cyan');
796
- log(' • 4-step verification (responsive + brand + tests + memory)', 'cyan');
797
- log(' • Storybook + Playwright testing templates with thresholds', 'cyan');
798
-
799
- log('\n📦 v3.8.0 Features (included):', 'bright');
800
- log(' • /hustle-combine command for API orchestration', 'cyan');
801
- log(' • Central registry (registry.json) tracks all created elements', 'cyan');
802
-
803
- log('\n🔒 User Checkpoint Enforcement:', 'bright');
804
- log(' • Phase 1: "Which interpretation?" (disambiguation)', 'cyan');
805
- log(' • Phase 2: "Scope correct?" (scope confirmation)', 'cyan');
806
- log(' • Phase 3: "Proceed to interview?" (research summary)', 'cyan');
807
- log(' • Phase 4: "Interview complete?" (all questions answered)', 'cyan');
808
- log(' • Phase 5: "Approve searches?" (deep research proposal)', 'cyan');
809
- log(' • Phase 6: "Schema matches interview?" (schema review)', 'cyan');
810
- log(' • Phase 7: "Ready for testing?" (environment check)', 'cyan');
811
- log(' • Phase 8: "Test plan looks good?" (test matrix)', 'cyan');
812
- log(' • Phase 10: "Fix gaps?" (verification decision)', 'cyan');
813
- log(' • Phase 12: "Documentation complete?" (final checklist)', 'cyan');
814
-
815
- log('\n📚 Available Commands:', 'bright');
816
- log(' API Development:', 'bright');
817
- log(' /hustle-api-create [endpoint] - Complete 13-phase API workflow', 'blue');
818
- log(' /hustle-combine [api|ui] - Combine existing APIs into orchestration', 'blue');
819
- log(' /hustle-api-interview [endpoint] - Questions FROM research', 'blue');
820
- log(' /hustle-api-research [library] - Adaptive propose-approve research', 'blue');
821
- log(' /hustle-api-verify [endpoint] - Manual Phase 10 verification', 'blue');
822
- log(' /hustle-api-env [endpoint] - Check API keys and environment', 'blue');
823
- log(' /hustle-api-status [endpoint] - Track 13-phase progress', 'blue');
824
- log('', 'blue');
825
- log(' UI Development:', 'bright');
826
- log(' /hustle-ui-create [name] - Create component or page (13-phase)', 'blue');
827
- log(' • Brand guide integration', 'blue');
828
- log(' • ShadCN component detection', 'blue');
829
- log(' • 4-step verification', 'blue');
830
- log(' • UI Showcase auto-update', 'blue');
831
-
832
- log('\n🚀 Quick Start:', 'bright');
833
- log(' API: /hustle-api-create my-endpoint', 'blue');
834
- log(' UI: /hustle-ui-create Button', 'blue');
835
-
836
- log('\n💡 Check progress anytime:', 'yellow');
837
- log(' cat .claude/api-dev-state.json | jq \'.phases\'', 'yellow');
838
-
839
- log('\n📖 Documentation:', 'bright');
840
- log(` ${path.join(commandsDir, 'README.md')}\n`, 'blue');
841
-
842
- log('\n🎨 Showcase Pages:', 'bright');
843
- log(' /dev-tools - Landing page with all dev tools', 'blue');
844
- log(' /api-showcase - Interactive API testing', 'blue');
845
- log(' /ui-showcase - Live component previews', 'blue');
846
-
847
- // Enhanced Optional Development Tools Section
848
- log('\n' + '─'.repeat(60), 'yellow');
849
- log('📦 OPTIONAL DEVELOPMENT TOOLS', 'yellow');
850
- log('─'.repeat(60), 'yellow');
851
- log(' The following tools are OPTIONAL but enhance UI development.', 'bright');
852
- log(' Install now with flags, or later when prompted during /hustle-ui-create.\n', 'bright');
853
-
854
- log(' ┌──────────────────────────────────────────────────────────┐', 'cyan');
855
- log(' │ STORYBOOK (Component Development) │', 'cyan');
856
- log(' ├──────────────────────────────────────────────────────────┤', 'cyan');
857
- log(' │ Size: ~50MB │', 'cyan');
858
- log(' │ Purpose: Visual component testing, interactive docs │', 'cyan');
859
- log(' │ Required: /hustle-ui-create (component mode) │', 'cyan');
860
- log(' │ Without: Cannot write .stories.tsx files │', 'cyan');
861
- log(' │ Install: --with-storybook flag OR during UI workflow │', 'cyan');
862
- log(' │ Command: npx storybook@latest init │', 'cyan');
863
- log(' └──────────────────────────────────────────────────────────┘', 'cyan');
864
-
865
- log(' ┌──────────────────────────────────────────────────────────┐', 'magenta');
866
- log(' │ PLAYWRIGHT (E2E Testing) │', 'magenta');
867
- log(' ├──────────────────────────────────────────────────────────┤', 'magenta');
868
- log(' │ Size: ~200MB (includes browser binaries) │', 'magenta');
869
- log(' │ Purpose: End-to-end page testing, cross-browser │', 'magenta');
870
- log(' │ Required: /hustle-ui-create-page (page mode) │', 'magenta');
871
- log(' │ Without: Cannot write .e2e.test.ts files │', 'magenta');
872
- log(' │ Install: --with-playwright flag OR during UI workflow │', 'magenta');
873
- log(' │ Command: npm init playwright@latest │', 'magenta');
874
- log(' └──────────────────────────────────────────────────────────┘', 'magenta');
875
-
876
- log(' ┌──────────────────────────────────────────────────────────┐', 'blue');
877
- log(' │ SANDPACK (Live Editing) │', 'blue');
878
- log(' ├──────────────────────────────────────────────────────────┤', 'blue');
879
- log(' │ Size: ~5MB │', 'blue');
880
- log(' │ Purpose: Live code editing in UI Showcase │', 'blue');
881
- log(' │ Required: Interactive component playground │', 'blue');
882
- log(' │ Without: Static previews only (still functional) │', 'blue');
883
- log(' │ Install: --with-sandpack flag │', 'blue');
884
- log(' │ Command: npm install @codesandbox/sandpack-react │', 'blue');
885
- log(' └──────────────────────────────────────────────────────────┘', 'blue');
886
-
887
- log('\n 💡 UI Workflow Behavior:', 'bright');
888
- log(' When you run /hustle-ui-create without Storybook or Playwright:', 'bright');
889
- log(' 1. The hook will BLOCK writing test/story files', 'bright');
890
- log(' 2. You\'ll see the install command and be prompted to install', 'bright');
891
- log(' 3. After installing, the workflow continues automatically', 'bright');
892
- log(' 4. You can skip and install later, but Phase 8-10 will be blocked', 'bright');
893
-
894
- log('\n Example with all optional tools:', 'yellow');
895
- log(' npx @hustle-together/api-dev-tools --with-storybook --with-playwright --with-sandpack\n', 'yellow');
896
-
897
- // ========================================
898
- // 5. Verify Installation
899
- // ========================================
900
- const verification = verifyInstallation(claudeDir, hooksDir);
901
- if (!verification.success) {
902
- log('⚠️ Installation verification found issues:', 'yellow');
903
- verification.failures.forEach(f => log(` • Missing: ${f}`, 'yellow'));
904
- log(' Some features may not work correctly.\n', 'yellow');
905
- }
525
+ // ─────────────────────────────────────────────────────────────────────────
526
+ // Summary
527
+ // ─────────────────────────────────────────────────────────────────────────
528
+
529
+ log(`
530
+ ${c.red}═══════════════════════════════════════════════════════════════${c.reset}
531
+ ${c.bold} Installation Complete${c.reset}
532
+ ${c.red}═══════════════════════════════════════════════════════════════${c.reset}
533
+
534
+ ${c.bold}Installed:${c.reset}
535
+ ● Commands .claude/commands/ (slash commands)
536
+ ● Hooks hooks/ (enforcement)
537
+ ● Subagents .claude/agents/ (parallel processing)
538
+ ● Config .claude/ (settings, state, registry)
539
+ ● Templates templates/ (.env.example)
540
+
541
+ ${c.bold}Quick Start:${c.reset}
542
+ ${c.gray}$${c.reset} /api-create my-endpoint ${c.dim}# Build API endpoint${c.reset}
543
+ ${c.gray}$${c.reset} /hustle-ui-create Button ${c.dim}# Build component${c.reset}
544
+ ${c.gray}$${c.reset} /hustle-ui-create-page Home ${c.dim}# Build page${c.reset}
545
+ ${c.gray}$${c.reset} /hustle-combine api ${c.dim}# Orchestrate APIs${c.reset}
546
+
547
+ ${c.bold}Next Steps:${c.reset}
548
+ 1. ${c.white}cp templates/.env.example .env${c.reset}
549
+ 2. ${c.white}Configure your API keys in .env${c.reset}
550
+ 3. ${c.white}Add GREPTILE_API_KEY + GITHUB_TOKEN${c.reset} for Phase 14 code review
551
+ 4. ${c.white}/ntfy-setup${c.reset} for push notifications (optional)
552
+ 5. ${c.white}Restart Claude Code${c.reset} for MCP tools
553
+
554
+ ${c.dim}Documentation: https://github.com/hustle-together/api-dev-tools${c.reset}
555
+ `);
906
556
  }
907
557
 
908
- /**
909
- * Merge MCP server configurations
910
- */
911
- function mergeMcpServers(existing, newMcp) {
912
- const merged = { ...existing };
913
- merged.mcpServers = merged.mcpServers || {};
914
-
915
- // Add new MCP servers that don't already exist
916
- if (newMcp.mcpServers) {
917
- for (const [name, config] of Object.entries(newMcp.mcpServers)) {
918
- if (!merged.mcpServers[name]) {
919
- merged.mcpServers[name] = config;
920
- }
921
- }
922
- }
923
-
924
- return merged;
925
- }
558
+ // ═══════════════════════════════════════════════════════════════════════════
559
+ // Settings Merge
560
+ // ═══════════════════════════════════════════════════════════════════════════
926
561
 
927
- /**
928
- * Merge two settings objects, combining hooks arrays
929
- */
930
562
  function mergeSettings(existing, newSettings) {
931
563
  const merged = { ...existing };
932
564
 
933
- // Merge hooks
934
565
  if (newSettings.hooks) {
935
566
  merged.hooks = merged.hooks || {};
936
567
 
@@ -939,11 +570,10 @@ function mergeSettings(existing, newSettings) {
939
570
  merged.hooks[hookType] = [];
940
571
  }
941
572
 
942
- // Add new hooks that don't already exist (check by command path)
943
573
  for (const newHook of newSettings.hooks[hookType]) {
944
- const hookCommand = newHook.hooks?.[0]?.command || '';
945
- const exists = merged.hooks[hookType].some(existing => {
946
- const existingCommand = existing.hooks?.[0]?.command || '';
574
+ const hookCommand = newHook.hooks?.[0]?.command || "";
575
+ const exists = merged.hooks[hookType].some((existing) => {
576
+ const existingCommand = existing.hooks?.[0]?.command || "";
947
577
  return existingCommand === hookCommand;
948
578
  });
949
579
 
@@ -954,14 +584,27 @@ function mergeSettings(existing, newSettings) {
954
584
  }
955
585
  }
956
586
 
587
+ if (newSettings.permissions) {
588
+ merged.permissions = merged.permissions || {};
589
+ merged.permissions.allow = [
590
+ ...new Set([
591
+ ...(merged.permissions.allow || []),
592
+ ...(newSettings.permissions.allow || []),
593
+ ]),
594
+ ];
595
+ merged.permissions.deny = [
596
+ ...new Set([
597
+ ...(merged.permissions.deny || []),
598
+ ...(newSettings.permissions.deny || []),
599
+ ]),
600
+ ];
601
+ }
602
+
957
603
  return merged;
958
604
  }
959
605
 
960
- // Run installer
961
- try {
962
- main();
963
- } catch (error) {
964
- log(`\n❌ Installation failed: ${error.message}`, 'red');
965
- log(` ${error.stack}\n`, 'red');
606
+ // Run
607
+ main().catch((error) => {
608
+ console.error(`\n${c.red}Installation failed:${c.reset} ${error.message}`);
966
609
  process.exit(1);
967
- }
610
+ });