wtf-p 0.2.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 +6 -2
- 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 +78 -629
- package/bin/lib/manifest.js +41 -0
- package/bin/lib/utils.js +37 -17
- package/bin/lib/wcn-compiler.js +226 -0
- package/bin/uninstall.js +51 -17
- 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 +12 -4
- package/vendors/claude/.claude-plugin/plugin.json +12 -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 +3 -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/github-contrib.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/contribute.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/report-bug.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/request-feature.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/README.md
CHANGED
|
@@ -2,14 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
# WTF-P
|
|
4
4
|
|
|
5
|
+
**Write The Freaking...**
|
|
6
|
+
- **P**aper
|
|
7
|
+
- **P**roposal
|
|
8
|
+
- **P**resentation
|
|
9
|
+
- **P**oster
|
|
10
|
+
|
|
5
11
|
**Context engineering for academic writing with Claude Code.**
|
|
6
12
|
|
|
7
13
|
```bash
|
|
8
14
|
npx wtf-p
|
|
9
15
|
```
|
|
10
16
|
|
|
11
|
-
*Write The Freaking Paper. Finally.*
|
|
12
|
-
|
|
13
17
|
</div>
|
|
14
18
|
|
|
15
19
|
---
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { getClaudeDir, normalizePath, getPathLabel, detectInstallation } = require('../lib/utils');
|
|
4
|
+
|
|
5
|
+
async function runDoctor(options) {
|
|
6
|
+
const { out, explicitConfigDir } = options;
|
|
7
|
+
const c = out.colors;
|
|
8
|
+
|
|
9
|
+
out.log(` ${c.yellow('WTF-P Installation Doctor')}\n`);
|
|
10
|
+
|
|
11
|
+
const issues = [];
|
|
12
|
+
const checks = [];
|
|
13
|
+
|
|
14
|
+
// Check 1: Node.js version
|
|
15
|
+
const nodeVersion = process.version;
|
|
16
|
+
const nodeMajor = parseInt(nodeVersion.slice(1).split('.')[0], 10);
|
|
17
|
+
if (nodeMajor < 16) {
|
|
18
|
+
issues.push(`Node.js ${nodeVersion} is below minimum (16.7.0)`);
|
|
19
|
+
checks.push({ name: 'Node.js version', status: 'fail', detail: nodeVersion });
|
|
20
|
+
} else {
|
|
21
|
+
checks.push({ name: 'Node.js version', status: 'pass', detail: nodeVersion });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Check 2: Claude directory exists
|
|
25
|
+
const globalDir = getClaudeDir(explicitConfigDir, true);
|
|
26
|
+
if (fs.existsSync(globalDir)) {
|
|
27
|
+
checks.push({ name: 'Claude config directory', status: 'pass', detail: getPathLabel(globalDir, true) });
|
|
28
|
+
} else {
|
|
29
|
+
checks.push({ name: 'Claude config directory', status: 'warn', detail: 'Does not exist (will be created on install)' });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Check 3: Write permissions
|
|
33
|
+
const testDir = globalDir.replace(/\.claude$/, '.claude-test-' + Date.now());
|
|
34
|
+
try {
|
|
35
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
36
|
+
fs.writeFileSync(path.join(testDir, 'test'), 'test');
|
|
37
|
+
fs.rmSync(testDir, { recursive: true });
|
|
38
|
+
checks.push({ name: 'Write permissions', status: 'pass', detail: 'Can write to config directory' });
|
|
39
|
+
} catch (err) {
|
|
40
|
+
issues.push(`Cannot write to ${getPathLabel(globalDir, true)}: ${err.message}`);
|
|
41
|
+
checks.push({ name: 'Write permissions', status: 'fail', detail: err.message });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check 4: Installation state
|
|
45
|
+
const detection = detectInstallation(globalDir);
|
|
46
|
+
if (detection.hasCommands || detection.hasWorkflows || detection.hasSkills) {
|
|
47
|
+
if (detection.partial) {
|
|
48
|
+
issues.push('Partial installation detected - some files are missing');
|
|
49
|
+
checks.push({ name: 'Installation integrity', status: 'warn', detail: 'Partial install' });
|
|
50
|
+
} else if (detection.corrupt) {
|
|
51
|
+
issues.push('Version file is corrupt - reinstall recommended');
|
|
52
|
+
checks.push({ name: 'Installation integrity', status: 'warn', detail: 'Corrupt version file' });
|
|
53
|
+
} else {
|
|
54
|
+
checks.push({ name: 'Installation integrity', status: 'pass', detail: `v${detection.version}` });
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
checks.push({ name: 'Installation integrity', status: 'info', detail: 'Not installed' });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check 5: CLAUDE_CONFIG_DIR env var
|
|
61
|
+
const configDirEnv = process.env.CLAUDE_CONFIG_DIR;
|
|
62
|
+
if (configDirEnv) {
|
|
63
|
+
const expanded = normalizePath(configDirEnv);
|
|
64
|
+
if (fs.existsSync(expanded)) {
|
|
65
|
+
checks.push({ name: 'CLAUDE_CONFIG_DIR', status: 'pass', detail: expanded });
|
|
66
|
+
} else {
|
|
67
|
+
issues.push(`CLAUDE_CONFIG_DIR points to non-existent path: ${configDirEnv}`);
|
|
68
|
+
checks.push({ name: 'CLAUDE_CONFIG_DIR', status: 'warn', detail: `${configDirEnv} (does not exist)` });
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
checks.push({ name: 'CLAUDE_CONFIG_DIR', status: 'info', detail: 'Not set (using default)' });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Output results
|
|
75
|
+
for (const check of checks) {
|
|
76
|
+
let icon, color;
|
|
77
|
+
switch (check.status) {
|
|
78
|
+
case 'pass': icon = '✓'; color = c.green; break;
|
|
79
|
+
case 'fail': icon = '✗'; color = c.red; break;
|
|
80
|
+
case 'warn': icon = '⚠'; color = c.yellow; break;
|
|
81
|
+
default: icon = 'ℹ'; color = c.cyan;
|
|
82
|
+
}
|
|
83
|
+
out.log(` ${color(icon)} ${check.name}: ${c.dim(check.detail)}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
out.log('');
|
|
87
|
+
|
|
88
|
+
if (issues.length > 0) {
|
|
89
|
+
out.log(` ${c.yellow('Issues found:')}`);
|
|
90
|
+
for (const issue of issues) {
|
|
91
|
+
out.log(` ${c.yellow('•')} ${issue}`);
|
|
92
|
+
}
|
|
93
|
+
out.log('');
|
|
94
|
+
out.log(` ${c.dim('Run')} ${c.cyan('npx wtf-p --global --force')} ${c.dim('to reinstall')}\n`);
|
|
95
|
+
} else {
|
|
96
|
+
out.log(` ${c.green('No issues found!')}\n`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = runDoctor;
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const MANIFEST = require('../lib/manifest');
|
|
5
|
+
const {
|
|
6
|
+
isValidPath,
|
|
7
|
+
getClaudeDir, // Legacy support, will refactor
|
|
8
|
+
getPathLabel,
|
|
9
|
+
getBackupPath,
|
|
10
|
+
writeVersionFile,
|
|
11
|
+
createRL,
|
|
12
|
+
prompt
|
|
13
|
+
} = require('../lib/utils');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Process file content with path replacement
|
|
17
|
+
*/
|
|
18
|
+
function processContent(srcPath, pathPrefix) {
|
|
19
|
+
if (srcPath.endsWith('.md') || srcPath.endsWith('.json')) {
|
|
20
|
+
let content = fs.readFileSync(srcPath, 'utf8');
|
|
21
|
+
return content.replace(/~\/\.claude\//g, pathPrefix);
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Collect files from a manifest component
|
|
28
|
+
*/
|
|
29
|
+
function collectComponentFiles(component, destBase, files = []) {
|
|
30
|
+
if (!fs.existsSync(component.src)) return files;
|
|
31
|
+
|
|
32
|
+
function recurse(currentSrc, currentRel) {
|
|
33
|
+
const entries = fs.readdirSync(currentSrc, { withFileTypes: true });
|
|
34
|
+
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
const srcPath = path.join(currentSrc, entry.name);
|
|
37
|
+
const relPath = path.join(currentRel, entry.name);
|
|
38
|
+
const destPath = path.join(destBase, component.dest, relPath);
|
|
39
|
+
|
|
40
|
+
if (entry.isDirectory()) {
|
|
41
|
+
recurse(srcPath, relPath);
|
|
42
|
+
} else {
|
|
43
|
+
files.push({
|
|
44
|
+
src: srcPath,
|
|
45
|
+
dest: destPath,
|
|
46
|
+
name: entry.name,
|
|
47
|
+
componentId: component.id
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
recurse(component.src, '.');
|
|
54
|
+
return files;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Install files with conflict resolution
|
|
59
|
+
*/
|
|
60
|
+
async function installWithConflictResolution(files, pathPrefix, targetDir, options) {
|
|
61
|
+
const { out, hasForce, hasBackupAll, isInteractive, showExplanations } = options;
|
|
62
|
+
const c = out.colors;
|
|
63
|
+
const rl = createRL();
|
|
64
|
+
let globalChoice = null;
|
|
65
|
+
const stats = { installed: 0, skipped: 0, backed: 0 };
|
|
66
|
+
|
|
67
|
+
const existingFiles = files.filter(f => fs.existsSync(f.dest));
|
|
68
|
+
|
|
69
|
+
if (existingFiles.length > 0 && !hasForce && !hasBackupAll && isInteractive) {
|
|
70
|
+
const relPath = getPathLabel(targetDir, true);
|
|
71
|
+
out.log(` ${c.yellow(`Found ${existingFiles.length} existing WTF-P file(s) in ${relPath}`)}
|
|
72
|
+
`);
|
|
73
|
+
|
|
74
|
+
if (showExplanations) {
|
|
75
|
+
out.log(` ${c.dim('For each file, choose:')}`);
|
|
76
|
+
out.log(` ${c.dim('[o]verwrite - Replace with new version')}`);
|
|
77
|
+
out.log(` ${c.dim('[s]kip - Keep your existing file')}`);
|
|
78
|
+
out.log(` ${c.dim('[b]ackup - Save existing, then install new')}`);
|
|
79
|
+
out.log(` ${c.dim('[a]ll - Apply same choice to remaining')}\n`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const file of files) {
|
|
84
|
+
const exists = fs.existsSync(file.dest);
|
|
85
|
+
const relDest = file.dest.replace(os.homedir(), '~').replace(process.cwd(), '.');
|
|
86
|
+
|
|
87
|
+
fs.mkdirSync(path.dirname(file.dest), { recursive: true });
|
|
88
|
+
|
|
89
|
+
if (exists && !hasForce && !hasBackupAll) {
|
|
90
|
+
let choice = globalChoice;
|
|
91
|
+
|
|
92
|
+
if (!choice && isInteractive) {
|
|
93
|
+
const answer = await prompt(rl,
|
|
94
|
+
` ${c.yellow('?')} ${c.dim(relDest)} exists. [o]verwrite/[s]kip/[b]ackup/[a]ll: `
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
if (answer.startsWith('a')) {
|
|
98
|
+
const allAnswer = await prompt(rl,
|
|
99
|
+
` Apply to all: [o]verwrite/[s]kip/[b]ackup: `
|
|
100
|
+
);
|
|
101
|
+
if (allAnswer.startsWith('o')) globalChoice = 'overwrite';
|
|
102
|
+
else if (allAnswer.startsWith('b')) globalChoice = 'backup';
|
|
103
|
+
else globalChoice = 'skip';
|
|
104
|
+
choice = globalChoice;
|
|
105
|
+
} else if (answer.startsWith('o')) {
|
|
106
|
+
choice = 'overwrite';
|
|
107
|
+
} else if (answer.startsWith('b')) {
|
|
108
|
+
choice = 'backup';
|
|
109
|
+
} else {
|
|
110
|
+
choice = 'skip';
|
|
111
|
+
}
|
|
112
|
+
} else if (!choice) {
|
|
113
|
+
choice = 'skip';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (choice === 'skip') {
|
|
117
|
+
out.verbose(` ${c.dim('○')} Skipped ${c.dim(relDest)}`);
|
|
118
|
+
stats.skipped++;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (choice === 'backup') {
|
|
123
|
+
const backupPath = getBackupPath(file.dest);
|
|
124
|
+
fs.copyFileSync(file.dest, backupPath);
|
|
125
|
+
out.log(` ${c.cyan('↻')} Backed up to ${c.dim(path.basename(backupPath))}`);
|
|
126
|
+
stats.backed++;
|
|
127
|
+
}
|
|
128
|
+
} else if (exists && hasBackupAll) {
|
|
129
|
+
const backupPath = getBackupPath(file.dest);
|
|
130
|
+
fs.copyFileSync(file.dest, backupPath);
|
|
131
|
+
out.verbose(` ${c.cyan('↻')} Backed up ${c.dim(relDest)}`);
|
|
132
|
+
stats.backed++;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const content = processContent(file.src, pathPrefix);
|
|
136
|
+
if (content !== null) {
|
|
137
|
+
fs.writeFileSync(file.dest, content);
|
|
138
|
+
} else {
|
|
139
|
+
fs.copyFileSync(file.src, file.dest);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!exists) {
|
|
143
|
+
out.verbose(` ${c.green('+')} ${c.dim(relDest)}`);
|
|
144
|
+
} else {
|
|
145
|
+
out.verbose(` ${c.green('✓')} ${c.dim(relDest)}`);
|
|
146
|
+
}
|
|
147
|
+
stats.installed++;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
rl.close();
|
|
151
|
+
return stats;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Main install logic
|
|
156
|
+
*/
|
|
157
|
+
async function install(isGlobal, isUpdate, options, pkg) {
|
|
158
|
+
const { out, explicitConfigDir, hasQuiet, onlyInstall, showExplanations } = options;
|
|
159
|
+
const c = out.colors;
|
|
160
|
+
|
|
161
|
+
// Use Claude vendor by default for now (can be passed as arg later)
|
|
162
|
+
const vendorConfig = MANIFEST.claude;
|
|
163
|
+
|
|
164
|
+
// Resolve Target Directory using the Vendor Strategy
|
|
165
|
+
// TODO: Use getVendorDir generic
|
|
166
|
+
const targetDir = getClaudeDir(explicitConfigDir, isGlobal);
|
|
167
|
+
const locationLabel = getPathLabel(targetDir, isGlobal);
|
|
168
|
+
|
|
169
|
+
// Validate path
|
|
170
|
+
if (!isValidPath(targetDir)) {
|
|
171
|
+
out.error(`Invalid path: ${targetDir}`);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const pathPrefix = isGlobal
|
|
176
|
+
? (explicitConfigDir ? `${targetDir}/` : `~/${vendorConfig.defaultDir}/`)
|
|
177
|
+
: `./${vendorConfig.defaultDir}/`;
|
|
178
|
+
|
|
179
|
+
if (!hasQuiet) {
|
|
180
|
+
out.log(` Installing to ${c.cyan(locationLabel)}
|
|
181
|
+
`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Collect files based on Manifest
|
|
185
|
+
const allFiles = [];
|
|
186
|
+
|
|
187
|
+
vendorConfig.components.forEach(component => {
|
|
188
|
+
// Check --only filters
|
|
189
|
+
if (onlyInstall !== 'all') {
|
|
190
|
+
// Map 'workflows' flag to 'workflows' component ID
|
|
191
|
+
// Map 'commands' flag to 'commands' component ID
|
|
192
|
+
// Map 'skills' flag to 'skills' component ID
|
|
193
|
+
if (onlyInstall !== component.id) {
|
|
194
|
+
// Special case: 'commands' usually implies 'skills' too in legacy logic
|
|
195
|
+
if (onlyInstall === 'commands' && component.id === 'skills') {
|
|
196
|
+
// Allow
|
|
197
|
+
} else {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// DEBUG LOG
|
|
204
|
+
// console.log(`Collecting: ${component.id} from ${component.src}`);
|
|
205
|
+
|
|
206
|
+
collectComponentFiles(component, targetDir, allFiles);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// DEBUG
|
|
210
|
+
// console.log(`Total files found: ${allFiles.length}`);
|
|
211
|
+
|
|
212
|
+
// Install with conflict resolution
|
|
213
|
+
const stats = await installWithConflictResolution(allFiles, pathPrefix, targetDir, options);
|
|
214
|
+
|
|
215
|
+
// Write version tracking file
|
|
216
|
+
writeVersionFile(targetDir, pkg.version, allFiles);
|
|
217
|
+
|
|
218
|
+
// Summary
|
|
219
|
+
if (!hasQuiet) {
|
|
220
|
+
out.log(`
|
|
221
|
+
${c.green('Done!')} Installed: ${stats.installed}, Skipped: ${stats.skipped}, Backed up: ${stats.backed}
|
|
222
|
+
|
|
223
|
+
Run ${c.cyan('/wtfp:help')} in Claude Code to get started.
|
|
224
|
+
`);
|
|
225
|
+
|
|
226
|
+
if (showExplanations && !isUpdate) {
|
|
227
|
+
out.log(` ${c.yellow('Quick Start:')}`);
|
|
228
|
+
out.log(` ${c.cyan('/wtfp:new-paper')} Start a new academic paper`);
|
|
229
|
+
out.log(` ${c.cyan('/wtfp:progress')} Check your writing status`);
|
|
230
|
+
out.log(` ${c.cyan('/wtfp:help')} Show all commands\n`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
module.exports = install;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const MANIFEST = require('../lib/manifest');
|
|
4
|
+
const { collectComponentFiles } = require('./install-logic'); // Need to export this or copy logic
|
|
5
|
+
|
|
6
|
+
// Re-implementing logic here to avoid circular dep if install-logic is messy
|
|
7
|
+
// But better to export valid logic.
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Collect files from a manifest component
|
|
11
|
+
*/
|
|
12
|
+
function collectComponentFilesLocal(component, files = []) {
|
|
13
|
+
if (!fs.existsSync(component.src)) return files;
|
|
14
|
+
|
|
15
|
+
function recurse(currentSrc, currentRel) {
|
|
16
|
+
const entries = fs.readdirSync(currentSrc, { withFileTypes: true });
|
|
17
|
+
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
const srcPath = path.join(currentSrc, entry.name);
|
|
20
|
+
const relPath = path.join(currentRel, entry.name);
|
|
21
|
+
// For listing, we just want the relative destination path
|
|
22
|
+
const destPath = path.join(component.dest, relPath);
|
|
23
|
+
|
|
24
|
+
if (entry.isDirectory()) {
|
|
25
|
+
recurse(srcPath, relPath);
|
|
26
|
+
} else {
|
|
27
|
+
files.push({
|
|
28
|
+
src: srcPath,
|
|
29
|
+
path: destPath, // display path
|
|
30
|
+
type: component.id
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
recurse(component.src, '.');
|
|
37
|
+
return files;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function showList(options) {
|
|
41
|
+
const { out, hasQuiet, onlyInstall } = options;
|
|
42
|
+
const c = out.colors;
|
|
43
|
+
|
|
44
|
+
// Use Claude by default
|
|
45
|
+
const vendorConfig = MANIFEST.claude;
|
|
46
|
+
|
|
47
|
+
const files = [];
|
|
48
|
+
|
|
49
|
+
vendorConfig.components.forEach(component => {
|
|
50
|
+
if (onlyInstall !== 'all') {
|
|
51
|
+
if (onlyInstall !== component.id) {
|
|
52
|
+
if (onlyInstall === 'commands' && component.id === 'skills') {
|
|
53
|
+
// Allow
|
|
54
|
+
} else {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
collectComponentFilesLocal(component, files);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!hasQuiet) {
|
|
63
|
+
out.log(` ${c.yellow('Files to install:')}\n`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Group by component
|
|
67
|
+
const groups = {};
|
|
68
|
+
files.forEach(f => {
|
|
69
|
+
if (!groups[f.type]) groups[f.type] = [];
|
|
70
|
+
groups[f.type].push(f);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
Object.keys(groups).forEach(type => {
|
|
74
|
+
const groupFiles = groups[type];
|
|
75
|
+
out.log(` ${c.cyan(type.charAt(0).toUpperCase() + type.slice(1))} (${groupFiles.length} files):`);
|
|
76
|
+
for (const f of groupFiles.slice(0, 10)) {
|
|
77
|
+
out.log(` ${c.dim(f.path)}`);
|
|
78
|
+
}
|
|
79
|
+
if (groupFiles.length > 10) {
|
|
80
|
+
out.log(` ${c.dim(`... and ${groupFiles.length - 10} more`)}`);
|
|
81
|
+
}
|
|
82
|
+
out.log('');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
out.log(` Total: ${files.length} files\n`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = showList;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const { execSync } = require('child_process');
|
|
3
|
+
const { getClaudeDir, detectInstallation, getPathLabel } = require('../lib/utils');
|
|
4
|
+
|
|
5
|
+
async function showStatus(options, pkg) {
|
|
6
|
+
const { out, explicitConfigDir, hasQuiet } = options;
|
|
7
|
+
const c = out.colors;
|
|
8
|
+
|
|
9
|
+
if (!hasQuiet) {
|
|
10
|
+
// Note: banner should be handled by the main entry point or passed in
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const locations = [];
|
|
14
|
+
|
|
15
|
+
// Check global
|
|
16
|
+
const globalDir = getClaudeDir(explicitConfigDir, true);
|
|
17
|
+
const globalDetection = detectInstallation(globalDir);
|
|
18
|
+
locations.push({
|
|
19
|
+
label: 'Global',
|
|
20
|
+
path: globalDir,
|
|
21
|
+
pathLabel: getPathLabel(globalDir, true),
|
|
22
|
+
...globalDetection
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Check local if different
|
|
26
|
+
const localDir = getClaudeDir(null, false);
|
|
27
|
+
if (localDir !== globalDir) {
|
|
28
|
+
const localDetection = detectInstallation(localDir);
|
|
29
|
+
locations.push({
|
|
30
|
+
label: 'Local',
|
|
31
|
+
path: localDir,
|
|
32
|
+
pathLabel: getPathLabel(localDir, false),
|
|
33
|
+
...localDetection
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
out.log(` ${c.yellow('WTF-P Installation Status')}\n`);
|
|
38
|
+
|
|
39
|
+
for (const loc of locations) {
|
|
40
|
+
const installed = loc.hasCommands || loc.hasWorkflows || loc.hasSkills;
|
|
41
|
+
|
|
42
|
+
out.log(` ${c.cyan(loc.label)} ${c.dim(`(${loc.pathLabel})`)}`);
|
|
43
|
+
|
|
44
|
+
if (!installed) {
|
|
45
|
+
out.log(` ${c.dim('Not installed')}\n`);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Version
|
|
50
|
+
if (loc.version) {
|
|
51
|
+
const versionLabel = loc.version === 'legacy' ? 'legacy (pre-0.2.0)' : loc.version;
|
|
52
|
+
out.log(` Version: ${c.green(versionLabel)}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Components
|
|
56
|
+
const components = [];
|
|
57
|
+
if (loc.hasCommands) components.push(`commands (${loc.commandFiles.length} files)`);
|
|
58
|
+
if (loc.hasWorkflows) components.push(`workflows (${loc.workflowFiles.length} files)`);
|
|
59
|
+
if (loc.hasSkills) components.push(`skills (${loc.skillFiles.length} files)`);
|
|
60
|
+
out.log(` Installed: ${components.join(', ')}`);
|
|
61
|
+
|
|
62
|
+
// Warnings
|
|
63
|
+
if (loc.partial) {
|
|
64
|
+
out.log(` ${c.yellow('⚠ Partial installation detected')}`);
|
|
65
|
+
}
|
|
66
|
+
if (loc.corrupt) {
|
|
67
|
+
out.log(` ${c.red('⚠ Corrupt version file')}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
out.log('');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Check for updates
|
|
74
|
+
try {
|
|
75
|
+
const latest = execSync('npm view wtf-p version 2>/dev/null', { encoding: 'utf8' }).trim();
|
|
76
|
+
if (latest && latest !== pkg.version) {
|
|
77
|
+
out.log(` ${c.yellow('Update available:')} ${pkg.version} → ${c.green(latest)}`);
|
|
78
|
+
out.log(` Run: ${c.cyan('npx wtf-p update --global')}\n`);
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// Ignore
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = showStatus;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const { getClaudeDir, detectInstallation, getPathLabel } = require('../lib/utils');
|
|
2
|
+
|
|
3
|
+
async function runUpdate(options, pkg, installFunc) {
|
|
4
|
+
const { out, explicitConfigDir, hasGlobal, hasLocal } = options;
|
|
5
|
+
const c = out.colors;
|
|
6
|
+
|
|
7
|
+
out.log(` ${c.yellow('Checking for updates...')}\n`);
|
|
8
|
+
|
|
9
|
+
// Find installed location
|
|
10
|
+
const globalDir = getClaudeDir(explicitConfigDir, true);
|
|
11
|
+
const localDir = getClaudeDir(null, false);
|
|
12
|
+
|
|
13
|
+
let targetDir = null;
|
|
14
|
+
let isGlobal = true;
|
|
15
|
+
|
|
16
|
+
const globalDetection = detectInstallation(globalDir);
|
|
17
|
+
const localDetection = detectInstallation(localDir);
|
|
18
|
+
|
|
19
|
+
if (hasGlobal || (!hasLocal && (globalDetection.hasCommands || globalDetection.hasWorkflows || globalDetection.hasSkills))) {
|
|
20
|
+
targetDir = globalDir;
|
|
21
|
+
isGlobal = true;
|
|
22
|
+
} else if (hasLocal || (localDetection.hasCommands || localDetection.hasWorkflows || localDetection.hasSkills)) {
|
|
23
|
+
targetDir = localDir;
|
|
24
|
+
isGlobal = false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!targetDir) {
|
|
28
|
+
out.log(` ${c.yellow('No existing installation found.')}`);
|
|
29
|
+
out.log(` Run ${c.cyan('npx wtf-p --global')} to install.\n`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const detection = isGlobal ? globalDetection : localDetection;
|
|
34
|
+
const currentVersion = detection.version === 'legacy' ? '0.0.0' : detection.version;
|
|
35
|
+
|
|
36
|
+
out.log(` Current: ${c.dim(`v${currentVersion}`)} in ${c.dim(getPathLabel(targetDir, isGlobal))}`);
|
|
37
|
+
out.log(` Package: ${c.dim(`v${pkg.version}`)}`);
|
|
38
|
+
|
|
39
|
+
if (pkg.version === currentVersion) {
|
|
40
|
+
out.log(`\n ${c.green('Already up to date!')}\n`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
out.log(`\n ${c.cyan('Updating...')}\n`);
|
|
45
|
+
|
|
46
|
+
// Run install with backup
|
|
47
|
+
await installFunc(isGlobal, true);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = runUpdate;
|