@gannonh/kata 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +439 -0
  3. package/agents/kata-codebase-mapper.md +738 -0
  4. package/agents/kata-debugger.md +1184 -0
  5. package/agents/kata-executor.md +753 -0
  6. package/agents/kata-integration-checker.md +423 -0
  7. package/agents/kata-phase-researcher.md +632 -0
  8. package/agents/kata-plan-checker.md +745 -0
  9. package/agents/kata-planner.md +1367 -0
  10. package/agents/kata-project-researcher.md +865 -0
  11. package/agents/kata-research-synthesizer.md +247 -0
  12. package/agents/kata-roadmapper.md +605 -0
  13. package/agents/kata-verifier.md +778 -0
  14. package/bin/install.js +569 -0
  15. package/commands/kata/add-phase.md +207 -0
  16. package/commands/kata/add-todo.md +182 -0
  17. package/commands/kata/audit-milestone.md +258 -0
  18. package/commands/kata/check-todos.md +217 -0
  19. package/commands/kata/complete-milestone.md +136 -0
  20. package/commands/kata/debug.md +149 -0
  21. package/commands/kata/discuss-phase.md +80 -0
  22. package/commands/kata/execute-phase.md +304 -0
  23. package/commands/kata/help.md +383 -0
  24. package/commands/kata/insert-phase.md +227 -0
  25. package/commands/kata/list-phase-assumptions.md +50 -0
  26. package/commands/kata/map-codebase.md +71 -0
  27. package/commands/kata/new-milestone.md +717 -0
  28. package/commands/kata/new-project.md +896 -0
  29. package/commands/kata/pause-work.md +123 -0
  30. package/commands/kata/plan-milestone-gaps.md +284 -0
  31. package/commands/kata/plan-phase.md +475 -0
  32. package/commands/kata/progress.md +356 -0
  33. package/commands/kata/remove-phase.md +338 -0
  34. package/commands/kata/research-phase.md +180 -0
  35. package/commands/kata/resume-work.md +40 -0
  36. package/commands/kata/update.md +172 -0
  37. package/commands/kata/verify-work.md +219 -0
  38. package/commands/kata/whats-new.md +124 -0
  39. package/hooks/kata-check-update.js +51 -0
  40. package/hooks/statusline.js +84 -0
  41. package/kata/references/checkpoints.md +788 -0
  42. package/kata/references/continuation-format.md +249 -0
  43. package/kata/references/git-integration.md +254 -0
  44. package/kata/references/questioning.md +141 -0
  45. package/kata/references/tdd.md +263 -0
  46. package/kata/references/ui-brand.md +160 -0
  47. package/kata/references/verification-patterns.md +595 -0
  48. package/kata/templates/DEBUG.md +159 -0
  49. package/kata/templates/UAT.md +247 -0
  50. package/kata/templates/codebase/architecture.md +255 -0
  51. package/kata/templates/codebase/concerns.md +310 -0
  52. package/kata/templates/codebase/conventions.md +307 -0
  53. package/kata/templates/codebase/integrations.md +280 -0
  54. package/kata/templates/codebase/stack.md +186 -0
  55. package/kata/templates/codebase/structure.md +285 -0
  56. package/kata/templates/codebase/testing.md +480 -0
  57. package/kata/templates/config.json +26 -0
  58. package/kata/templates/context.md +291 -0
  59. package/kata/templates/continue-here.md +78 -0
  60. package/kata/templates/debug-subagent-prompt.md +91 -0
  61. package/kata/templates/discovery.md +146 -0
  62. package/kata/templates/milestone-archive.md +123 -0
  63. package/kata/templates/milestone.md +115 -0
  64. package/kata/templates/phase-prompt.md +576 -0
  65. package/kata/templates/planner-subagent-prompt.md +117 -0
  66. package/kata/templates/project.md +184 -0
  67. package/kata/templates/requirements.md +231 -0
  68. package/kata/templates/research-project/ARCHITECTURE.md +204 -0
  69. package/kata/templates/research-project/FEATURES.md +147 -0
  70. package/kata/templates/research-project/PITFALLS.md +200 -0
  71. package/kata/templates/research-project/STACK.md +120 -0
  72. package/kata/templates/research-project/SUMMARY.md +170 -0
  73. package/kata/templates/research.md +529 -0
  74. package/kata/templates/roadmap.md +202 -0
  75. package/kata/templates/state.md +206 -0
  76. package/kata/templates/summary.md +269 -0
  77. package/kata/templates/user-setup.md +323 -0
  78. package/kata/templates/verification-report.md +322 -0
  79. package/kata/workflows/complete-milestone.md +750 -0
  80. package/kata/workflows/diagnose-issues.md +233 -0
  81. package/kata/workflows/discovery-phase.md +293 -0
  82. package/kata/workflows/discuss-phase.md +422 -0
  83. package/kata/workflows/execute-phase.md +552 -0
  84. package/kata/workflows/execute-plan.md +1831 -0
  85. package/kata/workflows/list-phase-assumptions.md +178 -0
  86. package/kata/workflows/map-codebase.md +289 -0
  87. package/kata/workflows/resume-project.md +311 -0
  88. package/kata/workflows/transition.md +564 -0
  89. package/kata/workflows/verify-phase.md +629 -0
  90. package/kata/workflows/verify-work.md +563 -0
  91. package/package.json +32 -0
