wtf-p 0.1.0 → 0.2.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/bin/install.js CHANGED
@@ -3,39 +3,51 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const os = require('os');
6
- const readline = require('readline');
7
6
 
8
- // Colors
9
- const cyan = '\x1b[36m';
10
- const green = '\x1b[32m';
11
- const yellow = '\x1b[33m';
12
- const red = '\x1b[31m';
13
- const magenta = '\x1b[35m';
14
- const dim = '\x1b[2m';
15
- const reset = '\x1b[0m';
7
+ const {
8
+ expandTilde,
9
+ normalizePath,
10
+ isValidPath,
11
+ getClaudeDir,
12
+ getPathLabel,
13
+ readInstalledVersion,
14
+ writeVersionFile,
15
+ detectInstallation,
16
+ collectFiles,
17
+ createColors,
18
+ createOutput,
19
+ createRL,
20
+ prompt,
21
+ getBackupPath
22
+ } = require('./lib/utils');
16
23
 
17
24
  // Get version from package.json
18
25
  const pkg = require('../package.json');
19
26
 
20
- const banner = `
21
- ${magenta}██╗ ██╗████████╗███████╗ ██████╗
22
- ██║ ██║╚══██╔══╝██╔════╝ ██╔══██╗
23
- ██║ █╗ ██║ ██║ █████╗ █████╗██████╔╝
24
- ██║███╗██║ ██║ ██╔══╝ ╚════╝██╔═══╝
25
- ╚███╔███╔╝ ██║ ██║ ██║
26
- ╚══╝╚══╝ ╚═╝ ╚═╝ ╚═╝${reset}
27
-
28
- ${cyan}Write The F***ing Paper${reset} ${dim}v${pkg.version}${reset}
29
- Context engineering for academic writing.
30
- Ship papers, not excuses.
31
- `;
27
+ // ============ Argument Parsing ============
32
28
 
33
- // Parse args
34
29
  const args = process.argv.slice(2);
30
+
31
+ // Detect subcommand (first non-flag argument)
32
+ const subcommand = args.find(arg => !arg.startsWith('-') && ['status', 'doctor', 'update'].includes(arg));
33
+ const subcommandIndex = args.indexOf(subcommand);
34
+ if (subcommandIndex !== -1) {
35
+ args.splice(subcommandIndex, 1);
36
+ }
37
+
38
+ // Parse flags
35
39
  const hasGlobal = args.includes('--global') || args.includes('-g');
36
40
  const hasLocal = args.includes('--local') || args.includes('-l');
37
41
  const hasForce = args.includes('--force') || args.includes('-f');
38
42
  const hasBackupAll = args.includes('--backup-all') || args.includes('-b');
43
+ const hasHelp = args.includes('--help') || args.includes('-h');
44
+ const hasVersion = args.includes('--version') || args.includes('-v');
45
+ const hasList = args.includes('--list');
46
+ const hasNoColor = args.includes('--no-color');
47
+ const hasQuiet = args.includes('--quiet') || args.includes('-q');
48
+ const hasVerbose = args.includes('--verbose');
49
+ const hasBeginner = args.includes('--beginner');
50
+ const hasAdvanced = args.includes('--advanced');
39
51
 
40
52
  // Parse --config-dir argument
