@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fermindi/pwn-cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
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';
@@ -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