@masslessai/push-todo 4.1.4 → 4.1.6
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/lib/agent-versions.js +159 -2
- package/lib/config.js +43 -2
- package/lib/daemon.js +283 -4
- package/lib/self-update.js +7 -3
- package/lib/update.js +46 -12
- package/package.json +1 -1
package/lib/agent-versions.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Agent version detection and
|
|
2
|
+
* Agent version detection, tracking, and auto-update for Push daemon.
|
|
3
3
|
*
|
|
4
4
|
* Detects installed versions of Claude Code, OpenAI Codex, and OpenClaw CLIs.
|
|
5
5
|
* Reports version parity with the push-todo CLI and flags outdated agents.
|
|
6
|
+
* Can auto-update agents via npm when enabled.
|
|
6
7
|
*
|
|
7
8
|
* Pattern: follows heartbeat.js — pure functions, internally throttled, non-fatal.
|
|
8
9
|
*/
|
|
@@ -11,25 +12,33 @@ import { execFileSync } from 'child_process';
|
|
|
11
12
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
12
13
|
import { homedir } from 'os';
|
|
13
14
|
import { join } from 'path';
|
|
15
|
+
import { compareSemver } from './self-update.js';
|
|
14
16
|
|
|
15
17
|
const PUSH_DIR = join(homedir(), '.push');
|
|
16
18
|
const VERSIONS_CACHE_FILE = join(PUSH_DIR, 'agent_versions.json');
|
|
19
|
+
const LAST_AGENT_UPDATE_FILE = join(PUSH_DIR, 'last_agent_update_check');
|
|
17
20
|
const CHECK_INTERVAL = 3600000; // 1 hour
|
|
21
|
+
const AGENT_UPDATE_CHECK_INTERVAL = 3600000; // 1 hour
|
|
22
|
+
const AGENT_VERSION_AGE_GATE = 3600000; // 1 hour — only install versions >1hr old
|
|
18
23
|
|
|
19
24
|
// ==================== Agent Definitions ====================
|
|
20
25
|
|
|
21
26
|
/**
|
|
22
|
-
* Agent CLI definitions: command name, version flag, and how to parse output.
|
|
27
|
+
* Agent CLI definitions: command name, version flag, npm package, and how to parse output.
|
|
23
28
|
*
|
|
24
29
|
* Each agent has:
|
|
25
30
|
* - cmd: the CLI binary name
|
|
26
31
|
* - versionArgs: args to get version string
|
|
32
|
+
* - npmPackage: the npm package name for install/update
|
|
27
33
|
* - parseVersion: extracts semver from command output
|
|
34
|
+
* - minVersion: minimum version required for push-todo compatibility (null = no minimum)
|
|
28
35
|
*/
|
|
29
36
|
const AGENTS = {
|
|
30
37
|
'claude-code': {
|
|
31
38
|
cmd: 'claude',
|
|
32
39
|
versionArgs: ['--version'],
|
|
40
|
+
npmPackage: '@anthropic-ai/claude-code',
|
|
41
|
+
minVersion: '2.0.0', // --worktree support
|
|
33
42
|
parseVersion(output) {
|
|
34
43
|
// "claude v2.1.41" or just "2.1.41"
|
|
35
44
|
const match = output.match(/(\d+\.\d+\.\d+)/);
|
|
@@ -39,6 +48,8 @@ const AGENTS = {
|
|
|
39
48
|
'openai-codex': {
|
|
40
49
|
cmd: 'codex',
|
|
41
50
|
versionArgs: ['--version'],
|
|
51
|
+
npmPackage: '@openai/codex',
|
|
52
|
+
minVersion: null,
|
|
42
53
|
parseVersion(output) {
|
|
43
54
|
const match = output.match(/(\d+\.\d+\.\d+)/);
|
|
44
55
|
return match ? match[1] : null;
|
|
@@ -47,6 +58,8 @@ const AGENTS = {
|
|
|
47
58
|
'openclaw': {
|
|
48
59
|
cmd: 'openclaw',
|
|
49
60
|
versionArgs: ['--version'],
|
|
61
|
+
npmPackage: 'openclaw',
|
|
62
|
+
minVersion: null,
|
|
50
63
|
parseVersion(output) {
|
|
51
64
|
const match = output.match(/(\d+\.\d+\.\d+)/);
|
|
52
65
|
return match ? match[1] : null;
|
|
@@ -202,3 +215,147 @@ export function formatAgentVersionSummary(versions) {
|
|
|
202
215
|
export function getKnownAgentTypes() {
|
|
203
216
|
return Object.keys(AGENTS);
|
|
204
217
|
}
|
|
218
|
+
|
|
219
|
+
// ==================== Agent Update ====================
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Fetch latest version info for an agent from npm.
|
|
223
|
+
*
|
|
224
|
+
* @param {string} agentType
|
|
225
|
+
* @returns {{ version: string, publishedAt: string|null }|null}
|
|
226
|
+
*/
|
|
227
|
+
function fetchLatestAgentVersion(agentType) {
|
|
228
|
+
const agent = AGENTS[agentType];
|
|
229
|
+
if (!agent?.npmPackage) return null;
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const result = execFileSync('npm', ['view', agent.npmPackage, '--json'], {
|
|
233
|
+
timeout: 15000,
|
|
234
|
+
encoding: 'utf8',
|
|
235
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
236
|
+
});
|
|
237
|
+
const data = JSON.parse(result);
|
|
238
|
+
const latest = data['dist-tags']?.latest || data.version;
|
|
239
|
+
return {
|
|
240
|
+
version: latest,
|
|
241
|
+
publishedAt: data.time?.[latest] || null,
|
|
242
|
+
};
|
|
243
|
+
} catch {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Check if an agent has an update available.
|
|
250
|
+
*
|
|
251
|
+
* @param {string} agentType
|
|
252
|
+
* @returns {{ available: boolean, current: string|null, latest: string|null, reason: string }}
|
|
253
|
+
*/
|
|
254
|
+
export function checkForAgentUpdate(agentType) {
|
|
255
|
+
const currentInfo = detectAgentVersion(agentType);
|
|
256
|
+
if (!currentInfo.installed || !currentInfo.version) {
|
|
257
|
+
return { available: false, current: null, latest: null, reason: 'not_installed' };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const latestInfo = fetchLatestAgentVersion(agentType);
|
|
261
|
+
if (!latestInfo) {
|
|
262
|
+
return { available: false, current: currentInfo.version, latest: null, reason: 'registry_unreachable' };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (compareSemver(currentInfo.version, latestInfo.version) >= 0) {
|
|
266
|
+
return { available: false, current: currentInfo.version, latest: latestInfo.version, reason: 'up_to_date' };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Safety: only update to versions published >1 hour ago
|
|
270
|
+
if (latestInfo.publishedAt) {
|
|
271
|
+
const publishedAge = Date.now() - new Date(latestInfo.publishedAt).getTime();
|
|
272
|
+
if (publishedAge < AGENT_VERSION_AGE_GATE) {
|
|
273
|
+
return { available: false, current: currentInfo.version, latest: latestInfo.version, reason: 'too_recent' };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return { available: true, current: currentInfo.version, latest: latestInfo.version, reason: 'update_available' };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Install a specific version of an agent CLI globally.
|
|
282
|
+
*
|
|
283
|
+
* @param {string} agentType
|
|
284
|
+
* @param {string} targetVersion
|
|
285
|
+
* @returns {boolean} true if update succeeded
|
|
286
|
+
*/
|
|
287
|
+
export function performAgentUpdate(agentType, targetVersion) {
|
|
288
|
+
const agent = AGENTS[agentType];
|
|
289
|
+
if (!agent?.npmPackage) return false;
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
execFileSync('npm', ['install', '-g', `${agent.npmPackage}@${targetVersion}`], {
|
|
293
|
+
timeout: 120000,
|
|
294
|
+
stdio: 'pipe',
|
|
295
|
+
});
|
|
296
|
+
return true;
|
|
297
|
+
} catch {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Check all installed agents for updates (throttled).
|
|
304
|
+
* Returns update results or null if throttled.
|
|
305
|
+
*
|
|
306
|
+
* @param {{ force?: boolean }} options
|
|
307
|
+
* @returns {Object.<string, { available: boolean, current: string|null, latest: string|null, reason: string }>|null}
|
|
308
|
+
*/
|
|
309
|
+
export function checkAllAgentUpdates({ force = false } = {}) {
|
|
310
|
+
// Throttle check
|
|
311
|
+
if (!force && existsSync(LAST_AGENT_UPDATE_FILE)) {
|
|
312
|
+
try {
|
|
313
|
+
const lastCheck = parseInt(readFileSync(LAST_AGENT_UPDATE_FILE, 'utf8').trim(), 10);
|
|
314
|
+
if (Date.now() - lastCheck < AGENT_UPDATE_CHECK_INTERVAL) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
} catch {}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Record check time
|
|
321
|
+
try {
|
|
322
|
+
mkdirSync(PUSH_DIR, { recursive: true });
|
|
323
|
+
writeFileSync(LAST_AGENT_UPDATE_FILE, String(Date.now()));
|
|
324
|
+
} catch {}
|
|
325
|
+
|
|
326
|
+
const results = {};
|
|
327
|
+
for (const agentType of Object.keys(AGENTS)) {
|
|
328
|
+
const info = detectAgentVersion(agentType);
|
|
329
|
+
if (info.installed) {
|
|
330
|
+
results[agentType] = checkForAgentUpdate(agentType);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return results;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ==================== Version Parity ====================
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Check version parity between installed agents and push-todo requirements.
|
|
340
|
+
* Returns warnings for agents that are below minimum required versions.
|
|
341
|
+
*
|
|
342
|
+
* @returns {{ agentType: string, installed: string, required: string }[]}
|
|
343
|
+
*/
|
|
344
|
+
export function checkVersionParity() {
|
|
345
|
+
const warnings = [];
|
|
346
|
+
for (const [agentType, agent] of Object.entries(AGENTS)) {
|
|
347
|
+
if (!agent.minVersion) continue;
|
|
348
|
+
|
|
349
|
+
const info = detectAgentVersion(agentType);
|
|
350
|
+
if (info.installed && info.version) {
|
|
351
|
+
if (compareSemver(info.version, agent.minVersion) < 0) {
|
|
352
|
+
warnings.push({
|
|
353
|
+
agentType,
|
|
354
|
+
installed: info.version,
|
|
355
|
+
required: agent.minVersion,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return warnings;
|
|
361
|
+
}
|
package/lib/config.js
CHANGED
|
@@ -213,6 +213,27 @@ export function setAutoUpdateEnabled(enabled) {
|
|
|
213
213
|
return setConfigValue('AUTO_UPDATE', enabled ? 'true' : 'false');
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
+
/**
|
|
217
|
+
* Check if auto-update-agents is enabled for daemon agent CLI updates.
|
|
218
|
+
* Default: false (agent updates are opt-in since they're third-party CLIs)
|
|
219
|
+
*
|
|
220
|
+
* @returns {boolean}
|
|
221
|
+
*/
|
|
222
|
+
export function getAutoUpdateAgentsEnabled() {
|
|
223
|
+
const value = getConfigValue('AUTO_UPDATE_AGENTS', 'false');
|
|
224
|
+
return value.toLowerCase() === 'true' || value === '1' || value.toLowerCase() === 'yes';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Set auto-update-agents setting.
|
|
229
|
+
*
|
|
230
|
+
* @param {boolean} enabled
|
|
231
|
+
* @returns {boolean} True if successful
|
|
232
|
+
*/
|
|
233
|
+
export function setAutoUpdateAgentsEnabled(enabled) {
|
|
234
|
+
return setConfigValue('AUTO_UPDATE_AGENTS', enabled ? 'true' : 'false');
|
|
235
|
+
}
|
|
236
|
+
|
|
216
237
|
/**
|
|
217
238
|
* Get the maximum batch size for queuing tasks.
|
|
218
239
|
* Default: 5
|
|
@@ -292,6 +313,7 @@ export function showSettings() {
|
|
|
292
313
|
const autoMerge = getAutoMergeEnabled();
|
|
293
314
|
const autoComplete = getAutoCompleteEnabled();
|
|
294
315
|
const autoUpdate = getAutoUpdateEnabled();
|
|
316
|
+
const autoUpdateAgents = getAutoUpdateAgentsEnabled();
|
|
295
317
|
const batchSize = getMaxBatchSize();
|
|
296
318
|
|
|
297
319
|
console.log(` auto-commit: ${autoCommit ? 'ON' : 'OFF'}`);
|
|
@@ -304,7 +326,10 @@ export function showSettings() {
|
|
|
304
326
|
console.log(' Mark task completed after successful merge');
|
|
305
327
|
console.log();
|
|
306
328
|
console.log(` auto-update: ${autoUpdate ? 'ON' : 'OFF'}`);
|
|
307
|
-
console.log(' Daemon auto-updates from npm when idle');
|
|
329
|
+
console.log(' Daemon auto-updates push-todo from npm when idle');
|
|
330
|
+
console.log();
|
|
331
|
+
console.log(` auto-update-agents: ${autoUpdateAgents ? 'ON' : 'OFF'}`);
|
|
332
|
+
console.log(' Daemon auto-updates agent CLIs (Claude, Codex, OpenClaw)');
|
|
308
333
|
console.log();
|
|
309
334
|
console.log(` batch-size: ${batchSize}`);
|
|
310
335
|
console.log(' Max tasks for batch queue (1-20)');
|
|
@@ -387,6 +412,22 @@ export function toggleSetting(settingName) {
|
|
|
387
412
|
return false;
|
|
388
413
|
}
|
|
389
414
|
|
|
415
|
+
if (normalized === 'auto-update-agents') {
|
|
416
|
+
const current = getAutoUpdateAgentsEnabled();
|
|
417
|
+
const newValue = !current;
|
|
418
|
+
if (setAutoUpdateAgentsEnabled(newValue)) {
|
|
419
|
+
console.log(`Auto-update-agents is now ${newValue ? 'ON' : 'OFF'}`);
|
|
420
|
+
if (newValue) {
|
|
421
|
+
console.log('Daemon will auto-update agent CLIs (Claude Code, Codex, OpenClaw) when idle.');
|
|
422
|
+
} else {
|
|
423
|
+
console.log('Agent CLIs will NOT be auto-updated. Use "push-todo update" for manual updates.');
|
|
424
|
+
}
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
console.error('Failed to update setting');
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
|
|
390
431
|
if (normalized === 'batch-size') {
|
|
391
432
|
const batchSize = getMaxBatchSize();
|
|
392
433
|
console.log(`Current batch size: ${batchSize}`);
|
|
@@ -395,7 +436,7 @@ export function toggleSetting(settingName) {
|
|
|
395
436
|
}
|
|
396
437
|
|
|
397
438
|
console.error(`Unknown setting: ${settingName}`);
|
|
398
|
-
console.error('Available settings: auto-commit, auto-merge, auto-complete, auto-update, batch-size');
|
|
439
|
+
console.error('Available settings: auto-commit, auto-merge, auto-complete, auto-update, auto-update-agents, batch-size');
|
|
399
440
|
return false;
|
|
400
441
|
}
|
|
401
442
|
|
package/lib/daemon.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { randomUUID } from 'crypto';
|
|
17
17
|
import { spawn, execSync, execFileSync } from 'child_process';
|
|
18
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, unlinkSync, statSync, renameSync } from 'fs';
|
|
18
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, unlinkSync, statSync, renameSync, readdirSync } from 'fs';
|
|
19
19
|
import { homedir, hostname, platform } from 'os';
|
|
20
20
|
import { join, dirname } from 'path';
|
|
21
21
|
import { fileURLToPath } from 'url';
|
|
@@ -25,7 +25,7 @@ import { getProjectContext, buildSmartPrompt, invalidateCache } from './context-
|
|
|
25
25
|
import { sendMacNotification } from './utils/notify.js';
|
|
26
26
|
import { checkAndRunDueJobs } from './cron.js';
|
|
27
27
|
import { runHeartbeatChecks } from './heartbeat.js';
|
|
28
|
-
import { getAgentVersions, formatAgentVersionSummary } from './agent-versions.js';
|
|
28
|
+
import { getAgentVersions, formatAgentVersionSummary, checkAllAgentUpdates, performAgentUpdate, checkVersionParity } from './agent-versions.js';
|
|
29
29
|
import { checkAllProjectsFreshness } from './project-freshness.js';
|
|
30
30
|
|
|
31
31
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -48,6 +48,10 @@ const RETRY_BACKOFF_FACTOR = 2;
|
|
|
48
48
|
const CERTAINTY_HIGH_THRESHOLD = 0.7;
|
|
49
49
|
const CERTAINTY_LOW_THRESHOLD = 0.4;
|
|
50
50
|
|
|
51
|
+
// Idle auto-recovery (Level B)
|
|
52
|
+
const IDLE_TIMEOUT_MS = 900000; // 15 min no stdout → kill (smarter than absolute timeout)
|
|
53
|
+
const HEARTBEAT_INTERVAL_MS = 300000; // 5 min progress heartbeat to Supabase (Level A)
|
|
54
|
+
|
|
51
55
|
// Stuck detection
|
|
52
56
|
const STUCK_IDLE_THRESHOLD = 600000; // 10 min
|
|
53
57
|
const STUCK_WARNING_THRESHOLD = 300000; // 5 min
|
|
@@ -110,6 +114,7 @@ const taskLastOutput = new Map(); // displayNumber -> timestamp
|
|
|
110
114
|
const taskStdoutBuffer = new Map(); // displayNumber -> lines[]
|
|
111
115
|
const taskStderrBuffer = new Map(); // displayNumber -> lines[]
|
|
112
116
|
const taskProjectPaths = new Map(); // displayNumber -> projectPath
|
|
117
|
+
const taskLastHeartbeat = new Map(); // displayNumber -> timestamp of last progress heartbeat
|
|
113
118
|
let daemonStartTime = null;
|
|
114
119
|
|
|
115
120
|
// ==================== Utilities ====================
|
|
@@ -277,12 +282,70 @@ function getAutoUpdateEnabled() {
|
|
|
277
282
|
return v.toLowerCase() === 'true' || v === '1' || v.toLowerCase() === 'yes';
|
|
278
283
|
}
|
|
279
284
|
|
|
285
|
+
function getAutoUpdateAgentsEnabled() {
|
|
286
|
+
const v = getConfigValueFromFile('AUTO_UPDATE_AGENTS', 'false');
|
|
287
|
+
return v.toLowerCase() === 'true' || v === '1' || v.toLowerCase() === 'yes';
|
|
288
|
+
}
|
|
289
|
+
|
|
280
290
|
// ==================== Capabilities Detection ====================
|
|
281
291
|
|
|
282
292
|
let cachedCapabilities = null;
|
|
283
293
|
let lastCapabilityCheck = 0;
|
|
284
294
|
const CAPABILITY_CHECK_INTERVAL = 3600000; // 1 hour
|
|
285
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Discover skills for all registered projects.
|
|
298
|
+
* Scans ~/.claude/skills/ (global) and <projectPath>/.claude/skills/ (per-project).
|
|
299
|
+
* Returns: { "github.com/user/repo": ["skill1", "skill2"], ... }
|
|
300
|
+
*/
|
|
301
|
+
function discoverProjectSkills() {
|
|
302
|
+
const globalSkillsDir = join(homedir(), '.claude', 'skills');
|
|
303
|
+
const globalSkills = [];
|
|
304
|
+
|
|
305
|
+
// Enumerate global skills
|
|
306
|
+
if (existsSync(globalSkillsDir)) {
|
|
307
|
+
try {
|
|
308
|
+
for (const entry of readdirSync(globalSkillsDir, { withFileTypes: true })) {
|
|
309
|
+
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
|
310
|
+
globalSkills.push(entry.name);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
} catch { /* ignore */ }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// For each registered project, enumerate project-local skills and merge with global
|
|
317
|
+
const result = {};
|
|
318
|
+
if (!existsSync(REGISTRY_FILE)) return result;
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
const data = JSON.parse(readFileSync(REGISTRY_FILE, 'utf8'));
|
|
322
|
+
for (const [, info] of Object.entries(data.projects || {})) {
|
|
323
|
+
const remote = info.gitRemote;
|
|
324
|
+
const localPath = info.localPath || info.local_path;
|
|
325
|
+
if (!remote || !localPath) continue;
|
|
326
|
+
|
|
327
|
+
const projectSkillsDir = join(localPath, '.claude', 'skills');
|
|
328
|
+
const projectSkills = new Set(globalSkills);
|
|
329
|
+
|
|
330
|
+
if (existsSync(projectSkillsDir)) {
|
|
331
|
+
try {
|
|
332
|
+
for (const entry of readdirSync(projectSkillsDir, { withFileTypes: true })) {
|
|
333
|
+
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
|
334
|
+
projectSkills.add(entry.name);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
} catch { /* ignore */ }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (projectSkills.size > 0) {
|
|
341
|
+
result[remote] = [...projectSkills].sort();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
} catch { /* ignore */ }
|
|
345
|
+
|
|
346
|
+
return result;
|
|
347
|
+
}
|
|
348
|
+
|
|
286
349
|
function detectCapabilities() {
|
|
287
350
|
const caps = {
|
|
288
351
|
auto_merge: getAutoMergeEnabled(),
|
|
@@ -302,6 +365,8 @@ function detectCapabilities() {
|
|
|
302
365
|
caps.gh_cli = 'not_installed';
|
|
303
366
|
}
|
|
304
367
|
|
|
368
|
+
caps.project_skills = discoverProjectSkills();
|
|
369
|
+
|
|
305
370
|
return caps;
|
|
306
371
|
}
|
|
307
372
|
|
|
@@ -1340,6 +1405,151 @@ function checkTaskIdle(displayNumber) {
|
|
|
1340
1405
|
return false;
|
|
1341
1406
|
}
|
|
1342
1407
|
|
|
1408
|
+
// ==================== Progress Heartbeat (Level A) ====================
|
|
1409
|
+
|
|
1410
|
+
async function sendProgressHeartbeats() {
|
|
1411
|
+
const now = Date.now();
|
|
1412
|
+
|
|
1413
|
+
for (const [displayNumber, taskInfo] of runningTasks) {
|
|
1414
|
+
const info = taskDetails.get(displayNumber) || {};
|
|
1415
|
+
|
|
1416
|
+
// Skip tasks awaiting user confirmation (not truly hanging)
|
|
1417
|
+
if (info.phase === 'awaiting_confirmation') continue;
|
|
1418
|
+
|
|
1419
|
+
// Throttle: only send every HEARTBEAT_INTERVAL_MS
|
|
1420
|
+
const lastHeartbeat = taskLastHeartbeat.get(displayNumber) || taskInfo.startTime;
|
|
1421
|
+
if (now - lastHeartbeat < HEARTBEAT_INTERVAL_MS) continue;
|
|
1422
|
+
|
|
1423
|
+
// Compute metrics
|
|
1424
|
+
const elapsedSec = Math.floor((now - taskInfo.startTime) / 1000);
|
|
1425
|
+
const elapsedMin = Math.floor(elapsedSec / 60);
|
|
1426
|
+
const lastOutputTs = taskLastOutput.get(displayNumber);
|
|
1427
|
+
const idleSec = lastOutputTs ? Math.floor((now - lastOutputTs) / 1000) : elapsedSec;
|
|
1428
|
+
const idleMin = Math.floor(idleSec / 60);
|
|
1429
|
+
|
|
1430
|
+
const activityDesc = idleSec < 60 ? 'active' : `idle ${idleMin}m`;
|
|
1431
|
+
const phase = info.phase || 'executing';
|
|
1432
|
+
const eventSummary = `Running for ${elapsedMin}m. Last activity: ${activityDesc}. Phase: ${phase}.`;
|
|
1433
|
+
|
|
1434
|
+
log(`Task #${displayNumber}: sending progress heartbeat (${eventSummary})`);
|
|
1435
|
+
|
|
1436
|
+
// Update throttle timestamp BEFORE the async call to prevent concurrent sends
|
|
1437
|
+
taskLastHeartbeat.set(displayNumber, now);
|
|
1438
|
+
|
|
1439
|
+
// Send event-only update (no status field) — non-fatal if it fails
|
|
1440
|
+
const taskId = info.taskId || null;
|
|
1441
|
+
apiRequest('update-task-execution', {
|
|
1442
|
+
method: 'PATCH',
|
|
1443
|
+
body: JSON.stringify({
|
|
1444
|
+
todoId: taskId,
|
|
1445
|
+
displayNumber,
|
|
1446
|
+
event: {
|
|
1447
|
+
type: 'progress',
|
|
1448
|
+
timestamp: new Date().toISOString(),
|
|
1449
|
+
machineName: getMachineName() || undefined,
|
|
1450
|
+
summary: eventSummary
|
|
1451
|
+
}
|
|
1452
|
+
// No status field — event-only update, won't change execution_status
|
|
1453
|
+
})
|
|
1454
|
+
}).catch(err => {
|
|
1455
|
+
log(`Task #${displayNumber}: heartbeat failed (non-fatal): ${err.message}`);
|
|
1456
|
+
});
|
|
1457
|
+
// NOTE: intentionally not awaited — heartbeats are fire-and-forget
|
|
1458
|
+
// to avoid blocking the poll loop when Supabase is slow
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// ==================== Idle Auto-Recovery (Level B) ====================
|
|
1463
|
+
|
|
1464
|
+
async function killIdleTasks() {
|
|
1465
|
+
const now = Date.now();
|
|
1466
|
+
const idleTimedOut = [];
|
|
1467
|
+
|
|
1468
|
+
for (const [displayNumber, taskInfo] of runningTasks) {
|
|
1469
|
+
const info = taskDetails.get(displayNumber) || {};
|
|
1470
|
+
|
|
1471
|
+
// Exempt: tasks awaiting user confirmation
|
|
1472
|
+
if (info.phase === 'awaiting_confirmation') continue;
|
|
1473
|
+
|
|
1474
|
+
const lastOutput = taskLastOutput.get(displayNumber);
|
|
1475
|
+
if (!lastOutput) continue; // No output tracking yet — not idle, just starting
|
|
1476
|
+
|
|
1477
|
+
const idleMs = now - lastOutput;
|
|
1478
|
+
if (idleMs > IDLE_TIMEOUT_MS) {
|
|
1479
|
+
log(`Task #${displayNumber} IDLE TIMEOUT: ${Math.floor(idleMs / 1000)}s since last output`);
|
|
1480
|
+
idleTimedOut.push(displayNumber);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
for (const displayNumber of idleTimedOut) {
|
|
1485
|
+
const taskInfo = runningTasks.get(displayNumber);
|
|
1486
|
+
if (!taskInfo) continue;
|
|
1487
|
+
|
|
1488
|
+
const info = taskDetails.get(displayNumber) || {};
|
|
1489
|
+
const projectPath = taskProjectPaths.get(displayNumber);
|
|
1490
|
+
const elapsedSec = Math.floor((now - taskInfo.startTime) / 1000);
|
|
1491
|
+
const idleSec = Math.floor((now - (taskLastOutput.get(displayNumber) || taskInfo.startTime)) / 1000);
|
|
1492
|
+
const durationStr = elapsedSec < 60 ? `${elapsedSec}s` : `${Math.floor(elapsedSec / 60)}m ${elapsedSec % 60}s`;
|
|
1493
|
+
const machineName = getMachineName() || 'Mac';
|
|
1494
|
+
|
|
1495
|
+
// Extract semantic summary WHILE session is still alive
|
|
1496
|
+
// (a live session produces a better "what have you done so far" answer)
|
|
1497
|
+
const sessionId = taskInfo.sessionId;
|
|
1498
|
+
const worktreePath = getWorktreePath(displayNumber, projectPath);
|
|
1499
|
+
const summaryPath = existsSync(worktreePath) ? worktreePath : (projectPath || process.cwd());
|
|
1500
|
+
log(`Task #${displayNumber}: extracting pre-kill summary...`);
|
|
1501
|
+
const idleSummary = extractSemanticSummary(summaryPath, sessionId);
|
|
1502
|
+
|
|
1503
|
+
// Now kill the process
|
|
1504
|
+
log(`Task #${displayNumber}: killing idle process (PID: ${taskInfo.process.pid})`);
|
|
1505
|
+
try {
|
|
1506
|
+
taskInfo.process.kill('SIGTERM');
|
|
1507
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
1508
|
+
taskInfo.process.kill('SIGKILL');
|
|
1509
|
+
} catch {}
|
|
1510
|
+
|
|
1511
|
+
runningTasks.delete(displayNumber);
|
|
1512
|
+
cleanupWorktree(displayNumber, projectPath);
|
|
1513
|
+
|
|
1514
|
+
const idleError = idleSummary
|
|
1515
|
+
? `${idleSummary}\nSession went idle for ${Math.floor(idleSec / 60)}m with no output. Killed after ${durationStr} on ${machineName}.`
|
|
1516
|
+
: `Session went idle for ${Math.floor(idleSec / 60)}m with no output (limit: ${IDLE_TIMEOUT_MS / 60000}m). Killed after ${durationStr} on ${machineName}.`;
|
|
1517
|
+
|
|
1518
|
+
await updateTaskStatus(displayNumber, 'failed', {
|
|
1519
|
+
error: idleError,
|
|
1520
|
+
sessionId
|
|
1521
|
+
}, info.taskId);
|
|
1522
|
+
|
|
1523
|
+
if (NOTIFY_ON_FAILURE) {
|
|
1524
|
+
sendMacNotification(
|
|
1525
|
+
`Task #${displayNumber} idle timeout`,
|
|
1526
|
+
`${(info.summary || 'Unknown').slice(0, 40)}... idle ${Math.floor(idleSec / 60)}m`,
|
|
1527
|
+
'Basso'
|
|
1528
|
+
);
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
trackCompleted({
|
|
1532
|
+
displayNumber,
|
|
1533
|
+
summary: info.summary || 'Unknown task',
|
|
1534
|
+
completedAt: new Date().toISOString(),
|
|
1535
|
+
duration: elapsedSec,
|
|
1536
|
+
status: 'idle_timeout'
|
|
1537
|
+
});
|
|
1538
|
+
|
|
1539
|
+
// Full cleanup of all tracking Maps
|
|
1540
|
+
taskDetails.delete(displayNumber);
|
|
1541
|
+
taskLastOutput.delete(displayNumber);
|
|
1542
|
+
taskStdoutBuffer.delete(displayNumber);
|
|
1543
|
+
taskStderrBuffer.delete(displayNumber);
|
|
1544
|
+
taskProjectPaths.delete(displayNumber);
|
|
1545
|
+
taskLastHeartbeat.delete(displayNumber);
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
if (idleTimedOut.length > 0) {
|
|
1549
|
+
updateStatusFile();
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1343
1553
|
// ==================== Session ID Extraction ====================
|
|
1344
1554
|
|
|
1345
1555
|
function extractSessionIdFromStdout(proc, buffer) {
|
|
@@ -1616,7 +1826,6 @@ async function executeTask(task) {
|
|
|
1616
1826
|
? [
|
|
1617
1827
|
'--continue', previousSessionId,
|
|
1618
1828
|
'-p', prompt,
|
|
1619
|
-
'--worktree', worktreeName,
|
|
1620
1829
|
'--allowedTools', allowedTools,
|
|
1621
1830
|
'--output-format', 'json',
|
|
1622
1831
|
'--permission-mode', 'bypassPermissions',
|
|
@@ -1624,7 +1833,6 @@ async function executeTask(task) {
|
|
|
1624
1833
|
]
|
|
1625
1834
|
: [
|
|
1626
1835
|
'-p', prompt,
|
|
1627
|
-
'--worktree', worktreeName,
|
|
1628
1836
|
'--allowedTools', allowedTools,
|
|
1629
1837
|
'--output-format', 'json',
|
|
1630
1838
|
'--permission-mode', 'bypassPermissions',
|
|
@@ -1660,6 +1868,7 @@ async function executeTask(task) {
|
|
|
1660
1868
|
taskLastOutput.set(displayNumber, Date.now());
|
|
1661
1869
|
taskStdoutBuffer.set(displayNumber, []);
|
|
1662
1870
|
taskStderrBuffer.set(displayNumber, []);
|
|
1871
|
+
taskLastHeartbeat.set(displayNumber, Date.now());
|
|
1663
1872
|
|
|
1664
1873
|
// Monitor stderr (critical for diagnosing fast exits)
|
|
1665
1874
|
child.stderr.on('data', (data) => {
|
|
@@ -1715,6 +1924,7 @@ async function executeTask(task) {
|
|
|
1715
1924
|
runningTasks.delete(displayNumber);
|
|
1716
1925
|
await updateTaskStatus(displayNumber, 'failed', { error: error.message }, taskId);
|
|
1717
1926
|
taskDetails.delete(displayNumber);
|
|
1927
|
+
taskLastHeartbeat.delete(displayNumber);
|
|
1718
1928
|
updateStatusFile();
|
|
1719
1929
|
});
|
|
1720
1930
|
|
|
@@ -1938,6 +2148,7 @@ async function handleTaskCompletion(displayNumber, exitCode) {
|
|
|
1938
2148
|
taskStdoutBuffer.delete(displayNumber);
|
|
1939
2149
|
taskStderrBuffer.delete(displayNumber);
|
|
1940
2150
|
taskProjectPaths.delete(displayNumber);
|
|
2151
|
+
taskLastHeartbeat.delete(displayNumber);
|
|
1941
2152
|
updateStatusFile();
|
|
1942
2153
|
}
|
|
1943
2154
|
|
|
@@ -2016,6 +2227,13 @@ function updateStatusFile() {
|
|
|
2016
2227
|
// ==================== Task Checking ====================
|
|
2017
2228
|
|
|
2018
2229
|
async function checkTimeouts() {
|
|
2230
|
+
// Level A: Send progress heartbeats for long-running tasks
|
|
2231
|
+
await sendProgressHeartbeats();
|
|
2232
|
+
|
|
2233
|
+
// Level B: Kill tasks that have been idle too long (fires at 15 min, before 60 min absolute)
|
|
2234
|
+
await killIdleTasks();
|
|
2235
|
+
|
|
2236
|
+
// Absolute timeout (safety net — 60 min wall clock)
|
|
2019
2237
|
const now = Date.now();
|
|
2020
2238
|
const timedOut = [];
|
|
2021
2239
|
|
|
@@ -2082,6 +2300,7 @@ async function checkTimeouts() {
|
|
|
2082
2300
|
taskStdoutBuffer.delete(displayNumber);
|
|
2083
2301
|
taskStderrBuffer.delete(displayNumber);
|
|
2084
2302
|
taskProjectPaths.delete(displayNumber);
|
|
2303
|
+
taskLastHeartbeat.delete(displayNumber);
|
|
2085
2304
|
cleanupWorktree(displayNumber, projectPath);
|
|
2086
2305
|
}
|
|
2087
2306
|
|
|
@@ -2141,6 +2360,55 @@ function checkAndApplyUpdate() {
|
|
|
2141
2360
|
}
|
|
2142
2361
|
}
|
|
2143
2362
|
|
|
2363
|
+
// ==================== Agent Auto-Update ====================
|
|
2364
|
+
|
|
2365
|
+
let pendingAgentUpdates = null; // { agentType: targetVersion, ... }
|
|
2366
|
+
|
|
2367
|
+
function checkAndApplyAgentUpdates() {
|
|
2368
|
+
// Check for updates (throttled internally to once per hour)
|
|
2369
|
+
if (!pendingAgentUpdates) {
|
|
2370
|
+
const results = checkAllAgentUpdates();
|
|
2371
|
+
if (!results) return; // throttled
|
|
2372
|
+
|
|
2373
|
+
const available = {};
|
|
2374
|
+
for (const [agentType, info] of Object.entries(results)) {
|
|
2375
|
+
if (info.available) {
|
|
2376
|
+
available[agentType] = info.latest;
|
|
2377
|
+
log(`Agent update available: ${agentType} v${info.current} -> v${info.latest}`);
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
if (Object.keys(available).length > 0) {
|
|
2381
|
+
pendingAgentUpdates = available;
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
// Only apply when no tasks are running
|
|
2386
|
+
if (pendingAgentUpdates && runningTasks.size === 0) {
|
|
2387
|
+
for (const [agentType, targetVersion] of Object.entries(pendingAgentUpdates)) {
|
|
2388
|
+
log(`Updating ${agentType} to v${targetVersion}...`);
|
|
2389
|
+
const success = performAgentUpdate(agentType, targetVersion);
|
|
2390
|
+
if (success) {
|
|
2391
|
+
log(`${agentType} updated to v${targetVersion}`);
|
|
2392
|
+
sendMacNotification(
|
|
2393
|
+
'Push: Agent Updated',
|
|
2394
|
+
`${agentType} updated to v${targetVersion}`,
|
|
2395
|
+
'Glass'
|
|
2396
|
+
);
|
|
2397
|
+
} else {
|
|
2398
|
+
logError(`${agentType} update to v${targetVersion} failed`);
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
pendingAgentUpdates = null;
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
function logVersionParityWarnings() {
|
|
2406
|
+
const warnings = checkVersionParity();
|
|
2407
|
+
for (const w of warnings) {
|
|
2408
|
+
log(`WARNING: ${w.agentType} v${w.installed} is below minimum v${w.required} — some features may not work`);
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2144
2412
|
// ==================== Main Loop ====================
|
|
2145
2413
|
|
|
2146
2414
|
async function pollAndExecute() {
|
|
@@ -2243,10 +2511,12 @@ async function mainLoop() {
|
|
|
2243
2511
|
log(`Max concurrent tasks: ${MAX_CONCURRENT_TASKS}`);
|
|
2244
2512
|
log(`E2EE: ${e2eeAvailable ? 'Available' : 'Not available'}`);
|
|
2245
2513
|
log(`Auto-update: ${getAutoUpdateEnabled() ? 'Enabled' : 'Disabled'}`);
|
|
2514
|
+
log(`Auto-update-agents: ${getAutoUpdateAgentsEnabled() ? 'Enabled' : 'Disabled'}`);
|
|
2246
2515
|
const caps = getCapabilities();
|
|
2247
2516
|
log(`Capabilities: gh=${caps.gh_cli}, auto-merge=${caps.auto_merge}, auto-complete=${caps.auto_complete}`);
|
|
2248
2517
|
const agentVersions = getAgentVersions({ force: true });
|
|
2249
2518
|
log(`Agent versions: ${formatAgentVersionSummary(agentVersions)}`);
|
|
2519
|
+
logVersionParityWarnings();
|
|
2250
2520
|
log(`Log file: ${LOG_FILE}`);
|
|
2251
2521
|
|
|
2252
2522
|
// Show registered projects
|
|
@@ -2345,6 +2615,15 @@ async function mainLoop() {
|
|
|
2345
2615
|
if (getAutoUpdateEnabled()) {
|
|
2346
2616
|
checkAndApplyUpdate();
|
|
2347
2617
|
}
|
|
2618
|
+
|
|
2619
|
+
// Agent CLI auto-update (throttled to once per hour, only applies when idle)
|
|
2620
|
+
if (getAutoUpdateAgentsEnabled()) {
|
|
2621
|
+
try {
|
|
2622
|
+
checkAndApplyAgentUpdates();
|
|
2623
|
+
} catch (error) {
|
|
2624
|
+
logError(`Agent auto-update error: ${error.message}`);
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2348
2627
|
} catch (error) {
|
|
2349
2628
|
logError(`Poll error: ${error.message}`);
|
|
2350
2629
|
}
|
package/lib/self-update.js
CHANGED
|
@@ -17,12 +17,16 @@ const LAST_UPDATE_CHECK_FILE = join(PUSH_DIR, 'last_update_check');
|
|
|
17
17
|
const UPDATE_CHECK_INTERVAL = 3600000; // 1 hour
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* Compare semver strings.
|
|
20
|
+
* Compare semver strings (strips pre-release/build metadata before comparing).
|
|
21
|
+
* Handles formats like "2.1.41", "2026.2.22-2", "1.0.0+build.123".
|
|
21
22
|
* @returns -1 if a < b, 0 if equal, 1 if a > b
|
|
22
23
|
*/
|
|
23
24
|
export function compareSemver(a, b) {
|
|
24
|
-
|
|
25
|
-
const
|
|
25
|
+
// Strip pre-release (-beta.1) and build metadata (+build.123)
|
|
26
|
+
const cleanA = a.split('-')[0].split('+')[0];
|
|
27
|
+
const cleanB = b.split('-')[0].split('+')[0];
|
|
28
|
+
const pa = cleanA.split('.').map(Number);
|
|
29
|
+
const pb = cleanB.split('.').map(Number);
|
|
26
30
|
for (let i = 0; i < 3; i++) {
|
|
27
31
|
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
|
|
28
32
|
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
|
package/lib/update.js
CHANGED
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Manual update orchestrator for Push CLI.
|
|
3
3
|
*
|
|
4
|
-
* `push-todo update` performs
|
|
4
|
+
* `push-todo update` performs four actions:
|
|
5
5
|
* 1. Self-update: check and install latest push-todo from npm
|
|
6
|
-
* 2. Agent
|
|
7
|
-
* 3.
|
|
6
|
+
* 2. Agent CLIs: detect versions, check for updates, and install if available
|
|
7
|
+
* 3. Version parity: warn if agents are below minimum required versions
|
|
8
|
+
* 4. Project freshness: fetch and rebase registered projects that are behind
|
|
8
9
|
*
|
|
9
10
|
* Separation of concerns:
|
|
10
|
-
* - Daemon: runs all
|
|
11
|
+
* - Daemon: runs all checks periodically (hourly, throttled, non-interactive)
|
|
12
|
+
* - Self-update: always (gated by auto-update setting)
|
|
13
|
+
* - Agent updates: opt-in (gated by auto-update-agents setting, default OFF)
|
|
14
|
+
* - Project freshness: always (gated by auto-update setting)
|
|
11
15
|
* - This module: runs on explicit user request (immediate, verbose, interactive)
|
|
16
|
+
* - Always checks and updates everything, no settings gate
|
|
12
17
|
*/
|
|
13
18
|
|
|
14
19
|
import { readFileSync } from 'fs';
|
|
15
20
|
import { join, dirname } from 'path';
|
|
16
21
|
import { fileURLToPath } from 'url';
|
|
17
22
|
|
|
18
|
-
import { checkForUpdate, performUpdate
|
|
19
|
-
import { getAgentVersions, getKnownAgentTypes } from './agent-versions.js';
|
|
23
|
+
import { checkForUpdate, performUpdate } from './self-update.js';
|
|
24
|
+
import { getAgentVersions, getKnownAgentTypes, checkForAgentUpdate, performAgentUpdate, checkVersionParity } from './agent-versions.js';
|
|
20
25
|
import { checkProjectFreshness } from './project-freshness.js';
|
|
21
26
|
import { getRegistry } from './project-registry.js';
|
|
22
|
-
import { bold, green, yellow, red,
|
|
27
|
+
import { bold, green, yellow, red, dim } from './utils/colors.js';
|
|
23
28
|
|
|
24
29
|
const __filename = fileURLToPath(import.meta.url);
|
|
25
30
|
const __dirname = dirname(__filename);
|
|
@@ -76,7 +81,7 @@ export async function runManualUpdate(values) {
|
|
|
76
81
|
}
|
|
77
82
|
console.log();
|
|
78
83
|
|
|
79
|
-
// ── 2. Agent
|
|
84
|
+
// ── 2. Agent CLIs ──────────────────────────────────────
|
|
80
85
|
console.log(bold(' Agent CLIs'));
|
|
81
86
|
|
|
82
87
|
const agentVersions = getAgentVersions({ force: true });
|
|
@@ -84,12 +89,41 @@ export async function runManualUpdate(values) {
|
|
|
84
89
|
const info = agentVersions[type];
|
|
85
90
|
const label = AGENT_LABELS[type] || type;
|
|
86
91
|
|
|
87
|
-
if (info.installed
|
|
88
|
-
console.log(` ${label}: ${
|
|
89
|
-
|
|
92
|
+
if (!info.installed) {
|
|
93
|
+
console.log(` ${label}: ${dim('not installed')}`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!info.version) {
|
|
90
98
|
console.log(` ${label}: ${yellow('installed')} ${dim('(version unknown)')}`);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check for update
|
|
103
|
+
const updateInfo = checkForAgentUpdate(type);
|
|
104
|
+
if (updateInfo.available) {
|
|
105
|
+
console.log(` ${label}: v${info.version} -> ${green('v' + updateInfo.latest)} available`);
|
|
106
|
+
console.log(` Updating ${label} to v${updateInfo.latest}...`);
|
|
107
|
+
const success = performAgentUpdate(type, updateInfo.latest);
|
|
108
|
+
if (success) {
|
|
109
|
+
console.log(` ${green('Updated successfully')}`);
|
|
110
|
+
} else {
|
|
111
|
+
console.log(` ${red('Update failed')}`);
|
|
112
|
+
}
|
|
113
|
+
} else if (updateInfo.reason === 'too_recent') {
|
|
114
|
+
console.log(` ${label}: ${green('v' + info.version)} ${dim(`(v${updateInfo.latest} published <1hr ago)`)}`);
|
|
91
115
|
} else {
|
|
92
|
-
console.log(` ${label}: ${
|
|
116
|
+
console.log(` ${label}: ${green('v' + info.version)}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Version parity warnings
|
|
121
|
+
const parityWarnings = checkVersionParity();
|
|
122
|
+
if (parityWarnings.length > 0) {
|
|
123
|
+
console.log();
|
|
124
|
+
for (const w of parityWarnings) {
|
|
125
|
+
const label = AGENT_LABELS[w.agentType] || w.agentType;
|
|
126
|
+
console.log(` ${yellow('Warning')}: ${label} v${w.installed} is below minimum v${w.required}`);
|
|
93
127
|
}
|
|
94
128
|
}
|
|
95
129
|
console.log();
|