@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 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)\n');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fermindi/pwn-cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Professional AI Workspace - Inject structured memory and automation into any project for AI-powered development",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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