41
53
  function parseConfigDirArg() {
@@ -43,7 +55,7 @@ function parseConfigDirArg() {
43
55
  if (configDirIndex !== -1) {
44
56
  const nextArg = args[configDirIndex + 1];
45
57
  if (!nextArg || nextArg.startsWith('-')) {
46
- console.error(` ${yellow}--config-dir requires a path argument${reset}`);
58
+ console.error(' --config-dir requires a path argument');
47
59
  process.exit(1);
48
60
  }
49
61
  return nextArg;
@@ -54,96 +66,411 @@ function parseConfigDirArg() {
54
66
  }
55
67
  return null;
56
68
  }
69
+
70
+ // Parse --only argument
71
+ function parseOnlyArg() {
72
+ const onlyArg = args.find(arg => arg.startsWith('--only='));
73
+ if (onlyArg) {
74
+ const value = onlyArg.split('=')[1];
75
+ if (!['commands', 'workflows', 'all'].includes(value)) {
76
+ console.error(' --only must be: commands, workflows, or all');
77
+ process.exit(1);
78
+ }
79
+ return value;
80
+ }
81
+ return 'all';
82
+ }
83
+
57
84
  const explicitConfigDir = parseConfigDirArg();
58
- const hasHelp = args.includes('--help') || args.includes('-h');
85
+ const onlyInstall = parseOnlyArg();
86
+
87
+ // Determine if interactive mode (TTY + not quiet/advanced)
88
+ const isTTY = process.stdout.isTTY && process.stdin.isTTY;
89
+ const isInteractive = isTTY && !hasQuiet && !hasAdvanced;
90
+ const showExplanations = hasBeginner || (isInteractive && !hasAdvanced);
91
+
92
+ // Setup output helpers
93
+ const useColors = !hasNoColor && (isTTY || process.env.FORCE_COLOR);
94
+ const out = createOutput({ quiet: hasQuiet, verbose: hasVerbose, useColors });
95
+ const c = out.colors;
96
+
97
+ // ============ Banner ============
98
+
99
+ const banner = `
100
+ ${c.magenta('██╗ ██╗████████╗███████╗ ██████╗')}
101
+ ${c.magenta('██║ ██║╚══██╔══╝██╔════╝ ██╔══██╗')}
102
+ ${c.magenta('██║ █╗ ██║ ██║ █████╗ █████╗██████╔╝')}
103
+ ${c.magenta('██║███╗██║ ██║ ██╔══╝ ╚════╝██╔═══╝')}
104
+ ${c.magenta('╚███╔███╔╝ ██║ ██║ ██║')}
105
+ ${c.magenta(' ╚══╝╚══╝ ╚═╝ ╚═╝ ╚═╝')}
106
+
107
+ ${c.cyan('Write The F***ing Paper')} ${c.dim(`v${pkg.version}`)}
108
+ Context engineering for academic writing.
109
+ `;
59
110
 
60
- console.log(banner);
111
+ // ============ Help Text ============
61
112
 
62
- // Show help if requested
63
- if (hasHelp) {
64
- console.log(` ${yellow}Usage:${reset} npx wtf-p [options]
113
+ function showHelp() {
114
+ console.log(banner);
115
+ console.log(` ${c.yellow('Usage:')} npx wtf-p [command] [options]
65
116
 
66
- ${yellow}Options:${reset}
67
- ${cyan}-g, --global${reset} Install globally (to Claude config directory)
68
- ${cyan}-l, --local${reset} Install locally (to ./.claude in current directory)
69
- ${cyan}-c, --config-dir <path>${reset} Specify custom Claude config directory
70
- ${cyan}-f, --force${reset} Overwrite all existing files without prompting
71
- ${cyan}-b, --backup-all${reset} Backup all existing files before overwriting
72
- ${cyan}-h, --help${reset} Show this help message
117
+ ${c.yellow('Commands:')}
118
+ ${c.cyan('status')} Show current installation state
119
+ ${c.cyan('doctor')} Diagnose installation issues
120
+ ${c.cyan('update')} Update existing installation
73
121
 
74
- ${yellow}Conflict Resolution:${reset}
75
- When existing WTF-P files are detected, you'll be prompted for each:
76
- ${dim}[o]verwrite${reset} - Replace with new version
77
- ${dim}[s]kip${reset} - Keep existing file
78
- ${dim}[b]ackup${reset} - Backup existing, then install new
79
- ${dim}[a]ll${reset} - Apply same choice to remaining conflicts
122
+ ${c.yellow('Install Options:')}
123
+ ${c.cyan('-g, --global')} Install globally (to Claude config directory)
124
+ ${c.cyan('-l, --local')} Install locally (to ./.claude in current directory)
125
+ ${c.cyan('-c, --config-dir <path>')} Specify custom Claude config directory
126
+ ${c.cyan('-f, --force')} Overwrite all existing files without prompting
127
+ ${c.cyan('-b, --backup-all')} Backup all existing files before overwriting
128
+ ${c.cyan('--only=<type>')} Install only: commands, workflows, or all
80
129
 
81
- ${yellow}Examples:${reset}
82
- ${dim}# Install with prompts for conflicts${reset}
83
- npx wtf-p --global
130
+ ${c.yellow('Output Options:')}
131
+ ${c.cyan('--beginner')} Show detailed explanations
132
+ ${c.cyan('--advanced')} Minimal output, skip confirmations
133
+ ${c.cyan('--no-color')} Disable colored output
134
+ ${c.cyan('-q, --quiet')} Suppress non-essential output
135
+ ${c.cyan('--verbose')} Show detailed progress
84
136
 
85
- ${dim}# Force overwrite all existing files${reset}
86
- npx wtf-p --global --force
137
+ ${c.yellow('Other:')}
138
+ ${c.cyan('--list')} Preview what would be installed
139
+ ${c.cyan('-v, --version')} Show version information
140
+ ${c.cyan('-h, --help')} Show this help message
87
141
 
88
- ${dim}# Backup all existing files before installing${reset}
89
- npx wtf-p --global --backup-all
142
+ ${c.yellow('Examples:')}
143
+ ${c.dim('# Interactive install with prompts')}
144
+ npx wtf-p
90
145
 
91
- ${dim}# Install to custom config directory${reset}
92
- npx wtf-p --global --config-dir ~/.claude-research
146
+ ${c.dim('# Quick global install')}
147
+ npx wtf-p --global --advanced
93
148
 
94
- ${yellow}Uninstall:${reset}
95
- npx wtf-p-uninstall [--global|--local] [--config-dir <path>]
149
+ ${c.dim('# Install only commands')}
150
+ npx wtf-p --global --only=commands
96
151
 
97
- ${yellow}After installation:${reset}
98
- Run ${cyan}/wtfp:help${reset} in Claude Code to get started.
152
+ ${c.dim('# Check installation status')}
153
+ npx wtf-p status
154
+
155
+ ${c.dim('# Diagnose issues')}
156
+ npx wtf-p doctor
157
+
158
+ ${c.yellow('After installation:')}
159
+ Run ${c.cyan('/wtfp:help')} in Claude Code to get started.
99
160
  `);
100
- process.exit(0);
101
161
  }
102
162
 
103
- /**
104
- * Expand ~ to home directory
105
- */
106
- function expandTilde(filePath) {
107
- if (filePath && filePath.startsWith('~/')) {
108
- return path.join(os.homedir(), filePath.slice(2));
163
+ // ============ Version Info ============
164
+
165
+ async function showVersion() {
166
+ console.log(`wtf-p v${pkg.version}`);
167
+
168
+ // Check for updates (non-blocking)
169
+ try {
170
+ const { execSync } = require('child_process');
171
+ const latest = execSync('npm view wtf-p version 2>/dev/null', { encoding: 'utf8' }).trim();
172
+ if (latest && latest !== pkg.version) {
173
+ console.log(`${c.yellow('Update available:')} ${pkg.version} → ${c.green(latest)}`);
174
+ console.log(`Run: ${c.cyan('npx wtf-p update')}`);
175
+ }
176
+ } catch {
177
+ // Ignore - can't check npm
109
178
  }
110
- return filePath;
111
179
  }
112
180
 
113
- /**
114
- * Create readline interface for prompts
115
- */
116
- function createRL() {
117
- return readline.createInterface({
118
- input: process.stdin,
119
- output: process.stdout
181
+ // ============ Status Command ============
182
+
183
+ async function showStatus() {
184
+ if (!hasQuiet) console.log(banner);
185
+
186
+ const locations = [];
187
+
188
+ // Check global
189
+ const globalDir = getClaudeDir(explicitConfigDir, true);
190
+ const globalDetection = detectInstallation(globalDir);
191
+ locations.push({
192
+ label: 'Global',
193
+ path: globalDir,
194
+ pathLabel: getPathLabel(globalDir, true),
195
+ ...globalDetection
120
196
  });
197
+
198
+ // Check local if different
199
+ const localDir = getClaudeDir(null, false);
200
+ if (localDir !== globalDir) {
201
+ const localDetection = detectInstallation(localDir);
202
+ locations.push({
203
+ label: 'Local',
204
+ path: localDir,
205
+ pathLabel: getPathLabel(localDir, false),
206
+ ...localDetection
207
+ });
208
+ }
209
+
210
+ out.log(` ${c.yellow('WTF-P Installation Status')}\n`);
211
+
212
+ for (const loc of locations) {
213
+ const installed = loc.hasCommands || loc.hasSkill;
214
+
215
+ out.log(` ${c.cyan(loc.label)} ${c.dim(`(${loc.pathLabel})`)}`);
216
+
217
+ if (!installed) {
218
+ out.log(` ${c.dim('Not installed')}\n`);
219
+ continue;
220
+ }
221
+
222
+ // Version
223
+ if (loc.version) {
224
+ const versionLabel = loc.version === 'legacy' ? 'legacy (pre-0.2.0)' : loc.version;
225
+ out.log(` Version: ${c.green(versionLabel)}`);
226
+ }
227
+
228
+ // Components
229
+ const components = [];
230
+ if (loc.hasCommands) components.push(`commands (${loc.commandFiles.length} files)`);
231
+ if (loc.hasSkill) components.push(`workflows (${loc.skillFiles.length} files)`);
232
+ out.log(` Installed: ${components.join(', ')}`);
233
+
234
+ // Warnings
235
+ if (loc.partial) {
236
+ out.log(` ${c.yellow('⚠ Partial installation detected')}`);
237
+ }
238
+ if (loc.corrupt) {
239
+ out.log(` ${c.red('⚠ Corrupt version file')}`);
240
+ }
241
+
242
+ out.log('');
243
+ }
244
+
245
+ // Check for updates
246
+ try {
247
+ const { execSync } = require('child_process');
248
+ const latest = execSync('npm view wtf-p version 2>/dev/null', { encoding: 'utf8' }).trim();
249
+ if (latest && latest !== pkg.version) {
250
+ out.log(` ${c.yellow('Update available:')} ${pkg.version} → ${c.green(latest)}`);
251
+ out.log(` Run: ${c.cyan('npx wtf-p update --global')}\n`);
252
+ }
253
+ } catch {
254
+ // Ignore
255
+ }
121
256
  }
122
257
 
123
- /**
124
- * Prompt user with a question
125
- */
126
- function prompt(rl, question) {
127
- return new Promise(resolve => {
128
- rl.question(question, answer => resolve(answer.trim().toLowerCase()));
129
- });
258
+ // ============ Doctor Command ============
259
+
260
+ async function runDoctor() {
261
+ if (!hasQuiet) console.log(banner);
262
+
263
+ out.log(` ${c.yellow('WTF-P Installation Doctor')}\n`);
264
+
265
+ const issues = [];
266
+ const checks = [];
267
+
268
+ // Check 1: Node.js version
269
+ const nodeVersion = process.version;
270
+ const nodeMajor = parseInt(nodeVersion.slice(1).split('.')[0], 10);
271
+ if (nodeMajor < 16) {
272
+ issues.push(`Node.js ${nodeVersion} is below minimum (16.7.0)`);
273
+ checks.push({ name: 'Node.js version', status: 'fail', detail: nodeVersion });
274
+ } else {
275
+ checks.push({ name: 'Node.js version', status: 'pass', detail: nodeVersion });
276
+ }
277
+
278
+ // Check 2: Claude directory exists
279
+ const globalDir = getClaudeDir(explicitConfigDir, true);
280
+ if (fs.existsSync(globalDir)) {
281
+ checks.push({ name: 'Claude config directory', status: 'pass', detail: getPathLabel(globalDir, true) });
282
+ } else {
283
+ checks.push({ name: 'Claude config directory', status: 'warn', detail: 'Does not exist (will be created on install)' });
284
+ }
285
+
286
+ // Check 3: Write permissions
287
+ const testDir = globalDir.replace(/\.claude$/, '.claude-test-' + Date.now());
288
+ try {
289
+ fs.mkdirSync(testDir, { recursive: true });
290
+ fs.writeFileSync(path.join(testDir, 'test'), 'test');
291
+ fs.rmSync(testDir, { recursive: true });
292
+ checks.push({ name: 'Write permissions', status: 'pass', detail: 'Can write to config directory' });
293
+ } catch (err) {
294
+ issues.push(`Cannot write to ${getPathLabel(globalDir, true)}: ${err.message}`);
295
+ checks.push({ name: 'Write permissions', status: 'fail', detail: err.message });
296
+ }
297
+
298
+ // Check 4: Installation state
299
+ const detection = detectInstallation(globalDir);
300
+ if (detection.hasCommands || detection.hasSkill) {
301
+ if (detection.partial) {
302
+ issues.push('Partial installation detected - some files are missing');
303
+ checks.push({ name: 'Installation integrity', status: 'warn', detail: 'Partial install' });
304
+ } else if (detection.corrupt) {
305
+ issues.push('Version file is corrupt - reinstall recommended');
306
+ checks.push({ name: 'Installation integrity', status: 'warn', detail: 'Corrupt version file' });
307
+ } else {
308
+ checks.push({ name: 'Installation integrity', status: 'pass', detail: `v${detection.version}` });
309
+ }
310
+ } else {
311
+ checks.push({ name: 'Installation integrity', status: 'info', detail: 'Not installed' });
312
+ }
313
+
314
+ // Check 5: CLAUDE_CONFIG_DIR env var
315
+ const configDirEnv = process.env.CLAUDE_CONFIG_DIR;
316
+ if (configDirEnv) {
317
+ const expanded = normalizePath(configDirEnv);
318
+ if (fs.existsSync(expanded)) {
319
+ checks.push({ name: 'CLAUDE_CONFIG_DIR', status: 'pass', detail: expanded });
320
+ } else {
321
+ issues.push(`CLAUDE_CONFIG_DIR points to non-existent path: ${configDirEnv}`);
322
+ checks.push({ name: 'CLAUDE_CONFIG_DIR', status: 'warn', detail: `${configDirEnv} (does not exist)` });
323
+ }
324
+ } else {
325
+ checks.push({ name: 'CLAUDE_CONFIG_DIR', status: 'info', detail: 'Not set (using default)' });
326
+ }
327
+
328
+ // Output results
329
+ for (const check of checks) {
330
+ let icon, color;
331
+ switch (check.status) {
332
+ case 'pass': icon = '✓'; color = c.green; break;
333
+ case 'fail': icon = '✗'; color = c.red; break;
334
+ case 'warn': icon = '⚠'; color = c.yellow; break;
335
+ default: icon = 'ℹ'; color = c.cyan;
336
+ }
337
+ out.log(` ${color(icon)} ${check.name}: ${c.dim(check.detail)}`);
338
+ }
339
+
340
+ out.log('');
341
+
342
+ if (issues.length > 0) {
343
+ out.log(` ${c.yellow('Issues found:')}`);
344
+ for (const issue of issues) {
345
+ out.log(` ${c.yellow('•')} ${issue}`);
346
+ }
347
+ out.log('');
348
+ out.log(` ${c.dim('Run')} ${c.cyan('npx wtf-p --global --force')} ${c.dim('to reinstall')}\n`);
349
+ } else {
350
+ out.log(` ${c.green('No issues found!')}\n`);
351
+ }
130
352
  }
131
353
 
354
+ // ============ Update Command ============
355
+
356
+ async function runUpdate() {
357
+ if (!hasQuiet) console.log(banner);
358
+
359
+ out.log(` ${c.yellow('Checking for updates...')}\n`);
360
+
361
+ // Find installed location
362
+ const globalDir = getClaudeDir(explicitConfigDir, true);
363
+ const localDir = getClaudeDir(null, false);
364
+
365
+ let targetDir = null;
366
+ let isGlobal = true;
367
+
368
+ const globalDetection = detectInstallation(globalDir);
369
+ const localDetection = detectInstallation(localDir);
370
+
371
+ if (hasGlobal || (!hasLocal && (globalDetection.hasCommands || globalDetection.hasSkill))) {
372
+ targetDir = globalDir;
373
+ isGlobal = true;
374
+ } else if (hasLocal || (localDetection.hasCommands || localDetection.hasSkill)) {
375
+ targetDir = localDir;
376
+ isGlobal = false;
377
+ }
378
+
379
+ if (!targetDir) {
380
+ out.log(` ${c.yellow('No existing installation found.')}`);
381
+ out.log(` Run ${c.cyan('npx wtf-p --global')} to install.\n`);
382
+ return;
383
+ }
384
+
385
+ const detection = isGlobal ? globalDetection : localDetection;
386
+ const currentVersion = detection.version === 'legacy' ? '0.0.0' : detection.version;
387
+
388
+ out.log(` Current: ${c.dim(`v${currentVersion}`)} in ${c.dim(getPathLabel(targetDir, isGlobal))}`);
389
+ out.log(` Package: ${c.dim(`v${pkg.version}`)}`);
390
+
391
+ if (pkg.version === currentVersion) {
392
+ out.log(`\n ${c.green('Already up to date!')}\n`);
393
+ return;
394
+ }
395
+
396
+ out.log(`\n ${c.cyan('Updating...')}\n`);
397
+
398
+ // Run install with backup
399
+ await install(isGlobal, true);
400
+ }
401
+
402
+ // ============ List Command ============
403
+
404
+ function showList() {
405
+ if (!hasQuiet) console.log(banner);
406
+
407
+ const src = path.join(__dirname, '..');
408
+ const wtfpSrc = path.join(src, 'commands', 'wtfp');
409
+ const skillSrc = path.join(src, 'write-the-f-paper');
410
+
411
+ const files = [];
412
+
413
+ if (onlyInstall === 'all' || onlyInstall === 'commands') {
414
+ const commandFiles = collectFiles(wtfpSrc);
415
+ for (const f of commandFiles) {
416
+ files.push({ type: 'command', path: f.replace(wtfpSrc, 'commands/wtfp') });
417
+ }
418
+ }
419
+
420
+ if (onlyInstall === 'all' || onlyInstall === 'workflows') {
421
+ const skillFiles = collectFiles(skillSrc);
422
+ for (const f of skillFiles) {
423
+ files.push({ type: 'workflow', path: f.replace(skillSrc, 'write-the-f-paper') });
424
+ }
425
+ }
426
+
427
+ out.log(` ${c.yellow('Files to install:')}\n`);
428
+
429
+ const commands = files.filter(f => f.type === 'command');
430
+ const workflows = files.filter(f => f.type === 'workflow');
431
+
432
+ if (commands.length > 0) {
433
+ out.log(` ${c.cyan('Commands')} (${commands.length} files):`);
434
+ for (const f of commands.slice(0, 10)) {
435
+ out.log(` ${c.dim(f.path)}`);
436
+ }
437
+ if (commands.length > 10) {
438
+ out.log(` ${c.dim(`... and ${commands.length - 10} more`)}`);
439
+ }
440
+ out.log('');
441
+ }
442
+
443
+ if (workflows.length > 0) {
444
+ out.log(` ${c.cyan('Workflows')} (${workflows.length} files):`);
445
+ for (const f of workflows.slice(0, 10)) {
446
+ out.log(` ${c.dim(f.path)}`);
447
+ }
448
+ if (workflows.length > 10) {
449
+ out.log(` ${c.dim(`... and ${workflows.length - 10} more`)}`);
450
+ }
451
+ out.log('');
452
+ }
453
+
454
+ out.log(` Total: ${files.length} files\n`);
455
+ }
456
+
457
+ // ============ Installation ============
458
+
132
459
  /**
133
- * Generate backup path with timestamp
460
+ * Process file content with path replacement
134
461
  */
135
- function getBackupPath(filePath) {
136
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
137
- const dir = path.dirname(filePath);
138
- const ext = path.extname(filePath);
139
- const base = path.basename(filePath, ext);
140
- return path.join(dir, `${base}.backup-${timestamp}${ext}`);
462
+ function processContent(srcPath, pathPrefix) {
463
+ if (srcPath.endsWith('.md') || srcPath.endsWith('.json')) {
464
+ let content = fs.readFileSync(srcPath, 'utf8');
465
+ return content.replace(/~\/\.claude\//g, pathPrefix);
466
+ }
467
+ return null;
141
468
  }
142
469
 
143
470
  /**
144
- * Collect all files that would be installed
471
+ * Collect files to install
145
472
  */
146
- function collectFiles(srcDir, destDir, pathPrefix, files = []) {
473
+ function collectInstallFiles(srcDir, destDir, pathPrefix, files = []) {
147
474
  if (!fs.existsSync(srcDir)) return files;
148
475
 
149
476
  const entries = fs.readdirSync(srcDir, { withFileTypes: true });
@@ -153,7 +480,7 @@ function collectFiles(srcDir, destDir, pathPrefix, files = []) {
153
480
  const destPath = path.join(destDir, entry.name);
154
481
 
155
482
  if (entry.isDirectory()) {
156
- collectFiles(srcPath, destPath, pathPrefix, files);
483
+ collectInstallFiles(srcPath, destPath, pathPrefix, files);
157
484
  } else {
158
485
  files.push({ src: srcPath, dest: destPath, name: entry.name });
159
486
  }
@@ -162,50 +489,44 @@ function collectFiles(srcDir, destDir, pathPrefix, files = []) {
162
489
  return files;
163
490
  }
164
491
 
165
- /**
166
- * Process file content with path replacement
167
- */
168
- function processContent(srcPath, pathPrefix) {
169
- if (srcPath.endsWith('.md') || srcPath.endsWith('.json')) {
170
- let content = fs.readFileSync(srcPath, 'utf8');
171
- return content.replace(/~\/\.claude\//g, pathPrefix);
172
- }
173
- return null; // Binary copy
174
- }
175
-
176
492
  /**
177
493
  * Install files with conflict resolution
178
494
  */
179
495
  async function installWithConflictResolution(files, pathPrefix, claudeDir) {
180
496
  const rl = createRL();
181
- let globalChoice = null; // 'overwrite', 'skip', 'backup'
497
+ let globalChoice = null;
182
498
  const stats = { installed: 0, skipped: 0, backed: 0 };
183
499
 
184
- // Check for existing installation
185
500
  const existingFiles = files.filter(f => fs.existsSync(f.dest));
186
501
 
187
- if (existingFiles.length > 0 && !hasForce && !hasBackupAll) {
188
- const relPath = claudeDir.replace(os.homedir(), '~');
189
- console.log(` ${yellow}Found ${existingFiles.length} existing WTF-P file(s) in ${relPath}${reset}\n`);
502
+ if (existingFiles.length > 0 && !hasForce && !hasBackupAll && isInteractive) {
503
+ const relPath = getPathLabel(claudeDir, true);
504
+ out.log(` ${c.yellow(`Found ${existingFiles.length} existing WTF-P file(s) in ${relPath}`)}\n`);
505
+
506
+ if (showExplanations) {
507
+ out.log(` ${c.dim('For each file, choose:')}`);
508
+ out.log(` ${c.dim('[o]verwrite - Replace with new version')}`);
509
+ out.log(` ${c.dim('[s]kip - Keep your existing file')}`);
510
+ out.log(` ${c.dim('[b]ackup - Save existing, then install new')}`);
511
+ out.log(` ${c.dim('[a]ll - Apply same choice to remaining')}\n`);
512
+ }
190
513
  }
191
514
 
192
515
  for (const file of files) {
193
516
  const exists = fs.existsSync(file.dest);
194
517
  const relDest = file.dest.replace(os.homedir(), '~').replace(process.cwd(), '.');
195
518
 
196
- // Ensure directory exists
197
519
  fs.mkdirSync(path.dirname(file.dest), { recursive: true });
198
520
 
199
521
  if (exists && !hasForce && !hasBackupAll) {
200
522
  let choice = globalChoice;
201
523
 
202
- if (!choice) {
524
+ if (!choice && isInteractive) {
203
525
  const answer = await prompt(rl,
204
- ` ${yellow}?${reset} ${dim}${relDest}${reset} exists. [o]verwrite/[s]kip/[b]ackup/[a]ll: `
526
+ ` ${c.yellow('?')} ${c.dim(relDest)} exists. [o]verwrite/[s]kip/[b]ackup/[a]ll: `
205
527
  );
206
528
 
207
529
  if (answer.startsWith('a')) {
208
- // Apply to all: ask which action
209
530
  const allAnswer = await prompt(rl,
210
531
  ` Apply to all: [o]verwrite/[s]kip/[b]ackup: `
211
532
  );
@@ -220,10 +541,13 @@ async function installWithConflictResolution(files, pathPrefix, claudeDir) {
220
541
  } else {
221
542
  choice = 'skip';
222
543
  }
544
+ } else if (!choice) {
545
+ // Non-interactive with existing file and no force: skip
546
+ choice = 'skip';
223
547
  }
224
548
 
225
549
  if (choice === 'skip') {
226
- console.log(` ${dim}○${reset} Skipped ${dim}${relDest}${reset}`);
550
+ out.verbose(` ${c.dim('○')} Skipped ${c.dim(relDest)}`);
227
551
  stats.skipped++;
228
552
  continue;
229
553
  }
@@ -231,17 +555,16 @@ async function installWithConflictResolution(files, pathPrefix, claudeDir) {
231
555
  if (choice === 'backup') {
232
556
  const backupPath = getBackupPath(file.dest);
233
557
  fs.copyFileSync(file.dest, backupPath);
234
- console.log(` ${cyan}↻${reset} Backed up to ${dim}${path.basename(backupPath)}${reset}`);
558
+ out.log(` ${c.cyan('↻')} Backed up to ${c.dim(path.basename(backupPath))}`);
235
559
  stats.backed++;
236
560
  }
237
561
  } else if (exists && hasBackupAll) {
238
562
  const backupPath = getBackupPath(file.dest);
239
563
  fs.copyFileSync(file.dest, backupPath);
240
- console.log(` ${cyan}↻${reset} Backed up ${dim}${relDest}${reset}`);
564
+ out.verbose(` ${c.cyan('↻')} Backed up ${c.dim(relDest)}`);
241
565
  stats.backed++;
242
566
  }
243
567
 
244
- // Install the file
245
568
  const content = processContent(file.src, pathPrefix);
246
569
  if (content !== null) {
247
570
  fs.writeFileSync(file.dest, content);
@@ -250,9 +573,9 @@ async function installWithConflictResolution(files, pathPrefix, claudeDir) {
250
573
  }
251
574
 
252
575
  if (!exists) {
253
- console.log(` ${green}+${reset} ${dim}${relDest}${reset}`);
576
+ out.verbose(` ${c.green('+')} ${c.dim(relDest)}`);
254
577
  } else {
255
- console.log(` ${green}✓${reset} ${dim}${relDest}${reset}`);
578
+ out.verbose(` ${c.green('✓')} ${c.dim(relDest)}`);
256
579
  }
257
580
  stats.installed++;
258
581
  }
@@ -262,69 +585,89 @@ async function installWithConflictResolution(files, pathPrefix, claudeDir) {
262
585
  }
263
586
 
264
587
  /**
265
- * Install to the specified directory
588
+ * Main install function
266
589
  */
267
- async function install(isGlobal) {
590
+ async function install(isGlobal, isUpdate = false) {
268
591
  const src = path.join(__dirname, '..');
269
- const configDir = expandTilde(explicitConfigDir) || expandTilde(process.env.CLAUDE_CONFIG_DIR);
270
- const defaultGlobalDir = configDir || path.join(os.homedir(), '.claude');
271
- const claudeDir = isGlobal
272
- ? defaultGlobalDir
273
- : path.join(process.cwd(), '.claude');
592
+ const claudeDir = getClaudeDir(explicitConfigDir, isGlobal);
593
+ const locationLabel = getPathLabel(claudeDir, isGlobal);
274
594
 
275
- const locationLabel = isGlobal
276
- ? claudeDir.replace(os.homedir(), '~')
277
- : claudeDir.replace(process.cwd(), '.');
595
+ // Validate path
596
+ if (!isValidPath(claudeDir)) {
597
+ out.error(`Invalid path: ${claudeDir}`);
598
+ process.exit(1);
599
+ }
278
600
 
279
601
  const pathPrefix = isGlobal
280
- ? (configDir ? `${claudeDir}/` : '~/.claude/')
602
+ ? (explicitConfigDir ? `${claudeDir}/` : '~/.claude/')
281
603
  : './.claude/';
282
604
 
283
- console.log(` Installing to ${cyan}${locationLabel}${reset}\n`);
605
+ if (!hasQuiet) {
606
+ out.log(` Installing to ${c.cyan(locationLabel)}\n`);
607
+ }
284
608
 
285
- // Collect all files to install
286
- const wtfpSrc = path.join(src, 'commands', 'wtfp');
287
- const wtfpDest = path.join(claudeDir, 'commands', 'wtfp');
288
- const skillSrc = path.join(src, 'write-the-f-paper');
289
- const skillDest = path.join(claudeDir, 'write-the-f-paper');
609
+ // Collect files based on --only flag
610
+ const allFiles = [];
290
611
 
291
- const allFiles = [
292
- ...collectFiles(wtfpSrc, wtfpDest, pathPrefix),
293
- ...collectFiles(skillSrc, skillDest, pathPrefix)
294
- ];
612
+ if (onlyInstall === 'all' || onlyInstall === 'commands') {
613
+ const wtfpSrc = path.join(src, 'commands', 'wtfp');
614
+ const wtfpDest = path.join(claudeDir, 'commands', 'wtfp');
615
+ collectInstallFiles(wtfpSrc, wtfpDest, pathPrefix, allFiles);
616
+ }
617
+
618
+ if (onlyInstall === 'all' || onlyInstall === 'workflows') {
619
+ const skillSrc = path.join(src, 'write-the-f-paper');
620
+ const skillDest = path.join(claudeDir, 'write-the-f-paper');
621
+ collectInstallFiles(skillSrc, skillDest, pathPrefix, allFiles);
622
+ }
295
623
 
296
624
  // Install with conflict resolution
297
625
  const stats = await installWithConflictResolution(allFiles, pathPrefix, claudeDir);
298
626
 
299
- console.log(`
300
- ${green}Done!${reset} Installed: ${stats.installed}, Skipped: ${stats.skipped}, Backed up: ${stats.backed}
627
+ // Write version tracking file
628
+ writeVersionFile(claudeDir, pkg.version, allFiles);
301
629
 
302
- Run ${cyan}/wtfp:help${reset} in Claude Code to get started.
630
+ // Summary
631
+ if (!hasQuiet) {
632
+ out.log(`
633
+ ${c.green('Done!')} Installed: ${stats.installed}, Skipped: ${stats.skipped}, Backed up: ${stats.backed}
303
634
 
304
- ${yellow}Quick Start:${reset}
305
- ${cyan}/wtfp:new-paper${reset} Start a new academic paper
306
- ${cyan}/wtfp:progress${reset} Check your writing status
307
- ${cyan}/wtfp:help${reset} Show all commands
635
+ Run ${c.cyan('/wtfp:help')} in Claude Code to get started.
308
636
  `);
637
+
638
+ if (showExplanations && !isUpdate) {
639
+ out.log(` ${c.yellow('Quick Start:')}`);
640
+ out.log(` ${c.cyan('/wtfp:new-paper')} Start a new academic paper`);
641
+ out.log(` ${c.cyan('/wtfp:progress')} Check your writing status`);
642
+ out.log(` ${c.cyan('/wtfp:help')} Show all commands\n`);
643
+ }
644
+ }
309
645
  }
310
646
 
311
647
  /**
312
- * Prompt for install location
648
+ * Prompt for install location (interactive mode)
313
649
  */
314
650
  async function promptLocation() {
315
651
  const rl = createRL();
316
652
 
317
- const configDir = expandTilde(explicitConfigDir) || expandTilde(process.env.CLAUDE_CONFIG_DIR);
318
- const globalPath = configDir || path.join(os.homedir(), '.claude');
319
- const globalLabel = globalPath.replace(os.homedir(), '~');
320
-
321
- console.log(` ${yellow}Where would you like to install?${reset}
653
+ const globalPath = getClaudeDir(explicitConfigDir, true);
654
+ const globalLabel = getPathLabel(globalPath, true);
322
655
 
323
- ${cyan}1${reset}) Global ${dim}(${globalLabel})${reset} - available in all projects
324
- ${cyan}2${reset}) Local ${dim}(./.claude)${reset} - this project only
656
+ out.log(` ${c.yellow('Where would you like to install?')}
325
657
  `);
326
658
 
327
- const answer = await prompt(rl, ` Choice ${dim}[1]${reset}: `);
659
+ if (showExplanations) {
660
+ out.log(` ${c.cyan('1)')} Global ${c.dim(`(${globalLabel})`)} - available in all projects`);
661
+ out.log(` ${c.dim('Recommended for most users')}`);
662
+ out.log(` ${c.cyan('2)')} Local ${c.dim('(./.claude)')} - this project only`);
663
+ out.log(` ${c.dim('Useful for project-specific configurations')}`);
664
+ } else {
665
+ out.log(` ${c.cyan('1)')} Global ${c.dim(`(${globalLabel})`)} - available in all projects`);
666
+ out.log(` ${c.cyan('2)')} Local ${c.dim('(./.claude)')} - this project only`);
667
+ }
668
+ out.log('');
669
+
670
+ const answer = await prompt(rl, ` Choice ${c.dim('[1]')}: `);
328
671
  rl.close();
329
672
 
330
673
  const choice = answer || '1';
@@ -332,27 +675,83 @@ async function promptLocation() {
332
675
  await install(isGlobal);
333
676
  }
334
677
 
335
- // Main
678
+ // ============ Main ============
679
+
336
680
  async function main() {
681
+ // Handle --version
682
+ if (hasVersion) {
683
+ await showVersion();
684
+ return;
685
+ }
686
+
687
+ // Handle --help
688
+ if (hasHelp) {
689
+ showHelp();
690
+ return;
691
+ }
692
+
693
+ // Handle subcommands
694
+ if (subcommand === 'status') {
695
+ await showStatus();
696
+ return;
697
+ }
698
+
699
+ if (subcommand === 'doctor') {
700
+ await runDoctor();
701
+ return;
702
+ }
703
+
704
+ if (subcommand === 'update') {
705
+ await runUpdate();
706
+ return;
707
+ }
708
+
709
+ // Handle --list
710
+ if (hasList) {
711
+ showList();
712
+ return;
713
+ }
714
+
715
+ // Show banner for install
716
+ if (!hasQuiet) {
717
+ console.log(banner);
718
+ }
719
+
720
+ // Validate conflicting flags
337
721
  if (hasGlobal && hasLocal) {
338
- console.error(` ${yellow}Cannot specify both --global and --local${reset}`);
722
+ out.error('Cannot specify both --global and --local');
723
+ process.exit(1);
724
+ }
725
+ if (explicitConfigDir && hasLocal) {
726
+ out.error('Cannot use --config-dir with --local');
339
727
  process.exit(1);
340
- } else if (explicitConfigDir && hasLocal) {
341
- console.error(` ${yellow}Cannot use --config-dir with --local${reset}`);
728
+ }
729
+ if (hasForce && hasBackupAll) {
730
+ out.error('Cannot use both --force and --backup-all');
342
731
  process.exit(1);
343
- } else if (hasForce && hasBackupAll) {
344
- console.error(` ${yellow}Cannot use both --force and --backup-all${reset}`);
732
+ }
733
+ if (hasBeginner && hasAdvanced) {
734
+ out.error('Cannot use both --beginner and --advanced');
345
735
  process.exit(1);
346
- } else if (hasGlobal) {
736
+ }
737
+
738
+ // Run install
739
+ if (hasGlobal) {
347
740
  await install(true);
348
741
  } else if (hasLocal) {
349
742
  await install(false);
350
- } else {
743
+ } else if (isInteractive) {
351
744
  await promptLocation();
745
+ } else {
746
+ // Non-interactive default: global
747
+ await install(true);
352
748
  }
353
749
  }
354
750
 
355
751
  main().catch(err => {
356
- console.error(` ${red}Error:${reset} ${err.message}`);
752
+ out.error(err.message);
753
+ if (hasVerbose) {
754
+ console.error(err.stack);
755
+ }
357
756
  process.exit(1);
358
757
  });