wtf-p 0.2.0 → 0.4.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 (122) hide show
  1. package/README.md +24 -2
  2. package/bin/commands/doctor.js +100 -0
  3. package/bin/commands/install-logic.js +231 -0
  4. package/bin/commands/list.js +88 -0
  5. package/bin/commands/status.js +85 -0
  6. package/bin/commands/update.js +50 -0
  7. package/bin/install.js +78 -629
  8. package/bin/lib/analyze-impact.js +105 -0
  9. package/bin/lib/bib-format.js +161 -0
  10. package/bin/lib/bib-index.js +104 -0
  11. package/bin/lib/citation-fetcher.js +299 -0
  12. package/bin/lib/citation-ranker.js +133 -0
  13. package/bin/lib/manifest.js +59 -0
  14. package/bin/lib/scholar-lookup.js +188 -0
  15. package/bin/lib/semantic-scholar.js +184 -0
  16. package/bin/lib/utils.js +37 -17
  17. package/bin/lib/wcn-compiler.js +226 -0
  18. package/bin/uninstall.js +51 -17
  19. package/core/write-the-f-paper/templates/posters/basic.md +34 -0
  20. package/core/write-the-f-paper/templates/slides/basic.md +31 -0
  21. package/package.json +12 -4
  22. package/vendors/claude/.claude-plugin/plugin.json +12 -0
  23. package/vendors/claude/agents/wtfp/citation-expert.md +45 -0
  24. package/vendors/claude/agents/wtfp/citation-formatter.md +42 -0
  25. package/vendors/claude/agents/wtfp/citation-retriever.md +31 -0
  26. package/{commands → vendors/claude/commands}/wtfp/analyze-bib.md +41 -52
  27. package/{commands → vendors/claude/commands}/wtfp/check-refs.md +11 -9
  28. package/vendors/claude/commands/wtfp/create-poster.md +42 -0
  29. package/vendors/claude/commands/wtfp/create-slides.md +41 -0
  30. package/{commands → vendors/claude/commands}/wtfp/help.md +3 -0
  31. package/{commands → vendors/claude/commands}/wtfp/new-paper.md +42 -81
  32. package/{commands → vendors/claude/commands}/wtfp/research-gap.md +3 -4
  33. package/vendors/claude/mcp/research-server/package.json +13 -0
  34. package/vendors/claude/mcp/research-server/src/index.js +133 -0
  35. package/vendors/claude/skills/wtfp/echarts/SKILL.md +47 -0
  36. package/vendors/claude/skills/wtfp/marp/SKILL.md +49 -0
  37. /package/{write-the-f-paper → core/write-the-f-paper}/references/checkpoints.md +0 -0
  38. /package/{write-the-f-paper → core/write-the-f-paper}/references/continuation-format.md +0 -0
  39. /package/{write-the-f-paper → core/write-the-f-paper}/references/git-integration.md +0 -0
  40. /package/{write-the-f-paper → core/write-the-f-paper}/references/github-contrib.md +0 -0
  41. /package/{write-the-f-paper → core/write-the-f-paper}/references/imrad-structure.md +0 -0
  42. /package/{write-the-f-paper → core/write-the-f-paper}/references/length-estimation.md +0 -0
  43. /package/{write-the-f-paper → core/write-the-f-paper}/references/plan-format.md +0 -0
  44. /package/{write-the-f-paper → core/write-the-f-paper}/references/principles.md +0 -0
  45. /package/{write-the-f-paper → core/write-the-f-paper}/references/questioning.md +0 -0
  46. /package/{write-the-f-paper → core/write-the-f-paper}/references/research-pitfalls.md +0 -0
  47. /package/{write-the-f-paper → core/write-the-f-paper}/templates/config.json +0 -0
  48. /package/{write-the-f-paper → core/write-the-f-paper}/templates/context.md +0 -0
  49. /package/{write-the-f-paper → core/write-the-f-paper}/templates/continue-here.md +0 -0
  50. /package/{write-the-f-paper → core/write-the-f-paper}/templates/data-inventory.md +0 -0
  51. /package/{write-the-f-paper → core/write-the-f-paper}/templates/discovery.md +0 -0
  52. /package/{write-the-f-paper → core/write-the-f-paper}/templates/issues.md +0 -0
  53. /package/{write-the-f-paper → core/write-the-f-paper}/templates/manuscript.md +0 -0
  54. /package/{write-the-f-paper → core/write-the-f-paper}/templates/narrative-arc.md +0 -0
  55. /package/{write-the-f-paper → core/write-the-f-paper}/templates/outline.md +0 -0
  56. /package/{write-the-f-paper → core/write-the-f-paper}/templates/phase-prompt.md +0 -0
  57. /package/{write-the-f-paper → core/write-the-f-paper}/templates/prior-drafts.md +0 -0
  58. /package/{write-the-f-paper → core/write-the-f-paper}/templates/project-context/argument-structure.md +0 -0
  59. /package/{write-the-f-paper → core/write-the-f-paper}/templates/project-context/bibliography.md +0 -0
  60. /package/{write-the-f-paper → core/write-the-f-paper}/templates/project-context/gaps.md +0 -0
  61. /package/{write-the-f-paper → core/write-the-f-paper}/templates/project-context/imrad.md +0 -0
  62. /package/{write-the-f-paper → core/write-the-f-paper}/templates/project-context/peer-review.md +0 -0
  63. /package/{write-the-f-paper → core/write-the-f-paper}/templates/project-context/style-guide.md +0 -0
  64. /package/{write-the-f-paper → core/write-the-f-paper}/templates/project-context/tools.md +0 -0
  65. /package/{write-the-f-paper → core/write-the-f-paper}/templates/project.md +0 -0
  66. /package/{write-the-f-paper → core/write-the-f-paper}/templates/research.md +0 -0
  67. /package/{write-the-f-paper → core/write-the-f-paper}/templates/review-comments.md +0 -0
  68. /package/{write-the-f-paper → core/write-the-f-paper}/templates/revision-issues.md +0 -0
  69. /package/{write-the-f-paper → core/write-the-f-paper}/templates/roadmap.md +0 -0
  70. /package/{write-the-f-paper → core/write-the-f-paper}/templates/section-archive.md +0 -0
  71. /package/{write-the-f-paper → core/write-the-f-paper}/templates/section-context.md +0 -0
  72. /package/{write-the-f-paper → core/write-the-f-paper}/templates/section.md +0 -0
  73. /package/{write-the-f-paper → core/write-the-f-paper}/templates/state.md +0 -0
  74. /package/{write-the-f-paper → core/write-the-f-paper}/templates/summary.md +0 -0
  75. /package/{write-the-f-paper → core/write-the-f-paper}/venues/acm-cs.yaml +0 -0
  76. /package/{write-the-f-paper → core/write-the-f-paper}/venues/arxiv-ml.yaml +0 -0
  77. /package/{write-the-f-paper → core/write-the-f-paper}/venues/ieee-cs.yaml +0 -0
  78. /package/{write-the-f-paper → core/write-the-f-paper}/venues/nature.yaml +0 -0
  79. /package/{write-the-f-paper → core/write-the-f-paper}/venues/thesis-chapter.yaml +0 -0
  80. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/create-outline.md +0 -0
  81. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/create-outline.wcn.md +0 -0
  82. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/discuss-section.md +0 -0
  83. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/discuss-section.wcn.md +0 -0
  84. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/execute-section.md +0 -0
  85. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/execute-section.wcn.md +0 -0
  86. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/list-assumptions.md +0 -0
  87. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/list-assumptions.wcn.md +0 -0
  88. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/lit-review-phase.md +0 -0
  89. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/lit-review-phase.wcn.md +0 -0
  90. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/map-project.md +0 -0
  91. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/map-project.wcn.md +0 -0
  92. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/plan-section.md +0 -0
  93. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/plan-section.wcn.md +0 -0
  94. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/research-gap.md +0 -0
  95. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/research-gap.wcn.md +0 -0
  96. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/resume-paper.md +0 -0
  97. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/resume-paper.wcn.md +0 -0
  98. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/review-section.md +0 -0
  99. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/review-section.wcn.md +0 -0
  100. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/submit-draft.md +0 -0
  101. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/submit-draft.wcn.md +0 -0
  102. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/transition.md +0 -0
  103. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/transition.wcn.md +0 -0
  104. /package/{commands → vendors/claude/commands}/wtfp/contribute.md +0 -0
  105. /package/{commands → vendors/claude/commands}/wtfp/create-outline.md +0 -0
  106. /package/{commands → vendors/claude/commands}/wtfp/discuss-section.md +0 -0
  107. /package/{commands → vendors/claude/commands}/wtfp/export-latex.md +0 -0
  108. /package/{commands → vendors/claude/commands}/wtfp/insert-section.md +0 -0
  109. /package/{commands → vendors/claude/commands}/wtfp/list-assumptions.md +0 -0
  110. /package/{commands → vendors/claude/commands}/wtfp/map-project.md +0 -0
  111. /package/{commands → vendors/claude/commands}/wtfp/pause-writing.md +0 -0
  112. /package/{commands → vendors/claude/commands}/wtfp/plan-revision.md +0 -0
  113. /package/{commands → vendors/claude/commands}/wtfp/plan-section.md +0 -0
  114. /package/{commands → vendors/claude/commands}/wtfp/polish-prose.md +0 -0
  115. /package/{commands → vendors/claude/commands}/wtfp/progress.md +0 -0
  116. /package/{commands → vendors/claude/commands}/wtfp/remove-section.md +0 -0
  117. /package/{commands → vendors/claude/commands}/wtfp/report-bug.md +0 -0
  118. /package/{commands → vendors/claude/commands}/wtfp/request-feature.md +0 -0
  119. /package/{commands → vendors/claude/commands}/wtfp/resume-writing.md +0 -0
  120. /package/{commands → vendors/claude/commands}/wtfp/review-section.md +0 -0
  121. /package/{commands → vendors/claude/commands}/wtfp/submit-milestone.md +0 -0
  122. /package/{commands → vendors/claude/commands}/wtfp/write-section.md +0 -0
