wolverine-ai 2.8.0 → 2.8.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/package.json +1 -1
- package/src/platform/auto-update.js +115 -91
- package/src/skills/update.js +5 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wolverine-ai",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.1",
|
|
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": {
|
|
@@ -6,83 +6,51 @@ const chalk = require("chalk");
|
|
|
6
6
|
/**
|
|
7
7
|
* Auto-Updater — self-updating wolverine framework.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* Wolverine can't edit files outside server/ directly, but it CAN
|
|
16
|
-
* run bash commands — so npm update is the upgrade path.
|
|
9
|
+
* CRITICAL SAFEGUARDS (learned from $6.92 infinite loop incident):
|
|
10
|
+
* 1. Never re-trigger on same version — tracks last attempted version on disk
|
|
11
|
+
* 2. Verify deps after update — npm ls must pass or update is rolled back
|
|
12
|
+
* 3. Cooldown after failed update — won't retry for 1 hour
|
|
13
|
+
* 4. Max 1 update attempt per boot — prevents restart loops
|
|
17
14
|
*/
|
|
18
15
|
|
|
19
16
|
const PACKAGE_NAME = "wolverine-ai";
|
|
20
|
-
const CHECK_INTERVAL_MS = 3600000;
|
|
17
|
+
const CHECK_INTERVAL_MS = 3600000;
|
|
18
|
+
const LOCKFILE = ".wolverine/update-lock.json"; // tracks last attempt
|
|
21
19
|
|
|
22
20
|
let _timer = null;
|
|
23
21
|
let _currentVersion = null;
|
|
24
22
|
let _checking = false;
|
|
23
|
+
let _attemptedThisBoot = false; // max 1 update per boot
|
|
25
24
|
|
|
26
|
-
/**
|
|
27
|
-
* Get the currently installed version.
|
|
28
|
-
*/
|
|
29
25
|
function getCurrentVersion() {
|
|
30
26
|
if (_currentVersion) return _currentVersion;
|
|
31
|
-
try {
|
|
32
|
-
const pkg = require("../../package.json");
|
|
33
|
-
_currentVersion = pkg.version;
|
|
34
|
-
} catch {
|
|
35
|
-
_currentVersion = "0.0.0";
|
|
36
|
-
}
|
|
27
|
+
try { _currentVersion = require("../../package.json").version; } catch { _currentVersion = "0.0.0"; }
|
|
37
28
|
return _currentVersion;
|
|
38
29
|
}
|
|
39
30
|
|
|
40
|
-
/**
|
|
41
|
-
* Check for the latest available version.
|
|
42
|
-
* For git repos: checks remote for newer commits via `git ls-remote`.
|
|
43
|
-
* For npm installs: checks npm registry via `npm view`.
|
|
44
|
-
*/
|
|
45
31
|
function getLatestVersion(cwd) {
|
|
46
|
-
// Try npm registry first (works for both git and npm installs)
|
|
47
32
|
try {
|
|
48
|
-
|
|
49
|
-
encoding: "utf-8",
|
|
50
|
-
|
|
51
|
-
cwd: cwd || process.cwd(),
|
|
52
|
-
}).trim();
|
|
53
|
-
if (result) return result;
|
|
33
|
+
return execSync(`npm view ${PACKAGE_NAME} version 2>/dev/null`, {
|
|
34
|
+
encoding: "utf-8", timeout: 15000, cwd: cwd || process.cwd(),
|
|
35
|
+
}).trim() || null;
|
|
54
36
|
} catch {}
|
|
55
37
|
|
|
56
|
-
//
|
|
38
|
+
// Git fallback
|
|
57
39
|
try {
|
|
58
|
-
if (
|
|
40
|
+
if (_isGitRepo(cwd || process.cwd())) {
|
|
59
41
|
execSync("git fetch origin --quiet", { cwd: cwd || process.cwd(), stdio: "pipe", timeout: 15000 });
|
|
60
|
-
const
|
|
42
|
+
const remoteVersion = execSync("git show origin/master:package.json", {
|
|
61
43
|
cwd: cwd || process.cwd(), encoding: "utf-8", timeout: 5000,
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
// There are newer commits — read version from remote package.json
|
|
65
|
-
try {
|
|
66
|
-
const remoteVersion = execSync("git show origin/master:package.json", {
|
|
67
|
-
cwd: cwd || process.cwd(), encoding: "utf-8", timeout: 5000,
|
|
68
|
-
});
|
|
69
|
-
const pkg = JSON.parse(remoteVersion);
|
|
70
|
-
return pkg.version || null;
|
|
71
|
-
} catch {}
|
|
72
|
-
}
|
|
44
|
+
});
|
|
45
|
+
return JSON.parse(remoteVersion).version || null;
|
|
73
46
|
}
|
|
74
47
|
} catch {}
|
|
75
|
-
|
|
76
48
|
return null;
|
|
77
49
|
}
|
|
78
50
|
|
|
79
|
-
/**
|
|
80
|
-
* Compare semver versions. Returns true if latest > current.
|
|
81
|
-
*/
|
|
82
51
|
function isNewer(latest, current) {
|
|
83
52
|
if (!latest || !current) return false;
|
|
84
|
-
const a = latest.split(".").map(Number);
|
|
85
|
-
const b = current.split(".").map(Number);
|
|
53
|
+
const a = latest.split(".").map(Number), b = current.split(".").map(Number);
|
|
86
54
|
for (let i = 0; i < 3; i++) {
|
|
87
55
|
if ((a[i] || 0) > (b[i] || 0)) return true;
|
|
88
56
|
if ((a[i] || 0) < (b[i] || 0)) return false;
|
|
@@ -90,38 +58,100 @@ function isNewer(latest, current) {
|
|
|
90
58
|
return false;
|
|
91
59
|
}
|
|
92
60
|
|
|
61
|
+
function _isGitRepo(cwd) {
|
|
62
|
+
try { execSync("git rev-parse --is-inside-work-tree", { cwd, stdio: "pipe", timeout: 3000 }); return true; } catch { return false; }
|
|
63
|
+
}
|
|
64
|
+
|
|
93
65
|
/**
|
|
94
|
-
*
|
|
66
|
+
* Read the update lock file — prevents retrying same version.
|
|
95
67
|
*/
|
|
96
|
-
function
|
|
68
|
+
function _readLock(cwd) {
|
|
97
69
|
try {
|
|
98
|
-
|
|
99
|
-
return
|
|
100
|
-
} catch {
|
|
70
|
+
const lockPath = path.join(cwd, LOCKFILE);
|
|
71
|
+
if (fs.existsSync(lockPath)) return JSON.parse(fs.readFileSync(lockPath, "utf-8"));
|
|
72
|
+
} catch {}
|
|
73
|
+
return {};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function _writeLock(cwd, data) {
|
|
77
|
+
try {
|
|
78
|
+
const lockPath = path.join(cwd, LOCKFILE);
|
|
79
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
80
|
+
fs.writeFileSync(lockPath, JSON.stringify(data, null, 2), "utf-8");
|
|
81
|
+
} catch {}
|
|
101
82
|
}
|
|
102
83
|
|
|
103
84
|
/**
|
|
104
|
-
*
|
|
105
|
-
* Supports both npm-installed and git-cloned wolverine.
|
|
85
|
+
* Verify deps are intact after update. Returns true if healthy.
|
|
106
86
|
*/
|
|
87
|
+
function _verifyDeps(cwd) {
|
|
88
|
+
try {
|
|
89
|
+
execSync("node -e \"require('fastify')\" 2>/dev/null", { cwd, stdio: "pipe", timeout: 5000 });
|
|
90
|
+
return true;
|
|
91
|
+
} catch {
|
|
92
|
+
// Try npm install to restore deps
|
|
93
|
+
try {
|
|
94
|
+
console.log(chalk.yellow(" ⚠️ Deps broken after update — running npm install to fix..."));
|
|
95
|
+
execSync("npm install", { cwd, stdio: "pipe", timeout: 120000 });
|
|
96
|
+
execSync("node -e \"require('fastify')\" 2>/dev/null", { cwd, stdio: "pipe", timeout: 5000 });
|
|
97
|
+
return true;
|
|
98
|
+
} catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
107
104
|
function upgrade(cwd, logger) {
|
|
108
105
|
const current = getCurrentVersion();
|
|
109
|
-
const latest = getLatestVersion();
|
|
106
|
+
const latest = getLatestVersion(cwd);
|
|
110
107
|
|
|
111
108
|
if (!latest || !isNewer(latest, current)) {
|
|
112
109
|
return { success: false, from: current, to: latest, error: "Already up to date" };
|
|
113
110
|
}
|
|
114
111
|
|
|
115
|
-
//
|
|
112
|
+
// Check lock — don't retry same version
|
|
113
|
+
const lock = _readLock(cwd);
|
|
114
|
+
if (lock.lastAttemptedVersion === latest) {
|
|
115
|
+
const elapsed = Date.now() - (lock.lastAttemptedAt || 0);
|
|
116
|
+
if (elapsed < 3600000) { // 1 hour cooldown
|
|
117
|
+
console.log(chalk.gray(` 🔄 Skipping ${latest} (already attempted ${Math.round(elapsed / 60000)}min ago)`));
|
|
118
|
+
return { success: false, from: current, to: latest, error: "Already attempted, cooldown active" };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Record attempt BEFORE trying (so we don't retry on crash)
|
|
123
|
+
_writeLock(cwd, { lastAttemptedVersion: latest, lastAttemptedAt: Date.now(), from: current });
|
|
124
|
+
|
|
116
125
|
const { safeUpdate } = require("../skills/update");
|
|
117
|
-
_currentVersion = null;
|
|
118
|
-
|
|
126
|
+
_currentVersion = null;
|
|
127
|
+
const result = safeUpdate(cwd, { logger });
|
|
128
|
+
|
|
129
|
+
// Verify deps after update
|
|
130
|
+
if (result.success) {
|
|
131
|
+
if (!_verifyDeps(cwd)) {
|
|
132
|
+
console.log(chalk.red(" ❌ Update broke dependencies — rolling back"));
|
|
133
|
+
if (logger) logger.error("update.deps_broken", "Update broke dependencies, rolling back");
|
|
134
|
+
// Restore from emergency backup
|
|
135
|
+
try {
|
|
136
|
+
const { restoreFromSafeBackup, listSafeBackups } = require("../skills/update");
|
|
137
|
+
const backups = listSafeBackups();
|
|
138
|
+
if (backups.length > 0) {
|
|
139
|
+
restoreFromSafeBackup(cwd, backups[0].dir);
|
|
140
|
+
console.log(chalk.yellow(" ↩️ Rolled back to pre-update state"));
|
|
141
|
+
}
|
|
142
|
+
} catch {}
|
|
143
|
+
_writeLock(cwd, { lastAttemptedVersion: latest, lastAttemptedAt: Date.now(), from: current, failed: true, reason: "deps_broken" });
|
|
144
|
+
return { success: false, from: current, to: latest, error: "Update broke dependencies" };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (result.success) {
|
|
149
|
+
_writeLock(cwd, { lastAttemptedVersion: latest, lastAttemptedAt: Date.now(), from: current, success: true });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return result;
|
|
119
153
|
}
|
|
120
154
|
|
|
121
|
-
/**
|
|
122
|
-
* Check for updates (non-blocking). Logs if update available.
|
|
123
|
-
* Call upgrade() separately to actually apply.
|
|
124
|
-
*/
|
|
125
155
|
function checkForUpdate(cwd) {
|
|
126
156
|
if (_checking) return null;
|
|
127
157
|
_checking = true;
|
|
@@ -129,52 +159,54 @@ function checkForUpdate(cwd) {
|
|
|
129
159
|
const current = getCurrentVersion();
|
|
130
160
|
const latest = getLatestVersion(cwd);
|
|
131
161
|
_checking = false;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return { available:
|
|
162
|
+
|
|
163
|
+
if (!latest || !isNewer(latest, current)) {
|
|
164
|
+
return { available: false, current, latest };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Check lock — don't report available if we already failed on this version
|
|
168
|
+
const lock = _readLock(cwd);
|
|
169
|
+
if (lock.lastAttemptedVersion === latest && lock.failed) {
|
|
170
|
+
const elapsed = Date.now() - (lock.lastAttemptedAt || 0);
|
|
171
|
+
if (elapsed < 3600000) return { available: false, current, latest, locked: true };
|
|
135
172
|
}
|
|
136
|
-
|
|
173
|
+
|
|
174
|
+
console.log(chalk.blue(` 🔄 Update available: ${PACKAGE_NAME} ${current} → ${latest}`));
|
|
175
|
+
return { available: true, current, latest };
|
|
137
176
|
} catch {
|
|
138
177
|
_checking = false;
|
|
139
178
|
return null;
|
|
140
179
|
}
|
|
141
180
|
}
|
|
142
181
|
|
|
143
|
-
/**
|
|
144
|
-
* Start auto-update schedule. Checks every hour (configurable).
|
|
145
|
-
* If autoUpdate is enabled and a new version is found, upgrades and signals restart.
|
|
146
|
-
*
|
|
147
|
-
* @param {object} options
|
|
148
|
-
* @param {string} options.cwd — project root
|
|
149
|
-
* @param {object} options.logger — EventLogger
|
|
150
|
-
* @param {function} options.onUpdate — called after successful update (trigger restart)
|
|
151
|
-
* @param {number} options.intervalMs — check interval (default: 1h)
|
|
152
|
-
*/
|
|
153
182
|
function startAutoUpdate({ cwd, logger, onUpdate, intervalMs }) {
|
|
154
183
|
const interval = intervalMs || CHECK_INTERVAL_MS;
|
|
155
184
|
|
|
156
|
-
// Check on startup (delayed 30s to not block boot)
|
|
157
185
|
console.log(chalk.gray(` 🔄 Auto-update scheduled: first check in 30s, then every ${Math.round(interval / 60000)}min`));
|
|
186
|
+
|
|
158
187
|
setTimeout(() => {
|
|
188
|
+
if (_attemptedThisBoot) return;
|
|
159
189
|
console.log(chalk.gray(` 🔄 Checking for updates (v${getCurrentVersion()})...`));
|
|
160
190
|
const result = checkForUpdate(cwd);
|
|
161
191
|
if (result?.available) {
|
|
192
|
+
_attemptedThisBoot = true;
|
|
162
193
|
const upgraded = upgrade(cwd, logger);
|
|
163
194
|
if (upgraded.success && onUpdate) {
|
|
164
195
|
console.log(chalk.blue(" 🔄 Restarting with new version..."));
|
|
165
196
|
onUpdate(upgraded);
|
|
166
197
|
}
|
|
167
198
|
} else if (result) {
|
|
168
|
-
console.log(chalk.gray(` 🔄 Up to date (v${result.current}${result.latest ? ", npm: " + result.latest : ""})`));
|
|
199
|
+
console.log(chalk.gray(` 🔄 Up to date (v${result.current}${result.latest ? ", npm: " + result.latest : ""}${result.locked ? " [cooldown]" : ""})`));
|
|
169
200
|
} else {
|
|
170
201
|
console.log(chalk.yellow(" 🔄 Update check failed (npm unreachable?)"));
|
|
171
202
|
}
|
|
172
203
|
}, 30000);
|
|
173
204
|
|
|
174
|
-
// Periodic check
|
|
175
205
|
_timer = setInterval(() => {
|
|
206
|
+
if (_attemptedThisBoot) return; // max 1 attempt per boot
|
|
176
207
|
const result = checkForUpdate(cwd);
|
|
177
208
|
if (result?.available) {
|
|
209
|
+
_attemptedThisBoot = true;
|
|
178
210
|
const upgraded = upgrade(cwd, logger);
|
|
179
211
|
if (upgraded.success && onUpdate) {
|
|
180
212
|
console.log(chalk.blue(" 🔄 Restarting with new version..."));
|
|
@@ -188,12 +220,4 @@ function stopAutoUpdate() {
|
|
|
188
220
|
if (_timer) { clearInterval(_timer); _timer = null; }
|
|
189
221
|
}
|
|
190
222
|
|
|
191
|
-
module.exports = {
|
|
192
|
-
getCurrentVersion,
|
|
193
|
-
getLatestVersion,
|
|
194
|
-
isNewer,
|
|
195
|
-
checkForUpdate,
|
|
196
|
-
upgrade,
|
|
197
|
-
startAutoUpdate,
|
|
198
|
-
stopAutoUpdate,
|
|
199
|
-
};
|
|
223
|
+
module.exports = { getCurrentVersion, getLatestVersion, isNewer, checkForUpdate, upgrade, startAutoUpdate, stopAutoUpdate };
|
package/src/skills/update.js
CHANGED
|
@@ -175,9 +175,12 @@ function safeUpdate(cwd, options = {}) {
|
|
|
175
175
|
// 3. Update framework ONLY — server/ is never touched
|
|
176
176
|
if (isGit) {
|
|
177
177
|
console.log(chalk.blue(" 📦 Selective git update (server/ untouched)"));
|
|
178
|
-
|
|
178
|
+
// ONLY update framework files — never touch server/ or its deps
|
|
179
|
+
const frameworkPaths = "src/ bin/ examples/ tests/ CLAUDE.md README.md CHANGELOG.md .npmignore";
|
|
179
180
|
execSync(`git checkout origin/master -- ${frameworkPaths}`, { cwd, stdio: "pipe", timeout: 30000 });
|
|
180
|
-
|
|
181
|
+
// Update package.json separately, then install deps to restore anything lost
|
|
182
|
+
execSync("git checkout origin/master -- package.json", { cwd, stdio: "pipe", timeout: 10000 });
|
|
183
|
+
execSync("npm install", { cwd, stdio: "pipe", timeout: 120000 });
|
|
181
184
|
} else {
|
|
182
185
|
const cmd = `npm install ${PACKAGE_NAME}@${latestVersion}`;
|
|
183
186
|
console.log(chalk.blue(` 📦 ${cmd}`));
|