wtf-p 0.1.0 → 0.3.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 +215 -230
- package/bin/commands/doctor.js +100 -0
- package/bin/commands/install-logic.js +235 -0
- package/bin/commands/list.js +88 -0
- package/bin/commands/status.js +85 -0
- package/bin/commands/update.js +50 -0
- package/bin/install.js +157 -309
- package/bin/lib/manifest.js +41 -0
- package/bin/lib/utils.js +338 -0
- package/bin/lib/wcn-compiler.js +226 -0
- package/bin/uninstall.js +202 -206
- package/core/write-the-f-paper/references/github-contrib.md +216 -0
- package/core/write-the-f-paper/templates/posters/basic.md +34 -0
- package/core/write-the-f-paper/templates/slides/basic.md +31 -0
- package/package.json +14 -4
- package/vendors/claude/.claude-plugin/plugin.json +12 -0
- package/vendors/claude/commands/wtfp/contribute.md +278 -0
- package/vendors/claude/commands/wtfp/create-poster.md +42 -0
- package/vendors/claude/commands/wtfp/create-slides.md +41 -0
- package/{commands → vendors/claude/commands}/wtfp/help.md +28 -0
- package/vendors/claude/commands/wtfp/report-bug.md +131 -0
- package/vendors/claude/commands/wtfp/request-feature.md +160 -0
- package/vendors/claude/skills/wtfp/echarts/SKILL.md +47 -0
- package/vendors/claude/skills/wtfp/marp/SKILL.md +49 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/references/checkpoints.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/references/continuation-format.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/references/git-integration.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/references/imrad-structure.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/references/length-estimation.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/references/plan-format.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/references/principles.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/references/questioning.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/references/research-pitfalls.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/config.json +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/context.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/continue-here.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/data-inventory.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/discovery.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/issues.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/manuscript.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/narrative-arc.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/outline.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/phase-prompt.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/prior-drafts.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/project-context/argument-structure.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/project-context/bibliography.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/project-context/gaps.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/project-context/imrad.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/project-context/peer-review.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/project-context/style-guide.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/project-context/tools.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/project.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/research.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/review-comments.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/revision-issues.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/roadmap.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/section-archive.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/section-context.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/section.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/state.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/summary.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/venues/acm-cs.yaml +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/venues/arxiv-ml.yaml +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/venues/ieee-cs.yaml +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/venues/nature.yaml +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/venues/thesis-chapter.yaml +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/create-outline.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/create-outline.wcn.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/discuss-section.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/discuss-section.wcn.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/execute-section.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/execute-section.wcn.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/list-assumptions.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/list-assumptions.wcn.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/lit-review-phase.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/lit-review-phase.wcn.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/map-project.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/map-project.wcn.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/plan-section.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/plan-section.wcn.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/research-gap.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/research-gap.wcn.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/resume-paper.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/resume-paper.wcn.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/review-section.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/review-section.wcn.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/submit-draft.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/submit-draft.wcn.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/transition.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/transition.wcn.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/analyze-bib.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/check-refs.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/create-outline.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/discuss-section.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/export-latex.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/insert-section.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/list-assumptions.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/map-project.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/new-paper.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/pause-writing.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/plan-revision.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/plan-section.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/polish-prose.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/progress.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/remove-section.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/research-gap.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/resume-writing.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/review-section.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/submit-milestone.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/write-section.md +0 -0
package/bin/lib/utils.js
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
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 configuration directory for a specific vendor
|
|
66
|
+
*/
|
|
67
|
+
function getVendorDir(vendor, explicitConfigDir, isGlobal = true) {
|
|
68
|
+
if (!isGlobal) {
|
|
69
|
+
return path.join(process.cwd(), `.${vendor}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const envVar = `${vendor.toUpperCase()}_CONFIG_DIR`;
|
|
73
|
+
const configDir = normalizePath(explicitConfigDir) ||
|
|
74
|
+
normalizePath(process.env[envVar]);
|
|
75
|
+
|
|
76
|
+
return configDir || path.join(os.homedir(), `.${vendor}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get the Claude config directory (respects CLAUDE_CONFIG_DIR env var)
|
|
81
|
+
*/
|
|
82
|
+
function getClaudeDir(explicitConfigDir, isGlobal = true) {
|
|
83
|
+
return getVendorDir('claude', explicitConfigDir, isGlobal);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get human-readable label for a path (with ~ for homedir)
|
|
88
|
+
*/
|
|
89
|
+
function getPathLabel(fullPath, isGlobal = true) {
|
|
90
|
+
if (isGlobal) {
|
|
91
|
+
return fullPath.replace(os.homedir(), '~');
|
|
92
|
+
}
|
|
93
|
+
return fullPath.replace(process.cwd(), '.');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ============ Version Tracking ============
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Read installed WTF-P version from .wtfp-version file
|
|
100
|
+
*/
|
|
101
|
+
function readInstalledVersion(claudeDir) {
|
|
102
|
+
const versionFile = path.join(claudeDir, VERSION_FILE);
|
|
103
|
+
if (!fs.existsSync(versionFile)) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const content = fs.readFileSync(versionFile, 'utf8').trim();
|
|
108
|
+
const data = JSON.parse(content);
|
|
109
|
+
return data;
|
|
110
|
+
} catch {
|
|
111
|
+
// Corrupt file or old format
|
|
112
|
+
return { version: 'unknown', corrupt: true };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Write version tracking file
|
|
118
|
+
*/
|
|
119
|
+
function writeVersionFile(claudeDir, version, installedFiles) {
|
|
120
|
+
if (!fs.existsSync(claudeDir)) {
|
|
121
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
122
|
+
}
|
|
123
|
+
const versionFile = path.join(claudeDir, VERSION_FILE);
|
|
124
|
+
const data = {
|
|
125
|
+
version,
|
|
126
|
+
installedAt: new Date().toISOString(),
|
|
127
|
+
files: installedFiles.length,
|
|
128
|
+
manifest: installedFiles.map(f => ({
|
|
129
|
+
path: f.dest.replace(claudeDir, '.'),
|
|
130
|
+
checksum: simpleChecksum(f.dest)
|
|
131
|
+
}))
|
|
132
|
+
};
|
|
133
|
+
fs.writeFileSync(versionFile, JSON.stringify(data, null, 2));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Simple checksum for file integrity (not cryptographic)
|
|
138
|
+
*/
|
|
139
|
+
function simpleChecksum(filePath) {
|
|
140
|
+
if (!fs.existsSync(filePath)) return null;
|
|
141
|
+
try {
|
|
142
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
143
|
+
let hash = 0;
|
|
144
|
+
for (let i = 0; i < content.length; i++) {
|
|
145
|
+
const char = content.charCodeAt(i);
|
|
146
|
+
hash = ((hash << 5) - hash) + char;
|
|
147
|
+
hash = hash & hash; // Convert to 32bit integer
|
|
148
|
+
}
|
|
149
|
+
return hash.toString(16);
|
|
150
|
+
} catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Detect installation state
|
|
157
|
+
*/
|
|
158
|
+
function detectInstallation(vendorDir) {
|
|
159
|
+
const result = {
|
|
160
|
+
hasCommands: false,
|
|
161
|
+
hasWorkflows: false,
|
|
162
|
+
hasSkills: false,
|
|
163
|
+
version: null,
|
|
164
|
+
partial: false,
|
|
165
|
+
corrupt: false,
|
|
166
|
+
commandFiles: [],
|
|
167
|
+
workflowFiles: [],
|
|
168
|
+
skillFiles: []
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const commandsDir = path.join(vendorDir, 'commands', 'wtfp');
|
|
172
|
+
const workflowsDir = path.join(vendorDir, 'write-the-f-paper');
|
|
173
|
+
const skillsDir = path.join(vendorDir, 'skills', 'wtfp');
|
|
174
|
+
|
|
175
|
+
result.hasCommands = fs.existsSync(commandsDir);
|
|
176
|
+
result.hasWorkflows = fs.existsSync(workflowsDir);
|
|
177
|
+
result.hasSkills = fs.existsSync(skillsDir);
|
|
178
|
+
|
|
179
|
+
if (result.hasCommands) {
|
|
180
|
+
result.commandFiles = collectFiles(commandsDir);
|
|
181
|
+
}
|
|
182
|
+
if (result.hasWorkflows) {
|
|
183
|
+
result.workflowFiles = collectFiles(workflowsDir);
|
|
184
|
+
}
|
|
185
|
+
if (result.hasSkills) {
|
|
186
|
+
result.skillFiles = collectFiles(skillsDir);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Read version info
|
|
190
|
+
const versionData = readInstalledVersion(vendorDir);
|
|
191
|
+
if (versionData) {
|
|
192
|
+
result.version = versionData.version;
|
|
193
|
+
result.corrupt = versionData.corrupt || false;
|
|
194
|
+
|
|
195
|
+
// Check for partial install
|
|
196
|
+
if (!versionData.corrupt) {
|
|
197
|
+
const expectedHasCommands = versionData.manifest?.some(f => f.path.includes('commands/wtfp'));
|
|
198
|
+
const expectedHasWorkflows = versionData.manifest?.some(f => f.path.includes('write-the-f-paper'));
|
|
199
|
+
const expectedHasSkills = versionData.manifest?.some(f => f.path.includes('skills/wtfp'));
|
|
200
|
+
|
|
201
|
+
if ((expectedHasCommands && !result.hasCommands) ||
|
|
202
|
+
(expectedHasWorkflows && !result.hasWorkflows) ||
|
|
203
|
+
(expectedHasSkills && !result.hasSkills)) {
|
|
204
|
+
result.partial = true;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
} else if (result.hasCommands || result.hasWorkflows || result.hasSkills) {
|
|
208
|
+
// Files exist but no version file - legacy install
|
|
209
|
+
result.version = 'legacy';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Collect all files recursively in a directory
|
|
217
|
+
*/
|
|
218
|
+
function collectFiles(dir, files = []) {
|
|
219
|
+
if (!fs.existsSync(dir)) return files;
|
|
220
|
+
|
|
221
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
222
|
+
for (const entry of entries) {
|
|
223
|
+
const fullPath = path.join(dir, entry.name);
|
|
224
|
+
if (entry.isDirectory()) {
|
|
225
|
+
collectFiles(fullPath, files);
|
|
226
|
+
} else {
|
|
227
|
+
files.push(fullPath);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return files;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ============ Output Utilities ============
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Create color functions (respects --no-color)
|
|
237
|
+
*/
|
|
238
|
+
function createColors(useColors = true) {
|
|
239
|
+
if (!useColors) {
|
|
240
|
+
return {
|
|
241
|
+
cyan: s => s,
|
|
242
|
+
green: s => s,
|
|
243
|
+
yellow: s => s,
|
|
244
|
+
red: s => s,
|
|
245
|
+
magenta: s => s,
|
|
246
|
+
dim: s => s,
|
|
247
|
+
reset: ''
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
cyan: s => `\x1b[36m${s}\x1b[0m`,
|
|
253
|
+
green: s => `\x1b[32m${s}\x1b[0m`,
|
|
254
|
+
yellow: s => `\x1b[33m${s}\x1b[0m`,
|
|
255
|
+
red: s => `\x1b[31m${s}\x1b[0m`,
|
|
256
|
+
magenta: s => `\x1b[35m${s}\x1b[0m`,
|
|
257
|
+
dim: s => `\x1b[2m${s}\x1b[0m`,
|
|
258
|
+
reset: '\x1b[0m'
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Create output helpers (respects --quiet/--verbose)
|
|
264
|
+
*/
|
|
265
|
+
function createOutput(options = {}) {
|
|
266
|
+
const { quiet = false, verbose = false, useColors = true } = options;
|
|
267
|
+
const c = createColors(useColors);
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
colors: c,
|
|
271
|
+
log: (...args) => !quiet && console.log(...args),
|
|
272
|
+
verbose: (...args) => verbose && !quiet && console.log(c.dim(...args)),
|
|
273
|
+
error: (...args) => console.error(c.red('Error:'), ...args),
|
|
274
|
+
warn: (...args) => !quiet && console.log(c.yellow('Warning:'), ...args),
|
|
275
|
+
success: (...args) => !quiet && console.log(c.green('✓'), ...args),
|
|
276
|
+
info: (...args) => !quiet && console.log(c.cyan('ℹ'), ...args)
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ============ Prompt Utilities ============
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Create readline interface
|
|
284
|
+
*/
|
|
285
|
+
function createRL() {
|
|
286
|
+
return readline.createInterface({
|
|
287
|
+
input: process.stdin,
|
|
288
|
+
output: process.stdout
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Prompt user with a question
|
|
294
|
+
*/
|
|
295
|
+
function prompt(rl, question) {
|
|
296
|
+
return new Promise(resolve => {
|
|
297
|
+
rl.question(question, answer => resolve(answer.trim().toLowerCase()));
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Generate backup path with timestamp
|
|
303
|
+
*/
|
|
304
|
+
function getBackupPath(filePath) {
|
|
305
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
306
|
+
const dir = path.dirname(filePath);
|
|
307
|
+
const ext = path.extname(filePath);
|
|
308
|
+
const base = path.basename(filePath, ext);
|
|
309
|
+
return path.join(dir, `${base}.backup-${timestamp}${ext}`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ============ Exports ============
|
|
313
|
+
|
|
314
|
+
module.exports = {
|
|
315
|
+
// Path utilities
|
|
316
|
+
expandTilde,
|
|
317
|
+
normalizePath,
|
|
318
|
+
isValidPath,
|
|
319
|
+
getClaudeDir,
|
|
320
|
+
getPathLabel,
|
|
321
|
+
|
|
322
|
+
// Version tracking
|
|
323
|
+
VERSION_FILE,
|
|
324
|
+
readInstalledVersion,
|
|
325
|
+
writeVersionFile,
|
|
326
|
+
simpleChecksum,
|
|
327
|
+
detectInstallation,
|
|
328
|
+
collectFiles,
|
|
329
|
+
|
|
330
|
+
// Output utilities
|
|
331
|
+
createColors,
|
|
332
|
+
createOutput,
|
|
333
|
+
|
|
334
|
+
// Prompt utilities
|
|
335
|
+
createRL,
|
|
336
|
+
prompt,
|
|
337
|
+
getBackupPath
|
|
338
|
+
};
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WCN Compiler (Workflow Compression Notation)
|
|
3
|
+
* A robust, state-machine based compiler for converting Verbose Markdown to Compressed WCN.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const FLUFF_PATTERNS = [
|
|
7
|
+
/This ensures[^.]*\./g,
|
|
8
|
+
/This is important[^.]*\./g,
|
|
9
|
+
/Note that[^.]*\./g,
|
|
10
|
+
/Keep in mind[^.]*\./g,
|
|
11
|
+
/Remember that[^.]*\./g,
|
|
12
|
+
/As mentioned[^.]*\./g,
|
|
13
|
+
/It is important to[^.]*\./g,
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
class WCNCompiler {
|
|
17
|
+
constructor() {
|
|
18
|
+
this.lines = [];
|
|
19
|
+
this.output = [];
|
|
20
|
+
this.state = 'ROOT'; // ROOT, STEP, BASH
|
|
21
|
+
this.context = {};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
compile(markdown) {
|
|
25
|
+
// 1. Pre-process: Remove fluff and normalize
|
|
26
|
+
let cleanMd = markdown;
|
|
27
|
+
FLUFF_PATTERNS.forEach(p => cleanMd = cleanMd.replace(p, ''));
|
|
28
|
+
|
|
29
|
+
this.lines = cleanMd.split('\n');
|
|
30
|
+
this.output = [];
|
|
31
|
+
this.state = 'ROOT';
|
|
32
|
+
|
|
33
|
+
// 2. Parse Line by Line
|
|
34
|
+
for (let i = 0; i < this.lines.length; i++) {
|
|
35
|
+
const line = this.lines[i].trim();
|
|
36
|
+
this.processLine(line, i);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 3. Post-process: Clean up excessive newlines
|
|
40
|
+
return this.output.join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
processLine(line, index) {
|
|
44
|
+
// Handle State Transitions
|
|
45
|
+
if (this.state === 'ROOT') {
|
|
46
|
+
if (line.startsWith('<step')) {
|
|
47
|
+
this.enterStep(line);
|
|
48
|
+
} else if (line.startsWith('<context>')) {
|
|
49
|
+
this.enterContext();
|
|
50
|
+
} else if (line.startsWith('**If') && line.endsWith(':**')) {
|
|
51
|
+
// Handle standalone conditionals (outside steps, rare but possible)
|
|
52
|
+
this.parseConditional(line, index);
|
|
53
|
+
} else if (line.startsWith('**Route')) {
|
|
54
|
+
this.parseRoute(line, index);
|
|
55
|
+
} else if (!line.startsWith('</')) {
|
|
56
|
+
// Pass through other content, but minimal
|
|
57
|
+
// We generally skip prose outside steps in WCN unless it's structural
|
|
58
|
+
if (line.startsWith('#') || line.startsWith('---')) {
|
|
59
|
+
this.output.push(line);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} else if (this.state === 'STEP') {
|
|
63
|
+
if (line.startsWith('</step>')) {
|
|
64
|
+
this.exitStep();
|
|
65
|
+
} else if (line.startsWith('```bash')) {
|
|
66
|
+
this.state = 'BASH';
|
|
67
|
+
} else if (line.startsWith('**If') && line.endsWith(':**')) {
|
|
68
|
+
this.parseConditional(line, index);
|
|
69
|
+
} else if (line.toLowerCase().startsWith('parse and internalize:')) {
|
|
70
|
+
this.parseParseBlock(index);
|
|
71
|
+
} else if (line.trim().startsWith('- ') || line.trim().startsWith('* ')) {
|
|
72
|
+
// Capture bullet points as instructions
|
|
73
|
+
this.output.push(line.trim());
|
|
74
|
+
} else if (line.trim().startsWith('**') && !line.includes('If')) {
|
|
75
|
+
// Capture bold instructions
|
|
76
|
+
this.output.push(line.trim());
|
|
77
|
+
}
|
|
78
|
+
} else if (this.state === 'BASH') {
|
|
79
|
+
if (line.startsWith('```')) {
|
|
80
|
+
this.state = 'STEP';
|
|
81
|
+
} else {
|
|
82
|
+
// Capture bash command (first line usually sufficient for WCN)
|
|
83
|
+
if (line && !line.startsWith('#')) {
|
|
84
|
+
this.output.push(`RUN: ${line}`);
|
|
85
|
+
// Skip rest of bash block for WCN brevity?
|
|
86
|
+
// Current logic captures all commands. Let's capture all.
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
enterStep(line) {
|
|
93
|
+
this.state = 'STEP';
|
|
94
|
+
// Parse attributes: name="foo" priority="bar"
|
|
95
|
+
const nameMatch = line.match(/name="([^"]+)"/);
|
|
96
|
+
const priorityMatch = line.match(/priority="([^"]+)"/);
|
|
97
|
+
|
|
98
|
+
const name = nameMatch ? nameMatch[1] : 'unknown';
|
|
99
|
+
const pAttr = priorityMatch ? ` p=${priorityMatch[1] === 'first' ? '1' : priorityMatch[1]}` : '';
|
|
100
|
+
|
|
101
|
+
this.output.push(`[step:${name}${pAttr}]`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
exitStep() {
|
|
105
|
+
this.output.push('[/step]');
|
|
106
|
+
this.state = 'ROOT';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
enterContext() {
|
|
110
|
+
// Context is usually multiline, handled by looking ahead or buffering
|
|
111
|
+
// For simplicity, we'll scan ahead here since we have full lines access
|
|
112
|
+
// But sticking to line-by-line:
|
|
113
|
+
// Actually, Regex is better for Context blocks as they are simple <context>...</context>
|
|
114
|
+
// Let's defer this to a specialized Regex pass *before* line processing?
|
|
115
|
+
// Or just look ahead now.
|
|
116
|
+
// Let's skip for now and rely on a pre-pass replacement for Context, similar to current tool.
|
|
117
|
+
// Why? Because Context is global metadata, usually at top.
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
parseConditional(line, index) {
|
|
121
|
+
// **If mode="yolo":**
|
|
122
|
+
const condition = line.replace(/\*\*If\s+/, '').replace(/:\*\*/, '').trim().replace(/["']/g, '');
|
|
123
|
+
|
|
124
|
+
// Look ahead for the action
|
|
125
|
+
// Usually the next line or the line after
|
|
126
|
+
let action = '';
|
|
127
|
+
for (let i = 1; i < 5; i++) {
|
|
128
|
+
const next = this.lines[index + i];
|
|
129
|
+
if (next && next.trim()) {
|
|
130
|
+
action = next.trim();
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (action) {
|
|
136
|
+
// Simplify action text
|
|
137
|
+
if (action.length > 50) action = action.substring(0, 47) + '...';
|
|
138
|
+
this.output.push(`IF ${condition} → ${action}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
parseParseBlock(index) {
|
|
143
|
+
// Parse and internalize:
|
|
144
|
+
// - Item 1
|
|
145
|
+
// - Item 2
|
|
146
|
+
const fields = [];
|
|
147
|
+
for (let i = 1; i < 10; i++) {
|
|
148
|
+
const next = this.lines[index + i];
|
|
149
|
+
if (!next || (!next.trim().startsWith('-') && !next.trim().startsWith('*'))) break;
|
|
150
|
+
|
|
151
|
+
const field = next.replace(/^[-*]\s+/, '').trim().split(' ')[0].toLowerCase().replace(/\s+/g, '_');
|
|
152
|
+
fields.push(field);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (fields.length > 0) {
|
|
156
|
+
this.output.push(`PARSE: ${fields.join(', ')}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
parseRoute(line, index) {
|
|
161
|
+
// **Route A: Title**
|
|
162
|
+
const match = line.match(/\*\*Route\s+([A-Z]):\s*([^*]+)\*\*/);
|
|
163
|
+
if (!match) return;
|
|
164
|
+
|
|
165
|
+
// We need to buffer the route table
|
|
166
|
+
// This is hard to do line-by-line without buffering.
|
|
167
|
+
// Let's assume we handle Routes via Regex pre-pass like the original,
|
|
168
|
+
// OR we detect the start of a route block and switch state.
|
|
169
|
+
// Given the complexity, let's stick to the Regex for Routes/Modes/Context
|
|
170
|
+
// and use State Machine for the detailed STEP logic which is where the fragility usually is.
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Hybrid Compiler:
|
|
176
|
+
* 1. Uses Regex for high-level blocks (Context, Routes, Modes)
|
|
177
|
+
* 2. Uses State Machine for Steps (the most complex part)
|
|
178
|
+
*/
|
|
179
|
+
function compile(markdown) {
|
|
180
|
+
let result = markdown;
|
|
181
|
+
|
|
182
|
+
// 1. Remove fluff
|
|
183
|
+
FLUFF_PATTERNS.forEach(p => result = result.replace(p, ''));
|
|
184
|
+
|
|
185
|
+
// 2. Compress Context (Regex is fine here)
|
|
186
|
+
result = result.replace(/<context>\s*([\s\S]*?)<\/context>/g, (match, content) => {
|
|
187
|
+
const files = content.match(/@[^\s\n]+/g) || [];
|
|
188
|
+
if (files.length === 0) return '';
|
|
189
|
+
const simplified = files.map(f => f.replace('@.planning/', ''));
|
|
190
|
+
return `@context{${simplified.join(',')}}`;
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// 3. Compress Routes (Regex is fine here)
|
|
194
|
+
const routes = [];
|
|
195
|
+
result = result.replace(/\*\*Route\s+([A-Z]):\s*([^*]+)\*\*\s*([\s\S]*?)(?=\*\*Route|<step|$)/g, (match, id, title, content) => {
|
|
196
|
+
// Extract next step logic
|
|
197
|
+
const nextMatch = content.match(/\`\/wtfp:([^`]+)\`/);
|
|
198
|
+
const next = nextMatch ? `/wtfp:${nextMatch[1]}` : 'continue';
|
|
199
|
+
routes.push(` ${title.trim().toLowerCase().replace(/\s+/g, '_')} → "${title.trim()}" → ${next}`);
|
|
200
|
+
return '';
|
|
201
|
+
});
|
|
202
|
+
if (routes.length > 0) {
|
|
203
|
+
result = result + '\n\nROUTE{condition → output → next}:\n' + routes.join('\n');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 4. Compress Modes (Regex is fine here)
|
|
207
|
+
const modes = [];
|
|
208
|
+
result = result.replace(/\*\*If\s+mode="([^"]+)":\*\*\s*\n((?:[-*]\s+[^\n]+\n?)+)/g, (match, mode, items) => {
|
|
209
|
+
const itemList = items.split('\n').filter(l => l.trim()).map(l => l.replace(/^[-*]\s+/, '').trim());
|
|
210
|
+
modes.push(` ${mode} | ${itemList[0] || ''}`);
|
|
211
|
+
return '';
|
|
212
|
+
});
|
|
213
|
+
if (modes.length > 0) {
|
|
214
|
+
result = 'MODE{name,action}:\n' + modes.join('\n') + '\n\n' + result;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 5. Compress Steps using State Machine (The Core Logic)
|
|
218
|
+
const compiler = new WCNCompiler();
|
|
219
|
+
// We only run the compiler on the text that remains (which should be mostly steps)
|
|
220
|
+
// But wait, the compiler expects full text to find steps.
|
|
221
|
+
// Let's pass the text to the compiler, but the compiler only outputs Step blocks and structural elements.
|
|
222
|
+
|
|
223
|
+
return compiler.compile(result);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = { compile, WCNCompiler };
|