package/bin/install.js CHANGED
@@ -1,35 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const fs = require('fs');
4
3
  const path = require('path');
5
- const os = require('os');
6
-
7
4
  const {
8
- expandTilde,
9
- normalizePath,
10
- isValidPath,
11
- getClaudeDir,
12
- getPathLabel,
13
- readInstalledVersion,
14
- writeVersionFile,
15
- detectInstallation,
16
- collectFiles,
17
- createColors,
18
5
  createOutput,
19
6
  createRL,
20
7
  prompt,
21
- getBackupPath
22
8
  } = require('./lib/utils');
23
9
 
24
10
  // Get version from package.json
25
11
  const pkg = require('../package.json');
26
12
 
27
- // ============ Argument Parsing ============
13
+ // Command modules
14
+ const showStatus = require('./commands/status');
15
+ const runDoctor = require('./commands/doctor');
16
+ const runUpdate = require('./commands/update');
17
+ const install = require('./commands/install-logic');
18
+ const showList = require('./commands/list');
19
+
20
+ // ============ Argument Parsing ============
28
21
 
29
22
  const args = process.argv.slice(2);
30
23
 
31
- // Detect subcommand (first non-flag argument)
32
- const subcommand = args.find(arg => !arg.startsWith('-') && ['status', 'doctor', 'update'].includes(arg));
24
+ // Detect subcommand
25
+ const subcommands = ['status', 'doctor', 'update'];
26
+ const subcommand = args.find(arg => !arg.startsWith('-') && subcommands.includes(arg));
33
27
  const subcommandIndex = args.indexOf(subcommand);
