wolverine-ai 2.6.1 → 2.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -3
- package/package.json +1 -1
- package/src/brain/brain.js +1 -1
- package/src/platform/auto-update.js +0 -119
- package/src/skills/update.js +83 -137
package/README.md
CHANGED
|
@@ -535,14 +535,22 @@ All demos use the `server/` directory pattern. Each demo:
|
|
|
535
535
|
|
|
536
536
|
## Backup System
|
|
537
537
|
|
|
538
|
-
|
|
538
|
+
All backups stored in **`~/.wolverine-safe-backups/`** — outside the project directory. Survives `git pull`, `npm install`, `rm -rf .wolverine`, even deleting the project entirely.
|
|
539
539
|
|
|
540
|
-
|
|
540
|
+
```
|
|
541
|
+
~/.wolverine-safe-backups/
|
|
542
|
+
manifest.json ← backup registry
|
|
543
|
+
snapshots/ ← heal snapshots (per fix attempt)
|
|
544
|
+
updates/ ← pre-update snapshots (before framework upgrades)
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
- Created before every repair attempt and every framework update (with reason string)
|
|
541
548
|
- Created on graceful shutdown (`createShutdownBackup()`)
|
|
542
549
|
- Includes all files: `.js`, `.json`, `.sql`, `.db`, `.yaml`, configs
|
|
550
|
+
- Old `.wolverine/backups/` auto-migrated to safe location on first run
|
|
543
551
|
- **Status lifecycle**: UNSTABLE → VERIFIED (fix passed) → STABLE (30min+ uptime)
|
|
544
552
|
- **Retention**: unstable/verified pruned after 7 days, stable keeps 1/day after 7 days
|
|
545
|
-
-
|
|
553
|
+
- Protected files never overwritten during rollback: `settings.json`, `db.js`, `.env.local`
|
|
546
554
|
|
|
547
555
|
**Rollback & Recovery:**
|
|
548
556
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wolverine-ai",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.3",
|
|
4
4
|
"description": "Self-healing Node.js server framework powered by AI. Catches crashes, diagnoses errors, generates fixes, verifies, and restarts — automatically.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
package/src/brain/brain.js
CHANGED
|
@@ -38,7 +38,7 @@ const SEED_DOCS = [
|
|
|
38
38
|
metadata: { topic: "heal-pipeline" },
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
|
-
text: "Wolverine backup system:
|
|
41
|
+
text: "Wolverine backup system: ALL backups stored in ~/.wolverine-safe-backups/ (OUTSIDE project, survives git pull/npm install/rm -rf). Structure: snapshots/ (heal backups per fix attempt), updates/ (pre-update snapshots), manifest.json (backup registry). Old .wolverine/backups/ auto-migrated on first run. Full server/ directory snapshots with lifecycle management. Every fix creates a backup with a reason string before patching. Status lifecycle: UNSTABLE (just created) → VERIFIED (fix passed boot probe) → STABLE (server ran 30min+ without crash). Features: rollbackTo(backupId) creates pre-rollback backup then restores files and restarts server. undoRollback() restores pre-rollback state. Hot-load: admin can load any backup as current server state from dashboard. Shutdown backup on graceful exit. Retention: unstable/verified pruned after 7 days. Stable backups older than 7 days keep 1 per day. Rollback log tracks all rollback/undo operations with timestamps and success status. Dashboard endpoints: POST /api/backups/:id/rollback, POST /api/backups/undo, POST /api/backups/:id/hotload (all require admin auth).",
|
|
42
42
|
metadata: { topic: "backup-system" },
|
|
43
43
|
},
|
|
44
44
|
{
|
|
@@ -90,80 +90,6 @@ function isNewer(latest, current) {
|
|
|
90
90
|
return false;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
/**
|
|
94
|
-
* Protect ALL user files before update and restore after.
|
|
95
|
-
* The entire server/ directory is sacred — auto-update must never touch it.
|
|
96
|
-
* Also protects .env files and any user config.
|
|
97
|
-
*/
|
|
98
|
-
function backupUserFiles(cwd) {
|
|
99
|
-
const backups = {};
|
|
100
|
-
|
|
101
|
-
// Protect config files
|
|
102
|
-
const protectedFiles = [".env.local", ".env"];
|
|
103
|
-
|
|
104
|
-
// Protect ALL .wolverine/ state (backups, brain, events, usage, repairs)
|
|
105
|
-
const wolvDir = path.join(cwd, ".wolverine");
|
|
106
|
-
if (fs.existsSync(wolvDir)) {
|
|
107
|
-
const walkState = (dir, base) => {
|
|
108
|
-
try {
|
|
109
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
110
|
-
for (const entry of entries) {
|
|
111
|
-
const fullPath = path.join(dir, entry.name);
|
|
112
|
-
const relPath = path.join(base, entry.name).replace(/\\/g, "/");
|
|
113
|
-
if (entry.isDirectory()) { walkState(fullPath, relPath); }
|
|
114
|
-
else {
|
|
115
|
-
try {
|
|
116
|
-
const stat = fs.statSync(fullPath);
|
|
117
|
-
if (stat.size <= 10 * 1024 * 1024) { // skip files > 10MB
|
|
118
|
-
backups[relPath] = fs.readFileSync(fullPath, "utf-8");
|
|
119
|
-
}
|
|
120
|
-
} catch {}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
} catch {}
|
|
124
|
-
};
|
|
125
|
-
walkState(wolvDir, ".wolverine");
|
|
126
|
-
}
|
|
127
|
-
for (const file of protectedFiles) {
|
|
128
|
-
const fullPath = path.join(cwd, file);
|
|
129
|
-
if (fs.existsSync(fullPath)) {
|
|
130
|
-
backups[file] = fs.readFileSync(fullPath, "utf-8");
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Protect entire server/ directory (recursive)
|
|
135
|
-
const serverDir = path.join(cwd, "server");
|
|
136
|
-
if (fs.existsSync(serverDir)) {
|
|
137
|
-
const walk = (dir, base) => {
|
|
138
|
-
try {
|
|
139
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
140
|
-
for (const entry of entries) {
|
|
141
|
-
if (entry.name === "node_modules") continue;
|
|
142
|
-
const fullPath = path.join(dir, entry.name);
|
|
143
|
-
const relPath = path.join(base, entry.name).replace(/\\/g, "/");
|
|
144
|
-
if (entry.isDirectory()) { walk(fullPath, relPath); }
|
|
145
|
-
else {
|
|
146
|
-
try { backups[relPath] = fs.readFileSync(fullPath, "utf-8"); } catch {}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
} catch {}
|
|
150
|
-
};
|
|
151
|
-
walk(serverDir, "server");
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return backups;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function restoreUserFiles(cwd, backups) {
|
|
158
|
-
for (const [file, content] of Object.entries(backups)) {
|
|
159
|
-
const fullPath = path.join(cwd, file);
|
|
160
|
-
try {
|
|
161
|
-
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
162
|
-
fs.writeFileSync(fullPath, content, "utf-8");
|
|
163
|
-
} catch {}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
93
|
/**
|
|
168
94
|
* Detect if this is a git repo or an npm install.
|
|
169
95
|
*/
|
|
@@ -214,51 +140,6 @@ function checkForUpdate(cwd) {
|
|
|
214
140
|
}
|
|
215
141
|
}
|
|
216
142
|
|
|
217
|
-
/**
|
|
218
|
-
* Merge new brain seed docs into existing brain.
|
|
219
|
-
* Reads the updated brain.js SEED_DOCS, compares topics with what's
|
|
220
|
-
* already stored in .wolverine/brain/, and appends only new ones.
|
|
221
|
-
* Existing memories (errors, fixes, learnings) are never touched.
|
|
222
|
-
*
|
|
223
|
-
* @returns {number} count of new seed docs added
|
|
224
|
-
*/
|
|
225
|
-
function _mergeBrainSeeds(cwd) {
|
|
226
|
-
try {
|
|
227
|
-
// Load the brain store directly
|
|
228
|
-
const storePath = path.join(cwd, ".wolverine", "brain", "store.json");
|
|
229
|
-
if (!fs.existsSync(storePath)) return 0;
|
|
230
|
-
|
|
231
|
-
const store = JSON.parse(fs.readFileSync(storePath, "utf-8"));
|
|
232
|
-
const existingTexts = new Set();
|
|
233
|
-
for (const ns of Object.values(store.namespaces || {})) {
|
|
234
|
-
for (const entry of (ns || [])) {
|
|
235
|
-
if (entry.text) existingTexts.add(entry.text.slice(0, 80));
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Load fresh seed docs from the updated brain.js
|
|
240
|
-
// Clear require cache to get the new version
|
|
241
|
-
const brainPath = path.join(cwd, "src", "brain", "brain.js");
|
|
242
|
-
delete require.cache[require.resolve(brainPath)];
|
|
243
|
-
const brainModule = require(brainPath);
|
|
244
|
-
|
|
245
|
-
// Access seed docs — they're in the module's closure, but brain.init() re-seeds them.
|
|
246
|
-
// Instead, we'll read the file and extract them
|
|
247
|
-
const brainSource = fs.readFileSync(brainPath, "utf-8");
|
|
248
|
-
const seedMatch = brainSource.match(/const SEED_DOCS = \[([\s\S]*?)\n\];/);
|
|
249
|
-
if (!seedMatch) return 0;
|
|
250
|
-
|
|
251
|
-
// Count how many seed doc topics are new
|
|
252
|
-
// We can't easily parse the JS array, but we can trigger brain.init() on next restart
|
|
253
|
-
// which will re-seed. The brain's init() already only adds seeds if namespace is empty.
|
|
254
|
-
// So we just need to signal that seeds should be refreshed.
|
|
255
|
-
const seedRefreshPath = path.join(cwd, ".wolverine", "brain", ".seed-refresh");
|
|
256
|
-
fs.writeFileSync(seedRefreshPath, new Date().toISOString(), "utf-8");
|
|
257
|
-
|
|
258
|
-
return 1; // signal that refresh is pending
|
|
259
|
-
} catch { return 0; }
|
|
260
|
-
}
|
|
261
|
-
|
|
262
143
|
/**
|
|
263
144
|
* Start auto-update schedule. Checks every hour (configurable).
|
|
264
145
|
* If autoUpdate is enabled and a new version is found, upgrades and signals restart.
|
package/src/skills/update.js
CHANGED
|
@@ -1,66 +1,62 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Update Skill — safe self-updating for the wolverine framework.
|
|
3
3
|
*
|
|
4
|
-
* WARNING: raw `npm install` or `git pull` can overwrite
|
|
5
|
-
*
|
|
6
|
-
* - .wolverine/ (brain memories, backups, events, repair history, usage)
|
|
7
|
-
* - .env.local (API keys, secrets)
|
|
4
|
+
* WARNING: raw `npm install` or `git pull` can overwrite server/ and .wolverine/.
|
|
5
|
+
* This skill updates ONLY framework files and never touches the server.
|
|
8
6
|
*
|
|
9
|
-
*
|
|
10
|
-
* 1. Creates
|
|
11
|
-
* 2.
|
|
12
|
-
* 3.
|
|
13
|
-
* 4.
|
|
14
|
-
* 5. Merges new brain seed docs (append, not replace)
|
|
15
|
-
* 6. Verifies the update didn't break anything
|
|
7
|
+
* What it does:
|
|
8
|
+
* 1. Creates emergency backup of server/ + brain (small, no node_modules)
|
|
9
|
+
* 2. Selectively updates ONLY framework files (src/, bin/, package.json)
|
|
10
|
+
* 3. server/ is NEVER touched — no backup/restore dance needed
|
|
11
|
+
* 4. Signals brain to merge new seed docs on next boot
|
|
16
12
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* npx wolverine-update (npm)
|
|
20
|
-
* require("wolverine-ai").safeUpdate(cwd) (programmatic)
|
|
13
|
+
* Emergency backup is for rollback ONLY if something goes wrong.
|
|
14
|
+
* Located in ~/.wolverine-safe-backups/ (outside project, survives everything).
|
|
21
15
|
*/
|
|
22
16
|
|
|
23
17
|
const { execSync } = require("child_process");
|
|
24
18
|
const fs = require("fs");
|
|
25
19
|
const path = require("path");
|
|
26
20
|
const chalk = require("chalk");
|
|
21
|
+
const os = require("os");
|
|
27
22
|
|
|
28
23
|
const PACKAGE_NAME = "wolverine-ai";
|
|
29
|
-
const SAFE_BACKUP_DIR = path.join(
|
|
30
|
-
|
|
24
|
+
const SAFE_BACKUP_DIR = path.join(os.homedir(), ".wolverine-safe-backups");
|
|
25
|
+
|
|
26
|
+
// Files/dirs to SKIP when backing up server/ (these are huge and not user code)
|
|
27
|
+
const SKIP_DIRS = new Set(["node_modules", ".git", ".wolverine", "dist", ".next", ".cache"]);
|
|
31
28
|
|
|
32
29
|
/**
|
|
33
|
-
* Create
|
|
34
|
-
*
|
|
30
|
+
* Create an emergency backup — server/ code + brain only.
|
|
31
|
+
* Small and fast. No node_modules, no backup-of-backups.
|
|
35
32
|
*/
|
|
36
33
|
function createSafeBackup(cwd) {
|
|
37
34
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
38
35
|
const backupDir = path.join(SAFE_BACKUP_DIR, "updates", timestamp);
|
|
39
36
|
fs.mkdirSync(backupDir, { recursive: true });
|
|
40
37
|
|
|
41
|
-
const dirsToBackup = [
|
|
42
|
-
{ src: ".wolverine", label: "brain/backups/events/usage" },
|
|
43
|
-
{ src: "server", label: "server code" },
|
|
44
|
-
];
|
|
45
|
-
const filesToBackup = [".env.local", ".env"];
|
|
46
|
-
|
|
47
38
|
let fileCount = 0;
|
|
48
39
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
_copyDirRecursive(srcPath, destPath);
|
|
54
|
-
fileCount += _countFiles(destPath);
|
|
40
|
+
// Backup server/ (user code only — skip node_modules, .git, etc.)
|
|
41
|
+
const serverDir = path.join(cwd, "server");
|
|
42
|
+
if (fs.existsSync(serverDir)) {
|
|
43
|
+
fileCount += _copyDirSelective(serverDir, path.join(backupDir, "server"));
|
|
55
44
|
}
|
|
56
45
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
fs.
|
|
46
|
+
// Backup brain vectors (the learned knowledge)
|
|
47
|
+
const brainStore = path.join(cwd, ".wolverine", "brain", "vectors.json");
|
|
48
|
+
if (fs.existsSync(brainStore)) {
|
|
49
|
+
fs.mkdirSync(path.join(backupDir, ".wolverine", "brain"), { recursive: true });
|
|
50
|
+
fs.copyFileSync(brainStore, path.join(backupDir, ".wolverine", "brain", "vectors.json"));
|
|
61
51
|
fileCount++;
|
|
62
52
|
}
|
|
63
53
|
|
|
54
|
+
// Backup .env files
|
|
55
|
+
for (const f of [".env.local", ".env"]) {
|
|
56
|
+
const fp = path.join(cwd, f);
|
|
57
|
+
if (fs.existsSync(fp)) { fs.copyFileSync(fp, path.join(backupDir, f)); fileCount++; }
|
|
58
|
+
}
|
|
59
|
+
|
|
64
60
|
// Write manifest
|
|
65
61
|
fs.writeFileSync(path.join(backupDir, "manifest.json"), JSON.stringify({
|
|
66
62
|
timestamp: Date.now(),
|
|
@@ -68,6 +64,7 @@ function createSafeBackup(cwd) {
|
|
|
68
64
|
cwd,
|
|
69
65
|
version: _getCurrentVersion(cwd),
|
|
70
66
|
fileCount,
|
|
67
|
+
type: "pre-update",
|
|
71
68
|
}, null, 2), "utf-8");
|
|
72
69
|
|
|
73
70
|
return { dir: backupDir, fileCount, timestamp };
|
|
@@ -80,10 +77,10 @@ function listSafeBackups() {
|
|
|
80
77
|
const updatesDir = path.join(SAFE_BACKUP_DIR, "updates");
|
|
81
78
|
if (!fs.existsSync(updatesDir)) return [];
|
|
82
79
|
return fs.readdirSync(updatesDir)
|
|
83
|
-
.filter(d => fs.statSync(path.join(updatesDir, d)).isDirectory())
|
|
80
|
+
.filter(d => { try { return fs.statSync(path.join(updatesDir, d)).isDirectory(); } catch { return false; } })
|
|
84
81
|
.map(d => {
|
|
85
82
|
try {
|
|
86
|
-
const manifest = JSON.parse(fs.readFileSync(path.join(
|
|
83
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(updatesDir, d, "manifest.json"), "utf-8"));
|
|
87
84
|
return { dir: d, ...manifest };
|
|
88
85
|
} catch { return { dir: d }; }
|
|
89
86
|
})
|
|
@@ -91,39 +88,41 @@ function listSafeBackups() {
|
|
|
91
88
|
}
|
|
92
89
|
|
|
93
90
|
/**
|
|
94
|
-
* Restore from a safe backup.
|
|
91
|
+
* Restore from a safe backup (emergency only).
|
|
95
92
|
*/
|
|
96
93
|
function restoreFromSafeBackup(cwd, backupName) {
|
|
97
94
|
const backupDir = path.join(SAFE_BACKUP_DIR, "updates", backupName);
|
|
98
95
|
if (!fs.existsSync(backupDir)) throw new Error(`Backup not found: ${backupName}`);
|
|
99
96
|
|
|
100
|
-
const dirsToRestore = [".wolverine", "server"];
|
|
101
|
-
const filesToRestore = [".env.local", ".env"];
|
|
102
|
-
|
|
103
97
|
let restored = 0;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
restored += _countFiles(srcPath);
|
|
98
|
+
|
|
99
|
+
// Restore server/
|
|
100
|
+
const serverSrc = path.join(backupDir, "server");
|
|
101
|
+
if (fs.existsSync(serverSrc)) {
|
|
102
|
+
restored += _copyDirSelective(serverSrc, path.join(cwd, "server"));
|
|
110
103
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
104
|
+
|
|
105
|
+
// Restore brain
|
|
106
|
+
const brainSrc = path.join(backupDir, ".wolverine", "brain", "vectors.json");
|
|
107
|
+
if (fs.existsSync(brainSrc)) {
|
|
108
|
+
const dest = path.join(cwd, ".wolverine", "brain", "vectors.json");
|
|
109
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
110
|
+
fs.copyFileSync(brainSrc, dest);
|
|
115
111
|
restored++;
|
|
116
112
|
}
|
|
113
|
+
|
|
114
|
+
// Restore .env files
|
|
115
|
+
for (const f of [".env.local", ".env"]) {
|
|
116
|
+
const src = path.join(backupDir, f);
|
|
117
|
+
if (fs.existsSync(src)) { fs.copyFileSync(src, path.join(cwd, f)); restored++; }
|
|
118
|
+
}
|
|
119
|
+
|
|
117
120
|
return { restored, backupDir };
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
/**
|
|
121
124
|
* Safe update — the main entry point.
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
* @param {string} cwd — project root
|
|
125
|
-
* @param {object} options — { logger, dryRun }
|
|
126
|
-
* @returns {{ success, from, to, backupDir, error? }}
|
|
125
|
+
* Updates framework files ONLY. server/ is never touched.
|
|
127
126
|
*/
|
|
128
127
|
function safeUpdate(cwd, options = {}) {
|
|
129
128
|
const { logger, dryRun } = options;
|
|
@@ -140,7 +139,6 @@ function safeUpdate(cwd, options = {}) {
|
|
|
140
139
|
}).trim();
|
|
141
140
|
} catch {}
|
|
142
141
|
|
|
143
|
-
// Also check git remote
|
|
144
142
|
const isGit = _isGitRepo(cwd);
|
|
145
143
|
if (isGit) {
|
|
146
144
|
try {
|
|
@@ -167,20 +165,16 @@ function safeUpdate(cwd, options = {}) {
|
|
|
167
165
|
return { success: true, from: currentVersion, to: latestVersion, dryRun: true };
|
|
168
166
|
}
|
|
169
167
|
|
|
170
|
-
// 2.
|
|
171
|
-
console.log(chalk.gray(" 🔒 Creating
|
|
168
|
+
// 2. Emergency backup (server code + brain only — small and fast)
|
|
169
|
+
console.log(chalk.gray(" 🔒 Creating emergency backup..."));
|
|
172
170
|
const backup = createSafeBackup(cwd);
|
|
173
171
|
console.log(chalk.gray(` 🔒 Backed up ${backup.fileCount} files to ${backup.dir}`));
|
|
174
|
-
if (logger) logger.info("update.backup", `
|
|
175
|
-
|
|
176
|
-
// 3. Backup user files to memory (belt + suspenders)
|
|
177
|
-
const memoryBackup = _backupToMemory(cwd);
|
|
178
|
-
console.log(chalk.gray(` 🔒 Memory backup: ${Object.keys(memoryBackup).length} files`));
|
|
172
|
+
if (logger) logger.info("update.backup", `Emergency backup: ${backup.fileCount} files`, { dir: backup.dir });
|
|
179
173
|
|
|
180
174
|
try {
|
|
181
|
-
//
|
|
175
|
+
// 3. Update framework ONLY — server/ is never touched
|
|
182
176
|
if (isGit) {
|
|
183
|
-
console.log(chalk.blue(" 📦 Selective git update (server/
|
|
177
|
+
console.log(chalk.blue(" 📦 Selective git update (server/ untouched)"));
|
|
184
178
|
const frameworkPaths = "src/ bin/ package.json package-lock.json examples/ tests/ CLAUDE.md README.md CHANGELOG.md .npmignore";
|
|
185
179
|
execSync(`git checkout origin/master -- ${frameworkPaths}`, { cwd, stdio: "pipe", timeout: 30000 });
|
|
186
180
|
execSync("npm install --production", { cwd, stdio: "pipe", timeout: 120000 });
|
|
@@ -190,29 +184,22 @@ function safeUpdate(cwd, options = {}) {
|
|
|
190
184
|
execSync(cmd, { cwd, stdio: "pipe", timeout: 120000 });
|
|
191
185
|
}
|
|
192
186
|
|
|
193
|
-
//
|
|
194
|
-
_restoreFromMemory(cwd, memoryBackup);
|
|
195
|
-
console.log(chalk.gray(` 🔒 Restored ${Object.keys(memoryBackup).length} user files`));
|
|
196
|
-
|
|
197
|
-
// 6. Signal brain to merge new seeds on next boot
|
|
187
|
+
// 4. Signal brain to merge new seeds on next boot
|
|
198
188
|
const seedRefreshDir = path.join(cwd, ".wolverine", "brain");
|
|
199
189
|
fs.mkdirSync(seedRefreshDir, { recursive: true });
|
|
200
190
|
fs.writeFileSync(path.join(seedRefreshDir, ".seed-refresh"), new Date().toISOString(), "utf-8");
|
|
201
191
|
console.log(chalk.gray(" 🧠 Brain seed merge scheduled for next boot"));
|
|
202
192
|
|
|
203
|
-
// 7. Verify
|
|
204
193
|
const newVersion = _getCurrentVersion(cwd);
|
|
205
194
|
console.log(chalk.green(` ✅ Updated: ${currentVersion} → ${newVersion}`));
|
|
206
|
-
console.log(chalk.gray(` 🔒
|
|
195
|
+
console.log(chalk.gray(` 🔒 Emergency backup at: ${backup.dir}`));
|
|
207
196
|
if (logger) logger.info("update.success", `Updated ${currentVersion} → ${newVersion}`, { from: currentVersion, to: newVersion });
|
|
208
197
|
|
|
209
198
|
return { success: true, from: currentVersion, to: newVersion, backupDir: backup.dir };
|
|
210
199
|
} catch (err) {
|
|
211
|
-
// Restore from memory on failure
|
|
212
|
-
_restoreFromMemory(cwd, memoryBackup);
|
|
213
200
|
const errMsg = (err.message || "").slice(0, 100);
|
|
214
201
|
console.log(chalk.red(` ❌ Update failed: ${errMsg}`));
|
|
215
|
-
console.log(chalk.yellow(` 🔒 Restore
|
|
202
|
+
console.log(chalk.yellow(` 🔒 Restore with: wolverine --restore ${backup.timestamp}`));
|
|
216
203
|
if (logger) logger.warn("update.failed", `Update failed: ${errMsg}`, { from: currentVersion });
|
|
217
204
|
return { success: false, from: currentVersion, to: latestVersion, error: errMsg, backupDir: backup.dir };
|
|
218
205
|
}
|
|
@@ -235,82 +222,41 @@ function _isNewer(a, b) {
|
|
|
235
222
|
return false;
|
|
236
223
|
}
|
|
237
224
|
|
|
238
|
-
|
|
225
|
+
/**
|
|
226
|
+
* Copy directory recursively, skipping node_modules/.git/.wolverine/dist etc.
|
|
227
|
+
* Returns file count.
|
|
228
|
+
*/
|
|
229
|
+
function _copyDirSelective(src, dest) {
|
|
239
230
|
fs.mkdirSync(dest, { recursive: true });
|
|
231
|
+
let count = 0;
|
|
240
232
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
241
|
-
if (entry.name
|
|
233
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
242
234
|
const s = path.join(src, entry.name), d = path.join(dest, entry.name);
|
|
243
|
-
if (entry.isDirectory())
|
|
244
|
-
else {
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function _countFiles(dir) {
|
|
249
|
-
let count = 0;
|
|
250
|
-
try {
|
|
251
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
252
|
-
if (entry.isDirectory()) count += _countFiles(path.join(dir, entry.name));
|
|
253
|
-
else count++;
|
|
254
|
-
}
|
|
255
|
-
} catch {}
|
|
256
|
-
return count;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function _backupToMemory(cwd) {
|
|
260
|
-
const backups = {};
|
|
261
|
-
const protect = ["server", ".wolverine"];
|
|
262
|
-
const protectFiles = [".env.local", ".env"];
|
|
263
|
-
|
|
264
|
-
for (const dir of protect) {
|
|
265
|
-
const dirPath = path.join(cwd, dir);
|
|
266
|
-
if (!fs.existsSync(dirPath)) continue;
|
|
267
|
-
const walk = (d, base) => {
|
|
235
|
+
if (entry.isDirectory()) { count += _copyDirSelective(s, d); }
|
|
236
|
+
else {
|
|
268
237
|
try {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const full = path.join(d, entry.name), rel = path.join(base, entry.name).replace(/\\/g, "/");
|
|
272
|
-
if (entry.isDirectory()) walk(full, rel);
|
|
273
|
-
else { try { const s = fs.statSync(full); if (s.size <= 10*1024*1024) backups[rel] = fs.readFileSync(full); } catch {} }
|
|
274
|
-
}
|
|
238
|
+
const stat = fs.statSync(s);
|
|
239
|
+
if (stat.size <= 5 * 1024 * 1024) { fs.copyFileSync(s, d); count++; } // skip >5MB
|
|
275
240
|
} catch {}
|
|
276
|
-
}
|
|
277
|
-
walk(dirPath, dir);
|
|
278
|
-
}
|
|
279
|
-
for (const f of protectFiles) {
|
|
280
|
-
const fp = path.join(cwd, f);
|
|
281
|
-
if (fs.existsSync(fp)) backups[f] = fs.readFileSync(fp);
|
|
282
|
-
}
|
|
283
|
-
return backups;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function _restoreFromMemory(cwd, backups) {
|
|
287
|
-
for (const [rel, content] of Object.entries(backups)) {
|
|
288
|
-
const fp = path.join(cwd, rel);
|
|
289
|
-
try { fs.mkdirSync(path.dirname(fp), { recursive: true }); fs.writeFileSync(fp, content); } catch {}
|
|
241
|
+
}
|
|
290
242
|
}
|
|
243
|
+
return count;
|
|
291
244
|
}
|
|
292
245
|
|
|
293
246
|
// ── Skill Metadata ──
|
|
294
247
|
|
|
295
248
|
const SKILL_NAME = "update";
|
|
296
|
-
const SKILL_DESCRIPTION = "Safe self-updating for wolverine framework. Creates
|
|
249
|
+
const SKILL_DESCRIPTION = "Safe self-updating for wolverine framework. Creates emergency backup of server code + brain (no node_modules), selectively updates only framework files (src/, bin/, package.json), never touches server/. Never use raw npm install or git pull.";
|
|
297
250
|
const SKILL_KEYWORDS = ["update", "upgrade", "version", "install", "pull", "self-update", "auto-update", "framework", "safe"];
|
|
298
251
|
const SKILL_USAGE = `// Safe update (programmatic)
|
|
299
252
|
const { safeUpdate } = require("wolverine-ai");
|
|
300
|
-
const result =
|
|
301
|
-
// { success: true, from: "2.5.3", to: "2.6.0", backupDir: "~/.wolverine-safe-backups/..." }
|
|
302
|
-
|
|
303
|
-
// List safe backups
|
|
304
|
-
const { listSafeBackups } = require("wolverine-ai");
|
|
305
|
-
const backups = listSafeBackups();
|
|
306
|
-
|
|
307
|
-
// Restore from safe backup
|
|
308
|
-
const { restoreFromSafeBackup } = require("wolverine-ai");
|
|
309
|
-
restoreFromSafeBackup(process.cwd(), "2026-04-02T21-15-00");
|
|
253
|
+
const result = safeUpdate(process.cwd());
|
|
310
254
|
|
|
311
|
-
// CLI
|
|
312
|
-
//
|
|
313
|
-
//
|
|
255
|
+
// CLI
|
|
256
|
+
// wolverine --update
|
|
257
|
+
// wolverine --update --dry-run
|
|
258
|
+
// wolverine --backups
|
|
259
|
+
// wolverine --restore 2026-04-02T21-15-00`;
|
|
314
260
|
|
|
315
261
|
module.exports = {
|
|
316
262
|
SKILL_NAME, SKILL_DESCRIPTION, SKILL_KEYWORDS, SKILL_USAGE,
|