@fermindi/pwn-cli 0.2.0 ā 0.3.1
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/cli/index.js +14 -0
- package/cli/inject.js +15 -1
- package/cli/save.js +206 -0
- package/cli/update.js +230 -0
- package/package.json +1 -1
- package/src/core/inject.js +122 -2
- package/templates/workspace/.claude/commands/save.md +142 -0
package/cli/index.js
CHANGED
|
@@ -21,7 +21,9 @@ if (!command || command === '--help' || command === '-h') {
|
|
|
21
21
|
console.log('Usage: pwn <command> [options]\n');
|
|
22
22
|
console.log('Commands:');
|
|
23
23
|
console.log(' inject Inject .ai/ workspace into current project');
|
|
24
|
+
console.log(' update Update PWN framework files to latest version');
|
|
24
25
|
console.log(' migrate Migrate existing AI files to PWN structure');
|
|
26
|
+
console.log(' save Save session context to memory');
|
|
25
27
|
console.log(' status Show workspace status');
|
|
26
28
|
console.log(' validate Validate workspace structure');
|
|
27
29
|
console.log(' notify Send notifications (test, send, config)');
|
|
@@ -33,7 +35,9 @@ if (!command || command === '--help' || command === '-h') {
|
|
|
33
35
|
console.log(' --help, -h Show help\n');
|
|
34
36
|
console.log('Options:');
|
|
35
37
|
console.log(' inject --force Overwrite existing .ai/ directory');
|
|
38
|
+
console.log(' update --dry-run Preview update without changes');
|
|
36
39
|
console.log(' migrate --dry-run Preview migration without changes');
|
|
40
|
+
console.log(' save --message=X Save with custom summary');
|
|
37
41
|
console.log(' validate --verbose Show detailed structure report');
|
|
38
42
|
console.log(' notify test [ch] Test notification channel');
|
|
39
43
|
console.log(' batch --count 5 Execute 5 tasks');
|
|
@@ -51,11 +55,21 @@ switch (command) {
|
|
|
51
55
|
await inject(args);
|
|
52
56
|
break;
|
|
53
57
|
|
|
58
|
+
case 'update':
|
|
59
|
+
const { default: update } = await import('./update.js');
|
|
60
|
+
await update(args);
|
|
61
|
+
break;
|
|
62
|
+
|
|
54
63
|
case 'migrate':
|
|
55
64
|
const { default: migrate } = await import('./migrate.js');
|
|
56
65
|
await migrate(args);
|
|
57
66
|
break;
|
|
58
67
|
|
|
68
|
+
case 'save':
|
|
69
|
+
const { default: save } = await import('./save.js');
|
|
70
|
+
await save(args);
|
|
71
|
+
break;
|
|
72
|
+
|
|
59
73
|
case 'status':
|
|
60
74
|
const { default: status } = await import('./status.js');
|
|
61
75
|
await status();
|
package/cli/inject.js
CHANGED
|
@@ -46,6 +46,18 @@ export default async function injectCommand(args = []) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
console.log('\nā
PWN workspace injected successfully!\n');
|
|
49
|
+
|
|
50
|
+
// Show backup info
|
|
51
|
+
if (result.backed_up && result.backed_up.length > 0) {
|
|
52
|
+
console.log('š¦ Backed up existing AI files:');
|
|
53
|
+
for (const b of result.backed_up) {
|
|
54
|
+
console.log(` ${b.from} ā ${b.to}`);
|
|
55
|
+
}
|
|
56
|
+
console.log('');
|
|
57
|
+
console.log(' š” Merge your custom instructions into CLAUDE.md');
|
|
58
|
+
console.log(' šļø Delete the backup files (~CLAUDE.md, etc.) when done\n');
|
|
59
|
+
}
|
|
60
|
+
|
|
49
61
|
console.log('š Created structure:');
|
|
50
62
|
console.log(' .ai/');
|
|
51
63
|
console.log(' āāā memory/ (decisions, patterns, dead-ends)');
|
|
@@ -53,7 +65,9 @@ export default async function injectCommand(args = []) {
|
|
|
53
65
|
console.log(' āāā patterns/ (auto-applied patterns)');
|
|
54
66
|
console.log(' āāā workflows/ (batch execution)');
|
|
55
67
|
console.log(' āāā agents/ (AI agent configs)');
|
|
56
|
-
console.log(' āāā config/ (notifications, etc)
|
|
68
|
+
console.log(' āāā config/ (notifications, etc)');
|
|
69
|
+
console.log(' .claude/');
|
|
70
|
+
console.log(' āāā commands/ (slash commands: /save)\n');
|
|
57
71
|
|
|
58
72
|
// Show ntfy topic if generated
|
|
59
73
|
const notifyPath = join(process.cwd(), '.ai', 'config', 'notifications.json');
|
package/cli/save.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { createInterface } from 'readline';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Interactive prompt helper
|
|
8
|
+
*/
|
|
9
|
+
function prompt(question) {
|
|
10
|
+
const rl = createInterface({
|
|
11
|
+
input: process.stdin,
|
|
12
|
+
output: process.stdout
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
rl.question(question, (answer) => {
|
|
17
|
+
rl.close();
|
|
18
|
+
resolve(answer);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generate timestamp-based filename
|
|
25
|
+
*/
|
|
26
|
+
function generateFilename() {
|
|
27
|
+
const now = new Date();
|
|
28
|
+
const date = now.toISOString().split('T')[0]; // YYYY-MM-DD
|
|
29
|
+
return `${date}-session.md`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Format current date
|
|
34
|
+
*/
|
|
35
|
+
function formatDate() {
|
|
36
|
+
return new Date().toISOString().split('T')[0];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default async function saveCommand(args = []) {
|
|
40
|
+
const cwd = process.cwd();
|
|
41
|
+
const aiDir = join(cwd, '.ai');
|
|
42
|
+
|
|
43
|
+
// Check if .ai/ exists
|
|
44
|
+
if (!existsSync(aiDir)) {
|
|
45
|
+
console.log('ā No .ai/ directory found');
|
|
46
|
+
console.log(' Run "pwn inject" first to initialize workspace');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log('š¾ PWN Save - Persist session context\n');
|
|
51
|
+
|
|
52
|
+
const isInteractive = !args.includes('--no-input') && process.stdin.isTTY;
|
|
53
|
+
const messageArg = args.find(a => a.startsWith('--message=') || a.startsWith('-m='));
|
|
54
|
+
let summary = messageArg ? messageArg.split('=')[1] : null;
|
|
55
|
+
|
|
56
|
+
// Paths
|
|
57
|
+
const archiveDir = join(aiDir, 'memory', 'archive');
|
|
58
|
+
const decisionsPath = join(aiDir, 'memory', 'decisions.md');
|
|
59
|
+
const patternsPath = join(aiDir, 'memory', 'patterns.md');
|
|
60
|
+
const deadendsPath = join(aiDir, 'memory', 'deadends.md');
|
|
61
|
+
const activePath = join(aiDir, 'tasks', 'active.md');
|
|
62
|
+
const statePath = join(aiDir, 'state.json');
|
|
63
|
+
|
|
64
|
+
// Ensure archive directory exists
|
|
65
|
+
if (!existsSync(archiveDir)) {
|
|
66
|
+
mkdirSync(archiveDir, { recursive: true });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Get summary
|
|
70
|
+
if (!summary && isInteractive) {
|
|
71
|
+
console.log('š What did you accomplish in this session?');
|
|
72
|
+
console.log(' (Brief summary - press Enter twice to finish)\n');
|
|
73
|
+
summary = await prompt('> ');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!summary) {
|
|
77
|
+
summary = 'Session checkpoint';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Create archive entry
|
|
81
|
+
const filename = generateFilename();
|
|
82
|
+
const archivePath = join(archiveDir, filename);
|
|
83
|
+
|
|
84
|
+
// Check if file already exists, append number if needed
|
|
85
|
+
let finalPath = archivePath;
|
|
86
|
+
let counter = 1;
|
|
87
|
+
while (existsSync(finalPath)) {
|
|
88
|
+
finalPath = join(archiveDir, filename.replace('.md', `-${counter}.md`));
|
|
89
|
+
counter++;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Gather current state
|
|
93
|
+
const sections = [];
|
|
94
|
+
|
|
95
|
+
sections.push(`# Session: ${formatDate()}\n`);
|
|
96
|
+
sections.push(`## Summary\n\n${summary}\n`);
|
|
97
|
+
|
|
98
|
+
// Capture active tasks
|
|
99
|
+
if (existsSync(activePath)) {
|
|
100
|
+
const active = readFileSync(activePath, 'utf8');
|
|
101
|
+
const completedTasks = active.match(/- \[x\].*/g) || [];
|
|
102
|
+
const pendingTasks = active.match(/- \[ \].*/g) || [];
|
|
103
|
+
|
|
104
|
+
if (completedTasks.length > 0 || pendingTasks.length > 0) {
|
|
105
|
+
sections.push(`## Tasks\n`);
|
|
106
|
+
if (completedTasks.length > 0) {
|
|
107
|
+
sections.push(`### Completed\n${completedTasks.join('\n')}\n`);
|
|
108
|
+
}
|
|
109
|
+
if (pendingTasks.length > 0) {
|
|
110
|
+
sections.push(`### In Progress\n${pendingTasks.join('\n')}\n`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Prompt for decisions
|
|
116
|
+
if (isInteractive) {
|
|
117
|
+
console.log('\nšÆ Any decisions made? (DEC-XXX format, or press Enter to skip)');
|
|
118
|
+
const decision = await prompt('> ');
|
|
119
|
+
if (decision.trim()) {
|
|
120
|
+
sections.push(`## Decisions Made\n\n${decision}\n`);
|
|
121
|
+
|
|
122
|
+
// Optionally append to decisions.md
|
|
123
|
+
if (existsSync(decisionsPath)) {
|
|
124
|
+
const decContent = readFileSync(decisionsPath, 'utf8');
|
|
125
|
+
// Find next DEC number
|
|
126
|
+
const decMatches = decContent.match(/DEC-(\d+)/g) || [];
|
|
127
|
+
const maxNum = decMatches.reduce((max, match) => {
|
|
128
|
+
const num = parseInt(match.replace('DEC-', ''));
|
|
129
|
+
return num > max ? num : max;
|
|
130
|
+
}, 0);
|
|
131
|
+
const nextNum = String(maxNum + 1).padStart(3, '0');
|
|
132
|
+
|
|
133
|
+
const newDecision = `\n## DEC-${nextNum}: ${decision.split(':')[0] || 'Decision'}\n**Date:** ${formatDate()}\n**Context:** Session save\n**Decision:** ${decision}\n`;
|
|
134
|
+
writeFileSync(decisionsPath, decContent + newDecision);
|
|
135
|
+
console.log(` ā Added DEC-${nextNum} to decisions.md`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Prompt for patterns
|
|
141
|
+
if (isInteractive) {
|
|
142
|
+
console.log('\nš Any patterns discovered? (press Enter to skip)');
|
|
143
|
+
const pattern = await prompt('> ');
|
|
144
|
+
if (pattern.trim()) {
|
|
145
|
+
sections.push(`## Patterns Learned\n\n${pattern}\n`);
|
|
146
|
+
|
|
147
|
+
// Optionally append to patterns.md
|
|
148
|
+
if (existsSync(patternsPath)) {
|
|
149
|
+
const patContent = readFileSync(patternsPath, 'utf8');
|
|
150
|
+
const newPattern = `\n### ${formatDate()} Pattern\n${pattern}\n`;
|
|
151
|
+
writeFileSync(patternsPath, patContent + newPattern);
|
|
152
|
+
console.log(' ā Added to patterns.md');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Prompt for dead-ends
|
|
158
|
+
if (isInteractive) {
|
|
159
|
+
console.log('\nā Any dead-ends to avoid? (press Enter to skip)');
|
|
160
|
+
const deadend = await prompt('> ');
|
|
161
|
+
if (deadend.trim()) {
|
|
162
|
+
sections.push(`## Dead-ends\n\n${deadend}\n`);
|
|
163
|
+
|
|
164
|
+
// Optionally append to deadends.md
|
|
165
|
+
if (existsSync(deadendsPath)) {
|
|
166
|
+
const deContent = readFileSync(deadendsPath, 'utf8');
|
|
167
|
+
// Find next DE number
|
|
168
|
+
const deMatches = deContent.match(/DE-(\d+)/g) || [];
|
|
169
|
+
const maxNum = deMatches.reduce((max, match) => {
|
|
170
|
+
const num = parseInt(match.replace('DE-', ''));
|
|
171
|
+
return num > max ? num : max;
|
|
172
|
+
}, 0);
|
|
173
|
+
const nextNum = String(maxNum + 1).padStart(3, '0');
|
|
174
|
+
|
|
175
|
+
const newDeadend = `\n## DE-${nextNum}: ${deadend.split(':')[0] || 'Dead-end'}\n**Date:** ${formatDate()}\n**Attempted:** ${deadend}\n**Problem:** See above\n**Solution:** TBD\n`;
|
|
176
|
+
writeFileSync(deadendsPath, deContent + newDeadend);
|
|
177
|
+
console.log(` ā Added DE-${nextNum} to deadends.md`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Add timestamp
|
|
183
|
+
sections.push(`\n---\n*Saved: ${new Date().toISOString()}*\n`);
|
|
184
|
+
|
|
185
|
+
// Write archive file
|
|
186
|
+
writeFileSync(finalPath, sections.join('\n'));
|
|
187
|
+
|
|
188
|
+
// Update state.json
|
|
189
|
+
if (existsSync(statePath)) {
|
|
190
|
+
try {
|
|
191
|
+
const state = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
192
|
+
state.last_save = new Date().toISOString();
|
|
193
|
+
state.last_save_file = finalPath.replace(cwd, '').replace(/^[\/\\]/, '');
|
|
194
|
+
writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
195
|
+
} catch {
|
|
196
|
+
// Ignore state errors
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
console.log(`\nā
Session saved to ${finalPath.replace(cwd, '').replace(/^[\/\\]/, '')}`);
|
|
201
|
+
console.log('\nš Next session will have access to:');
|
|
202
|
+
console.log(' - .ai/memory/archive/ (session history)');
|
|
203
|
+
console.log(' - .ai/memory/decisions.md');
|
|
204
|
+
console.log(' - .ai/memory/patterns.md');
|
|
205
|
+
console.log(' - .ai/memory/deadends.md\n');
|
|
206
|
+
}
|
package/cli/update.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, cpSync, renameSync, mkdirSync, readdirSync } from 'fs';
|
|
3
|
+
import { join, dirname } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Files that should be updated (framework files)
|
|
10
|
+
* These are safe to overwrite - they don't contain user data
|
|
11
|
+
*/
|
|
12
|
+
const FRAMEWORK_FILES = [
|
|
13
|
+
'agents/claude.md',
|
|
14
|
+
'agents/README.md',
|
|
15
|
+
'patterns/index.md',
|
|
16
|
+
'patterns/frontend/README.md',
|
|
17
|
+
'patterns/backend/README.md',
|
|
18
|
+
'patterns/universal/README.md',
|
|
19
|
+
'workflows/batch-task.md',
|
|
20
|
+
'config/README.md',
|
|
21
|
+
'README.md',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Claude Code slash commands (in .claude/commands/)
|
|
26
|
+
*/
|
|
27
|
+
const CLAUDE_COMMANDS = [
|
|
28
|
+
'save.md',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Files that should NOT be updated (user data)
|
|
33
|
+
* These contain user customizations
|
|
34
|
+
*/
|
|
35
|
+
const USER_FILES = [
|
|
36
|
+
'memory/decisions.md',
|
|
37
|
+
'memory/patterns.md',
|
|
38
|
+
'memory/deadends.md',
|
|
39
|
+
'memory/archive/',
|
|
40
|
+
'tasks/active.md',
|
|
41
|
+
'tasks/backlog.md',
|
|
42
|
+
'state.json',
|
|
43
|
+
'config/notifications.json',
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
export default async function updateCommand(args = []) {
|
|
47
|
+
const cwd = process.cwd();
|
|
48
|
+
const aiDir = join(cwd, '.ai');
|
|
49
|
+
const dryRun = args.includes('--dry-run');
|
|
50
|
+
|
|
51
|
+
console.log('š PWN Update\n');
|
|
52
|
+
|
|
53
|
+
// Check if .ai/ exists
|
|
54
|
+
if (!existsSync(aiDir)) {
|
|
55
|
+
console.log('ā No .ai/ directory found');
|
|
56
|
+
console.log(' Run "pwn inject" first to initialize workspace');
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Get template path
|
|
61
|
+
const templateDir = join(__dirname, '../templates/workspace/.ai');
|
|
62
|
+
|
|
63
|
+
if (!existsSync(templateDir)) {
|
|
64
|
+
console.log('ā Template directory not found');
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Get installed version from state.json or README
|
|
69
|
+
let currentVersion = 'unknown';
|
|
70
|
+
const statePath = join(aiDir, 'state.json');
|
|
71
|
+
if (existsSync(statePath)) {
|
|
72
|
+
try {
|
|
73
|
+
const state = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
74
|
+
currentVersion = state.pwn_version || 'unknown';
|
|
75
|
+
} catch {
|
|
76
|
+
// Ignore
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Get new version from package.json
|
|
81
|
+
const packagePath = join(__dirname, '../package.json');
|
|
82
|
+
const pkg = JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
83
|
+
const newVersion = pkg.version;
|
|
84
|
+
|
|
85
|
+
console.log(` Current: ${currentVersion}`);
|
|
86
|
+
console.log(` New: ${newVersion}\n`);
|
|
87
|
+
|
|
88
|
+
if (dryRun) {
|
|
89
|
+
console.log('š Dry run - showing what would be updated:\n');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const updated = [];
|
|
93
|
+
const skipped = [];
|
|
94
|
+
const backed_up = [];
|
|
95
|
+
|
|
96
|
+
// Update framework files in .ai/
|
|
97
|
+
for (const file of FRAMEWORK_FILES) {
|
|
98
|
+
const templateFile = join(templateDir, file);
|
|
99
|
+
const targetFile = join(aiDir, file);
|
|
100
|
+
|
|
101
|
+
if (!existsSync(templateFile)) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const templateContent = readFileSync(templateFile, 'utf8');
|
|
106
|
+
const targetExists = existsSync(targetFile);
|
|
107
|
+
const targetContent = targetExists ? readFileSync(targetFile, 'utf8') : '';
|
|
108
|
+
|
|
109
|
+
if (templateContent !== targetContent) {
|
|
110
|
+
if (dryRun) {
|
|
111
|
+
console.log(` š Would update: .ai/${file}`);
|
|
112
|
+
} else {
|
|
113
|
+
// Ensure directory exists
|
|
114
|
+
const dir = dirname(targetFile);
|
|
115
|
+
if (!existsSync(dir)) {
|
|
116
|
+
const { mkdirSync } = await import('fs');
|
|
117
|
+
mkdirSync(dir, { recursive: true });
|
|
118
|
+
}
|
|
119
|
+
writeFileSync(targetFile, templateContent);
|
|
120
|
+
console.log(` š Updated: .ai/${file}`);
|
|
121
|
+
}
|
|
122
|
+
updated.push(file);
|
|
123
|
+
} else {
|
|
124
|
+
skipped.push(file);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Update CLAUDE.md in root
|
|
129
|
+
const claudeMdPath = join(cwd, 'CLAUDE.md');
|
|
130
|
+
const templateClaudeMd = join(templateDir, 'agents', 'claude.md');
|
|
131
|
+
const backupClaudeMdPath = join(cwd, '~CLAUDE.md');
|
|
132
|
+
|
|
133
|
+
if (existsSync(templateClaudeMd)) {
|
|
134
|
+
const templateContent = readFileSync(templateClaudeMd, 'utf8');
|
|
135
|
+
const currentContent = existsSync(claudeMdPath) ? readFileSync(claudeMdPath, 'utf8') : '';
|
|
136
|
+
|
|
137
|
+
if (templateContent !== currentContent) {
|
|
138
|
+
if (dryRun) {
|
|
139
|
+
console.log(` š Would update: CLAUDE.md`);
|
|
140
|
+
if (currentContent) {
|
|
141
|
+
console.log(` š¦ Would backup: CLAUDE.md ā ~CLAUDE.md`);
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
// Backup existing CLAUDE.md
|
|
145
|
+
if (existsSync(claudeMdPath) && currentContent) {
|
|
146
|
+
renameSync(claudeMdPath, backupClaudeMdPath);
|
|
147
|
+
console.log(` š¦ Backed up: CLAUDE.md ā ~CLAUDE.md`);
|
|
148
|
+
backed_up.push('CLAUDE.md');
|
|
149
|
+
}
|
|
150
|
+
// Copy new template
|
|
151
|
+
writeFileSync(claudeMdPath, templateContent);
|
|
152
|
+
console.log(` š Updated: CLAUDE.md`);
|
|
153
|
+
}
|
|
154
|
+
updated.push('CLAUDE.md');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Update .claude/commands/ (slash commands)
|
|
159
|
+
const claudeCommandsDir = join(cwd, '.claude', 'commands');
|
|
160
|
+
const templateCommandsDir = join(__dirname, '../templates/workspace/.claude/commands');
|
|
161
|
+
|
|
162
|
+
if (existsSync(templateCommandsDir)) {
|
|
163
|
+
for (const cmdFile of CLAUDE_COMMANDS) {
|
|
164
|
+
const templateFile = join(templateCommandsDir, cmdFile);
|
|
165
|
+
const targetFile = join(claudeCommandsDir, cmdFile);
|
|
166
|
+
|
|
167
|
+
if (!existsSync(templateFile)) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const templateContent = readFileSync(templateFile, 'utf8');
|
|
172
|
+
const targetExists = existsSync(targetFile);
|
|
173
|
+
const targetContent = targetExists ? readFileSync(targetFile, 'utf8') : '';
|
|
174
|
+
|
|
175
|
+
if (templateContent !== targetContent) {
|
|
176
|
+
if (dryRun) {
|
|
177
|
+
console.log(` š Would update: .claude/commands/${cmdFile}`);
|
|
178
|
+
} else {
|
|
179
|
+
// Ensure directory exists
|
|
180
|
+
if (!existsSync(claudeCommandsDir)) {
|
|
181
|
+
mkdirSync(claudeCommandsDir, { recursive: true });
|
|
182
|
+
}
|
|
183
|
+
writeFileSync(targetFile, templateContent);
|
|
184
|
+
console.log(` š Updated: .claude/commands/${cmdFile}`);
|
|
185
|
+
}
|
|
186
|
+
updated.push(`.claude/commands/${cmdFile}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Update state.json with new version
|
|
192
|
+
if (!dryRun && existsSync(statePath)) {
|
|
193
|
+
try {
|
|
194
|
+
const state = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
195
|
+
state.pwn_version = newVersion;
|
|
196
|
+
state.last_updated = new Date().toISOString();
|
|
197
|
+
writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
198
|
+
} catch {
|
|
199
|
+
// Ignore
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Summary
|
|
204
|
+
console.log('');
|
|
205
|
+
if (updated.length === 0) {
|
|
206
|
+
console.log('ā
Already up to date!\n');
|
|
207
|
+
} else if (dryRun) {
|
|
208
|
+
console.log(`š Would update ${updated.length} file(s)\n`);
|
|
209
|
+
console.log(' Run without --dry-run to apply updates');
|
|
210
|
+
} else {
|
|
211
|
+
console.log(`ā
Updated ${updated.length} file(s)\n`);
|
|
212
|
+
|
|
213
|
+
if (backed_up.length > 0) {
|
|
214
|
+
console.log('š¦ Backed up files:');
|
|
215
|
+
for (const f of backed_up) {
|
|
216
|
+
console.log(` ${f} ā ~${f}`);
|
|
217
|
+
}
|
|
218
|
+
console.log('');
|
|
219
|
+
console.log(' š” Merge your custom instructions into CLAUDE.md');
|
|
220
|
+
console.log(' šļø Delete backup files when done\n');
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Show what was preserved
|
|
225
|
+
console.log('š Preserved (user data):');
|
|
226
|
+
console.log(' .ai/memory/ (decisions, patterns, deadends)');
|
|
227
|
+
console.log(' .ai/tasks/ (active, backlog)');
|
|
228
|
+
console.log(' .ai/state.json (session state)');
|
|
229
|
+
console.log(' .claude/ (your custom commands preserved)\n');
|
|
230
|
+
}
|
package/package.json
CHANGED
package/src/core/inject.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, cpSync, renameSync, readFileSync, appendFileSync, writeFileSync, readdirSync } from 'fs';
|
|
1
|
+
import { existsSync, cpSync, renameSync, readFileSync, appendFileSync, writeFileSync, readdirSync, mkdirSync } from 'fs';
|
|
2
2
|
import { join, dirname } from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { randomUUID } from 'crypto';
|
|
@@ -86,6 +86,14 @@ export function getTemplatePath() {
|
|
|
86
86
|
return join(__dirname, '../../templates/workspace/.ai');
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Get the path to the .claude commands template
|
|
91
|
+
* @returns {string} Path to .claude template directory
|
|
92
|
+
*/
|
|
93
|
+
export function getClaudeCommandsTemplatePath() {
|
|
94
|
+
return join(__dirname, '../../templates/workspace/.claude');
|
|
95
|
+
}
|
|
96
|
+
|
|
89
97
|
/**
|
|
90
98
|
* Inject PWN workspace into a project
|
|
91
99
|
* @param {object} options - Injection options
|
|
@@ -104,7 +112,9 @@ export async function inject(options = {}) {
|
|
|
104
112
|
} = options;
|
|
105
113
|
|
|
106
114
|
const templateDir = getTemplatePath();
|
|
115
|
+
const claudeTemplateDir = getClaudeCommandsTemplatePath();
|
|
107
116
|
const targetDir = join(cwd, '.ai');
|
|
117
|
+
const claudeDir = join(cwd, '.claude');
|
|
108
118
|
|
|
109
119
|
const log = silent ? () => {} : console.log;
|
|
110
120
|
|
|
@@ -121,6 +131,22 @@ export async function inject(options = {}) {
|
|
|
121
131
|
};
|
|
122
132
|
}
|
|
123
133
|
|
|
134
|
+
// Backup existing AI files content before overwriting
|
|
135
|
+
const backedUpContent = {};
|
|
136
|
+
if (detectedFiles.length > 0 && detectedFiles.some(f => f.migratable)) {
|
|
137
|
+
log('š Backing up existing AI files...');
|
|
138
|
+
for (const file of detectedFiles.filter(f => f.migratable)) {
|
|
139
|
+
try {
|
|
140
|
+
backedUpContent[file.file] = {
|
|
141
|
+
content: readFileSync(file.path, 'utf8'),
|
|
142
|
+
type: file.type
|
|
143
|
+
};
|
|
144
|
+
} catch {
|
|
145
|
+
// Ignore read errors
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
124
150
|
try {
|
|
125
151
|
// Copy workspace template
|
|
126
152
|
log('š¦ Copying workspace template...');
|
|
@@ -149,11 +175,67 @@ export async function inject(options = {}) {
|
|
|
149
175
|
// Update .gitignore
|
|
150
176
|
updateGitignore(cwd, silent);
|
|
151
177
|
|
|
178
|
+
// Handle CLAUDE.md: backup existing and copy PWN template to root
|
|
179
|
+
let backupInfo = { backed_up: [] };
|
|
180
|
+
const claudeMdPath = join(cwd, 'CLAUDE.md');
|
|
181
|
+
const backupClaudeMdPath = join(cwd, '~CLAUDE.md');
|
|
182
|
+
const templateClaudeMd = join(targetDir, 'agents', 'claude.md');
|
|
183
|
+
|
|
184
|
+
// Backup existing CLAUDE.md if present
|
|
185
|
+
if (backedUpContent['CLAUDE.md'] || backedUpContent['claude.md']) {
|
|
186
|
+
const originalName = backedUpContent['CLAUDE.md'] ? 'CLAUDE.md' : 'claude.md';
|
|
187
|
+
const originalPath = join(cwd, originalName);
|
|
188
|
+
|
|
189
|
+
if (existsSync(originalPath)) {
|
|
190
|
+
renameSync(originalPath, backupClaudeMdPath);
|
|
191
|
+
backupInfo.backed_up.push({ from: originalName, to: '~CLAUDE.md' });
|
|
192
|
+
if (!silent) {
|
|
193
|
+
console.log(`š¦ Backed up ${originalName} ā ~CLAUDE.md`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Copy PWN template to CLAUDE.md in root
|
|
199
|
+
if (existsSync(templateClaudeMd)) {
|
|
200
|
+
cpSync(templateClaudeMd, claudeMdPath);
|
|
201
|
+
if (!silent) {
|
|
202
|
+
console.log('š Created CLAUDE.md with PWN template');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Copy .claude/commands/ for slash commands
|
|
207
|
+
if (existsSync(claudeTemplateDir)) {
|
|
208
|
+
// Create .claude/commands/ if it doesn't exist
|
|
209
|
+
const commandsDir = join(claudeDir, 'commands');
|
|
210
|
+
if (!existsSync(commandsDir)) {
|
|
211
|
+
mkdirSync(commandsDir, { recursive: true });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Copy all command files
|
|
215
|
+
const claudeCommandsSource = join(claudeTemplateDir, 'commands');
|
|
216
|
+
if (existsSync(claudeCommandsSource)) {
|
|
217
|
+
cpSync(claudeCommandsSource, commandsDir, { recursive: true });
|
|
218
|
+
if (!silent) {
|
|
219
|
+
console.log('š Created .claude/commands/ with PWN slash commands');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Backup other AI files (not CLAUDE.md) to .ai/
|
|
225
|
+
const otherFiles = Object.fromEntries(
|
|
226
|
+
Object.entries(backedUpContent).filter(([k]) => k.toLowerCase() !== 'claude.md')
|
|
227
|
+
);
|
|
228
|
+
if (Object.keys(otherFiles).length > 0) {
|
|
229
|
+
const otherBackups = backupExistingAIFiles(targetDir, otherFiles, silent);
|
|
230
|
+
backupInfo.backed_up.push(...otherBackups.backed_up);
|
|
231
|
+
}
|
|
232
|
+
|
|
152
233
|
return {
|
|
153
234
|
success: true,
|
|
154
235
|
message: 'PWN workspace injected successfully',
|
|
155
236
|
path: targetDir,
|
|
156
|
-
detected: detectedFiles
|
|
237
|
+
detected: detectedFiles,
|
|
238
|
+
backed_up: backupInfo.backed_up
|
|
157
239
|
};
|
|
158
240
|
|
|
159
241
|
} catch (error) {
|
|
@@ -185,6 +267,44 @@ function initNotifications(notifyFile) {
|
|
|
185
267
|
}
|
|
186
268
|
}
|
|
187
269
|
|
|
270
|
+
/**
|
|
271
|
+
* Backup existing AI files to .ai/ root with ~ prefix
|
|
272
|
+
* - PWN template stays as the base (it's the correct one)
|
|
273
|
+
* - User's existing files are backed up for manual merge
|
|
274
|
+
* @param {string} targetDir - Path to .ai/ directory
|
|
275
|
+
* @param {Object} backedUpContent - Map of filename -> {content, type}
|
|
276
|
+
* @param {boolean} silent - Suppress output
|
|
277
|
+
* @returns {Object} Backup info
|
|
278
|
+
*/
|
|
279
|
+
function backupExistingAIFiles(targetDir, backedUpContent, silent = false) {
|
|
280
|
+
const result = { backed_up: [] };
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
// Backup all existing AI files with ~ prefix
|
|
284
|
+
for (const [filename, data] of Object.entries(backedUpContent)) {
|
|
285
|
+
// Convert filename to backup name: CLAUDE.md -> ~CLAUDE.md, .cursorrules -> ~cursorrules.md
|
|
286
|
+
const safeName = filename.replace(/^\./, '').replace(/[\/\\]/g, '-');
|
|
287
|
+
const backupName = `~${safeName}${safeName.endsWith('.md') ? '' : '.md'}`;
|
|
288
|
+
const backupPath = join(targetDir, backupName);
|
|
289
|
+
|
|
290
|
+
writeFileSync(backupPath, data.content);
|
|
291
|
+
|
|
292
|
+
result.backed_up.push({ from: filename, to: backupName });
|
|
293
|
+
|
|
294
|
+
if (!silent) {
|
|
295
|
+
console.log(`š¦ Backed up ${filename} ā .ai/${backupName}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
} catch (error) {
|
|
300
|
+
if (!silent) {
|
|
301
|
+
console.error('ā ļø Backup warning:', error.message);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return result;
|
|
306
|
+
}
|
|
307
|
+
|
|
188
308
|
/**
|
|
189
309
|
* Update .gitignore to exclude PWN personal files
|
|
190
310
|
* @param {string} cwd - Working directory
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# /save - PWN Session Save
|
|
2
|
+
|
|
3
|
+
Save your current work state to PWN framework files (.ai/state.json and .ai/memory/).
|
|
4
|
+
|
|
5
|
+
## What this command does
|
|
6
|
+
|
|
7
|
+
1. Updates .ai/state.json with current session state
|
|
8
|
+
2. Updates .ai/tasks/active.md with task progress
|
|
9
|
+
3. Logs new dead ends to .ai/memory/deadends.md
|
|
10
|
+
4. Optionally updates .ai/memory/decisions.md with new decisions
|
|
11
|
+
5. Creates archive entry in .ai/memory/archive/
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Instructions
|
|
16
|
+
|
|
17
|
+
First, identify the current developer by running:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
git config user.name
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Then, analyze the current session and update PWN files accordingly.
|
|
24
|
+
|
|
25
|
+
## Files to Update
|
|
26
|
+
|
|
27
|
+
### 1. State File (.ai/state.json)
|
|
28
|
+
|
|
29
|
+
Update the session state:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"developer": "{username from git config}",
|
|
34
|
+
"session_started": "{ISO timestamp}",
|
|
35
|
+
"session_mode": "interactive",
|
|
36
|
+
"current_task": "{task ID or null}",
|
|
37
|
+
"context_loaded": ["memory", "patterns", "tasks"],
|
|
38
|
+
"last_save": "{current ISO timestamp}"
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 2. Active Tasks (.ai/tasks/active.md)
|
|
43
|
+
|
|
44
|
+
If there are tasks in progress:
|
|
45
|
+
- Update checkbox status ([ ] or [x])
|
|
46
|
+
- Add notes about blockers
|
|
47
|
+
- Update priority if changed
|
|
48
|
+
|
|
49
|
+
### 3. Dead Ends (.ai/memory/deadends.md)
|
|
50
|
+
|
|
51
|
+
When a new dead end is discovered:
|
|
52
|
+
|
|
53
|
+
1. **Get next ID** from existing entries (last DE-XXX + 1)
|
|
54
|
+
|
|
55
|
+
2. **Add detailed entry**:
|
|
56
|
+
```markdown
|
|
57
|
+
## DE-XXX: Title
|
|
58
|
+
|
|
59
|
+
**Date:** YYYY-MM-DD
|
|
60
|
+
**Attempted:** What was tried
|
|
61
|
+
**Problem:** Why it failed
|
|
62
|
+
**Solution:** What worked instead
|
|
63
|
+
**Tags:** relevant, keywords
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 4. Decisions (.ai/memory/decisions.md)
|
|
67
|
+
|
|
68
|
+
If new architectural decisions were made:
|
|
69
|
+
|
|
70
|
+
1. **Get next ID** from existing entries (last DEC-XXX + 1)
|
|
71
|
+
|
|
72
|
+
2. **Add entry**:
|
|
73
|
+
```markdown
|
|
74
|
+
## DEC-XXX: Decision Title
|
|
75
|
+
|
|
76
|
+
**Date:** YYYY-MM-DD
|
|
77
|
+
**Status:** Active
|
|
78
|
+
**Context:** Why this decision was needed
|
|
79
|
+
**Decision:** What was decided
|
|
80
|
+
**Rationale:** Why this is the best choice
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 5. Archive Entry (.ai/memory/archive/{date}-session.md)
|
|
84
|
+
|
|
85
|
+
Create a session summary:
|
|
86
|
+
|
|
87
|
+
```markdown
|
|
88
|
+
# Session: YYYY-MM-DD
|
|
89
|
+
|
|
90
|
+
## Summary
|
|
91
|
+
{brief description of what was done this session}
|
|
92
|
+
|
|
93
|
+
## Tasks
|
|
94
|
+
### Completed
|
|
95
|
+
- [x] Task descriptions...
|
|
96
|
+
|
|
97
|
+
### In Progress
|
|
98
|
+
- [ ] Task descriptions...
|
|
99
|
+
|
|
100
|
+
## Decisions Made
|
|
101
|
+
- DEC-XXX: description (if any)
|
|
102
|
+
|
|
103
|
+
## Dead Ends
|
|
104
|
+
- DE-XXX: description (if any)
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
*Saved: {ISO timestamp}*
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Output Format
|
|
113
|
+
|
|
114
|
+
After updating all files, confirm to the user:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
PWN Save Complete:
|
|
118
|
+
- State: .ai/state.json updated (developer: {username})
|
|
119
|
+
- Tasks: {X} active, {Y} completed
|
|
120
|
+
- Dead Ends: {new count or "no new"}
|
|
121
|
+
- Decisions: {new count or "no new"}
|
|
122
|
+
- Archive: .ai/memory/archive/{filename}
|
|
123
|
+
- Last updated: {timestamp}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Rules
|
|
129
|
+
|
|
130
|
+
1. **state.json**: Only current session state, no history
|
|
131
|
+
2. **deadends.md**: Detailed entries with proper DE-XXX IDs
|
|
132
|
+
3. **decisions.md**: Organized entries with proper DEC-XXX IDs
|
|
133
|
+
4. **active.md**: Only current sprint tasks
|
|
134
|
+
5. **archive/**: One file per session with summary
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Notes
|
|
139
|
+
|
|
140
|
+
- This command replaces the old /checkpoint command
|
|
141
|
+
- Uses PWN framework structure in .ai/ directory
|
|
142
|
+
- Always get the next available ID number before adding entries
|