34
28
  if (subcommandIndex !== -1) {
35
29
  args.splice(subcommandIndex, 1);
@@ -49,70 +43,62 @@ const hasVerbose = args.includes('--verbose');
49
43
  const hasBeginner = args.includes('--beginner');
50
44
  const hasAdvanced = args.includes('--advanced');
51
45
 
52
- // Parse --config-dir argument
53
46
  function parseConfigDirArg() {
54
47
  const configDirIndex = args.findIndex(arg => arg === '--config-dir' || arg === '-c');
55
48
  if (configDirIndex !== -1) {
56
49
  const nextArg = args[configDirIndex + 1];
57
- if (!nextArg || nextArg.startsWith('-')) {
58
- console.error(' --config-dir requires a path argument');
59
- process.exit(1);
60
- }
61
- return nextArg;
50
+ if (nextArg && !nextArg.startsWith('-')) return nextArg;
62
51
  }
63
52
  const configDirArg = args.find(arg => arg.startsWith('--config-dir=') || arg.startsWith('-c='));
64
- if (configDirArg) {
65
- return configDirArg.split('=')[1];
66
- }
53
+ if (configDirArg) return configDirArg.split('=')[1];
67
54
  return null;
68
55
  }
69
56
 
70
- // Parse --only argument
71
57
  function parseOnlyArg() {
72
58
  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
- }
59
+ if (onlyArg) return onlyArg.split('=')[1];
81
60
  return 'all';
82
61
  }
83
62
 
