wolverine-ai 2.5.2 → 2.6.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/README.md +17 -4
- package/bin/wolverine.js +33 -0
- package/package.json +1 -1
- package/src/brain/brain.js +6 -0
- package/src/index.js +5 -0
- package/src/platform/auto-update.js +4 -54
- package/src/skills/update.js +316 -0
package/README.md
CHANGED
|
@@ -462,11 +462,24 @@ Wolverine checks npm for new versions hourly and upgrades itself automatically.
|
|
|
462
462
|
}
|
|
463
463
|
```
|
|
464
464
|
|
|
465
|
+
```bash
|
|
466
|
+
# Manual safe update
|
|
467
|
+
wolverine --update # check + upgrade safely
|
|
468
|
+
wolverine --update --dry-run # check only, no changes
|
|
469
|
+
wolverine --backups # list safe backups
|
|
470
|
+
wolverine --restore 2026-04-02 # restore from safe backup
|
|
471
|
+
```
|
|
472
|
+
|
|
465
473
|
**How it works:**
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
474
|
+
1. Creates safe backup in `~/.wolverine-safe-backups/` (outside project, survives everything)
|
|
475
|
+
2. Backs up `server/`, `.wolverine/`, `.env` to memory
|
|
476
|
+
3. Selectively updates ONLY `src/`, `bin/`, `package.json` (git checkout or npm install)
|
|
477
|
+
4. Restores all user files (server code, brain, backups, events, config)
|
|
478
|
+
5. Signals brain to merge new seed docs on next boot (append, not replace)
|
|
479
|
+
6. Auto-check: 30s after startup, then every 5 minutes (configurable)
|
|
480
|
+
|
|
481
|
+
**Never run raw `npm install` or `git pull`** — they overwrite server code and brain memories. Always use `wolverine --update` or let auto-update handle it.
|
|
482
|
+
|
|
470
483
|
- **Disable:** `"autoUpdate": { "enabled": false }` or `WOLVERINE_AUTO_UPDATE=false`
|
|
471
484
|
|
|
472
485
|
---
|
package/bin/wolverine.js
CHANGED
|
@@ -54,6 +54,39 @@ if (args.includes("--info")) {
|
|
|
54
54
|
process.exit(0);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// --update: safe framework update
|
|
58
|
+
if (args.includes("--update")) {
|
|
59
|
+
const { safeUpdate } = require("../src/skills/update");
|
|
60
|
+
const dryRun = args.includes("--dry-run");
|
|
61
|
+
const result = safeUpdate(process.cwd(), { dryRun });
|
|
62
|
+
process.exit(result.success ? 0 : 1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// --restore: restore from safe backup
|
|
66
|
+
if (args.includes("--restore")) {
|
|
67
|
+
const backupName = args[args.indexOf("--restore") + 1];
|
|
68
|
+
if (!backupName) { console.log("Usage: wolverine --restore <backup-name>"); process.exit(1); }
|
|
69
|
+
const { restoreFromSafeBackup } = require("../src/skills/update");
|
|
70
|
+
const result = restoreFromSafeBackup(process.cwd(), backupName);
|
|
71
|
+
console.log(chalk.green(` ✅ Restored ${result.restored} files from ${backupName}`));
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// --backups: list safe backups
|
|
76
|
+
if (args.includes("--backups")) {
|
|
77
|
+
const { listSafeBackups } = require("../src/skills/update");
|
|
78
|
+
const backups = listSafeBackups();
|
|
79
|
+
if (backups.length === 0) { console.log("No safe backups found."); }
|
|
80
|
+
else {
|
|
81
|
+
console.log(chalk.bold("\n Safe Backups (~/.wolverine-safe-backups/):\n"));
|
|
82
|
+
for (const b of backups) {
|
|
83
|
+
console.log(` ${b.dir} v${b.version || "?"} ${b.fileCount || "?"} files ${b.iso || ""}`);
|
|
84
|
+
}
|
|
85
|
+
console.log("");
|
|
86
|
+
}
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
|
|
57
90
|
const scriptPath = args.find(a => !a.startsWith("--")) || "server/index.js";
|
|
58
91
|
|
|
59
92
|
// System detection (for analytics + dashboard, NOT for forking)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wolverine-ai",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
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
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
1
3
|
const chalk = require("chalk");
|
|
2
4
|
const { VectorStore } = require("./vector-store");
|
|
3
5
|
const { embed, embedBatch, compactAndEmbed } = require("./embedder");
|
|
@@ -235,6 +237,10 @@ const SEED_DOCS = [
|
|
|
235
237
|
text: "Dependency manager skill (src/skills/deps.js): structured npm dependency analysis + repair. diagnose(errorMessage, cwd) returns {diagnosed, category, summary, fixes} — categories: missing_install, missing_package, version_conflict, outdated_api, corrupted_modules. healthReport(cwd) returns full health check: npm audit (vulnerabilities), outdated packages, peer dep conflicts, unused packages, lock file status, health score 0-100. getMigration(packageName) returns known upgrade paths: express→fastify (5.6x faster), moment→dayjs (2KB vs 70KB), request→node-fetch (deprecated), body-parser→built-in, callbacks→async/await. Agent tools: audit_deps (full health check), check_migration (upgrade paths). Heal pipeline uses diagnose() in tryOperationalFix before AI — zero tokens for dependency issues.",
|
|
236
238
|
metadata: { topic: "skill-deps" },
|
|
237
239
|
},
|
|
240
|
+
{
|
|
241
|
+
text: "CRITICAL: Never run raw 'npm install wolverine-ai' or 'git pull' to update — these OVERWRITE server/, .wolverine/ (brain, backups, events), and .env.local. Always use the safe update skill: wolverine --update (CLI), safeUpdate(cwd) (programmatic), or let auto-update handle it. The update skill: (1) creates safe backup in ~/.wolverine-safe-backups/ (outside project, survives everything), (2) backs up server/ + .wolverine/ + .env to memory, (3) selectively updates ONLY src/ bin/ package.json via git checkout, (4) restores all user files, (5) signals brain to merge new seed docs. Restore with: wolverine --restore <backup-name>. List backups: wolverine --backups.",
|
|
242
|
+
metadata: { topic: "safe-update-warning" },
|
|
243
|
+
},
|
|
238
244
|
{
|
|
239
245
|
text: "Auto-update: wolverine checks npm registry hourly for new versions. When found, runs npm install wolverine-ai@latest, backs up settings.json/.env.local before update and restores after. Config: autoUpdate.enabled (default true) in settings.json. Disable with WOLVERINE_AUTO_UPDATE=false env var. On successful update, signals runner to restart. Protected files never overwritten: settings.json, .env.local, .env, db.js. Update check runs 30s after startup then every hour (configurable via autoUpdate.intervalMs).",
|
|
240
246
|
metadata: { topic: "auto-update" },
|
package/src/index.js
CHANGED
|
@@ -36,6 +36,7 @@ const { loadConfig, getConfig } = require("./core/config");
|
|
|
36
36
|
const { sqlGuard, SafeDB, scanForInjection, idempotencyGuard, idempotencyAfterHook } = require("./skills/sql");
|
|
37
37
|
const { diagnose: diagnoseDeps, healthReport: depsHealthReport, getMigration } = require("./skills/deps");
|
|
38
38
|
const { checkForUpdate, upgrade: upgradeWolverine, getCurrentVersion } = require("./platform/auto-update");
|
|
39
|
+
const { safeUpdate, createSafeBackup, listSafeBackups, restoreFromSafeBackup } = require("./skills/update");
|
|
39
40
|
|
|
40
41
|
module.exports = {
|
|
41
42
|
// Core
|
|
@@ -103,4 +104,8 @@ module.exports = {
|
|
|
103
104
|
checkForUpdate,
|
|
104
105
|
upgradeWolverine,
|
|
105
106
|
getCurrentVersion,
|
|
107
|
+
safeUpdate,
|
|
108
|
+
createSafeBackup,
|
|
109
|
+
listSafeBackups,
|
|
110
|
+
restoreFromSafeBackup,
|
|
106
111
|
};
|
|
@@ -186,60 +186,10 @@ function upgrade(cwd, logger) {
|
|
|
186
186
|
return { success: false, from: current, to: latest, error: "Already up to date" };
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const userBackups = backupUserFiles(cwd);
|
|
194
|
-
console.log(chalk.gray(` 🔒 Backed up ${Object.keys(userBackups).length} user files (server/ protected)`));
|
|
195
|
-
|
|
196
|
-
try {
|
|
197
|
-
const useGit = isGitRepo(cwd);
|
|
198
|
-
|
|
199
|
-
if (useGit) {
|
|
200
|
-
// Git-cloned: ONLY update framework files, NEVER touch server/
|
|
201
|
-
// Fetch latest, then selectively checkout only framework dirs
|
|
202
|
-
console.log(chalk.blue(` 📦 Git repo — selective framework update (server/ untouched)`));
|
|
203
|
-
execSync("git fetch origin master", { cwd, stdio: "pipe", timeout: 30000 });
|
|
204
|
-
// Only update: src/, bin/, package.json, examples/, tests/, CLAUDE.md, README.md, CHANGELOG.md
|
|
205
|
-
const frameworkPaths = "src/ bin/ package.json package-lock.json examples/ tests/ CLAUDE.md README.md CHANGELOG.md .npmignore";
|
|
206
|
-
execSync(`git checkout origin/master -- ${frameworkPaths}`, { cwd, stdio: "pipe", timeout: 30000 });
|
|
207
|
-
execSync("npm install", { cwd, stdio: "pipe", timeout: 120000 });
|
|
208
|
-
} else {
|
|
209
|
-
// npm-installed: update the package
|
|
210
|
-
const isGlobal = __dirname.includes("node_modules") && !cwd.includes("node_modules");
|
|
211
|
-
const cmd = isGlobal
|
|
212
|
-
? `npm install -g ${PACKAGE_NAME}@${latest}`
|
|
213
|
-
: `npm install ${PACKAGE_NAME}@${latest}`;
|
|
214
|
-
console.log(chalk.blue(` 📦 Running: ${cmd}`));
|
|
215
|
-
execSync(cmd, { cwd, stdio: "pipe", timeout: 120000 });
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Restore ALL user files (server/, .env, .wolverine/) — belt AND suspenders
|
|
219
|
-
restoreUserFiles(cwd, userBackups);
|
|
220
|
-
console.log(chalk.gray(` 🔒 Restored ${Object.keys(userBackups).length} user files`));
|
|
221
|
-
|
|
222
|
-
// Merge new brain seed docs into existing brain (append, don't replace)
|
|
223
|
-
try {
|
|
224
|
-
const brainMerged = _mergeBrainSeeds(cwd);
|
|
225
|
-
if (brainMerged > 0) console.log(chalk.gray(` 🧠 Merged ${brainMerged} new seed docs into brain`));
|
|
226
|
-
} catch {}
|
|
227
|
-
|
|
228
|
-
// Clear version cache
|
|
229
|
-
_currentVersion = null;
|
|
230
|
-
|
|
231
|
-
console.log(chalk.green(` ✅ Updated to ${latest}`));
|
|
232
|
-
if (logger) logger.info("update.success", `Upgraded to ${latest}`, { from: current, to: latest });
|
|
233
|
-
|
|
234
|
-
return { success: true, from: current, to: latest };
|
|
235
|
-
} catch (err) {
|
|
236
|
-
// Restore user files on failure
|
|
237
|
-
restoreUserFiles(cwd, userBackups);
|
|
238
|
-
const errMsg = (err.message || "").slice(0, 100);
|
|
239
|
-
console.log(chalk.yellow(` ⚠️ Update failed: ${errMsg}`));
|
|
240
|
-
if (logger) logger.warn("update.failed", `Upgrade failed: ${errMsg}`, { from: current, to: latest });
|
|
241
|
-
return { success: false, from: current, to: latest, error: errMsg };
|
|
242
|
-
}
|
|
189
|
+
// Delegate to the update skill for the full safe upgrade routine
|
|
190
|
+
const { safeUpdate } = require("../skills/update");
|
|
191
|
+
_currentVersion = null; // clear cache so next check sees new version
|
|
192
|
+
return safeUpdate(cwd, { logger });
|
|
243
193
|
}
|
|
244
194
|
|
|
245
195
|
/**
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update Skill — safe self-updating for the wolverine framework.
|
|
3
|
+
*
|
|
4
|
+
* WARNING: raw `npm install` or `git pull` can overwrite:
|
|
5
|
+
* - server/ (user's live code, routes, config, database)
|
|
6
|
+
* - .wolverine/ (brain memories, backups, events, repair history, usage)
|
|
7
|
+
* - .env.local (API keys, secrets)
|
|
8
|
+
*
|
|
9
|
+
* This skill does it safely:
|
|
10
|
+
* 1. Creates a pre-update snapshot in ~/.wolverine-safe-backups/ (outside project, never erased)
|
|
11
|
+
* 2. Backs up all user files to memory
|
|
12
|
+
* 3. Selectively updates ONLY framework files (src/, bin/, package.json)
|
|
13
|
+
* 4. Restores all user files
|
|
14
|
+
* 5. Merges new brain seed docs (append, not replace)
|
|
15
|
+
* 6. Verifies the update didn't break anything
|
|
16
|
+
*
|
|
17
|
+
* Callable as:
|
|
18
|
+
* wolverine --update (CLI)
|
|
19
|
+
* npx wolverine-update (npm)
|
|
20
|
+
* require("wolverine-ai").safeUpdate(cwd) (programmatic)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const { execSync } = require("child_process");
|
|
24
|
+
const fs = require("fs");
|
|
25
|
+
const path = require("path");
|
|
26
|
+
const chalk = require("chalk");
|
|
27
|
+
|
|
28
|
+
const PACKAGE_NAME = "wolverine-ai";
|
|
29
|
+
const SAFE_BACKUP_DIR = path.join(require("os").homedir(), ".wolverine-safe-backups");
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create a safe backup snapshot outside the project directory.
|
|
33
|
+
* These survive git clean, rm -rf node_modules, even rm -rf .wolverine.
|
|
34
|
+
*/
|
|
35
|
+
function createSafeBackup(cwd) {
|
|
36
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
37
|
+
const backupDir = path.join(SAFE_BACKUP_DIR, timestamp);
|
|
38
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
39
|
+
|
|
40
|
+
const dirsToBackup = [
|
|
41
|
+
{ src: ".wolverine", label: "brain/backups/events/usage" },
|
|
42
|
+
{ src: "server", label: "server code" },
|
|
43
|
+
];
|
|
44
|
+
const filesToBackup = [".env.local", ".env"];
|
|
45
|
+
|
|
46
|
+
let fileCount = 0;
|
|
47
|
+
|
|
48
|
+
for (const { src } of dirsToBackup) {
|
|
49
|
+
const srcPath = path.join(cwd, src);
|
|
50
|
+
if (!fs.existsSync(srcPath)) continue;
|
|
51
|
+
const destPath = path.join(backupDir, src);
|
|
52
|
+
_copyDirRecursive(srcPath, destPath);
|
|
53
|
+
fileCount += _countFiles(destPath);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const file of filesToBackup) {
|
|
57
|
+
const srcPath = path.join(cwd, file);
|
|
58
|
+
if (!fs.existsSync(srcPath)) continue;
|
|
59
|
+
fs.copyFileSync(srcPath, path.join(backupDir, file));
|
|
60
|
+
fileCount++;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Write manifest
|
|
64
|
+
fs.writeFileSync(path.join(backupDir, "manifest.json"), JSON.stringify({
|
|
65
|
+
timestamp: Date.now(),
|
|
66
|
+
iso: new Date().toISOString(),
|
|
67
|
+
cwd,
|
|
68
|
+
version: _getCurrentVersion(cwd),
|
|
69
|
+
fileCount,
|
|
70
|
+
}, null, 2), "utf-8");
|
|
71
|
+
|
|
72
|
+
return { dir: backupDir, fileCount, timestamp };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* List available safe backups.
|
|
77
|
+
*/
|
|
78
|
+
function listSafeBackups() {
|
|
79
|
+
if (!fs.existsSync(SAFE_BACKUP_DIR)) return [];
|
|
80
|
+
return fs.readdirSync(SAFE_BACKUP_DIR)
|
|
81
|
+
.filter(d => fs.statSync(path.join(SAFE_BACKUP_DIR, d)).isDirectory())
|
|
82
|
+
.map(d => {
|
|
83
|
+
try {
|
|
84
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(SAFE_BACKUP_DIR, d, "manifest.json"), "utf-8"));
|
|
85
|
+
return { dir: d, ...manifest };
|
|
86
|
+
} catch { return { dir: d }; }
|
|
87
|
+
})
|
|
88
|
+
.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Restore from a safe backup.
|
|
93
|
+
*/
|
|
94
|
+
function restoreFromSafeBackup(cwd, backupName) {
|
|
95
|
+
const backupDir = path.join(SAFE_BACKUP_DIR, backupName);
|
|
96
|
+
if (!fs.existsSync(backupDir)) throw new Error(`Backup not found: ${backupName}`);
|
|
97
|
+
|
|
98
|
+
const dirsToRestore = [".wolverine", "server"];
|
|
99
|
+
const filesToRestore = [".env.local", ".env"];
|
|
100
|
+
|
|
101
|
+
let restored = 0;
|
|
102
|
+
for (const dir of dirsToRestore) {
|
|
103
|
+
const srcPath = path.join(backupDir, dir);
|
|
104
|
+
if (!fs.existsSync(srcPath)) continue;
|
|
105
|
+
const destPath = path.join(cwd, dir);
|
|
106
|
+
_copyDirRecursive(srcPath, destPath);
|
|
107
|
+
restored += _countFiles(srcPath);
|
|
108
|
+
}
|
|
109
|
+
for (const file of filesToRestore) {
|
|
110
|
+
const srcPath = path.join(backupDir, file);
|
|
111
|
+
if (!fs.existsSync(srcPath)) continue;
|
|
112
|
+
fs.copyFileSync(srcPath, path.join(cwd, file));
|
|
113
|
+
restored++;
|
|
114
|
+
}
|
|
115
|
+
return { restored, backupDir };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Safe update — the main entry point.
|
|
120
|
+
* Call this instead of raw npm install or git pull.
|
|
121
|
+
*
|
|
122
|
+
* @param {string} cwd — project root
|
|
123
|
+
* @param {object} options — { logger, dryRun }
|
|
124
|
+
* @returns {{ success, from, to, backupDir, error? }}
|
|
125
|
+
*/
|
|
126
|
+
function safeUpdate(cwd, options = {}) {
|
|
127
|
+
const { logger, dryRun } = options;
|
|
128
|
+
const currentVersion = _getCurrentVersion(cwd);
|
|
129
|
+
|
|
130
|
+
console.log(chalk.blue("\n 🔄 Wolverine Safe Update"));
|
|
131
|
+
console.log(chalk.gray(` Current version: ${currentVersion}`));
|
|
132
|
+
|
|
133
|
+
// 1. Check for updates
|
|
134
|
+
let latestVersion;
|
|
135
|
+
try {
|
|
136
|
+
latestVersion = execSync(`npm view ${PACKAGE_NAME} version 2>/dev/null`, {
|
|
137
|
+
encoding: "utf-8", timeout: 15000, cwd,
|
|
138
|
+
}).trim();
|
|
139
|
+
} catch {}
|
|
140
|
+
|
|
141
|
+
// Also check git remote
|
|
142
|
+
const isGit = _isGitRepo(cwd);
|
|
143
|
+
if (isGit) {
|
|
144
|
+
try {
|
|
145
|
+
execSync("git fetch origin --quiet", { cwd, stdio: "pipe", timeout: 15000 });
|
|
146
|
+
const remoteVersion = execSync("git show origin/master:package.json", {
|
|
147
|
+
cwd, encoding: "utf-8", timeout: 5000,
|
|
148
|
+
});
|
|
149
|
+
const remotePkg = JSON.parse(remoteVersion);
|
|
150
|
+
if (!latestVersion || _isNewer(remotePkg.version, latestVersion)) {
|
|
151
|
+
latestVersion = remotePkg.version;
|
|
152
|
+
}
|
|
153
|
+
} catch {}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!latestVersion || !_isNewer(latestVersion, currentVersion)) {
|
|
157
|
+
console.log(chalk.green(` ✅ Already up to date (${currentVersion})`));
|
|
158
|
+
return { success: true, from: currentVersion, to: currentVersion, upToDate: true };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
console.log(chalk.blue(` 📦 Update available: ${currentVersion} → ${latestVersion}`));
|
|
162
|
+
|
|
163
|
+
if (dryRun) {
|
|
164
|
+
console.log(chalk.gray(" (dry run — no changes made)"));
|
|
165
|
+
return { success: true, from: currentVersion, to: latestVersion, dryRun: true };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 2. Create safe backup (outside project, survives everything)
|
|
169
|
+
console.log(chalk.gray(" 🔒 Creating safe backup..."));
|
|
170
|
+
const backup = createSafeBackup(cwd);
|
|
171
|
+
console.log(chalk.gray(` 🔒 Backed up ${backup.fileCount} files to ${backup.dir}`));
|
|
172
|
+
if (logger) logger.info("update.backup", `Safe backup: ${backup.fileCount} files`, { dir: backup.dir });
|
|
173
|
+
|
|
174
|
+
// 3. Backup user files to memory (belt + suspenders)
|
|
175
|
+
const memoryBackup = _backupToMemory(cwd);
|
|
176
|
+
console.log(chalk.gray(` 🔒 Memory backup: ${Object.keys(memoryBackup).length} files`));
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
// 4. Update framework ONLY
|
|
180
|
+
if (isGit) {
|
|
181
|
+
console.log(chalk.blue(" 📦 Selective git update (server/ + .wolverine/ untouched)"));
|
|
182
|
+
const frameworkPaths = "src/ bin/ package.json package-lock.json examples/ tests/ CLAUDE.md README.md CHANGELOG.md .npmignore";
|
|
183
|
+
execSync(`git checkout origin/master -- ${frameworkPaths}`, { cwd, stdio: "pipe", timeout: 30000 });
|
|
184
|
+
execSync("npm install --production", { cwd, stdio: "pipe", timeout: 120000 });
|
|
185
|
+
} else {
|
|
186
|
+
const cmd = `npm install ${PACKAGE_NAME}@${latestVersion}`;
|
|
187
|
+
console.log(chalk.blue(` 📦 ${cmd}`));
|
|
188
|
+
execSync(cmd, { cwd, stdio: "pipe", timeout: 120000 });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 5. Restore user files from memory
|
|
192
|
+
_restoreFromMemory(cwd, memoryBackup);
|
|
193
|
+
console.log(chalk.gray(` 🔒 Restored ${Object.keys(memoryBackup).length} user files`));
|
|
194
|
+
|
|
195
|
+
// 6. Signal brain to merge new seeds on next boot
|
|
196
|
+
const seedRefreshDir = path.join(cwd, ".wolverine", "brain");
|
|
197
|
+
fs.mkdirSync(seedRefreshDir, { recursive: true });
|
|
198
|
+
fs.writeFileSync(path.join(seedRefreshDir, ".seed-refresh"), new Date().toISOString(), "utf-8");
|
|
199
|
+
console.log(chalk.gray(" 🧠 Brain seed merge scheduled for next boot"));
|
|
200
|
+
|
|
201
|
+
// 7. Verify
|
|
202
|
+
const newVersion = _getCurrentVersion(cwd);
|
|
203
|
+
console.log(chalk.green(` ✅ Updated: ${currentVersion} → ${newVersion}`));
|
|
204
|
+
console.log(chalk.gray(` 🔒 Safe backup at: ${backup.dir}`));
|
|
205
|
+
if (logger) logger.info("update.success", `Updated ${currentVersion} → ${newVersion}`, { from: currentVersion, to: newVersion });
|
|
206
|
+
|
|
207
|
+
return { success: true, from: currentVersion, to: newVersion, backupDir: backup.dir };
|
|
208
|
+
} catch (err) {
|
|
209
|
+
// Restore from memory on failure
|
|
210
|
+
_restoreFromMemory(cwd, memoryBackup);
|
|
211
|
+
const errMsg = (err.message || "").slice(0, 100);
|
|
212
|
+
console.log(chalk.red(` ❌ Update failed: ${errMsg}`));
|
|
213
|
+
console.log(chalk.yellow(` 🔒 Restore from safe backup: wolverine --restore ${backup.timestamp}`));
|
|
214
|
+
if (logger) logger.warn("update.failed", `Update failed: ${errMsg}`, { from: currentVersion });
|
|
215
|
+
return { success: false, from: currentVersion, to: latestVersion, error: errMsg, backupDir: backup.dir };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── Helpers ──
|
|
220
|
+
|
|
221
|
+
function _getCurrentVersion(cwd) {
|
|
222
|
+
try { return require(path.join(cwd, "package.json")).version; } catch { return "0.0.0"; }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function _isGitRepo(cwd) {
|
|
226
|
+
try { execSync("git rev-parse --is-inside-work-tree", { cwd, stdio: "pipe", timeout: 3000 }); return true; } catch { return false; }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function _isNewer(a, b) {
|
|
230
|
+
if (!a || !b) return false;
|
|
231
|
+
const av = a.split(".").map(Number), bv = b.split(".").map(Number);
|
|
232
|
+
for (let i = 0; i < 3; i++) { if ((av[i]||0) > (bv[i]||0)) return true; if ((av[i]||0) < (bv[i]||0)) return false; }
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function _copyDirRecursive(src, dest) {
|
|
237
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
238
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
239
|
+
if (entry.name === "node_modules") continue;
|
|
240
|
+
const s = path.join(src, entry.name), d = path.join(dest, entry.name);
|
|
241
|
+
if (entry.isDirectory()) _copyDirRecursive(s, d);
|
|
242
|
+
else { try { fs.copyFileSync(s, d); } catch {} }
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function _countFiles(dir) {
|
|
247
|
+
let count = 0;
|
|
248
|
+
try {
|
|
249
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
250
|
+
if (entry.isDirectory()) count += _countFiles(path.join(dir, entry.name));
|
|
251
|
+
else count++;
|
|
252
|
+
}
|
|
253
|
+
} catch {}
|
|
254
|
+
return count;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function _backupToMemory(cwd) {
|
|
258
|
+
const backups = {};
|
|
259
|
+
const protect = ["server", ".wolverine"];
|
|
260
|
+
const protectFiles = [".env.local", ".env"];
|
|
261
|
+
|
|
262
|
+
for (const dir of protect) {
|
|
263
|
+
const dirPath = path.join(cwd, dir);
|
|
264
|
+
if (!fs.existsSync(dirPath)) continue;
|
|
265
|
+
const walk = (d, base) => {
|
|
266
|
+
try {
|
|
267
|
+
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
|
268
|
+
if (entry.name === "node_modules") continue;
|
|
269
|
+
const full = path.join(d, entry.name), rel = path.join(base, entry.name).replace(/\\/g, "/");
|
|
270
|
+
if (entry.isDirectory()) walk(full, rel);
|
|
271
|
+
else { try { const s = fs.statSync(full); if (s.size <= 10*1024*1024) backups[rel] = fs.readFileSync(full); } catch {} }
|
|
272
|
+
}
|
|
273
|
+
} catch {}
|
|
274
|
+
};
|
|
275
|
+
walk(dirPath, dir);
|
|
276
|
+
}
|
|
277
|
+
for (const f of protectFiles) {
|
|
278
|
+
const fp = path.join(cwd, f);
|
|
279
|
+
if (fs.existsSync(fp)) backups[f] = fs.readFileSync(fp);
|
|
280
|
+
}
|
|
281
|
+
return backups;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function _restoreFromMemory(cwd, backups) {
|
|
285
|
+
for (const [rel, content] of Object.entries(backups)) {
|
|
286
|
+
const fp = path.join(cwd, rel);
|
|
287
|
+
try { fs.mkdirSync(path.dirname(fp), { recursive: true }); fs.writeFileSync(fp, content); } catch {}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ── Skill Metadata ──
|
|
292
|
+
|
|
293
|
+
const SKILL_NAME = "update";
|
|
294
|
+
const SKILL_DESCRIPTION = "Safe self-updating for wolverine framework. Creates safe backup outside project (~/. wolverine-safe-backups/), selectively updates only framework files (src/, bin/, package.json), restores all user files (server/, .wolverine/, .env), merges new brain seeds. Never use raw npm install or git pull — they overwrite server code and brain memories.";
|
|
295
|
+
const SKILL_KEYWORDS = ["update", "upgrade", "version", "install", "pull", "self-update", "auto-update", "framework", "safe"];
|
|
296
|
+
const SKILL_USAGE = `// Safe update (programmatic)
|
|
297
|
+
const { safeUpdate } = require("wolverine-ai");
|
|
298
|
+
const result = await safeUpdate(process.cwd());
|
|
299
|
+
// { success: true, from: "2.5.3", to: "2.6.0", backupDir: "~/.wolverine-safe-backups/..." }
|
|
300
|
+
|
|
301
|
+
// List safe backups
|
|
302
|
+
const { listSafeBackups } = require("wolverine-ai");
|
|
303
|
+
const backups = listSafeBackups();
|
|
304
|
+
|
|
305
|
+
// Restore from safe backup
|
|
306
|
+
const { restoreFromSafeBackup } = require("wolverine-ai");
|
|
307
|
+
restoreFromSafeBackup(process.cwd(), "2026-04-02T21-15-00");
|
|
308
|
+
|
|
309
|
+
// CLI: wolverine --update
|
|
310
|
+
// CLI: wolverine --update --dry-run
|
|
311
|
+
// CLI: wolverine --restore 2026-04-02T21-15-00`;
|
|
312
|
+
|
|
313
|
+
module.exports = {
|
|
314
|
+
SKILL_NAME, SKILL_DESCRIPTION, SKILL_KEYWORDS, SKILL_USAGE,
|
|
315
|
+
safeUpdate, createSafeBackup, listSafeBackups, restoreFromSafeBackup,
|
|
316
|
+
};
|