@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.
- package/LICENSE +21 -0
- package/README.md +439 -0
- package/agents/kata-codebase-mapper.md +738 -0
- package/agents/kata-debugger.md +1184 -0
- package/agents/kata-executor.md +753 -0
- package/agents/kata-integration-checker.md +423 -0
- package/agents/kata-phase-researcher.md +632 -0
- package/agents/kata-plan-checker.md +745 -0
- package/agents/kata-planner.md +1367 -0
- package/agents/kata-project-researcher.md +865 -0
- package/agents/kata-research-synthesizer.md +247 -0
- package/agents/kata-roadmapper.md +605 -0
- package/agents/kata-verifier.md +778 -0
- package/bin/install.js +569 -0
- package/commands/kata/add-phase.md +207 -0
- package/commands/kata/add-todo.md +182 -0
- package/commands/kata/audit-milestone.md +258 -0
- package/commands/kata/check-todos.md +217 -0
- package/commands/kata/complete-milestone.md +136 -0
- package/commands/kata/debug.md +149 -0
- package/commands/kata/discuss-phase.md +80 -0
- package/commands/kata/execute-phase.md +304 -0
- package/commands/kata/help.md +383 -0
- package/commands/kata/insert-phase.md +227 -0
- package/commands/kata/list-phase-assumptions.md +50 -0
- package/commands/kata/map-codebase.md +71 -0
- package/commands/kata/new-milestone.md +717 -0
- package/commands/kata/new-project.md +896 -0
- package/commands/kata/pause-work.md +123 -0
- package/commands/kata/plan-milestone-gaps.md +284 -0
- package/commands/kata/plan-phase.md +475 -0
- package/commands/kata/progress.md +356 -0
- package/commands/kata/remove-phase.md +338 -0
- package/commands/kata/research-phase.md +180 -0
- package/commands/kata/resume-work.md +40 -0
- package/commands/kata/update.md +172 -0
- package/commands/kata/verify-work.md +219 -0
- package/commands/kata/whats-new.md +124 -0
- package/hooks/kata-check-update.js +51 -0
- package/hooks/statusline.js +84 -0
- package/kata/references/checkpoints.md +788 -0
- package/kata/references/continuation-format.md +249 -0
- package/kata/references/git-integration.md +254 -0
- package/kata/references/questioning.md +141 -0
- package/kata/references/tdd.md +263 -0
- package/kata/references/ui-brand.md +160 -0
- package/kata/references/verification-patterns.md +595 -0
- package/kata/templates/DEBUG.md +159 -0
- package/kata/templates/UAT.md +247 -0
- package/kata/templates/codebase/architecture.md +255 -0
- package/kata/templates/codebase/concerns.md +310 -0
- package/kata/templates/codebase/conventions.md +307 -0
- package/kata/templates/codebase/integrations.md +280 -0
- package/kata/templates/codebase/stack.md +186 -0
- package/kata/templates/codebase/structure.md +285 -0
- package/kata/templates/codebase/testing.md +480 -0
- package/kata/templates/config.json +26 -0
- package/kata/templates/context.md +291 -0
- package/kata/templates/continue-here.md +78 -0
- package/kata/templates/debug-subagent-prompt.md +91 -0
- package/kata/templates/discovery.md +146 -0
- package/kata/templates/milestone-archive.md +123 -0
- package/kata/templates/milestone.md +115 -0
- package/kata/templates/phase-prompt.md +576 -0
- package/kata/templates/planner-subagent-prompt.md +117 -0
- package/kata/templates/project.md +184 -0
- package/kata/templates/requirements.md +231 -0
- package/kata/templates/research-project/ARCHITECTURE.md +204 -0
- package/kata/templates/research-project/FEATURES.md +147 -0
- package/kata/templates/research-project/PITFALLS.md +200 -0
- package/kata/templates/research-project/STACK.md +120 -0
- package/kata/templates/research-project/SUMMARY.md +170 -0
- package/kata/templates/research.md +529 -0
- package/kata/templates/roadmap.md +202 -0
- package/kata/templates/state.md +206 -0
- package/kata/templates/summary.md +269 -0
- package/kata/templates/user-setup.md +323 -0
- package/kata/templates/verification-report.md +322 -0
- package/kata/workflows/complete-milestone.md +750 -0
- package/kata/workflows/diagnose-issues.md +233 -0
- package/kata/workflows/discovery-phase.md +293 -0
- package/kata/workflows/discuss-phase.md +422 -0
- package/kata/workflows/execute-phase.md +552 -0
- package/kata/workflows/execute-plan.md +1831 -0
- package/kata/workflows/list-phase-assumptions.md +178 -0
- package/kata/workflows/map-codebase.md +289 -0
- package/kata/workflows/resume-project.md +311 -0
- package/kata/workflows/transition.md +564 -0
- package/kata/workflows/verify-phase.md +629 -0
- package/kata/workflows/verify-work.md +563 -0
- 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
|
+
}
|