84
- const explicitConfigDir = parseConfigDirArg();
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);
63
+ const options = {
64
+ explicitConfigDir: parseConfigDirArg(),
65
+ onlyInstall: parseOnlyArg(),
66
+ hasGlobal,
67
+ hasLocal,
68
+ hasForce,
69
+ hasBackupAll,
70
+ hasQuiet,
71
+ hasVerbose,
72
+ hasBeginner,
73
+ hasAdvanced,
74
+ isInteractive: process.stdout.isTTY && process.stdin.isTTY && !hasQuiet && !hasAdvanced,
75
+ };
76
+ options.showExplanations = options.hasBeginner || (options.isInteractive && !options.hasAdvanced);
77
+
78
+ const useColors = !hasNoColor && (process.stdout.isTTY || process.env.FORCE_COLOR);
94
79
  const out = createOutput({ quiet: hasQuiet, verbose: hasVerbose, useColors });
95
- const c = out.colors;
80
+ options.out = out;
96
81
 
97
- // ============ Banner ============
82
+ // ============ Banner ============
98
83
 
99
84
  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}`)}
85
+ ${out.colors.magenta('██╗ ██╗████████╗███████╗ ██████╗')}
86
+ ${out.colors.magenta('██║ ██║╚══██╔══╝██╔════╝ ██╔══██╗')}
87
+ ${out.colors.magenta('██║ █╗ ██║ ██║ █████╗ █████╗██████╔╝')}
88
+ ${out.colors.magenta('██║███╗██║ ██║ ██╔══╝ ╚════╝██╔═══╝')}
89
+ ${out.colors.magenta('╚███╔███╔╝ ██║ ██║ ██║')}
90
+ ${out.colors.magenta(' ╚══╝╚══╝ ╚═╝ ╚═╝ ╚═╝')}
91
+
92
+ ${out.colors.cyan('Write The F***ing Paper')} ${out.colors.dim(`v${pkg.version}`)}
108
93
  Context engineering for academic writing.
109
94
  `;
110
95
 
111
- // ============ Help Text ============
96
+ // ============ Help Text ============
112
97
 
