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.
@@ -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
+ };