@virtengine/openfleet 0.25.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/.env.example +914 -0
- package/LICENSE +190 -0
- package/README.md +500 -0
- package/agent-endpoint.mjs +918 -0
- package/agent-hook-bridge.mjs +230 -0
- package/agent-hooks.mjs +1188 -0
- package/agent-pool.mjs +2403 -0
- package/agent-prompts.mjs +689 -0
- package/agent-sdk.mjs +141 -0
- package/anomaly-detector.mjs +1195 -0
- package/autofix.mjs +1294 -0
- package/claude-shell.mjs +708 -0
- package/cli.mjs +906 -0
- package/codex-config.mjs +1274 -0
- package/codex-model-profiles.mjs +135 -0
- package/codex-shell.mjs +762 -0
- package/config-doctor.mjs +613 -0
- package/config.mjs +1720 -0
- package/conflict-resolver.mjs +248 -0
- package/container-runner.mjs +450 -0
- package/copilot-shell.mjs +827 -0
- package/daemon-restart-policy.mjs +56 -0
- package/diff-stats.mjs +282 -0
- package/error-detector.mjs +829 -0
- package/fetch-runtime.mjs +34 -0
- package/fleet-coordinator.mjs +838 -0
- package/get-telegram-chat-id.mjs +71 -0
- package/git-safety.mjs +170 -0
- package/github-reconciler.mjs +403 -0
- package/hook-profiles.mjs +651 -0
- package/kanban-adapter.mjs +4491 -0
- package/lib/logger.mjs +645 -0
- package/maintenance.mjs +828 -0
- package/merge-strategy.mjs +1171 -0
- package/monitor.mjs +12207 -0
- package/openfleet.config.example.json +115 -0
- package/openfleet.schema.json +465 -0
- package/package.json +203 -0
- package/postinstall.mjs +187 -0
- package/pr-cleanup-daemon.mjs +978 -0
- package/preflight.mjs +408 -0
- package/prepublish-check.mjs +90 -0
- package/presence.mjs +328 -0
- package/primary-agent.mjs +282 -0
- package/publish.mjs +151 -0
- package/repo-root.mjs +29 -0
- package/restart-controller.mjs +100 -0
- package/review-agent.mjs +557 -0
- package/rotate-agent-logs.sh +133 -0
- package/sdk-conflict-resolver.mjs +973 -0
- package/session-tracker.mjs +880 -0
- package/setup.mjs +3937 -0
- package/shared-knowledge.mjs +410 -0
- package/shared-state-manager.mjs +841 -0
- package/shared-workspace-cli.mjs +199 -0
- package/shared-workspace-registry.mjs +537 -0
- package/shared-workspaces.json +18 -0
- package/startup-service.mjs +1070 -0
- package/sync-engine.mjs +1063 -0
- package/task-archiver.mjs +801 -0
- package/task-assessment.mjs +550 -0
- package/task-claims.mjs +924 -0
- package/task-complexity.mjs +581 -0
- package/task-executor.mjs +5111 -0
- package/task-store.mjs +753 -0
- package/telegram-bot.mjs +9281 -0
- package/telegram-sentinel.mjs +2010 -0
- package/ui/app.js +867 -0
- package/ui/app.legacy.js +1464 -0
- package/ui/app.monolith.js +2488 -0
- package/ui/components/charts.js +226 -0
- package/ui/components/chat-view.js +567 -0
- package/ui/components/command-palette.js +587 -0
- package/ui/components/diff-viewer.js +190 -0
- package/ui/components/forms.js +327 -0
- package/ui/components/kanban-board.js +451 -0
- package/ui/components/session-list.js +305 -0
- package/ui/components/shared.js +473 -0
- package/ui/index.html +70 -0
- package/ui/modules/api.js +297 -0
- package/ui/modules/icons.js +461 -0
- package/ui/modules/router.js +81 -0
- package/ui/modules/settings-schema.js +261 -0
- package/ui/modules/state.js +679 -0
- package/ui/modules/telegram.js +331 -0
- package/ui/modules/utils.js +270 -0
- package/ui/styles/animations.css +140 -0
- package/ui/styles/base.css +98 -0
- package/ui/styles/components.css +1915 -0
- package/ui/styles/kanban.css +286 -0
- package/ui/styles/layout.css +809 -0
- package/ui/styles/sessions.css +827 -0
- package/ui/styles/variables.css +188 -0
- package/ui/styles.css +141 -0
- package/ui/styles.monolith.css +1046 -0
- package/ui/tabs/agents.js +1417 -0
- package/ui/tabs/chat.js +74 -0
- package/ui/tabs/control.js +887 -0
- package/ui/tabs/dashboard.js +515 -0
- package/ui/tabs/infra.js +537 -0
- package/ui/tabs/logs.js +783 -0
- package/ui/tabs/settings.js +1487 -0
- package/ui/tabs/tasks.js +1385 -0
- package/ui-server.mjs +4073 -0
- package/update-check.mjs +465 -0
- package/utils.mjs +172 -0
- package/ve-kanban.mjs +654 -0
- package/ve-kanban.ps1 +1365 -0
- package/ve-kanban.sh +18 -0
- package/ve-orchestrator.mjs +340 -0
- package/ve-orchestrator.ps1 +6546 -0
- package/ve-orchestrator.sh +18 -0
- package/vibe-kanban-wrapper.mjs +41 -0
- package/vk-error-resolver.mjs +470 -0
- package/vk-log-stream.mjs +914 -0
- package/whatsapp-channel.mjs +520 -0
- package/workspace-monitor.mjs +581 -0
- package/workspace-reaper.mjs +405 -0
- package/workspace-registry.mjs +238 -0
- package/worktree-manager.mjs +1266 -0
package/update-check.mjs
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* update-check.mjs — Self-updating system for openfleet.
|
|
3
|
+
*
|
|
4
|
+
* Capabilities:
|
|
5
|
+
* - `checkForUpdate(currentVersion)` — non-blocking startup check, prints notice
|
|
6
|
+
* - `forceUpdate(currentVersion)` — interactive `npm install -g` with confirmation
|
|
7
|
+
* - `startAutoUpdateLoop(opts)` — background polling loop (default 10 min) that
|
|
8
|
+
* auto-installs updates and restarts the process. Zero user interaction.
|
|
9
|
+
*
|
|
10
|
+
* Respects:
|
|
11
|
+
* - CODEX_MONITOR_SKIP_UPDATE_CHECK=1 — disable startup check
|
|
12
|
+
* - CODEX_MONITOR_SKIP_AUTO_UPDATE=1 — disable polling auto-update
|
|
13
|
+
* - CODEX_MONITOR_UPDATE_INTERVAL_MS — override poll interval (default 10 min)
|
|
14
|
+
* - Caches the last check timestamp so we don't query npm too aggressively
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { execFileSync } from "node:child_process";
|
|
18
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
19
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
20
|
+
import { resolve, dirname, join } from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
import { createInterface } from "node:readline";
|
|
23
|
+
|
|
24
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const PKG_NAME = "@virtengine/openfleet";
|
|
26
|
+
const CACHE_FILE = resolve(__dirname, "logs", ".update-check-cache.json");
|
|
27
|
+
const STARTUP_CHECK_INTERVAL_MS = 60 * 60 * 1000; // 1 hour (startup notice)
|
|
28
|
+
const AUTO_UPDATE_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes (polling loop)
|
|
29
|
+
|
|
30
|
+
function runNpmCommand(args, options = {}) {
|
|
31
|
+
const npmExecPath = process.env.npm_execpath;
|
|
32
|
+
if (npmExecPath && existsSync(npmExecPath)) {
|
|
33
|
+
return execFileSync(process.execPath, [npmExecPath, ...args], options);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const nodeBinDir = dirname(process.execPath);
|
|
37
|
+
const candidates =
|
|
38
|
+
process.platform === "win32"
|
|
39
|
+
? [
|
|
40
|
+
join(nodeBinDir, "npm.cmd"),
|
|
41
|
+
join(nodeBinDir, "npm.exe"),
|
|
42
|
+
join(nodeBinDir, "npm"),
|
|
43
|
+
]
|
|
44
|
+
: [join(nodeBinDir, "npm")];
|
|
45
|
+
|
|
46
|
+
for (const candidate of candidates) {
|
|
47
|
+
if (existsSync(candidate)) {
|
|
48
|
+
return execFileSync(candidate, args, options);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const fallback = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
53
|
+
return execFileSync(fallback, args, options);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── Semver comparison ────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
function parseVersion(v) {
|
|
59
|
+
const parts = v.replace(/^v/, "").split(".").map(Number);
|
|
60
|
+
return { major: parts[0] || 0, minor: parts[1] || 0, patch: parts[2] || 0 };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isNewer(remote, local) {
|
|
64
|
+
const r = parseVersion(remote);
|
|
65
|
+
const l = parseVersion(local);
|
|
66
|
+
if (r.major !== l.major) return r.major > l.major;
|
|
67
|
+
if (r.minor !== l.minor) return r.minor > l.minor;
|
|
68
|
+
return r.patch > l.patch;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Cache ────────────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
async function readCache() {
|
|
74
|
+
try {
|
|
75
|
+
const raw = await readFile(CACHE_FILE, "utf8");
|
|
76
|
+
return JSON.parse(raw);
|
|
77
|
+
} catch {
|
|
78
|
+
return {};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function writeCache(data) {
|
|
83
|
+
try {
|
|
84
|
+
await mkdir(dirname(CACHE_FILE), { recursive: true });
|
|
85
|
+
await writeFile(CACHE_FILE, JSON.stringify(data, null, 2));
|
|
86
|
+
} catch {
|
|
87
|
+
// non-critical
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Registry query ───────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
async function fetchLatestVersion() {
|
|
94
|
+
// Try native fetch (Node 18+), fall back to npm view
|
|
95
|
+
try {
|
|
96
|
+
const res = await fetch(`https://registry.npmjs.org/${PKG_NAME}/latest`, {
|
|
97
|
+
headers: { Accept: "application/json" },
|
|
98
|
+
signal: AbortSignal.timeout(10000),
|
|
99
|
+
});
|
|
100
|
+
if (res.ok) {
|
|
101
|
+
const data = await res.json();
|
|
102
|
+
return data.version || null;
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
// fetch failed, try npm view
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const out = runNpmCommand(["view", PKG_NAME, "version"], {
|
|
110
|
+
encoding: "utf8",
|
|
111
|
+
timeout: 15000,
|
|
112
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
113
|
+
}).trim();
|
|
114
|
+
return out || null;
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── Public API ───────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Non-blocking update check. Prints a notice if an update is available.
|
|
124
|
+
* Called on startup — must never throw or delay the main process.
|
|
125
|
+
*/
|
|
126
|
+
export async function checkForUpdate(currentVersion) {
|
|
127
|
+
if (process.env.CODEX_MONITOR_SKIP_UPDATE_CHECK) return;
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
// Rate-limit: at most once per hour
|
|
131
|
+
const cache = await readCache();
|
|
132
|
+
const now = Date.now();
|
|
133
|
+
if (cache.lastCheck && now - cache.lastCheck < STARTUP_CHECK_INTERVAL_MS) {
|
|
134
|
+
// Use cached result if still fresh
|
|
135
|
+
if (cache.latestVersion && isNewer(cache.latestVersion, currentVersion)) {
|
|
136
|
+
printUpdateNotice(currentVersion, cache.latestVersion);
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const latest = await fetchLatestVersion();
|
|
142
|
+
await writeCache({ lastCheck: now, latestVersion: latest });
|
|
143
|
+
|
|
144
|
+
if (latest && isNewer(latest, currentVersion)) {
|
|
145
|
+
printUpdateNotice(currentVersion, latest);
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
// Silent — never interfere with startup
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Force-update to the latest version.
|
|
154
|
+
* Prompts for confirmation, then runs npm install -g.
|
|
155
|
+
*/
|
|
156
|
+
export async function forceUpdate(currentVersion) {
|
|
157
|
+
console.log(`\n Current version: v${currentVersion}`);
|
|
158
|
+
console.log(" Checking npm registry...\n");
|
|
159
|
+
|
|
160
|
+
const latest = await fetchLatestVersion();
|
|
161
|
+
|
|
162
|
+
if (!latest) {
|
|
163
|
+
console.log(" ❌ Could not reach npm registry. Check your connection.\n");
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!isNewer(latest, currentVersion)) {
|
|
168
|
+
console.log(` ✅ Already up to date (v${currentVersion})\n`);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
console.log(` 📦 Update available: v${currentVersion} → v${latest}\n`);
|
|
173
|
+
|
|
174
|
+
const confirmed = await promptConfirm(" Install update now? [Y/n]: ");
|
|
175
|
+
|
|
176
|
+
if (!confirmed) {
|
|
177
|
+
console.log(" Skipped.\n");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
console.log(`\n Installing ${PKG_NAME}@${latest}...\n`);
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
runNpmCommand(["install", "-g", `${PKG_NAME}@${latest}`], {
|
|
185
|
+
stdio: "inherit",
|
|
186
|
+
timeout: 120000,
|
|
187
|
+
});
|
|
188
|
+
console.log(
|
|
189
|
+
`\n ✅ Updated to v${latest}. Restart openfleet to use the new version.\n`,
|
|
190
|
+
);
|
|
191
|
+
} catch (err) {
|
|
192
|
+
console.error(`\n ❌ Update failed: ${err.message}`);
|
|
193
|
+
console.error(` Try manually: npm install -g ${PKG_NAME}@latest\n`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Read the current version from package.json (on-disk, not cached import).
|
|
201
|
+
* After an auto-update, the on-disk package.json reflects the new version.
|
|
202
|
+
*/
|
|
203
|
+
export function getCurrentVersion() {
|
|
204
|
+
try {
|
|
205
|
+
const pkg = JSON.parse(
|
|
206
|
+
readFileSync(resolve(__dirname, "package.json"), "utf8"),
|
|
207
|
+
);
|
|
208
|
+
return pkg.version || "0.0.0";
|
|
209
|
+
} catch {
|
|
210
|
+
return "0.0.0";
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── Auto-update polling loop ─────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
let autoUpdateTimer = null;
|
|
217
|
+
let autoUpdateRunning = false;
|
|
218
|
+
let parentPid = null;
|
|
219
|
+
let parentCheckInterval = null;
|
|
220
|
+
let cleanupHandlersRegistered = false;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Start a background polling loop that checks for updates every `intervalMs`
|
|
224
|
+
* (default 10 min). When a newer version is found, it:
|
|
225
|
+
* 1. Runs `npm install -g @virtengine/openfleet@<version>`
|
|
226
|
+
* 2. Calls `onRestart()` (or `process.exit(0)` if not provided)
|
|
227
|
+
*
|
|
228
|
+
* This is fully autonomous — no user interaction required.
|
|
229
|
+
*
|
|
230
|
+
* Safety features to prevent zombie processes:
|
|
231
|
+
* - Monitors parent process health (terminates if parent dies)
|
|
232
|
+
* - Registers cleanup handlers for SIGTERM, SIGINT, SIGHUP
|
|
233
|
+
* - Cleans up intervals on process exit or uncaught exceptions
|
|
234
|
+
* - Periodic parent health check every 30 seconds
|
|
235
|
+
*
|
|
236
|
+
* @param {object} opts
|
|
237
|
+
* @param {function} [opts.onRestart] - Called after successful update (should restart process)
|
|
238
|
+
* @param {function} [opts.onNotify] - Called with message string for Telegram/log
|
|
239
|
+
* @param {number} [opts.intervalMs] - Poll interval (default: 10 min)
|
|
240
|
+
* @param {number} [opts.parentPid] - Parent process PID to monitor (default: process.ppid)
|
|
241
|
+
*/
|
|
242
|
+
export function startAutoUpdateLoop(opts = {}) {
|
|
243
|
+
if (process.env.CODEX_MONITOR_SKIP_AUTO_UPDATE === "1") {
|
|
244
|
+
console.log("[auto-update] Disabled via CODEX_MONITOR_SKIP_AUTO_UPDATE=1");
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const intervalMs =
|
|
249
|
+
Number(process.env.CODEX_MONITOR_UPDATE_INTERVAL_MS) ||
|
|
250
|
+
opts.intervalMs ||
|
|
251
|
+
AUTO_UPDATE_INTERVAL_MS;
|
|
252
|
+
const onRestart = opts.onRestart || (() => process.exit(0));
|
|
253
|
+
const onNotify = opts.onNotify || ((msg) => console.log(msg));
|
|
254
|
+
|
|
255
|
+
// Register cleanup handlers to prevent zombie processes
|
|
256
|
+
registerCleanupHandlers();
|
|
257
|
+
|
|
258
|
+
// Track parent process if provided
|
|
259
|
+
if (opts.parentPid) {
|
|
260
|
+
parentPid = opts.parentPid;
|
|
261
|
+
console.log(`[auto-update] Monitoring parent process PID ${parentPid}`);
|
|
262
|
+
} else {
|
|
263
|
+
parentPid = process.ppid; // Track parent by default
|
|
264
|
+
console.log(`[auto-update] Monitoring parent process PID ${parentPid}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
console.log(
|
|
268
|
+
`[auto-update] Polling every ${Math.round(intervalMs / 1000 / 60)} min for upstream changes`,
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
async function poll() {
|
|
272
|
+
// Safety check: Is parent process still alive?
|
|
273
|
+
if (!isParentAlive()) {
|
|
274
|
+
console.log(
|
|
275
|
+
`[auto-update] Parent process ${parentPid} no longer exists. Terminating.`,
|
|
276
|
+
);
|
|
277
|
+
stopAutoUpdateLoop();
|
|
278
|
+
process.exit(0);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (autoUpdateRunning) return;
|
|
282
|
+
autoUpdateRunning = true;
|
|
283
|
+
try {
|
|
284
|
+
const currentVersion = getCurrentVersion();
|
|
285
|
+
const latest = await fetchLatestVersion();
|
|
286
|
+
|
|
287
|
+
if (!latest) {
|
|
288
|
+
autoUpdateRunning = false;
|
|
289
|
+
return; // registry unreachable — try again next cycle
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!isNewer(latest, currentVersion)) {
|
|
293
|
+
autoUpdateRunning = false;
|
|
294
|
+
return; // already up to date
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ── Update detected! ──────────────────────────────────────────────
|
|
298
|
+
const msg = `[auto-update] 🔄 Update detected: v${currentVersion} → v${latest}. Installing...`;
|
|
299
|
+
console.log(msg);
|
|
300
|
+
onNotify(msg);
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
runNpmCommand(["install", "-g", `${PKG_NAME}@${latest}`], {
|
|
304
|
+
timeout: 180000,
|
|
305
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
306
|
+
});
|
|
307
|
+
} catch (installErr) {
|
|
308
|
+
const errMsg = `[auto-update] ❌ Install failed: ${installErr.message || installErr}`;
|
|
309
|
+
console.error(errMsg);
|
|
310
|
+
onNotify(errMsg);
|
|
311
|
+
autoUpdateRunning = false;
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Verify the install actually changed the on-disk version
|
|
316
|
+
const newVersion = getCurrentVersion();
|
|
317
|
+
if (!isNewer(newVersion, currentVersion) && newVersion !== latest) {
|
|
318
|
+
const errMsg = `[auto-update] ⚠️ Install ran but version unchanged (${newVersion}). Skipping restart.`;
|
|
319
|
+
console.warn(errMsg);
|
|
320
|
+
onNotify(errMsg);
|
|
321
|
+
autoUpdateRunning = false;
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
await writeCache({ lastCheck: Date.now(), latestVersion: latest });
|
|
326
|
+
|
|
327
|
+
const successMsg = `[auto-update] ✅ Updated to v${latest}. Restarting...`;
|
|
328
|
+
console.log(successMsg);
|
|
329
|
+
onNotify(successMsg);
|
|
330
|
+
|
|
331
|
+
// Give Telegram a moment to deliver the notification
|
|
332
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
333
|
+
|
|
334
|
+
onRestart(`auto-update v${currentVersion} → v${latest}`);
|
|
335
|
+
} catch (err) {
|
|
336
|
+
console.warn(`[auto-update] Poll error: ${err.message || err}`);
|
|
337
|
+
} finally {
|
|
338
|
+
autoUpdateRunning = false;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Set up parent health check (every 30s)
|
|
343
|
+
if (parentPid) {
|
|
344
|
+
parentCheckInterval = setInterval(() => {
|
|
345
|
+
if (!isParentAlive()) {
|
|
346
|
+
console.log(`[auto-update] Parent process ${parentPid} died. Exiting.`);
|
|
347
|
+
stopAutoUpdateLoop();
|
|
348
|
+
process.exit(0);
|
|
349
|
+
}
|
|
350
|
+
}, 30 * 1000);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// First poll after 60s (let startup settle), then every intervalMs
|
|
354
|
+
setTimeout(() => {
|
|
355
|
+
void poll();
|
|
356
|
+
autoUpdateTimer = setInterval(() => void poll(), intervalMs);
|
|
357
|
+
}, 60 * 1000);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Stop the auto-update polling loop (for clean shutdown).
|
|
362
|
+
*/
|
|
363
|
+
export function stopAutoUpdateLoop() {
|
|
364
|
+
if (autoUpdateTimer) {
|
|
365
|
+
clearInterval(autoUpdateTimer);
|
|
366
|
+
autoUpdateTimer = null;
|
|
367
|
+
}
|
|
368
|
+
if (parentCheckInterval) {
|
|
369
|
+
clearInterval(parentCheckInterval);
|
|
370
|
+
parentCheckInterval = null;
|
|
371
|
+
}
|
|
372
|
+
parentPid = null;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Check if parent process is still alive.
|
|
377
|
+
* If parent dies, this child polling loop should terminate too.
|
|
378
|
+
*/
|
|
379
|
+
function isParentAlive() {
|
|
380
|
+
if (!parentPid) return true; // No parent tracking configured
|
|
381
|
+
try {
|
|
382
|
+
// On Windows and Unix, kill(pid, 0) checks if process exists without sending signal
|
|
383
|
+
process.kill(parentPid, 0);
|
|
384
|
+
return true;
|
|
385
|
+
} catch (err) {
|
|
386
|
+
// ESRCH = no such process
|
|
387
|
+
if (err.code === "ESRCH") {
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
// Other errors (EPERM) mean process exists but we can't signal it
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Register cleanup handlers to prevent zombie processes.
|
|
397
|
+
*/
|
|
398
|
+
function registerCleanupHandlers() {
|
|
399
|
+
if (cleanupHandlersRegistered) return;
|
|
400
|
+
cleanupHandlersRegistered = true;
|
|
401
|
+
|
|
402
|
+
const cleanup = (signal) => {
|
|
403
|
+
console.log(`[auto-update] Received ${signal}, cleaning up...`);
|
|
404
|
+
stopAutoUpdateLoop();
|
|
405
|
+
// Don't call process.exit() - let the signal handler chain continue
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// Handle graceful shutdown signals
|
|
409
|
+
process.on("SIGTERM", () => cleanup("SIGTERM"));
|
|
410
|
+
process.on("SIGINT", () => cleanup("SIGINT"));
|
|
411
|
+
process.on("SIGHUP", () => cleanup("SIGHUP"));
|
|
412
|
+
|
|
413
|
+
// Handle process exit
|
|
414
|
+
process.on("exit", () => {
|
|
415
|
+
stopAutoUpdateLoop();
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// Handle uncaught exceptions (last resort)
|
|
419
|
+
const originalUncaughtException = process.listeners("uncaughtException");
|
|
420
|
+
process.on("uncaughtException", (err) => {
|
|
421
|
+
console.error(`[auto-update] Uncaught exception, cleaning up:`, err);
|
|
422
|
+
stopAutoUpdateLoop();
|
|
423
|
+
// Re-emit for other handlers
|
|
424
|
+
if (originalUncaughtException.length > 0) {
|
|
425
|
+
for (const handler of originalUncaughtException) {
|
|
426
|
+
handler(err);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function printUpdateNotice(current, latest) {
|
|
433
|
+
console.log("");
|
|
434
|
+
console.log(" ╭──────────────────────────────────────────────────────────╮");
|
|
435
|
+
console.log(
|
|
436
|
+
` │ Update available: v${current} → v${latest}${" ".repeat(Math.max(0, 38 - current.length - latest.length))}│`,
|
|
437
|
+
);
|
|
438
|
+
console.log(" │ │");
|
|
439
|
+
console.log(` │ Run: npm install -g ${PKG_NAME}@latest │`);
|
|
440
|
+
console.log(" │ Or: openfleet --update │");
|
|
441
|
+
console.log(" ╰──────────────────────────────────────────────────────────╯");
|
|
442
|
+
console.log("");
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function promptConfirm(question) {
|
|
446
|
+
return new Promise((res) => {
|
|
447
|
+
const rl = createInterface({
|
|
448
|
+
input: process.stdin,
|
|
449
|
+
output: process.stdout,
|
|
450
|
+
terminal: process.stdin.isTTY && process.stdout.isTTY,
|
|
451
|
+
});
|
|
452
|
+
rl.question(question, (answer) => {
|
|
453
|
+
try {
|
|
454
|
+
rl.close();
|
|
455
|
+
} catch (err) {
|
|
456
|
+
const msg = err?.message || String(err || "");
|
|
457
|
+
if (!msg.includes("setRawMode EIO")) {
|
|
458
|
+
throw err;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
const a = answer.trim().toLowerCase();
|
|
462
|
+
res(!a || a === "y" || a === "yes");
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
}
|
package/utils.mjs
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure utility functions for openfleet
|
|
3
|
+
* Extracted for unit testing (no I/O, no side effects)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Normalize text for deduplication by stripping timestamps and IDs
|
|
8
|
+
* @param {string} text - Input text to normalize
|
|
9
|
+
* @returns {string} Normalized text with numbers replaced by N
|
|
10
|
+
*/
|
|
11
|
+
export function normalizeDedupKey(text) {
|
|
12
|
+
return (
|
|
13
|
+
String(text || "")
|
|
14
|
+
.trim()
|
|
15
|
+
// Replace numbers (integers and decimals) with N, preserving surrounding text
|
|
16
|
+
.replaceAll(/\d+(\.\d+)?/g, "N")
|
|
17
|
+
// Collapse any resulting multi-N sequences (e.g., "N.N" → "N")
|
|
18
|
+
.replaceAll(/N[.\-/:]N/g, "N")
|
|
19
|
+
// Collapse whitespace
|
|
20
|
+
.replaceAll(/\s+/g, " ")
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Strip ANSI escape codes from text
|
|
26
|
+
* PowerShell and colored CLI output includes \x1b[...m sequences that show
|
|
27
|
+
* as garbage in Telegram messages.
|
|
28
|
+
* @param {string} text - Input text with potential ANSI codes
|
|
29
|
+
* @returns {string} Clean text without ANSI codes
|
|
30
|
+
*/
|
|
31
|
+
export function stripAnsi(text) {
|
|
32
|
+
// eslint-disable-next-line no-control-regex
|
|
33
|
+
return String(text || "")
|
|
34
|
+
.replace(/\x1b\[[0-9;]*m/g, "")
|
|
35
|
+
.replace(/\[\d+;?\d*m/g, "");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if a line mentions "error" in a benign/summary context
|
|
40
|
+
* (e.g. "errors=0", "0 errors", "no errors found").
|
|
41
|
+
* Shared by isErrorLine() and autofix fallback to avoid false positives.
|
|
42
|
+
* @param {string} line - Log line to check
|
|
43
|
+
* @returns {boolean} True if the "error" mention is benign
|
|
44
|
+
*/
|
|
45
|
+
export function isBenignErrorMention(line) {
|
|
46
|
+
const benign = [
|
|
47
|
+
/errors?[=:]\s*0\b/i,
|
|
48
|
+
/\b0\s+errors?\b/i,
|
|
49
|
+
/\bno\s+errors?\b/i,
|
|
50
|
+
/\bcomplete\b.*\berrors?[=:]\s*0/i,
|
|
51
|
+
/\bpassed\b.*\berrors?\b/i,
|
|
52
|
+
/\bclean\b.*\berrors?\b/i,
|
|
53
|
+
/\bsuccess\b.*\berrors?\b/i,
|
|
54
|
+
/errors?\s*(count|total|sum|rate)\s*[=:]\s*0/i,
|
|
55
|
+
];
|
|
56
|
+
return benign.some((rx) => rx.test(line));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check if a line matches error patterns (excluding noise patterns)
|
|
61
|
+
* @param {string} line - Log line to check
|
|
62
|
+
* @param {RegExp[]} errorPatterns - Patterns that indicate errors
|
|
63
|
+
* @param {RegExp[]} errorNoisePatterns - Patterns to exclude from error detection
|
|
64
|
+
* @returns {boolean} True if line is an error
|
|
65
|
+
*/
|
|
66
|
+
export function isErrorLine(line, errorPatterns, errorNoisePatterns) {
|
|
67
|
+
if (errorNoisePatterns.some((pattern) => pattern.test(line))) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
if (isBenignErrorMention(line)) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
return errorPatterns.some((pattern) => pattern.test(line));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Escape HTML special characters
|
|
78
|
+
* @param {any} value - Value to escape
|
|
79
|
+
* @returns {string} HTML-escaped string
|
|
80
|
+
*/
|
|
81
|
+
export function escapeHtml(value) {
|
|
82
|
+
return String(value)
|
|
83
|
+
.replace(/&/g, "&")
|
|
84
|
+
.replace(/"/g, """)
|
|
85
|
+
.replace(/'/g, "'")
|
|
86
|
+
.replace(/</g, "<")
|
|
87
|
+
.replace(/>/g, ">");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Format an HTML link with proper escaping
|
|
92
|
+
* @param {string} url - URL for the link
|
|
93
|
+
* @param {string} label - Display text for the link
|
|
94
|
+
* @returns {string} HTML anchor tag or escaped label if no URL
|
|
95
|
+
*/
|
|
96
|
+
export function formatHtmlLink(url, label) {
|
|
97
|
+
if (!url) {
|
|
98
|
+
return escapeHtml(label);
|
|
99
|
+
}
|
|
100
|
+
return `<a href="${escapeHtml(url)}">${escapeHtml(label)}</a>`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Generate a normalized fingerprint for an error line (for deduplication)
|
|
105
|
+
* Strips timestamps, attempt IDs, and branch-specific parts
|
|
106
|
+
* @param {string} line - Error line to fingerprint
|
|
107
|
+
* @returns {string} Normalized fingerprint
|
|
108
|
+
*/
|
|
109
|
+
export function getErrorFingerprint(line) {
|
|
110
|
+
// Normalize: strip timestamps, attempt IDs, branch-specific parts
|
|
111
|
+
return line
|
|
112
|
+
.replace(/\[\d{2}:\d{2}:\d{2}\]\s*/g, "")
|
|
113
|
+
.replace(/\b[0-9a-f]{8}\b/gi, "<ID>") // attempt IDs
|
|
114
|
+
.replace(/ve\/[\w.-]+/g, "ve/<BRANCH>") // branch names
|
|
115
|
+
.trim();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Parse -MaxParallel argument from command line arguments
|
|
120
|
+
* Supports: -MaxParallel N, --maxparallel=N, --max-parallel N
|
|
121
|
+
* Falls back to VK_MAX_PARALLEL or MAX_PARALLEL env vars
|
|
122
|
+
* @param {string[]} argsList - Command line arguments array
|
|
123
|
+
* @returns {number|null} Maximum parallel value or null if not found
|
|
124
|
+
*/
|
|
125
|
+
export function getMaxParallelFromArgs(argsList) {
|
|
126
|
+
if (!Array.isArray(argsList)) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
for (let i = 0; i < argsList.length; i += 1) {
|
|
130
|
+
const arg = String(argsList[i] ?? "");
|
|
131
|
+
const directMatch =
|
|
132
|
+
arg.match(/^-{1,2}maxparallel(?:=|:)?(\d+)$/i) ||
|
|
133
|
+
arg.match(/^--max-parallel(?:=|:)?(\d+)$/i);
|
|
134
|
+
if (directMatch) {
|
|
135
|
+
const value = Number(directMatch[1]);
|
|
136
|
+
if (Number.isFinite(value) && value > 0) {
|
|
137
|
+
return value;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const normalized = arg.toLowerCase();
|
|
141
|
+
if (
|
|
142
|
+
normalized === "-maxparallel" ||
|
|
143
|
+
normalized === "--maxparallel" ||
|
|
144
|
+
normalized === "--max-parallel"
|
|
145
|
+
) {
|
|
146
|
+
const next = Number(argsList[i + 1]);
|
|
147
|
+
if (Number.isFinite(next) && next > 0) {
|
|
148
|
+
return next;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const envValue = Number(
|
|
153
|
+
process.env.VK_MAX_PARALLEL || process.env.MAX_PARALLEL,
|
|
154
|
+
);
|
|
155
|
+
if (Number.isFinite(envValue) && envValue > 0) {
|
|
156
|
+
return envValue;
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Extract PR number from a GitHub pull request URL
|
|
163
|
+
* @param {string} url - GitHub PR URL
|
|
164
|
+
* @returns {number|null} PR number or null if not found
|
|
165
|
+
*/
|
|
166
|
+
export function parsePrNumberFromUrl(url) {
|
|
167
|
+
if (!url) return null;
|
|
168
|
+
const match = String(url).match(/\/pull\/(\d+)/i);
|
|
169
|
+
if (!match) return null;
|
|
170
|
+
const num = Number(match[1]);
|
|
171
|
+
return Number.isFinite(num) ? num : null;
|
|
172
|
+
}
|