@fermindi/pwn-cli 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/cli/index.js +14 -0
- package/cli/inject.js +12 -0
- package/cli/save.js +206 -0
- package/cli/update.js +189 -0
- package/package.json +1 -1
- package/src/core/inject.js +94 -2
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)');
|
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,189 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, cpSync, renameSync } 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
|
+
* Files that should NOT be updated (user data)
|
|
26
|
+
* These contain user customizations
|
|
27
|
+
*/
|
|
28
|
+
const USER_FILES = [
|
|
29
|
+
'memory/decisions.md',
|
|
30
|
+
'memory/patterns.md',
|
|
31
|
+
'memory/deadends.md',
|
|
32
|
+
'memory/archive/',
|
|
33
|
+
'tasks/active.md',
|
|
34
|
+
'tasks/backlog.md',
|
|
35
|
+
'state.json',
|
|
36
|
+
'config/notifications.json',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
export default async function updateCommand(args = []) {
|
|
40
|
+
const cwd = process.cwd();
|
|
41
|
+
const aiDir = join(cwd, '.ai');
|
|
42
|
+
const dryRun = args.includes('--dry-run');
|
|
43
|
+
|
|
44
|
+
console.log('š PWN Update\n');
|
|
45
|
+
|
|
46
|
+
// Check if .ai/ exists
|
|
47
|
+
if (!existsSync(aiDir)) {
|
|
48
|
+
console.log('ā No .ai/ directory found');
|
|
49
|
+
console.log(' Run "pwn inject" first to initialize workspace');
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Get template path
|
|
54
|
+
const templateDir = join(__dirname, '../templates/workspace/.ai');
|
|
55
|
+
|
|
56
|
+
if (!existsSync(templateDir)) {
|
|
57
|
+
console.log('ā Template directory not found');
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Get installed version from state.json or README
|
|
62
|
+
let currentVersion = 'unknown';
|
|
63
|
+
const statePath = join(aiDir, 'state.json');
|
|
64
|
+
if (existsSync(statePath)) {
|
|
65
|
+
try {
|
|
66
|
+
const state = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
67
|
+
currentVersion = state.pwn_version || 'unknown';
|
|
68
|
+
} catch {
|
|
69
|
+
// Ignore
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Get new version from package.json
|
|
74
|
+
const packagePath = join(__dirname, '../package.json');
|
|
75
|
+
const pkg = JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
76
|
+
const newVersion = pkg.version;
|
|
77
|
+
|
|
78
|
+
console.log(` Current: ${currentVersion}`);
|
|
79
|
+
console.log(` New: ${newVersion}\n`);
|
|
80
|
+
|
|
81
|
+
if (dryRun) {
|
|
82
|
+
console.log('š Dry run - showing what would be updated:\n');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const updated = [];
|
|
86
|
+
const skipped = [];
|
|
87
|
+
const backed_up = [];
|
|
88
|
+
|
|
89
|
+
// Update framework files in .ai/
|
|
90
|
+
for (const file of FRAMEWORK_FILES) {
|
|
91
|
+
const templateFile = join(templateDir, file);
|
|
92
|
+
const targetFile = join(aiDir, file);
|
|
93
|
+
|
|
94
|
+
if (!existsSync(templateFile)) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const templateContent = readFileSync(templateFile, 'utf8');
|
|
99
|
+
const targetExists = existsSync(targetFile);
|
|
100
|
+
const targetContent = targetExists ? readFileSync(targetFile, 'utf8') : '';
|
|
101
|
+
|
|
102
|
+
if (templateContent !== targetContent) {
|
|
103
|
+
if (dryRun) {
|
|
104
|
+
console.log(` š Would update: .ai/${file}`);
|
|
105
|
+
} else {
|
|
106
|
+
// Ensure directory exists
|
|
107
|
+
const dir = dirname(targetFile);
|
|
108
|
+
if (!existsSync(dir)) {
|
|
109
|
+
const { mkdirSync } = await import('fs');
|
|
110
|
+
mkdirSync(dir, { recursive: true });
|
|
111
|
+
}
|
|
112
|
+
writeFileSync(targetFile, templateContent);
|
|
113
|
+
console.log(` š Updated: .ai/${file}`);
|
|
114
|
+
}
|
|
115
|
+
updated.push(file);
|
|
116
|
+
} else {
|
|
117
|
+
skipped.push(file);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Update CLAUDE.md in root
|
|
122
|
+
const claudeMdPath = join(cwd, 'CLAUDE.md');
|
|
123
|
+
const templateClaudeMd = join(templateDir, 'agents', 'claude.md');
|
|
124
|
+
const backupClaudeMdPath = join(cwd, '~CLAUDE.md');
|
|
125
|
+
|
|
126
|
+
if (existsSync(templateClaudeMd)) {
|
|
127
|
+
const templateContent = readFileSync(templateClaudeMd, 'utf8');
|
|
128
|
+
const currentContent = existsSync(claudeMdPath) ? readFileSync(claudeMdPath, 'utf8') : '';
|
|
129
|
+
|
|
130
|
+
if (templateContent !== currentContent) {
|
|
131
|
+
if (dryRun) {
|
|
132
|
+
console.log(` š Would update: CLAUDE.md`);
|
|
133
|
+
if (currentContent) {
|
|
134
|
+
console.log(` š¦ Would backup: CLAUDE.md ā ~CLAUDE.md`);
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
// Backup existing CLAUDE.md
|
|
138
|
+
if (existsSync(claudeMdPath) && currentContent) {
|
|
139
|
+
renameSync(claudeMdPath, backupClaudeMdPath);
|
|
140
|
+
console.log(` š¦ Backed up: CLAUDE.md ā ~CLAUDE.md`);
|
|
141
|
+
backed_up.push('CLAUDE.md');
|
|
142
|
+
}
|
|
143
|
+
// Copy new template
|
|
144
|
+
writeFileSync(claudeMdPath, templateContent);
|
|
145
|
+
console.log(` š Updated: CLAUDE.md`);
|
|
146
|
+
}
|
|
147
|
+
updated.push('CLAUDE.md');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Update state.json with new version
|
|
152
|
+
if (!dryRun && existsSync(statePath)) {
|
|
153
|
+
try {
|
|
154
|
+
const state = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
155
|
+
state.pwn_version = newVersion;
|
|
156
|
+
state.last_updated = new Date().toISOString();
|
|
157
|
+
writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
158
|
+
} catch {
|
|
159
|
+
// Ignore
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Summary
|
|
164
|
+
console.log('');
|
|
165
|
+
if (updated.length === 0) {
|
|
166
|
+
console.log('ā
Already up to date!\n');
|
|
167
|
+
} else if (dryRun) {
|
|
168
|
+
console.log(`š Would update ${updated.length} file(s)\n`);
|
|
169
|
+
console.log(' Run without --dry-run to apply updates');
|
|
170
|
+
} else {
|
|
171
|
+
console.log(`ā
Updated ${updated.length} file(s)\n`);
|
|
172
|
+
|
|
173
|
+
if (backed_up.length > 0) {
|
|
174
|
+
console.log('š¦ Backed up files:');
|
|
175
|
+
for (const f of backed_up) {
|
|
176
|
+
console.log(` ${f} ā ~${f}`);
|
|
177
|
+
}
|
|
178
|
+
console.log('');
|
|
179
|
+
console.log(' š” Merge your custom instructions into CLAUDE.md');
|
|
180
|
+
console.log(' šļø Delete backup files when done\n');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Show what was preserved
|
|
185
|
+
console.log('š Preserved (user data):');
|
|
186
|
+
console.log(' .ai/memory/ (decisions, patterns, deadends)');
|
|
187
|
+
console.log(' .ai/tasks/ (active, backlog)');
|
|
188
|
+
console.log(' .ai/state.json (session state)\n');
|
|
189
|
+
}
|
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';
|
|
@@ -121,6 +121,22 @@ export async function inject(options = {}) {
|
|
|
121
121
|
};
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
// Backup existing AI files content before overwriting
|
|
125
|
+
const backedUpContent = {};
|
|
126
|
+
if (detectedFiles.length > 0 && detectedFiles.some(f => f.migratable)) {
|
|
127
|
+
log('š Backing up existing AI files...');
|
|
128
|
+
for (const file of detectedFiles.filter(f => f.migratable)) {
|
|
129
|
+
try {
|
|
130
|
+
backedUpContent[file.file] = {
|
|
131
|
+
content: readFileSync(file.path, 'utf8'),
|
|
132
|
+
type: file.type
|
|
133
|
+
};
|
|
134
|
+
} catch {
|
|
135
|
+
// Ignore read errors
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
124
140
|
try {
|
|
125
141
|
// Copy workspace template
|
|
126
142
|
log('š¦ Copying workspace template...');
|
|
@@ -149,11 +165,49 @@ export async function inject(options = {}) {
|
|
|
149
165
|
// Update .gitignore
|
|
150
166
|
updateGitignore(cwd, silent);
|
|
151
167
|
|
|
168
|
+
// Handle CLAUDE.md: backup existing and copy PWN template to root
|
|
169
|
+
let backupInfo = { backed_up: [] };
|
|
170
|
+
const claudeMdPath = join(cwd, 'CLAUDE.md');
|
|
171
|
+
const backupClaudeMdPath = join(cwd, '~CLAUDE.md');
|
|
172
|
+
const templateClaudeMd = join(targetDir, 'agents', 'claude.md');
|
|
173
|
+
|
|
174
|
+
// Backup existing CLAUDE.md if present
|
|
175
|
+
if (backedUpContent['CLAUDE.md'] || backedUpContent['claude.md']) {
|
|
176
|
+
const originalName = backedUpContent['CLAUDE.md'] ? 'CLAUDE.md' : 'claude.md';
|
|
177
|
+
const originalPath = join(cwd, originalName);
|
|
178
|
+
|
|
179
|
+
if (existsSync(originalPath)) {
|
|
180
|
+
renameSync(originalPath, backupClaudeMdPath);
|
|
181
|
+
backupInfo.backed_up.push({ from: originalName, to: '~CLAUDE.md' });
|
|
182
|
+
if (!silent) {
|
|
183
|
+
console.log(`š¦ Backed up ${originalName} ā ~CLAUDE.md`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Copy PWN template to CLAUDE.md in root
|
|
189
|
+
if (existsSync(templateClaudeMd)) {
|
|
190
|
+
cpSync(templateClaudeMd, claudeMdPath);
|
|
191
|
+
if (!silent) {
|
|
192
|
+
console.log('š Created CLAUDE.md with PWN template');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Backup other AI files (not CLAUDE.md) to .ai/
|
|
197
|
+
const otherFiles = Object.fromEntries(
|
|
198
|
+
Object.entries(backedUpContent).filter(([k]) => k.toLowerCase() !== 'claude.md')
|
|
199
|
+
);
|
|
200
|
+
if (Object.keys(otherFiles).length > 0) {
|
|
201
|
+
const otherBackups = backupExistingAIFiles(targetDir, otherFiles, silent);
|
|
202
|
+
backupInfo.backed_up.push(...otherBackups.backed_up);
|
|
203
|
+
}
|
|
204
|
+
|
|
152
205
|
return {
|
|
153
206
|
success: true,
|
|
154
207
|
message: 'PWN workspace injected successfully',
|
|
155
208
|
path: targetDir,
|
|
156
|
-
detected: detectedFiles
|
|
209
|
+
detected: detectedFiles,
|
|
210
|
+
backed_up: backupInfo.backed_up
|
|
157
211
|
};
|
|
158
212
|
|
|
159
213
|
} catch (error) {
|
|
@@ -185,6 +239,44 @@ function initNotifications(notifyFile) {
|
|
|
185
239
|
}
|
|
186
240
|
}
|
|
187
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Backup existing AI files to .ai/ root with ~ prefix
|
|
244
|
+
* - PWN template stays as the base (it's the correct one)
|
|
245
|
+
* - User's existing files are backed up for manual merge
|
|
246
|
+
* @param {string} targetDir - Path to .ai/ directory
|
|
247
|
+
* @param {Object} backedUpContent - Map of filename -> {content, type}
|
|
248
|
+
* @param {boolean} silent - Suppress output
|
|
249
|
+
* @returns {Object} Backup info
|
|
250
|
+
*/
|
|
251
|
+
function backupExistingAIFiles(targetDir, backedUpContent, silent = false) {
|
|
252
|
+
const result = { backed_up: [] };
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
// Backup all existing AI files with ~ prefix
|
|
256
|
+
for (const [filename, data] of Object.entries(backedUpContent)) {
|
|
257
|
+
// Convert filename to backup name: CLAUDE.md -> ~CLAUDE.md, .cursorrules -> ~cursorrules.md
|
|
258
|
+
const safeName = filename.replace(/^\./, '').replace(/[\/\\]/g, '-');
|
|
259
|
+
const backupName = `~${safeName}${safeName.endsWith('.md') ? '' : '.md'}`;
|
|
260
|
+
const backupPath = join(targetDir, backupName);
|
|
261
|
+
|
|
262
|
+
writeFileSync(backupPath, data.content);
|
|
263
|
+
|
|
264
|
+
result.backed_up.push({ from: filename, to: backupName });
|
|
265
|
+
|
|
266
|
+
if (!silent) {
|
|
267
|
+
console.log(`š¦ Backed up ${filename} ā .ai/${backupName}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
} catch (error) {
|
|
272
|
+
if (!silent) {
|
|
273
|
+
console.error('ā ļø Backup warning:', error.message);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
|
|
188
280
|
/**
|
|
189
281
|
* Update .gitignore to exclude PWN personal files
|
|
190
282
|
* @param {string} cwd - Working directory
|