package/bin/install.js ADDED
@@ -0,0 +1,569 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const readline = require('readline');
7
+
8
+ // Colors
9
+ const cyan = '\x1b[36m';
10
+ const green = '\x1b[32m';
11
+ const yellow = '\x1b[33m';
12
+ const dim = '\x1b[2m';
13
+ const reset = '\x1b[0m';
14
+
15
+ // Get version from package.json
16
+ const pkg = require('../package.json');
17
+
18
+ const banner = `
19
+ ${cyan} ██╗ ██╗ █████╗ ████████╗ █████╗
20
+ ██║ ██╔╝██╔══██╗╚══██╔══╝██╔══██╗
21
+ █████╔╝ ███████║ ██║ ███████║
22
+ ██╔═██╗ ██╔══██║ ██║ ██╔══██║
23
+ ██║ ██╗██║ ██║ ██║ ██║ ██║
24
+ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝${reset}
25
+
26
+ Kata ${dim}v${pkg.version}${reset}
27
+ A meta-prompting, context engineering and spec-driven
28
+ development system for Claude Code.
29
+ `;
30
+
31
+ // Parse args
32
+ const args = process.argv.slice(2);
33
+ const hasGlobal = args.includes('--global') || args.includes('-g');
34
+ const hasLocal = args.includes('--local') || args.includes('-l');
35
+
36
+ // Parse --config-dir argument
37
+ function parseConfigDirArg() {
38
+ const configDirIndex = args.findIndex(arg => arg === '--config-dir' || arg === '-c');
39
+ if (configDirIndex !== -1) {
40
+ const nextArg = args[configDirIndex + 1];
41
+ // Error if --config-dir is provided without a value or next arg is another flag
42
+ if (!nextArg || nextArg.startsWith('-')) {
43
+ console.error(` ${yellow}--config-dir requires a path argument${reset}`);
44
+ process.exit(1);
45
+ }
46
+ return nextArg;
47
+ }
48
+ // Also handle --config-dir=value format
49
+ const configDirArg = args.find(arg => arg.startsWith('--config-dir=') || arg.startsWith('-c='));
50
+ if (configDirArg) {
51
+ return configDirArg.split('=')[1];
52
+ }
53
+ return null;
54
+ }
55
+ const explicitConfigDir = parseConfigDirArg();
56
+ const hasHelp = args.includes('--help') || args.includes('-h');
57
+ const forceStatusline = args.includes('--force-statusline');
58
+
59
+ console.log(banner);
60
+
61
+ // Show help if requested
62
+ if (hasHelp) {
63
+ console.log(` ${yellow}Usage:${reset} npx @gannonh/kata [options]
64
+
65
+ ${yellow}Options:${reset}
66
+ ${cyan}-g, --global${reset} Install globally (to Claude config directory)
67
+ ${cyan}-l, --local${reset} Install locally (to ./.claude in current directory)
68
+ ${cyan}-c, --config-dir <path>${reset} Specify custom Claude config directory
69
+ ${cyan}-h, --help${reset} Show this help message
70
+ ${cyan}--force-statusline${reset} Replace existing statusline config
71
+
72
+ ${yellow}Examples:${reset}
73
+ ${dim}# Install to default ~/.claude directory${reset}
74
+ npx @gannonh/kata --global
75
+
76
+ ${dim}# Install to custom config directory (for multiple Claude accounts)${reset}
77
+ npx @gannonh/kata --global --config-dir ~/.claude-bc
78
+
79
+ ${dim}# Using environment variable${reset}
80
+ CLAUDE_CONFIG_DIR=~/.claude-bc npx @gannonh/kata --global
81
+
82
+ ${dim}# Install to current project only${reset}
83
+ npx @gannonh/kata --local
84
+
85
+ ${yellow}Notes:${reset}
86
+ The --config-dir option is useful when you have multiple Claude Code
87
+ configurations (e.g., for different subscriptions). It takes priority
88
+ over the CLAUDE_CONFIG_DIR environment variable.
89
+ `);
90
+ process.exit(0);
91
+ }
92
+
93
+ /**
94
+ * Expand ~ to home directory (shell doesn't expand in env vars passed to node)
95
+ */
96
+ function expandTilde(filePath) {
97
+ if (filePath && filePath.startsWith('~/')) {
98
+ return path.join(os.homedir(), filePath.slice(2));
99
+ }
100
+ return filePath;
101
+ }
102
+
103
+ /**
104
+ * Read and parse settings.json, returning empty object if doesn't exist
105
+ */
106
+ function readSettings(settingsPath) {
107
+ if (fs.existsSync(settingsPath)) {
108
+ try {
109
+ return JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
110
+ } catch (e) {
111
+ return {};
112
+ }
113
+ }
114
+ return {};
115
+ }
116
+
117
+ /**
118
+ * Write settings.json with proper formatting
119
+ */
120
+ function writeSettings(settingsPath, settings) {
121
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
122
+ }
123
+
124
+ /**
125
+ * Recursively copy directory, replacing paths in .md files
126
+ * Deletes existing destDir first to remove orphaned files from previous versions
127
+ */
128
+ function copyWithPathReplacement(srcDir, destDir, pathPrefix) {
129
+ // Clean install: remove existing destination to prevent orphaned files
130
+ if (fs.existsSync(destDir)) {
131
+ fs.rmSync(destDir, { recursive: true });
132
+ }
133
+ fs.mkdirSync(destDir, { recursive: true });
134
+
135
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true });
136
+
137
+ for (const entry of entries) {
138
+ const srcPath = path.join(srcDir, entry.name);
139
+ const destPath = path.join(destDir, entry.name);
140
+
141
+ if (entry.isDirectory()) {
142
+ copyWithPathReplacement(srcPath, destPath, pathPrefix);
143
+ } else if (entry.name.endsWith('.md')) {
144
+ // Replace ~/.claude/ with the appropriate prefix in markdown files
145
+ let content = fs.readFileSync(srcPath, 'utf8');
146
+ content = content.replace(/~\/\.claude\//g, pathPrefix);
147
+ fs.writeFileSync(destPath, content);
148
+ } else {
149
+ fs.copyFileSync(srcPath, destPath);
150
+ }
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Clean up orphaned files from previous Kata versions
156
+ */
157
+ function cleanupOrphanedFiles(claudeDir) {
158
+ const orphanedFiles = [
159
+ 'hooks/gsd-notify.sh', // Removed in v1.6.x
160
+ ];
161
+
162
+ for (const relPath of orphanedFiles) {
163
+ const fullPath = path.join(claudeDir, relPath);
164
+ if (fs.existsSync(fullPath)) {
165
+ fs.unlinkSync(fullPath);
166
+ console.log(` ${green}✓${reset} Removed orphaned ${relPath}`);
167
+ }
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Clean up orphaned hook registrations from settings.json
173
+ */
174
+ function cleanupOrphanedHooks(settings) {
175
+ const orphanedHookPatterns = [
176
+ 'gsd-notify.sh', // Removed in v1.6.x
177
+ ];
178
+
179
+ let cleaned = false;
180
+
181
+ // Check all hook event types (Stop, SessionStart, etc.)
182
+ if (settings.hooks) {
183
+ for (const eventType of Object.keys(settings.hooks)) {
184
+ const hookEntries = settings.hooks[eventType];
185
+ if (Array.isArray(hookEntries)) {
186
+ // Filter out entries that contain orphaned hooks
187
+ const filtered = hookEntries.filter(entry => {
188
+ if (entry.hooks && Array.isArray(entry.hooks)) {
189
+ // Check if any hook in this entry matches orphaned patterns
190
+ const hasOrphaned = entry.hooks.some(h =>
191
+ h.command && orphanedHookPatterns.some(pattern => h.command.includes(pattern))
192
+ );
193
+ if (hasOrphaned) {
194
+ cleaned = true;
195
+ return false; // Remove this entry
196
+ }
197
+ }
198
+ return true; // Keep this entry
199
+ });
200
+ settings.hooks[eventType] = filtered;
201
+ }
202
+ }
203
+ }
204
+
205
+ if (cleaned) {
206
+ console.log(` ${green}✓${reset} Removed orphaned hook registrations`);
207
+ }
208
+
209
+ return settings;
210
+ }
211
+
212
+ /**
213
+ * Verify a directory exists and contains files
214
+ */
215
+ function verifyInstalled(dirPath, description) {
216
+ if (!fs.existsSync(dirPath)) {
217
+ console.error(` ${yellow}✗${reset} Failed to install ${description}: directory not created`);
218
+ return false;
219
+ }
220
+ try {
221
+ const entries = fs.readdirSync(dirPath);
222
+ if (entries.length === 0) {
223
+ console.error(` ${yellow}✗${reset} Failed to install ${description}: directory is empty`);
224
+ return false;
225
+ }
226
+ } catch (e) {
227
+ console.error(` ${yellow}✗${reset} Failed to install ${description}: ${e.message}`);
228
+ return false;
229
+ }
230
+ return true;
231
+ }
232
+
233
+ /**
234
+ * Verify a file exists
235
+ */
236
+ function verifyFileInstalled(filePath, description) {
237
+ if (!fs.existsSync(filePath)) {
238
+ console.error(` ${yellow}✗${reset} Failed to install ${description}: file not created`);
239
+ return false;
240
+ }
241
+ return true;
242
+ }
243
+
244
+ /**
245
+ * Install to the specified directory
246
+ */
247
+ function install(isGlobal) {
248
+ const src = path.join(__dirname, '..');
249
+ // Priority: explicit --config-dir arg > CLAUDE_CONFIG_DIR env var > default ~/.claude
250
+ const configDir = expandTilde(explicitConfigDir) || expandTilde(process.env.CLAUDE_CONFIG_DIR);
251
+ const defaultGlobalDir = configDir || path.join(os.homedir(), '.claude');
252
+ const claudeDir = isGlobal
253
+ ? defaultGlobalDir
254
+ : path.join(process.cwd(), '.claude');
255
+
256
+ const locationLabel = isGlobal
257
+ ? claudeDir.replace(os.homedir(), '~')
258
+ : claudeDir.replace(process.cwd(), '.');
259
+
260
+ // Path prefix for file references
261
+ // Use actual path when CLAUDE_CONFIG_DIR is set, otherwise use ~ shorthand
262
+ const pathPrefix = isGlobal
263
+ ? (configDir ? `${claudeDir}/` : '~/.claude/')
264
+ : './.claude/';
265
+
266
+ console.log(` Installing to ${cyan}${locationLabel}${reset}\n`);
267
+
268
+ // Track installation failures
269
+ const failures = [];
270
+
271
+ // Clean up orphaned files from previous versions
272
+ cleanupOrphanedFiles(claudeDir);
273
+
274
+ // Create commands directory
275
+ const commandsDir = path.join(claudeDir, 'commands');
276
+ fs.mkdirSync(commandsDir, { recursive: true });
277
+
278
+ // Copy commands/kata with path replacement
279
+ const kataSrc = path.join(src, 'commands', 'kata');
280
+ const kataDest = path.join(commandsDir, 'kata');
281
+ copyWithPathReplacement(kataSrc, kataDest, pathPrefix);
282
+ if (verifyInstalled(kataDest, 'commands/kata')) {
283
+ console.log(` ${green}✓${reset} Installed commands/kata`);
284
+ } else {
285
+ failures.push('commands/kata');
286
+ }
287
+
288
+ // Copy kata skill with path replacement
289
+ const skillSrc = path.join(src, 'kata');
290
+ const skillDest = path.join(claudeDir, 'kata');
291
+ copyWithPathReplacement(skillSrc, skillDest, pathPrefix);
292
+ if (verifyInstalled(skillDest, 'kata')) {
293
+ console.log(` ${green}✓${reset} Installed kata`);
294
+ } else {
295
+ failures.push('kata');
296
+ }
297
+
298
+ // Copy agents to ~/.claude/agents (subagents must be at root level)
299
+ // Only delete gsd-*.md files to preserve user's custom agents
300
+ const agentsSrc = path.join(src, 'agents');
301
+ if (fs.existsSync(agentsSrc)) {
302
+ const agentsDest = path.join(claudeDir, 'agents');
303
+ fs.mkdirSync(agentsDest, { recursive: true });
304
+
305
+ // Remove old Kata agents (kata-*.md and gsd-*.md for migration) before copying new ones
306
+ if (fs.existsSync(agentsDest)) {
307
+ for (const file of fs.readdirSync(agentsDest)) {
308
+ if ((file.startsWith('kata-') || file.startsWith('gsd-')) && file.endsWith('.md')) {
309
+ fs.unlinkSync(path.join(agentsDest, file));
310
+ }
311
+ }
312
+ }
313
+
314
+ // Copy new agents (don't use copyWithPathReplacement which would wipe the folder)
315
+ const agentEntries = fs.readdirSync(agentsSrc, { withFileTypes: true });
316
+ for (const entry of agentEntries) {
317
+ if (entry.isFile() && entry.name.endsWith('.md')) {
318
+ let content = fs.readFileSync(path.join(agentsSrc, entry.name), 'utf8');
319
+ content = content.replace(/~\/\.claude\//g, pathPrefix);
320
+ fs.writeFileSync(path.join(agentsDest, entry.name), content);
321
+ }
322
+ }
323
+ if (verifyInstalled(agentsDest, 'agents')) {
324
+ console.log(` ${green}✓${reset} Installed agents`);
325
+ } else {
326
+ failures.push('agents');
327
+ }
328
+ }
329
+
330
+ // Copy CHANGELOG.md
331
+ const changelogSrc = path.join(src, 'CHANGELOG.md');
332
+ const changelogDest = path.join(claudeDir, 'kata', 'CHANGELOG.md');
333
+ if (fs.existsSync(changelogSrc)) {
334
+ fs.copyFileSync(changelogSrc, changelogDest);
335
+ if (verifyFileInstalled(changelogDest, 'CHANGELOG.md')) {
336
+ console.log(` ${green}✓${reset} Installed CHANGELOG.md`);
337
+ } else {
338
+ failures.push('CHANGELOG.md');
339
+ }
340
+ }
341
+
342
+ // Write VERSION file for whats-new command
343
+ const versionDest = path.join(claudeDir, 'kata', 'VERSION');
344
+ fs.writeFileSync(versionDest, pkg.version);
345
+ if (verifyFileInstalled(versionDest, 'VERSION')) {
346
+ console.log(` ${green}✓${reset} Wrote VERSION (${pkg.version})`);
347
+ } else {
348
+ failures.push('VERSION');
349
+ }
350
+
351
+ // Copy hooks
352
+ const hooksSrc = path.join(src, 'hooks');
353
+ if (fs.existsSync(hooksSrc)) {
354
+ const hooksDest = path.join(claudeDir, 'hooks');
355
+ fs.mkdirSync(hooksDest, { recursive: true });
356
+ const hookEntries = fs.readdirSync(hooksSrc);
357
+ for (const entry of hookEntries) {
358
+ const srcFile = path.join(hooksSrc, entry);
359
+ const destFile = path.join(hooksDest, entry);
360
+ fs.copyFileSync(srcFile, destFile);
361
+ }
362
+ if (verifyInstalled(hooksDest, 'hooks')) {
363
+ console.log(` ${green}✓${reset} Installed hooks`);
364
+ } else {
365
+ failures.push('hooks');
366
+ }
367
+ }
368
+
369
+ // If critical components failed, exit with error
370
+ if (failures.length > 0) {
371
+ console.error(`\n ${yellow}Installation incomplete!${reset} Failed: ${failures.join(', ')}`);
372
+ console.error(` Try running directly: node ~/.npm/_npx/*/node_modules/@gannonh/kata/bin/install.js --global\n`);
373
+ process.exit(1);
374
+ }
375
+
376
+ // Configure statusline and hooks in settings.json
377
+ const settingsPath = path.join(claudeDir, 'settings.json');
378
+ const settings = cleanupOrphanedHooks(readSettings(settingsPath));
379
+ const statuslineCommand = isGlobal
380
+ ? 'node "$HOME/.claude/hooks/statusline.js"'
381
+ : 'node .claude/hooks/statusline.js';
382
+ const updateCheckCommand = isGlobal
383
+ ? 'node "$HOME/.claude/hooks/kata-check-update.js"'
384
+ : 'node .claude/hooks/kata-check-update.js';
385
+
386
+ // Configure SessionStart hook for update checking
387
+ if (!settings.hooks) {
388
+ settings.hooks = {};
389
+ }
390
+ if (!settings.hooks.SessionStart) {
391
+ settings.hooks.SessionStart = [];
392
+ }
393
+
394
+ // Check if Kata update hook already exists (also check for old gsd- hook for migration)
395
+ const hasKataUpdateHook = settings.hooks.SessionStart.some(entry =>
396
+ entry.hooks && entry.hooks.some(h => h.command && (h.command.includes('kata-check-update') || h.command.includes('gsd-check-update')))
397
+ );
398
+
399
+ if (!hasKataUpdateHook) {
400
+ settings.hooks.SessionStart.push({
401
+ hooks: [
402
+ {
403
+ type: 'command',
404
+ command: updateCheckCommand
405
+ }
406
+ ]
407
+ });
408
+ console.log(` ${green}✓${reset} Configured update check hook`);
409
+ }
410
+
411
+ return { settingsPath, settings, statuslineCommand };
412
+ }
413
+
414
+ /**
415
+ * Apply statusline config, then print completion message
416
+ */
417
+ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline) {
418
+ if (shouldInstallStatusline) {
419
+ settings.statusLine = {
420
+ type: 'command',
421
+ command: statuslineCommand
422
+ };
423
+ console.log(` ${green}✓${reset} Configured statusline`);
424
+ }
425
+
426
+ // Always write settings (hooks were already configured in install())
427
+ writeSettings(settingsPath, settings);
428
+
429
+ console.log(`
430
+ ${green}Done!${reset} Launch Claude Code and run ${cyan}/kata:help${reset}.
431
+ `);
432
+ }
433
+
434
+ /**
435
+ * Handle statusline configuration with optional prompt
436
+ */
437
+ function handleStatusline(settings, isInteractive, callback) {
438
+ const hasExisting = settings.statusLine != null;
439
+
440
+ // No existing statusline - just install it
441
+ if (!hasExisting) {
442
+ callback(true);
443
+ return;
444
+ }
445
+
446
+ // Has existing and --force-statusline flag
447
+ if (forceStatusline) {
448
+ callback(true);
449
+ return;
450
+ }
451
+
452
+ // Has existing, non-interactive mode - skip
453
+ if (!isInteractive) {
454
+ console.log(` ${yellow}⚠${reset} Skipping statusline (already configured)`);
455
+ console.log(` Use ${cyan}--force-statusline${reset} to replace\n`);
456
+ callback(false);
457
+ return;
458
+ }
459
+
460
+ // Has existing, interactive mode - prompt user
461
+ const existingCmd = settings.statusLine.command || settings.statusLine.url || '(custom)';
462
+
463
+ const rl = readline.createInterface({
464
+ input: process.stdin,
465
+ output: process.stdout
466
+ });
467
+
468
+ console.log(`
469
+ ${yellow}⚠${reset} Existing statusline detected
470
+
471
+ Your current statusline:
472
+ ${dim}command: ${existingCmd}${reset}
473
+
474
+ Kata includes a statusline showing:
475
+ • Model name
476
+ • Current task (from todo list)
477
+ • Context window usage (color-coded)
478
+
479
+ ${cyan}1${reset}) Keep existing
480
+ ${cyan}2${reset}) Replace with Kata statusline
481
+ `);
482
+
483
+ rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
484
+ rl.close();
485
+ const choice = answer.trim() || '1';
486
+ callback(choice === '2');
487
+ });
488
+ }
489
+
490
+ /**
491
+ * Prompt for install location
492
+ */
493
+ function promptLocation() {
494
+ // Check if stdin is a TTY - if not, fall back to global install
495
+ // This handles npx execution in environments like WSL2 where stdin may not be properly connected
496
+ if (!process.stdin.isTTY) {
497
+ console.log(` ${yellow}Non-interactive terminal detected, defaulting to global install${reset}\n`);
498
+ const { settingsPath, settings, statuslineCommand } = install(true);
499
+ handleStatusline(settings, false, (shouldInstallStatusline) => {
500
+ finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline);
501
+ });
502
+ return;
503
+ }
504
+
505
+ const rl = readline.createInterface({
506
+ input: process.stdin,
507
+ output: process.stdout
508
+ });
509
+
510
+ // Track whether we've processed the answer to prevent double-execution
511
+ let answered = false;
512
+
513
+ // Handle readline close event to detect premature stdin closure
514
+ rl.on('close', () => {
515
+ if (!answered) {
516
+ answered = true;
517
+ console.log(`\n ${yellow}Input stream closed, defaulting to global install${reset}\n`);
518
+ const { settingsPath, settings, statuslineCommand } = install(true);
519
+ handleStatusline(settings, false, (shouldInstallStatusline) => {
520
+ finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline);
521
+ });
522
+ }
523
+ });
524
+
525
+ const configDir = expandTilde(explicitConfigDir) || expandTilde(process.env.CLAUDE_CONFIG_DIR);
526
+ const globalPath = configDir || path.join(os.homedir(), '.claude');
527
+ const globalLabel = globalPath.replace(os.homedir(), '~');
528
+
529
+ console.log(` ${yellow}Where would you like to install?${reset}
530
+
531
+ ${cyan}1${reset}) Global ${dim}(${globalLabel})${reset} - available in all projects
532
+ ${cyan}2${reset}) Local ${dim}(./.claude)${reset} - this project only
533
+ `);
534
+
535
+ rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
536
+ answered = true;
537
+ rl.close();
538
+ const choice = answer.trim() || '1';
539
+ const isGlobal = choice !== '2';
540
+ const { settingsPath, settings, statuslineCommand } = install(isGlobal);
541
+ // Interactive mode - prompt for optional features
542
+ handleStatusline(settings, true, (shouldInstallStatusline) => {
543
+ finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline);
544
+ });
545
+ });
546
+ }
547
+
548
+ // Main
549
+ if (hasGlobal && hasLocal) {
550
+ console.error(` ${yellow}Cannot specify both --global and --local${reset}`);
551
+ process.exit(1);
552
+ } else if (explicitConfigDir && hasLocal) {
553
+ console.error(` ${yellow}Cannot use --config-dir with --local${reset}`);
554
+ process.exit(1);
555
+ } else if (hasGlobal) {
556
+ const { settingsPath, settings, statuslineCommand } = install(true);
557
+ // Non-interactive - respect flags
558
+ handleStatusline(settings, false, (shouldInstallStatusline) => {
559
+ finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline);
560
+ });
561
+ } else if (hasLocal) {
562
+ const { settingsPath, settings, statuslineCommand } = install(false);
563
+ // Non-interactive - respect flags
564
+ handleStatusline(settings, false, (shouldInstallStatusline) => {
565
+ finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline);
566
+ });
567
+ } else {
568
+ promptLocation();
569
+ }