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/lib/utils.js
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for WTF-P CLI
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const readline = require('readline');
|
|
9
|
+
|
|
10
|
+
// Version tracking file name
|
|
11
|
+
const VERSION_FILE = '.wtfp-version';
|
|
12
|
+
|
|
13
|
+
// ============ Path Utilities ============
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Expand ~ to home directory
|
|
17
|
+
*/
|
|
18
|
+
function expandTilde(filePath) {
|
|
19
|
+
if (filePath && filePath.startsWith('~/')) {
|
|
20
|
+
return path.join(os.homedir(), filePath.slice(2));
|
|
21
|
+
}
|
|
22
|
+
return filePath;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Normalize path: expand tilde, resolve to absolute, resolve symlinks
|
|
27
|
+
*/
|
|
28
|
+
function normalizePath(inputPath) {
|
|
29
|
+
if (!inputPath) return inputPath;
|
|
30
|
+
|
|
31
|
+
// Expand tilde
|
|
32
|
+
let normalized = expandTilde(inputPath);
|
|
33
|
+
|
|
34
|
+
// Resolve to absolute path
|
|
35
|
+
normalized = path.resolve(normalized);
|
|
36
|
+
|
|
37
|
+
// Resolve symlinks if path exists
|
|
38
|
+
if (fs.existsSync(normalized)) {
|
|
39
|
+
try {
|
|
40
|
+
normalized = fs.realpathSync(normalized);
|
|
41
|
+
} catch {
|
|
42
|
+
// Keep as-is if realpath fails (permissions, etc.)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return normalized;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Validate path for safety
|
|
51
|
+
*/
|
|
52
|
+
function isValidPath(inputPath) {
|
|
53
|
+
if (!inputPath || typeof inputPath !== 'string') return false;
|
|
54
|
+
|
|
55
|
+
// Check for null bytes (security issue)
|
|
56
|
+
if (inputPath.includes('\0')) return false;
|
|
57
|
+
|
|
58
|
+
// Check path length (Windows ~260, but we use 1024 for safety)
|
|
59
|
+
if (inputPath.length > 1024) return false;
|
|
60
|
+
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get the Claude config directory (respects CLAUDE_CONFIG_DIR env var)
|
|
66
|
+
*/
|
|
67
|
+
function getClaudeDir(explicitConfigDir, isGlobal = true) {
|
|
68
|
+
if (!isGlobal) {
|
|
69
|
+
return path.join(process.cwd(), '.claude');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const configDir = normalizePath(explicitConfigDir) ||
|
|
73
|
+
normalizePath(process.env.CLAUDE_CONFIG_DIR);
|
|
74
|
+
|
|
75
|
+
return configDir || path.join(os.homedir(), '.claude');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get human-readable label for a path (with ~ for homedir)
|
|
80
|
+
*/
|
|
81
|
+
function getPathLabel(fullPath, isGlobal = true) {
|
|
82
|
+
if (isGlobal) {
|
|
83
|
+
return fullPath.replace(os.homedir(), '~');
|
|
84
|
+
}
|
|
85
|
+
return fullPath.replace(process.cwd(), '.');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ============ Version Tracking ============
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Read installed WTF-P version from .wtfp-version file
|
|
92
|
+
*/
|
|
93
|
+
function readInstalledVersion(claudeDir) {
|
|
94
|
+
const versionFile = path.join(claudeDir, VERSION_FILE);
|
|
95
|
+
if (!fs.existsSync(versionFile)) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const content = fs.readFileSync(versionFile, 'utf8').trim();
|
|
100
|
+
const data = JSON.parse(content);
|
|
101
|
+
return data;
|
|
102
|
+
} catch {
|
|
103
|
+
// Corrupt file or old format
|
|
104
|
+
return { version: 'unknown', corrupt: true };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Write version tracking file
|
|
110
|
+
*/
|
|
111
|
+
function writeVersionFile(claudeDir, version, installedFiles) {
|
|
112
|
+
const versionFile = path.join(claudeDir, VERSION_FILE);
|
|
113
|
+
const data = {
|
|
114
|
+
version,
|
|
115
|
+
installedAt: new Date().toISOString(),
|
|
116
|
+
files: installedFiles.length,
|
|
117
|
+
manifest: installedFiles.map(f => ({
|
|
118
|
+
path: f.dest.replace(claudeDir, '.'),
|
|
119
|
+
checksum: simpleChecksum(f.dest)
|
|
120
|
+
}))
|
|
121
|
+
};
|
|
122
|
+
fs.writeFileSync(versionFile, JSON.stringify(data, null, 2));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Simple checksum for file integrity (not cryptographic)
|
|
127
|
+
*/
|
|
128
|
+
function simpleChecksum(filePath) {
|
|
129
|
+
if (!fs.existsSync(filePath)) return null;
|
|
130
|
+
try {
|
|
131
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
132
|
+
let hash = 0;
|
|
133
|
+
for (let i = 0; i < content.length; i++) {
|
|
134
|
+
const char = content.charCodeAt(i);
|
|
135
|
+
hash = ((hash << 5) - hash) + char;
|
|
136
|
+
hash = hash & hash; // Convert to 32bit integer
|
|
137
|
+
}
|
|
138
|
+
return hash.toString(16);
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Detect installation state
|
|
146
|
+
*/
|
|
147
|
+
function detectInstallation(claudeDir) {
|
|
148
|
+
const result = {
|
|
149
|
+
hasCommands: false,
|
|
150
|
+
hasSkill: false,
|
|
151
|
+
version: null,
|
|
152
|
+
partial: false,
|
|
153
|
+
corrupt: false,
|
|
154
|
+
commandFiles: [],
|
|
155
|
+
skillFiles: []
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const commandsDir = path.join(claudeDir, 'commands', 'wtfp');
|
|
159
|
+
const skillDir = path.join(claudeDir, 'write-the-f-paper');
|
|
160
|
+
|
|
161
|
+
result.hasCommands = fs.existsSync(commandsDir);
|
|
162
|
+
result.hasSkill = fs.existsSync(skillDir);
|
|
163
|
+
|
|
164
|
+
if (result.hasCommands) {
|
|
165
|
+
result.commandFiles = collectFiles(commandsDir);
|
|
166
|
+
}
|
|
167
|
+
if (result.hasSkill) {
|
|
168
|
+
result.skillFiles = collectFiles(skillDir);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Read version info
|
|
172
|
+
const versionData = readInstalledVersion(claudeDir);
|
|
173
|
+
if (versionData) {
|
|
174
|
+
result.version = versionData.version;
|
|
175
|
+
result.corrupt = versionData.corrupt || false;
|
|
176
|
+
|
|
177
|
+
// Check for partial install (version file exists but missing dirs)
|
|
178
|
+
if (!versionData.corrupt) {
|
|
179
|
+
const expectedHasCommands = versionData.manifest?.some(f => f.path.includes('commands/wtfp'));
|
|
180
|
+
const expectedHasSkill = versionData.manifest?.some(f => f.path.includes('write-the-f-paper'));
|
|
181
|
+
|
|
182
|
+
if ((expectedHasCommands && !result.hasCommands) ||
|
|
183
|
+
(expectedHasSkill && !result.hasSkill)) {
|
|
184
|
+
result.partial = true;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} else if (result.hasCommands || result.hasSkill) {
|
|
188
|
+
// Files exist but no version file - legacy install
|
|
189
|
+
result.version = 'legacy';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Collect all files recursively in a directory
|
|
197
|
+
*/
|
|
198
|
+
function collectFiles(dir, files = []) {
|
|
199
|
+
if (!fs.existsSync(dir)) return files;
|
|
200
|
+
|
|
201
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
202
|
+
for (const entry of entries) {
|
|
203
|
+
const fullPath = path.join(dir, entry.name);
|
|
204
|
+
if (entry.isDirectory()) {
|
|
205
|
+
collectFiles(fullPath, files);
|
|
206
|
+
} else {
|
|
207
|
+
files.push(fullPath);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return files;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ============ Output Utilities ============
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Create color functions (respects --no-color)
|
|
217
|
+
*/
|
|
218
|
+
function createColors(useColors = true) {
|
|
219
|
+
if (!useColors) {
|
|
220
|
+
return {
|
|
221
|
+
cyan: s => s,
|
|
222
|
+
green: s => s,
|
|
223
|
+
yellow: s => s,
|
|
224
|
+
red: s => s,
|
|
225
|
+
magenta: s => s,
|
|
226
|
+
dim: s => s,
|
|
227
|
+
reset: ''
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
cyan: s => `\x1b[36m${s}\x1b[0m`,
|
|
233
|
+
green: s => `\x1b[32m${s}\x1b[0m`,
|
|
234
|
+
yellow: s => `\x1b[33m${s}\x1b[0m`,
|
|
235
|
+
red: s => `\x1b[31m${s}\x1b[0m`,
|
|
236
|
+
magenta: s => `\x1b[35m${s}\x1b[0m`,
|
|
237
|
+
dim: s => `\x1b[2m${s}\x1b[0m`,
|
|
238
|
+
reset: '\x1b[0m'
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Create output helpers (respects --quiet/--verbose)
|
|
244
|
+
*/
|
|
245
|
+
function createOutput(options = {}) {
|
|
246
|
+
const { quiet = false, verbose = false, useColors = true } = options;
|
|
247
|
+
const c = createColors(useColors);
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
colors: c,
|
|
251
|
+
log: (...args) => !quiet && console.log(...args),
|
|
252
|
+
verbose: (...args) => verbose && !quiet && console.log(c.dim(...args)),
|
|
253
|
+
error: (...args) => console.error(c.red('Error:'), ...args),
|
|
254
|
+
warn: (...args) => !quiet && console.log(c.yellow('Warning:'), ...args),
|
|
255
|
+
success: (...args) => !quiet && console.log(c.green('✓'), ...args),
|
|
256
|
+
info: (...args) => !quiet && console.log(c.cyan('ℹ'), ...args)
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ============ Prompt Utilities ============
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Create readline interface
|
|
264
|
+
*/
|
|
265
|
+
function createRL() {
|
|
266
|
+
return readline.createInterface({
|
|
267
|
+
input: process.stdin,
|
|
268
|
+
output: process.stdout
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Prompt user with a question
|
|
274
|
+
*/
|
|
275
|
+
function prompt(rl, question) {
|
|
276
|
+
return new Promise(resolve => {
|
|
277
|
+
rl.question(question, answer => resolve(answer.trim().toLowerCase()));
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Generate backup path with timestamp
|
|
283
|
+
*/
|
|
284
|
+
function getBackupPath(filePath) {
|
|
285
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
286
|
+
const dir = path.dirname(filePath);
|
|
287
|
+
const ext = path.extname(filePath);
|
|
288
|
+
const base = path.basename(filePath, ext);
|
|
289
|
+
return path.join(dir, `${base}.backup-${timestamp}${ext}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ============ Exports ============
|
|
293
|
+
|
|
294
|
+
module.exports = {
|
|
295
|
+
// Path utilities
|
|
296
|
+
expandTilde,
|
|
297
|
+
normalizePath,
|
|
298
|
+
isValidPath,
|
|
299
|
+
getClaudeDir,
|
|
300
|
+
getPathLabel,
|
|
301
|
+
|
|
302
|
+
// Version tracking
|
|
303
|
+
VERSION_FILE,
|
|
304
|
+
readInstalledVersion,
|
|
305
|
+
writeVersionFile,
|
|
306
|
+
simpleChecksum,
|
|
307
|
+
detectInstallation,
|
|
308
|
+
collectFiles,
|
|
309
|
+
|
|
310
|
+
// Output utilities
|
|
311
|
+
createColors,
|
|
312
|
+
createOutput,
|
|
313
|
+
|
|
314
|
+
// Prompt utilities
|
|
315
|
+
createRL,
|
|
316
|
+
prompt,
|
|
317
|
+
getBackupPath
|
|
318
|
+
};
|