113
98
  function showHelp() {
99
+ const c = out.colors;
114
100
  console.log(banner);
115
- console.log(` ${c.yellow('Usage:')} npx wtf-p [command] [options]
101
+ console.log(` ${c.yellow('Usage:')} npx wtf-p [command] [options]
116
102
 
117
103
  ${c.yellow('Commands:')}
118
104
  ${c.cyan('status')} Show current installation state
@@ -135,7 +121,6 @@ function showHelp() {
135
121
  ${c.cyan('--verbose')} Show detailed progress
136
122
 
137
123
  ${c.yellow('Other:')}
138
- ${c.cyan('--list')} Preview what would be installed
139
124
  ${c.cyan('-v, --version')} Show version information
140
125
  ${c.cyan('-h, --help')} Show this help message
141
126
 
@@ -146,612 +131,76 @@ function showHelp() {
146
131
  ${c.dim('# Quick global install')}
147
132
  npx wtf-p --global --advanced
148
133
 
149
- ${c.dim('# Install only commands')}
150
- npx wtf-p --global --only=commands
151
-
152
- ${c.dim('# Check installation status')}
153
- npx wtf-p status
154
-
155
- ${c.dim('# Diagnose issues')}
156
- npx wtf-p doctor
157
-
158
134
  ${c.yellow('After installation:')}
159
135
  Run ${c.cyan('/wtfp:help')} in Claude Code to get started.
160
136
  `);
161
137
  }
162
138
 
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
178
- }
179
- }
180
-
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
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
- }
256
- }
257
-
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
- }
352
- }
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
-
459
- /**
460
- * Process file content with path replacement
461
- */
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;
468
- }
469
-
470
- /**
471
- * Collect files to install
472
- */
473
- function collectInstallFiles(srcDir, destDir, pathPrefix, files = []) {
474
- if (!fs.existsSync(srcDir)) return files;
475
-
476
- const entries = fs.readdirSync(srcDir, { withFileTypes: true });
477
-
478
- for (const entry of entries) {
479
- const srcPath = path.join(srcDir, entry.name);
480
- const destPath = path.join(destDir, entry.name);
481
-
482
- if (entry.isDirectory()) {
483
- collectInstallFiles(srcPath, destPath, pathPrefix, files);
484
- } else {
485
- files.push({ src: srcPath, dest: destPath, name: entry.name });
486
- }
487
- }
488
-
489
- return files;
490
- }
491
-
492
- /**
493
- * Install files with conflict resolution
494
- */
495
- async function installWithConflictResolution(files, pathPrefix, claudeDir) {
496
- const rl = createRL();
497
- let globalChoice = null;
498
- const stats = { installed: 0, skipped: 0, backed: 0 };
499
-
500
- const existingFiles = files.filter(f => fs.existsSync(f.dest));
501
-
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
- }
513
- }
514
-
515
- for (const file of files) {
516
- const exists = fs.existsSync(file.dest);
517
- const relDest = file.dest.replace(os.homedir(), '~').replace(process.cwd(), '.');
518
-
519
- fs.mkdirSync(path.dirname(file.dest), { recursive: true });
520
-
521
- if (exists && !hasForce && !hasBackupAll) {
522
- let choice = globalChoice;
523
-
524
- if (!choice && isInteractive) {
525
- const answer = await prompt(rl,
526
- ` ${c.yellow('?')} ${c.dim(relDest)} exists. [o]verwrite/[s]kip/[b]ackup/[a]ll: `
527
- );
528
-
529
- if (answer.startsWith('a')) {
530
- const allAnswer = await prompt(rl,
531
- ` Apply to all: [o]verwrite/[s]kip/[b]ackup: `
532
- );
533
- if (allAnswer.startsWith('o')) globalChoice = 'overwrite';
534
- else if (allAnswer.startsWith('b')) globalChoice = 'backup';
535
- else globalChoice = 'skip';
536
- choice = globalChoice;
537
- } else if (answer.startsWith('o')) {
538
- choice = 'overwrite';
539
- } else if (answer.startsWith('b')) {
540
- choice = 'backup';
541
- } else {
542
- choice = 'skip';
543
- }
544
- } else if (!choice) {
545
- // Non-interactive with existing file and no force: skip
546
- choice = 'skip';
547
- }
548
-
549
- if (choice === 'skip') {
550
- out.verbose(` ${c.dim('○')} Skipped ${c.dim(relDest)}`);
551
- stats.skipped++;
552
- continue;
553
- }
554
-
555
- if (choice === 'backup') {
556
- const backupPath = getBackupPath(file.dest);
557
- fs.copyFileSync(file.dest, backupPath);
558
- out.log(` ${c.cyan('↻')} Backed up to ${c.dim(path.basename(backupPath))}`);
559
- stats.backed++;
560
- }
561
- } else if (exists && hasBackupAll) {
562
- const backupPath = getBackupPath(file.dest);
563
- fs.copyFileSync(file.dest, backupPath);
564
- out.verbose(` ${c.cyan('↻')} Backed up ${c.dim(relDest)}`);
565
- stats.backed++;
566
- }
567
-
568
- const content = processContent(file.src, pathPrefix);
569
- if (content !== null) {
570
- fs.writeFileSync(file.dest, content);
571
- } else {
572
- fs.copyFileSync(file.src, file.dest);
573
- }
574
-
575
- if (!exists) {
576
- out.verbose(` ${c.green('+')} ${c.dim(relDest)}`);
577
- } else {
578
- out.verbose(` ${c.green('✓')} ${c.dim(relDest)}`);
579
- }
580
- stats.installed++;
581
- }
582
-
583
- rl.close();
584
- return stats;
585
- }
586
-
587
- /**
588
- * Main install function
589
- */
590
- async function install(isGlobal, isUpdate = false) {
591
- const src = path.join(__dirname, '..');
592
- const claudeDir = getClaudeDir(explicitConfigDir, isGlobal);
593
- const locationLabel = getPathLabel(claudeDir, isGlobal);
594
-
595
- // Validate path
596
- if (!isValidPath(claudeDir)) {
597
- out.error(`Invalid path: ${claudeDir}`);
598
- process.exit(1);
599
- }
600
-
601
- const pathPrefix = isGlobal
602
- ? (explicitConfigDir ? `${claudeDir}/` : '~/.claude/')
603
- : './.claude/';
604
-
605
- if (!hasQuiet) {
606
- out.log(` Installing to ${c.cyan(locationLabel)}\n`);
607
- }
608
-
609
- // Collect files based on --only flag
610
- const allFiles = [];
611
-
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
- }
623
-
624
- // Install with conflict resolution
625
- const stats = await installWithConflictResolution(allFiles, pathPrefix, claudeDir);
626
-
627
- // Write version tracking file
628
- writeVersionFile(claudeDir, pkg.version, allFiles);
629
-
630
- // Summary
631
- if (!hasQuiet) {
632
- out.log(`
633
- ${c.green('Done!')} Installed: ${stats.installed}, Skipped: ${stats.skipped}, Backed up: ${stats.backed}
634
-
635
- Run ${c.cyan('/wtfp:help')} in Claude Code to get started.
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
- }
645
- }
646
-
647
- /**
648
- * Prompt for install location (interactive mode)
649
- */
650
- async function promptLocation() {
651
- const rl = createRL();
652
-
653
- const globalPath = getClaudeDir(explicitConfigDir, true);
654
- const globalLabel = getPathLabel(globalPath, true);
655
-
656
- out.log(` ${c.yellow('Where would you like to install?')}
657
- `);
658
-
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]')}: `);
671
- rl.close();
672
-
673
- const choice = answer || '1';
674
- const isGlobal = choice !== '2';
675
- await install(isGlobal);
676
- }
677
-
678
- // ============ Main ============
139
+ // ============ Main ============
679
140
 
680
141
  async function main() {
681
- // Handle --version
682
142
  if (hasVersion) {
683
- await showVersion();
143
+ console.log(`wtf-p v${pkg.version}`);
684
144
  return;
685
145
  }
686
146
 
687
- // Handle --help
688
147
  if (hasHelp) {
689
148
  showHelp();
690
149
  return;
691
150
  }
692
151
 
693
- // Handle subcommands
694
- if (subcommand === 'status') {
695
- await showStatus();
152
+ // Handle --list
153
+ if (hasList) {
154
+ if (!hasQuiet) console.log(banner);
155
+ showList(options);
696
156
  return;
697
157
  }
698
158
 
699
- if (subcommand === 'doctor') {
700
- await runDoctor();
159
+ if (subcommand === 'status') {
160
+ if (!hasQuiet) console.log(banner);
161
+ await showStatus(options, pkg);
701
162
  return;
702
163
  }
703
164
 
704
- if (subcommand === 'update') {
705
- await runUpdate();
165
+ if (subcommand === 'doctor') {
166
+ if (!hasQuiet) console.log(banner);
167
+ await runDoctor(options);
706
168
  return;
707
169
  }
708
170
 
709
- // Handle --list
710
- if (hasList) {
711
- showList();
171
+ if (subcommand === 'update') {
172
+ if (!hasQuiet) console.log(banner);
173
+ await runUpdate(options, pkg, install);
712
174
  return;
713
175
  }
714
176
 
715
- // Show banner for install
716
- if (!hasQuiet) {
717
- console.log(banner);
718
- }
719
-
720
- // Validate conflicting flags
721
- if (hasGlobal && hasLocal) {
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');
727
- process.exit(1);
728
- }
729
- if (hasForce && hasBackupAll) {
730
- out.error('Cannot use both --force and --backup-all');
731
- process.exit(1);
732
- }
733
- if (hasBeginner && hasAdvanced) {
734
- out.error('Cannot use both --beginner and --advanced');
735
- process.exit(1);
736
- }
177
+ // Default: Install
178
+ if (!hasQuiet) console.log(banner);
737
179
 
738
- // Run install
739
180
  if (hasGlobal) {
740
- await install(true);
181
+ await install(true, false, options, pkg);
741
182
  } else if (hasLocal) {
742
- await install(false);
743
- } else if (isInteractive) {
744
- await promptLocation();
183
+ await install(false, false, options, pkg);
184
+ } else if (options.isInteractive) {
185
+ const rl = createRL();
186
+ out.log(` ${out.colors.yellow('Where would you like to install?')}
187
+ `);
188
+ out.log(` ${out.colors.cyan('1)')} Global (~/.claude) - available in all projects`);
189
+ out.log(` ${out.colors.cyan('2)')} Local (./.claude) - this project only
190
+ `);
191
+
192
+ const answer = await prompt(rl, ` Choice ${out.colors.dim('[1]')}: `);
193
+ rl.close();
194
+
195
+ const choice = answer || '1';
196
+ await install(choice !== '2', false, options, pkg);
745
197
  } else {
746
- // Non-interactive default: global
747
- await install(true);
198
+ await install(true, false, options, pkg);
748
199
  }
749
200
  }
750
201
 
751
202
  main().catch(err => {
752
203
  out.error(err.message);
753
- if (hasVerbose) {
754
- console.error(err.stack);
755
- }
204
+ if (hasVerbose) console.error(err.stack);
756
205
  process.exit(1);
757
- });
206
+ });