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/README.md +210 -229
- package/bin/install.js +557 -158
- package/bin/lib/utils.js +318 -0
- package/bin/uninstall.js +163 -201
- package/commands/wtfp/contribute.md +278 -0
- package/commands/wtfp/help.md +25 -0
- package/commands/wtfp/report-bug.md +131 -0
- package/commands/wtfp/request-feature.md +160 -0
- package/package.json +4 -2
- package/write-the-f-paper/references/github-contrib.md +216 -0
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
111
|
+
// ============ Help Text ============
|
|
61
112
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
console.log(` ${yellow
|
|
113
|
+
function showHelp() {
|
|
114
|
+
console.log(banner);
|
|
115
|
+
console.log(` ${c.yellow('Usage:')} npx wtf-p [command] [options]
|
|
65
116
|
|
|
66
|
-
${yellow}
|
|
67
|
-
${cyan}
|
|
68
|
-
${cyan}
|
|
69
|
-
${cyan}
|
|
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
|
|
75
|
-
|
|
76
|
-
${
|
|
77
|
-
${
|
|
78
|
-
${
|
|
79
|
-
${
|
|
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}
|
|
82
|
-
${
|
|
83
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
89
|
-
|
|
142
|
+
${c.yellow('Examples:')}
|
|
143
|
+
${c.dim('# Interactive install with prompts')}
|
|
144
|
+
npx wtf-p
|
|
90
145
|
|
|
91
|
-
${dim
|
|
92
|
-
npx wtf-p --global --
|
|
146
|
+
${c.dim('# Quick global install')}
|
|
147
|
+
npx wtf-p --global --advanced
|
|
93
148
|
|
|
94
|
-
|
|
95
|
-
npx wtf-p
|
|
149
|
+
${c.dim('# Install only commands')}
|
|
150
|
+
npx wtf-p --global --only=commands
|
|
96
151
|
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
*
|
|
460
|
+
* Process file content with path replacement
|
|
134
461
|
*/
|
|
135
|
-
function
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
return
|
|
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
|
|
471
|
+
* Collect files to install
|
|
145
472
|
*/
|
|
146
|
-
function
|
|
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
|
-
|
|
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;
|
|
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
|
|
189
|
-
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
576
|
+
out.verbose(` ${c.green('+')} ${c.dim(relDest)}`);
|
|
254
577
|
} else {
|
|
255
|
-
|
|
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
|
-
*
|
|
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
|
|
270
|
-
const
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
: claudeDir
|
|
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
|
-
? (
|
|
602
|
+
? (explicitConfigDir ? `${claudeDir}/` : '~/.claude/')
|
|
281
603
|
: './.claude/';
|
|
282
604
|
|
|
283
|
-
|
|
605
|
+
if (!hasQuiet) {
|
|
606
|
+
out.log(` Installing to ${c.cyan(locationLabel)}\n`);
|
|
607
|
+
}
|
|
284
608
|
|
|
285
|
-
// Collect
|
|
286
|
-
const
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
300
|
-
|
|
627
|
+
// Write version tracking file
|
|
628
|
+
writeVersionFile(claudeDir, pkg.version, allFiles);
|
|
301
629
|
|
|
302
|
-
|
|
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
|
-
${
|
|
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
|
|
318
|
-
const
|
|
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
|
-
${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
341
|
-
|
|
728
|
+
}
|
|
729
|
+
if (hasForce && hasBackupAll) {
|
|
730
|
+
out.error('Cannot use both --force and --backup-all');
|
|
342
731
|
process.exit(1);
|
|
343
|
-
}
|
|
344
|
-
|
|
732
|
+
}
|
|
733
|
+
if (hasBeginner && hasAdvanced) {
|
|
734
|
+
out.error('Cannot use both --beginner and --advanced');
|
|
345
735
|
process.exit(1);
|
|
346
|
-
}
|
|
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
|
-
|
|
752
|
+
out.error(err.message);
|
|
753
|
+
if (hasVerbose) {
|
|
754
|
+
console.error(err.stack);
|
|
755
|
+
}
|
|
357
756
|
process.exit(1);
|
|
358
757
|
});
|