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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wolverine-ai",
3
- "version": "2.8.0",
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
- * Checks npm registry for newer wolverine-ai versions on a schedule.
10
- * When a new version is found, upgrades via npm and restarts.
11
- *
12
- * Config/settings are protected: backed up before update, restored after.
13
- * Disable in settings.json: "autoUpdate": { "enabled": false }
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; // 1 hour
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
- const result = execSync(`npm view ${PACKAGE_NAME} version 2>/dev/null`, {
49
- encoding: "utf-8",
50
- timeout: 15000,
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
- // Fallback for git repos: check if remote has newer commits
38
+ // Git fallback
57
39
  try {
58
- if (isGitRepo(cwd || process.cwd())) {
40
+ if (_isGitRepo(cwd || process.cwd())) {
59
41
  execSync("git fetch origin --quiet", { cwd: cwd || process.cwd(), stdio: "pipe", timeout: 15000 });
60
- const behind = execSync("git rev-list HEAD..origin/master --count", {
42
+ const remoteVersion = execSync("git show origin/master:package.json", {
61
43
  cwd: cwd || process.cwd(), encoding: "utf-8", timeout: 5000,
62
- }).trim();
63
- if (parseInt(behind, 10) > 0) {
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
- * Detect if this is a git repo or an npm install.
66
+ * Read the update lock file prevents retrying same version.
95
67
  */
96
- function isGitRepo(cwd) {
68
+ function _readLock(cwd) {
97
69
  try {
98
- execSync("git rev-parse --is-inside-work-tree", { cwd, stdio: "pipe", timeout: 3000 });
99
- return true;
100
- } catch { return false; }
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
- * Perform the upgrade. Returns { success, from, to, error? }
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
- // Delegate to the update skill for the full safe upgrade routine
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; // clear cache so next check sees new version
118
- return safeUpdate(cwd, { logger });
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
- if (latest && isNewer(latest, current)) {
133
- console.log(chalk.blue(` 🔄 Update available: ${PACKAGE_NAME} ${current} → ${latest}`));
134
- return { available: true, current, latest };
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
- return { available: false, current, latest };
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 };
@@ -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
- const frameworkPaths = "src/ bin/ package.json package-lock.json examples/ tests/ CLAUDE.md README.md CHANGELOG.md .npmignore";
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
- execSync("npm install --production", { cwd, stdio: "pipe", timeout: 120000 });
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}`));