wolverine-ai 1.0.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/PLATFORM.md +442 -0
- package/README.md +475 -0
- package/SERVER_BEST_PRACTICES.md +62 -0
- package/TELEMETRY.md +108 -0
- package/bin/wolverine.js +95 -0
- package/examples/01-basic-typo.js +31 -0
- package/examples/02-multi-file/routes/users.js +15 -0
- package/examples/02-multi-file/server.js +25 -0
- package/examples/03-syntax-error.js +23 -0
- package/examples/04-secret-leak.js +14 -0
- package/examples/05-expired-key.js +27 -0
- package/examples/06-json-config/config.json +13 -0
- package/examples/06-json-config/server.js +28 -0
- package/examples/07-rate-limit-loop.js +11 -0
- package/examples/08-sandbox-escape.js +20 -0
- package/examples/buggy-server.js +39 -0
- package/examples/demos/01-basic-typo/index.js +20 -0
- package/examples/demos/01-basic-typo/routes/api.js +13 -0
- package/examples/demos/01-basic-typo/routes/health.js +4 -0
- package/examples/demos/02-multi-file/index.js +24 -0
- package/examples/demos/02-multi-file/routes/api.js +13 -0
- package/examples/demos/02-multi-file/routes/health.js +4 -0
- package/examples/demos/03-syntax-error/index.js +18 -0
- package/examples/demos/04-secret-leak/index.js +16 -0
- package/examples/demos/05-expired-key/index.js +21 -0
- package/examples/demos/06-json-config/config.json +9 -0
- package/examples/demos/06-json-config/index.js +20 -0
- package/examples/demos/07-null-crash/index.js +16 -0
- package/examples/run-demo.js +110 -0
- package/package.json +67 -0
- package/server/config/settings.json +62 -0
- package/server/index.js +33 -0
- package/server/routes/api.js +12 -0
- package/server/routes/health.js +16 -0
- package/server/routes/time.js +12 -0
- package/src/agent/agent-engine.js +727 -0
- package/src/agent/goal-loop.js +140 -0
- package/src/agent/research-agent.js +120 -0
- package/src/agent/sub-agents.js +176 -0
- package/src/backup/backup-manager.js +321 -0
- package/src/brain/brain.js +315 -0
- package/src/brain/embedder.js +131 -0
- package/src/brain/function-map.js +263 -0
- package/src/brain/vector-store.js +267 -0
- package/src/core/ai-client.js +387 -0
- package/src/core/cluster-manager.js +144 -0
- package/src/core/config.js +89 -0
- package/src/core/error-parser.js +87 -0
- package/src/core/health-monitor.js +129 -0
- package/src/core/models.js +132 -0
- package/src/core/patcher.js +55 -0
- package/src/core/runner.js +464 -0
- package/src/core/system-info.js +141 -0
- package/src/core/verifier.js +146 -0
- package/src/core/wolverine.js +290 -0
- package/src/dashboard/server.js +1332 -0
- package/src/index.js +94 -0
- package/src/logger/event-logger.js +237 -0
- package/src/logger/pricing.js +96 -0
- package/src/logger/repair-history.js +109 -0
- package/src/logger/token-tracker.js +277 -0
- package/src/mcp/mcp-client.js +224 -0
- package/src/mcp/mcp-registry.js +228 -0
- package/src/mcp/mcp-security.js +152 -0
- package/src/monitor/perf-monitor.js +300 -0
- package/src/monitor/process-monitor.js +231 -0
- package/src/monitor/route-prober.js +191 -0
- package/src/notifications/notifier.js +227 -0
- package/src/platform/heartbeat.js +93 -0
- package/src/platform/queue.js +53 -0
- package/src/platform/register.js +64 -0
- package/src/platform/telemetry.js +76 -0
- package/src/security/admin-auth.js +150 -0
- package/src/security/injection-detector.js +174 -0
- package/src/security/rate-limiter.js +152 -0
- package/src/security/sandbox.js +128 -0
- package/src/security/secret-redactor.js +217 -0
- package/src/skills/skill-registry.js +129 -0
- package/src/skills/sql.js +375 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
const { spawn, execSync } = require("child_process");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const chalk = require("chalk");
|
|
4
|
+
const { heal } = require("./wolverine");
|
|
5
|
+
const { HealthMonitor } = require("./health-monitor");
|
|
6
|
+
const { Sandbox } = require("../security/sandbox");
|
|
7
|
+
const { RateLimiter } = require("../security/rate-limiter");
|
|
8
|
+
const { BackupManager, STABILITY_THRESHOLD_MS } = require("../backup/backup-manager");
|
|
9
|
+
const { EventLogger, EVENT_TYPES } = require("../logger/event-logger");
|
|
10
|
+
const { DashboardServer } = require("../dashboard/server");
|
|
11
|
+
const { PerfMonitor } = require("../monitor/perf-monitor");
|
|
12
|
+
const { Brain } = require("../brain/brain");
|
|
13
|
+
const { SecretRedactor } = require("../security/secret-redactor");
|
|
14
|
+
const { McpRegistry } = require("../mcp/mcp-registry");
|
|
15
|
+
const { TokenTracker } = require("../logger/token-tracker");
|
|
16
|
+
const { RepairHistory } = require("../logger/repair-history");
|
|
17
|
+
const { setTokenTracker } = require("./ai-client");
|
|
18
|
+
const { SkillRegistry } = require("../skills/skill-registry");
|
|
19
|
+
const { ProcessMonitor } = require("../monitor/process-monitor");
|
|
20
|
+
const { RouteProber } = require("../monitor/route-prober");
|
|
21
|
+
const { startHeartbeat, stopHeartbeat } = require("../platform/heartbeat");
|
|
22
|
+
const { Notifier } = require("../notifications/notifier");
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The Wolverine process runner — v3.
|
|
26
|
+
*
|
|
27
|
+
* Full autonomous server agent:
|
|
28
|
+
* - Process management (spawn, crash detection, restart)
|
|
29
|
+
* - AI-powered self-healing (fast path + multi-file agent)
|
|
30
|
+
* - Health check monitoring
|
|
31
|
+
* - Performance monitoring with proactive optimization
|
|
32
|
+
* - Real-time web dashboard
|
|
33
|
+
* - Comprehensive event logging
|
|
34
|
+
*/
|
|
35
|
+
class WolverineRunner {
|
|
36
|
+
constructor(scriptPath, options = {}) {
|
|
37
|
+
this.scriptPath = path.resolve(scriptPath);
|
|
38
|
+
this.cwd = options.cwd || path.dirname(this.scriptPath);
|
|
39
|
+
this.maxRetries = parseInt(process.env.WOLVERINE_MAX_RETRIES, 10) || 3;
|
|
40
|
+
this.retryCount = 0;
|
|
41
|
+
this.child = null;
|
|
42
|
+
this.running = false;
|
|
43
|
+
|
|
44
|
+
// Core subsystems
|
|
45
|
+
this.sandbox = new Sandbox(this.cwd);
|
|
46
|
+
this.redactor = new SecretRedactor(this.cwd);
|
|
47
|
+
this.rateLimiter = new RateLimiter({
|
|
48
|
+
maxCallsPerWindow: parseInt(process.env.WOLVERINE_RATE_MAX_CALLS, 10) || 10,
|
|
49
|
+
windowMs: parseInt(process.env.WOLVERINE_RATE_WINDOW_MS, 10) || 600000,
|
|
50
|
+
minGapMs: parseInt(process.env.WOLVERINE_RATE_MIN_GAP_MS, 10) || 5000,
|
|
51
|
+
maxTokensPerHour: parseInt(process.env.WOLVERINE_RATE_MAX_TOKENS_HOUR, 10) || 100000,
|
|
52
|
+
});
|
|
53
|
+
this.backupManager = new BackupManager(this.cwd);
|
|
54
|
+
this.logger = new EventLogger(this.cwd);
|
|
55
|
+
this.logger.setRedactor(this.redactor);
|
|
56
|
+
this.tokenTracker = new TokenTracker(this.cwd);
|
|
57
|
+
setTokenTracker(this.tokenTracker);
|
|
58
|
+
this.repairHistory = new RepairHistory(this.cwd);
|
|
59
|
+
this.notifier = new Notifier({
|
|
60
|
+
logger: this.logger,
|
|
61
|
+
redactor: this.redactor,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Health monitoring
|
|
65
|
+
const port = parseInt(process.env.PORT, 10) || 3000;
|
|
66
|
+
this.healthMonitor = new HealthMonitor({
|
|
67
|
+
port,
|
|
68
|
+
path: options.healthPath || "/health",
|
|
69
|
+
intervalMs: parseInt(process.env.WOLVERINE_HEALTH_INTERVAL_MS, 10) || 15000,
|
|
70
|
+
timeoutMs: parseInt(process.env.WOLVERINE_HEALTH_TIMEOUT_MS, 10) || 5000,
|
|
71
|
+
failThreshold: parseInt(process.env.WOLVERINE_HEALTH_FAIL_THRESHOLD, 10) || 3,
|
|
72
|
+
startDelayMs: parseInt(process.env.WOLVERINE_HEALTH_START_DELAY_MS, 10) || 10000,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Performance monitoring
|
|
76
|
+
this.perfMonitor = new PerfMonitor({
|
|
77
|
+
logger: this.logger,
|
|
78
|
+
sandbox: this.sandbox,
|
|
79
|
+
cwd: this.cwd,
|
|
80
|
+
port,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Process monitor — heartbeat, memory, CPU, leak detection
|
|
84
|
+
this.processMonitor = new ProcessMonitor({ logger: this.logger });
|
|
85
|
+
|
|
86
|
+
// Route prober — tests all routes periodically
|
|
87
|
+
this.routeProber = new RouteProber({
|
|
88
|
+
port,
|
|
89
|
+
logger: this.logger,
|
|
90
|
+
brain: this.brain,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Brain — semantic memory + project context
|
|
94
|
+
this.brain = new Brain(this.cwd);
|
|
95
|
+
|
|
96
|
+
// Skills — discoverable capabilities
|
|
97
|
+
this.skills = new SkillRegistry();
|
|
98
|
+
this.skills.load();
|
|
99
|
+
|
|
100
|
+
// MCP — external tool servers
|
|
101
|
+
this.mcp = new McpRegistry({
|
|
102
|
+
projectRoot: this.cwd,
|
|
103
|
+
redactor: this.redactor,
|
|
104
|
+
logger: this.logger,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Web dashboard
|
|
108
|
+
this.dashboard = new DashboardServer({
|
|
109
|
+
logger: this.logger,
|
|
110
|
+
backupManager: this.backupManager,
|
|
111
|
+
perfMonitor: this.perfMonitor,
|
|
112
|
+
healthMonitor: this.healthMonitor,
|
|
113
|
+
brain: this.brain,
|
|
114
|
+
sandbox: this.sandbox,
|
|
115
|
+
redactor: this.redactor,
|
|
116
|
+
scriptPath: this.scriptPath,
|
|
117
|
+
runner: this,
|
|
118
|
+
tokenTracker: this.tokenTracker,
|
|
119
|
+
skills: this.skills,
|
|
120
|
+
repairHistory: this.repairHistory,
|
|
121
|
+
processMonitor: this.processMonitor,
|
|
122
|
+
routeProber: this.routeProber,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Stability tracking
|
|
126
|
+
this._lastStartTime = null;
|
|
127
|
+
this._lastBackupId = null;
|
|
128
|
+
this._stabilityTimer = null;
|
|
129
|
+
this._stderrBuffer = "";
|
|
130
|
+
this._healInProgress = false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async start() {
|
|
134
|
+
this.running = true;
|
|
135
|
+
this.retryCount = 0;
|
|
136
|
+
|
|
137
|
+
this.logger.info(EVENT_TYPES.PROCESS_START, "Wolverine started", {
|
|
138
|
+
script: this.scriptPath,
|
|
139
|
+
cwd: this.cwd,
|
|
140
|
+
maxRetries: this.maxRetries,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Initialize brain (scan project, seed docs, embed function map)
|
|
144
|
+
try {
|
|
145
|
+
await this.brain.init();
|
|
146
|
+
} catch (err) {
|
|
147
|
+
console.log(chalk.yellow(` ⚠️ Brain init failed (non-fatal): ${err.message}`));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Initialize MCP servers
|
|
151
|
+
try {
|
|
152
|
+
await this.mcp.init();
|
|
153
|
+
} catch (err) {
|
|
154
|
+
console.log(chalk.yellow(` ⚠️ MCP init failed (non-fatal): ${err.message}`));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Log redactor stats
|
|
158
|
+
const redactorStats = this.redactor.getStats();
|
|
159
|
+
console.log(chalk.gray(` 🔐 Secret redactor: ${redactorStats.trackedSecrets} secrets tracked from ${redactorStats.envFiles} env file(s)`));
|
|
160
|
+
|
|
161
|
+
// Log backup stats
|
|
162
|
+
const stats = this.backupManager.getStats();
|
|
163
|
+
if (stats.total > 0) {
|
|
164
|
+
console.log(chalk.gray(` 📁 Backups: ${stats.total} total (${stats.stable} stable, ${stats.verified} verified, ${stats.unstable} unstable)`));
|
|
165
|
+
}
|
|
166
|
+
console.log(chalk.gray(` 💓 Health checks: every ${this.healthMonitor.intervalMs / 1000}s on :${this.healthMonitor.port}${this.healthMonitor.path}`));
|
|
167
|
+
|
|
168
|
+
// Prune old backups
|
|
169
|
+
this.backupManager.prune();
|
|
170
|
+
|
|
171
|
+
// Start dashboard
|
|
172
|
+
this.dashboard.start();
|
|
173
|
+
|
|
174
|
+
// Start performance monitor
|
|
175
|
+
this.perfMonitor.start();
|
|
176
|
+
|
|
177
|
+
// Start platform telemetry (heartbeats to analytics backend)
|
|
178
|
+
startHeartbeat({
|
|
179
|
+
processMonitor: this.processMonitor,
|
|
180
|
+
routeProber: this.routeProber,
|
|
181
|
+
tokenTracker: this.tokenTracker,
|
|
182
|
+
repairHistory: this.repairHistory,
|
|
183
|
+
backupManager: this.backupManager,
|
|
184
|
+
brain: this.brain,
|
|
185
|
+
redactor: this.redactor,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
this._spawn();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
restart() {
|
|
192
|
+
console.log(chalk.blue("\n 🔄 Restarting server..."));
|
|
193
|
+
this.healthMonitor.stop();
|
|
194
|
+
this._clearStabilityTimer();
|
|
195
|
+
|
|
196
|
+
if (this.child) {
|
|
197
|
+
const oldChild = this.child;
|
|
198
|
+
this.child = null;
|
|
199
|
+
|
|
200
|
+
// Wait for old process to actually exit before spawning new one
|
|
201
|
+
const onExit = () => {
|
|
202
|
+
// Give port time to fully release (TIME_WAIT)
|
|
203
|
+
setTimeout(() => {
|
|
204
|
+
this._ensurePortFree();
|
|
205
|
+
setTimeout(() => this._spawn(), 200);
|
|
206
|
+
}, 300);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
oldChild.removeAllListeners("exit");
|
|
210
|
+
oldChild.once("exit", onExit);
|
|
211
|
+
oldChild.kill("SIGTERM");
|
|
212
|
+
|
|
213
|
+
// Force kill if it doesn't exit in 3s
|
|
214
|
+
setTimeout(() => {
|
|
215
|
+
try { oldChild.kill("SIGKILL"); } catch {}
|
|
216
|
+
onExit();
|
|
217
|
+
}, 3000);
|
|
218
|
+
} else {
|
|
219
|
+
this._ensurePortFree();
|
|
220
|
+
setTimeout(() => this._spawn(), 500);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
stop() {
|
|
225
|
+
this.running = false;
|
|
226
|
+
this._clearStabilityTimer();
|
|
227
|
+
this.healthMonitor.stop();
|
|
228
|
+
this.perfMonitor.stop();
|
|
229
|
+
this.processMonitor.stop();
|
|
230
|
+
this.routeProber.stop();
|
|
231
|
+
stopHeartbeat();
|
|
232
|
+
this.mcp.shutdown();
|
|
233
|
+
this.tokenTracker.save();
|
|
234
|
+
this.dashboard.stop();
|
|
235
|
+
|
|
236
|
+
this.logger.info(EVENT_TYPES.PROCESS_STOP, "Wolverine stopped");
|
|
237
|
+
|
|
238
|
+
if (this.child) {
|
|
239
|
+
this.child.kill("SIGTERM");
|
|
240
|
+
this.child = null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
_spawn() {
|
|
245
|
+
if (!this.running) return;
|
|
246
|
+
|
|
247
|
+
this._ensurePortFree();
|
|
248
|
+
|
|
249
|
+
console.log(chalk.blue(`\n🚀 Starting: node ${this.scriptPath}`));
|
|
250
|
+
console.log(chalk.gray(` Attempt ${this.retryCount + 1}/${this.maxRetries + 1}\n`));
|
|
251
|
+
|
|
252
|
+
this._stderrBuffer = "";
|
|
253
|
+
this._lastStartTime = Date.now();
|
|
254
|
+
|
|
255
|
+
this.child = spawn("node", [this.scriptPath], {
|
|
256
|
+
cwd: this.cwd,
|
|
257
|
+
env: { ...process.env },
|
|
258
|
+
stdio: ["inherit", "inherit", "pipe"],
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
this.child.stderr.on("data", (data) => {
|
|
262
|
+
const text = data.toString();
|
|
263
|
+
this._stderrBuffer += text;
|
|
264
|
+
process.stderr.write(text);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
this._startStabilityTimer();
|
|
268
|
+
|
|
269
|
+
// Start process monitor (memory, CPU, heartbeat)
|
|
270
|
+
if (this.child && this.child.pid) {
|
|
271
|
+
this.processMonitor.reset(this.child.pid);
|
|
272
|
+
if (!this.processMonitor._running) {
|
|
273
|
+
this.processMonitor.start(this.child.pid, (reason) => {
|
|
274
|
+
if (this._healInProgress) return;
|
|
275
|
+
console.log(chalk.red(`\n🚨 Process monitor triggered restart: ${reason}`));
|
|
276
|
+
this.logger.error("process.monitor_restart", `Restart: ${reason}`, { reason, pid: this.child?.pid });
|
|
277
|
+
this.restart();
|
|
278
|
+
});
|
|
279
|
+
} else {
|
|
280
|
+
this.processMonitor.reset(this.child.pid);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Start route prober (auto-discovers and tests all routes)
|
|
285
|
+
if (!this.routeProber._running) this.routeProber.start();
|
|
286
|
+
|
|
287
|
+
// Start health monitoring
|
|
288
|
+
this.healthMonitor.stop();
|
|
289
|
+
this.healthMonitor.reset();
|
|
290
|
+
this.healthMonitor.start((reason) => {
|
|
291
|
+
if (this._healInProgress) return;
|
|
292
|
+
console.log(chalk.red(`\n🚨 Health check triggered restart (reason: ${reason})`));
|
|
293
|
+
this.logger.error(EVENT_TYPES.HEALTH_UNRESPONSIVE, `Server unresponsive: ${reason}`, { reason });
|
|
294
|
+
if (this.child) {
|
|
295
|
+
this.child.kill("SIGKILL");
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
this.child.on("exit", async (code, signal) => {
|
|
300
|
+
this._clearStabilityTimer();
|
|
301
|
+
this.healthMonitor.stop();
|
|
302
|
+
|
|
303
|
+
if (!this.running) return;
|
|
304
|
+
|
|
305
|
+
if (code === 0 || signal === "SIGTERM") {
|
|
306
|
+
console.log(chalk.green("\n✅ Process exited cleanly."));
|
|
307
|
+
this.logger.info(EVENT_TYPES.PROCESS_HEALTHY, "Process exited cleanly");
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const uptime = Date.now() - this._lastStartTime;
|
|
312
|
+
console.log(chalk.red(`\n💥 Process crashed with exit code ${code} (uptime: ${Math.round(uptime / 1000)}s)`));
|
|
313
|
+
this.logger.error(EVENT_TYPES.PROCESS_CRASH, `Crashed with code ${code}`, {
|
|
314
|
+
exitCode: code,
|
|
315
|
+
uptime,
|
|
316
|
+
stderr: this._stderrBuffer.slice(0, 1000),
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
if (this.retryCount >= this.maxRetries) {
|
|
320
|
+
console.log(chalk.red(`\n🛑 Max retries (${this.maxRetries}) reached. Giving up.`));
|
|
321
|
+
this._logRollbackHint();
|
|
322
|
+
this.running = false;
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
this.retryCount++;
|
|
327
|
+
await this._healAndRestart();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
this.child.on("error", (err) => {
|
|
331
|
+
console.log(chalk.red(`Failed to start process: ${err.message}`));
|
|
332
|
+
this.logger.error(EVENT_TYPES.PROCESS_CRASH, `Failed to start: ${err.message}`);
|
|
333
|
+
this.running = false;
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async _healAndRestart() {
|
|
338
|
+
if (this._healInProgress) return;
|
|
339
|
+
this._healInProgress = true;
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
const result = await heal({
|
|
343
|
+
stderr: this._stderrBuffer,
|
|
344
|
+
cwd: this.cwd,
|
|
345
|
+
sandbox: this.sandbox,
|
|
346
|
+
redactor: this.redactor,
|
|
347
|
+
notifier: this.notifier,
|
|
348
|
+
rateLimiter: this.rateLimiter,
|
|
349
|
+
backupManager: this.backupManager,
|
|
350
|
+
logger: this.logger,
|
|
351
|
+
brain: this.brain,
|
|
352
|
+
mcp: this.mcp,
|
|
353
|
+
skills: this.skills,
|
|
354
|
+
repairHistory: this.repairHistory,
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
if (result.healed) {
|
|
358
|
+
this._lastBackupId = result.backupId;
|
|
359
|
+
const mode = result.mode === "agent" ? "multi-file agent" : "fast path";
|
|
360
|
+
console.log(chalk.green(`\n🐺 Wolverine healed the error via ${mode}! Restarting...\n`));
|
|
361
|
+
|
|
362
|
+
if (result.agentStats) {
|
|
363
|
+
console.log(chalk.gray(` Agent stats: ${result.agentStats.turns} turns, ${result.agentStats.tokens} tokens, ${result.agentStats.filesModified.length} files modified`));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
this._healInProgress = false;
|
|
367
|
+
this._spawn();
|
|
368
|
+
} else {
|
|
369
|
+
console.log(chalk.red(`\n🐺 Wolverine could not heal: ${result.explanation}`));
|
|
370
|
+
|
|
371
|
+
if (result.waitMs) {
|
|
372
|
+
const waitSec = Math.ceil(result.waitMs / 1000);
|
|
373
|
+
console.log(chalk.yellow(` Waiting ${waitSec}s before next attempt...`));
|
|
374
|
+
setTimeout(() => {
|
|
375
|
+
this._healInProgress = false;
|
|
376
|
+
if (this.running && this.retryCount < this.maxRetries) {
|
|
377
|
+
this._spawn();
|
|
378
|
+
}
|
|
379
|
+
}, result.waitMs);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
this._healInProgress = false;
|
|
384
|
+
if (this.retryCount < this.maxRetries) {
|
|
385
|
+
console.log(chalk.yellow(" Retrying...\n"));
|
|
386
|
+
this._spawn();
|
|
387
|
+
} else {
|
|
388
|
+
console.log(chalk.red(" Max retries reached."));
|
|
389
|
+
this._logRollbackHint();
|
|
390
|
+
this.running = false;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
} catch (err) {
|
|
394
|
+
console.log(chalk.red(`\n🐺 Wolverine encountered an error: ${err.message}`));
|
|
395
|
+
this._healInProgress = false;
|
|
396
|
+
this.running = false;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
_startStabilityTimer() {
|
|
401
|
+
this._clearStabilityTimer();
|
|
402
|
+
this._stabilityTimer = setTimeout(() => {
|
|
403
|
+
if (this._lastBackupId && this.running) {
|
|
404
|
+
this.backupManager.markStable(this._lastBackupId);
|
|
405
|
+
this.retryCount = 0;
|
|
406
|
+
const healthStats = this.healthMonitor.getStats();
|
|
407
|
+
if (healthStats.totalChecks > 0) {
|
|
408
|
+
console.log(chalk.green(` 📊 Uptime: ${healthStats.uptimePercent}% (${healthStats.totalPasses}/${healthStats.totalChecks} checks passed)`));
|
|
409
|
+
}
|
|
410
|
+
this.logger.info(EVENT_TYPES.BACKUP_STABLE, `Backup ${this._lastBackupId} promoted to stable`, { backupId: this._lastBackupId });
|
|
411
|
+
}
|
|
412
|
+
}, STABILITY_THRESHOLD_MS);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
_clearStabilityTimer() {
|
|
416
|
+
if (this._stabilityTimer) {
|
|
417
|
+
clearTimeout(this._stabilityTimer);
|
|
418
|
+
this._stabilityTimer = null;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
_ensurePortFree() {
|
|
423
|
+
const port = parseInt(process.env.PORT, 10) || 3000;
|
|
424
|
+
try {
|
|
425
|
+
if (process.platform === "win32") {
|
|
426
|
+
const output = execSync(`netstat -ano | findstr ":${port}" | findstr "LISTENING"`, { encoding: "utf-8", timeout: 3000 }).trim();
|
|
427
|
+
if (output) {
|
|
428
|
+
const lines = output.split("\n");
|
|
429
|
+
const pids = new Set();
|
|
430
|
+
for (const line of lines) {
|
|
431
|
+
const parts = line.trim().split(/\s+/);
|
|
432
|
+
const pid = parseInt(parts[parts.length - 1], 10);
|
|
433
|
+
if (pid && pid !== process.pid) pids.add(pid);
|
|
434
|
+
}
|
|
435
|
+
for (const pid of pids) {
|
|
436
|
+
try {
|
|
437
|
+
execSync(`taskkill /PID ${pid} /F`, { timeout: 3000 });
|
|
438
|
+
console.log(chalk.gray(` 🔌 Killed stale process on port ${port} (PID ${pid})`));
|
|
439
|
+
} catch { /* already dead */ }
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
} else {
|
|
443
|
+
const output = execSync(`lsof -ti:${port}`, { encoding: "utf-8", timeout: 3000 }).trim();
|
|
444
|
+
if (output) {
|
|
445
|
+
const pids = output.split("\n").map(p => parseInt(p, 10)).filter(p => p && p !== process.pid);
|
|
446
|
+
for (const pid of pids) {
|
|
447
|
+
try { process.kill(pid, "SIGKILL"); console.log(chalk.gray(` 🔌 Killed stale process on port ${port} (PID ${pid})`)); }
|
|
448
|
+
catch { /* already dead */ }
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
} catch { /* no process on port */ }
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
_logRollbackHint() {
|
|
456
|
+
const stats = this.backupManager.getStats();
|
|
457
|
+
if (stats.total > 0) {
|
|
458
|
+
console.log(chalk.yellow(` Backups available: ${stats.total}. Rollback with:`));
|
|
459
|
+
console.log(chalk.gray(` node -e "const {BackupManager}=require('./src/backup/backup-manager');new BackupManager('.').rollbackLatest()"`));
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
module.exports = { WolverineRunner };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
const os = require("os");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const { execSync } = require("child_process");
|
|
5
|
+
const chalk = require("chalk");
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* System Info — detects machine capabilities for auto-scaling decisions.
|
|
9
|
+
*
|
|
10
|
+
* Detects: CPU cores, total/free memory, disk space, platform,
|
|
11
|
+
* container environment (Docker/K8s), cloud provider hints.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
function detect() {
|
|
15
|
+
const cpus = os.cpus();
|
|
16
|
+
const totalMemGB = Math.round(os.totalmem() / 1024 / 1024 / 1024 * 10) / 10;
|
|
17
|
+
const freeMemGB = Math.round(os.freemem() / 1024 / 1024 / 1024 * 10) / 10;
|
|
18
|
+
const disk = getDiskInfo();
|
|
19
|
+
|
|
20
|
+
const info = {
|
|
21
|
+
platform: os.platform(),
|
|
22
|
+
arch: os.arch(),
|
|
23
|
+
hostname: os.hostname(),
|
|
24
|
+
nodeVersion: process.version,
|
|
25
|
+
|
|
26
|
+
cpu: {
|
|
27
|
+
cores: cpus.length,
|
|
28
|
+
model: cpus[0]?.model?.trim() || "unknown",
|
|
29
|
+
speed: cpus[0]?.speed || 0,
|
|
30
|
+
// Usable workers = cores - 1 (reserve 1 for master)
|
|
31
|
+
workers: Math.max(1, cpus.length - 1),
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
memory: {
|
|
35
|
+
totalGB: totalMemGB,
|
|
36
|
+
freeGB: freeMemGB,
|
|
37
|
+
usedPercent: Math.round((1 - os.freemem() / os.totalmem()) * 100),
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
disk: {
|
|
41
|
+
totalGB: disk.totalGB,
|
|
42
|
+
freeGB: disk.freeGB,
|
|
43
|
+
usedPercent: disk.usedPercent,
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
environment: detectEnvironment(),
|
|
47
|
+
|
|
48
|
+
// Auto-scaling recommendation
|
|
49
|
+
recommended: {
|
|
50
|
+
workers: recommendWorkers(cpus.length, totalMemGB),
|
|
51
|
+
maxMemoryPerWorkerMB: Math.floor((totalMemGB * 1024 * 0.7) / Math.max(1, cpus.length - 1)),
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return info;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getDiskInfo() {
|
|
59
|
+
try {
|
|
60
|
+
if (os.platform() === "win32") {
|
|
61
|
+
const drive = process.cwd().slice(0, 2);
|
|
62
|
+
const output = execSync(`wmic logicaldisk where "DeviceID='${drive}'" get FreeSpace,Size /VALUE`, { encoding: "utf-8", timeout: 3000 });
|
|
63
|
+
const free = parseInt((output.match(/FreeSpace=(\d+)/) || [])[1] || "0", 10);
|
|
64
|
+
const total = parseInt((output.match(/Size=(\d+)/) || [])[1] || "0", 10);
|
|
65
|
+
return {
|
|
66
|
+
totalGB: Math.round(total / 1024 / 1024 / 1024 * 10) / 10,
|
|
67
|
+
freeGB: Math.round(free / 1024 / 1024 / 1024 * 10) / 10,
|
|
68
|
+
usedPercent: total > 0 ? Math.round((1 - free / total) * 100) : 0,
|
|
69
|
+
};
|
|
70
|
+
} else {
|
|
71
|
+
const output = execSync("df -BG / | tail -1", { encoding: "utf-8", timeout: 3000 });
|
|
72
|
+
const parts = output.trim().split(/\s+/);
|
|
73
|
+
return {
|
|
74
|
+
totalGB: parseInt(parts[1], 10) || 0,
|
|
75
|
+
freeGB: parseInt(parts[3], 10) || 0,
|
|
76
|
+
usedPercent: parseInt(parts[4], 10) || 0,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
return { totalGB: 0, freeGB: 0, usedPercent: 0 };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function detectEnvironment() {
|
|
85
|
+
const env = {
|
|
86
|
+
type: "bare-metal",
|
|
87
|
+
docker: false,
|
|
88
|
+
kubernetes: false,
|
|
89
|
+
cloud: null,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Docker detection
|
|
93
|
+
if (fs.existsSync("/.dockerenv") || (process.env.DOCKER_CONTAINER === "true")) {
|
|
94
|
+
env.type = "docker";
|
|
95
|
+
env.docker = true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Kubernetes detection
|
|
99
|
+
if (process.env.KUBERNETES_SERVICE_HOST || fs.existsSync("/var/run/secrets/kubernetes.io")) {
|
|
100
|
+
env.type = "kubernetes";
|
|
101
|
+
env.kubernetes = true;
|
|
102
|
+
env.docker = true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Cloud hints
|
|
106
|
+
if (process.env.AWS_REGION || process.env.AWS_EXECUTION_ENV) env.cloud = "aws";
|
|
107
|
+
else if (process.env.GOOGLE_CLOUD_PROJECT || process.env.GCP_PROJECT) env.cloud = "gcp";
|
|
108
|
+
else if (process.env.AZURE_FUNCTIONS_ENVIRONMENT || process.env.WEBSITE_SITE_NAME) env.cloud = "azure";
|
|
109
|
+
else if (process.env.RAILWAY_ENVIRONMENT) env.cloud = "railway";
|
|
110
|
+
else if (process.env.FLY_APP_NAME) env.cloud = "fly";
|
|
111
|
+
else if (process.env.RENDER) env.cloud = "render";
|
|
112
|
+
else if (process.env.VERCEL) env.cloud = "vercel";
|
|
113
|
+
else if (process.env.HEROKU_APP_NAME || process.env.DYNO) env.cloud = "heroku";
|
|
114
|
+
|
|
115
|
+
return env;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function recommendWorkers(cores, memGB) {
|
|
119
|
+
// 1 core or <1GB: can't cluster
|
|
120
|
+
if (cores <= 1 || memGB < 1) return 1;
|
|
121
|
+
// 2 cores: 2 workers (master is lightweight, both cores serve requests)
|
|
122
|
+
if (cores === 2) return 2;
|
|
123
|
+
// 3-4 cores: cores - 1
|
|
124
|
+
if (cores <= 4) return cores - 1;
|
|
125
|
+
// 5-8 cores: cores - 1 but cap at 6
|
|
126
|
+
if (cores <= 8) return Math.min(cores - 1, 6);
|
|
127
|
+
// 9+ cores: half the cores (diminishing returns per worker)
|
|
128
|
+
return Math.min(Math.floor(cores / 2), 16);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Log system info on startup.
|
|
133
|
+
*/
|
|
134
|
+
function logSystemInfo(info) {
|
|
135
|
+
console.log(chalk.gray(` 💻 System: ${info.platform}/${info.arch} — ${info.cpu.cores} cores, ${info.memory.totalGB}GB RAM, ${info.disk.freeGB}GB free disk`));
|
|
136
|
+
console.log(chalk.gray(` 💻 CPU: ${info.cpu.model.slice(0, 50)}`));
|
|
137
|
+
console.log(chalk.gray(` 💻 Env: ${info.environment.type}${info.environment.cloud ? ` (${info.environment.cloud})` : ""}`));
|
|
138
|
+
console.log(chalk.gray(` 💻 Recommended: ${info.recommended.workers} workers, ${info.recommended.maxMemoryPerWorkerMB}MB/worker`));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = { detect, logSystemInfo, recommendWorkers };
|