@masslessai/push-todo 4.1.5 → 4.1.7
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 +292 -18
- 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
|
@@ -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);
|
|
@@ -115,6 +115,12 @@ const taskStdoutBuffer = new Map(); // displayNumber -> lines[]
|
|
|
115
115
|
const taskStderrBuffer = new Map(); // displayNumber -> lines[]
|
|
116
116
|
const taskProjectPaths = new Map(); // displayNumber -> projectPath
|
|
117
117
|
const taskLastHeartbeat = new Map(); // displayNumber -> timestamp of last progress heartbeat
|
|
118
|
+
|
|
119
|
+
// Stream-json state (Phase 1: real-time progress)
|
|
120
|
+
const taskStreamLineBuffer = new Map(); // displayNumber -> partial NDJSON line fragment
|
|
121
|
+
const taskActivityState = new Map(); // displayNumber -> { filesRead: Set, filesEdited: Set, currentTool: string, lastText: string }
|
|
122
|
+
const taskLastStreamProgress = new Map(); // displayNumber -> timestamp of last stream progress sent
|
|
123
|
+
const STREAM_PROGRESS_THROTTLE_MS = 30000; // 30s between stream progress updates to Supabase
|
|
118
124
|
let daemonStartTime = null;
|
|
119
125
|
|
|
120
126
|
// ==================== Utilities ====================
|
|
@@ -282,6 +288,11 @@ function getAutoUpdateEnabled() {
|
|
|
282
288
|
return v.toLowerCase() === 'true' || v === '1' || v.toLowerCase() === 'yes';
|
|
283
289
|
}
|
|
284
290
|
|
|
291
|
+
function getAutoUpdateAgentsEnabled() {
|
|
292
|
+
const v = getConfigValueFromFile('AUTO_UPDATE_AGENTS', 'false');
|
|
293
|
+
return v.toLowerCase() === 'true' || v === '1' || v.toLowerCase() === 'yes';
|
|
294
|
+
}
|
|
295
|
+
|
|
285
296
|
// ==================== Capabilities Detection ====================
|
|
286
297
|
|
|
287
298
|
let cachedCapabilities = null;
|
|
@@ -1327,6 +1338,155 @@ function checkStuckPatterns(displayNumber, line) {
|
|
|
1327
1338
|
return null;
|
|
1328
1339
|
}
|
|
1329
1340
|
|
|
1341
|
+
// ==================== Stream-JSON Parsing (Phase 1) ====================
|
|
1342
|
+
|
|
1343
|
+
function parseStreamJsonLine(line) {
|
|
1344
|
+
const trimmed = line.trim();
|
|
1345
|
+
if (!trimmed || !trimmed.startsWith('{')) return null;
|
|
1346
|
+
try {
|
|
1347
|
+
return JSON.parse(trimmed);
|
|
1348
|
+
} catch {
|
|
1349
|
+
return null;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
function extractTextFromContent(content) {
|
|
1354
|
+
if (!Array.isArray(content)) return '';
|
|
1355
|
+
return content
|
|
1356
|
+
.filter(block => block.type === 'text' && block.text)
|
|
1357
|
+
.map(block => block.text)
|
|
1358
|
+
.join('\n');
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
function extractToolCallsFromContent(content) {
|
|
1362
|
+
if (!Array.isArray(content)) return [];
|
|
1363
|
+
return content
|
|
1364
|
+
.filter(block => block.type === 'tool_use')
|
|
1365
|
+
.map(block => ({ name: block.name, input: block.input || {} }));
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
function processStreamEvent(displayNumber, event) {
|
|
1369
|
+
if (!event || !event.type) return;
|
|
1370
|
+
|
|
1371
|
+
const activity = taskActivityState.get(displayNumber) || {
|
|
1372
|
+
filesRead: new Set(),
|
|
1373
|
+
filesEdited: new Set(),
|
|
1374
|
+
currentTool: null,
|
|
1375
|
+
lastText: '',
|
|
1376
|
+
sessionId: null
|
|
1377
|
+
};
|
|
1378
|
+
|
|
1379
|
+
// Extract session_id from system init or result messages
|
|
1380
|
+
if (event.type === 'system' && event.session_id) {
|
|
1381
|
+
activity.sessionId = event.session_id;
|
|
1382
|
+
}
|
|
1383
|
+
if (event.type === 'result' && event.session_id) {
|
|
1384
|
+
activity.sessionId = event.session_id;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// Extract activity from assistant messages
|
|
1388
|
+
if (event.type === 'assistant') {
|
|
1389
|
+
const content = event.message?.content;
|
|
1390
|
+
if (!content) { taskActivityState.set(displayNumber, activity); return; }
|
|
1391
|
+
|
|
1392
|
+
const text = extractTextFromContent(content);
|
|
1393
|
+
if (text) {
|
|
1394
|
+
activity.lastText = text.slice(0, 200);
|
|
1395
|
+
|
|
1396
|
+
// Run existing text-based checks on extracted content
|
|
1397
|
+
if (text.includes('[push-confirm] Waiting for')) {
|
|
1398
|
+
updateTaskDetail(displayNumber, {
|
|
1399
|
+
phase: 'awaiting_confirmation',
|
|
1400
|
+
detail: 'Waiting for user confirmation on iPhone'
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
const stuckReason = checkStuckPatterns(displayNumber, text);
|
|
1405
|
+
if (stuckReason) {
|
|
1406
|
+
log(`Task #${displayNumber} may be stuck: ${stuckReason}`);
|
|
1407
|
+
updateTaskDetail(displayNumber, {
|
|
1408
|
+
phase: 'stuck',
|
|
1409
|
+
detail: `Waiting for input: ${stuckReason}`
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
const toolCalls = extractToolCallsFromContent(content);
|
|
1415
|
+
for (const tool of toolCalls) {
|
|
1416
|
+
activity.currentTool = tool.name;
|
|
1417
|
+
const filePath = tool.input?.file_path || tool.input?.path;
|
|
1418
|
+
if (filePath) {
|
|
1419
|
+
if (tool.name === 'Read') {
|
|
1420
|
+
activity.filesRead.add(filePath);
|
|
1421
|
+
} else if (tool.name === 'Edit' || tool.name === 'Write') {
|
|
1422
|
+
activity.filesEdited.add(filePath);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
taskActivityState.set(displayNumber, activity);
|
|
1429
|
+
|
|
1430
|
+
// Throttled progress reporting to Supabase
|
|
1431
|
+
maybesSendStreamProgress(displayNumber, activity);
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
function maybesSendStreamProgress(displayNumber, activity) {
|
|
1435
|
+
const now = Date.now();
|
|
1436
|
+
const lastSent = taskLastStreamProgress.get(displayNumber) || 0;
|
|
1437
|
+
if (now - lastSent < STREAM_PROGRESS_THROTTLE_MS) return;
|
|
1438
|
+
taskLastStreamProgress.set(displayNumber, now);
|
|
1439
|
+
|
|
1440
|
+
const info = taskDetails.get(displayNumber) || {};
|
|
1441
|
+
const taskId = info.taskId || null;
|
|
1442
|
+
const taskInfo = runningTasks.get(displayNumber);
|
|
1443
|
+
if (!taskInfo) return;
|
|
1444
|
+
|
|
1445
|
+
const elapsedSec = Math.floor((now - taskInfo.startTime) / 1000);
|
|
1446
|
+
const elapsedMin = Math.floor(elapsedSec / 60);
|
|
1447
|
+
|
|
1448
|
+
// Build a meaningful summary from stream activity
|
|
1449
|
+
const parts = [`Running for ${elapsedMin}m.`];
|
|
1450
|
+
if (activity.filesEdited.size > 0) {
|
|
1451
|
+
const editedList = [...activity.filesEdited].map(f => f.split('/').pop()).slice(-5);
|
|
1452
|
+
parts.push(`Edited: ${editedList.join(', ')}`);
|
|
1453
|
+
}
|
|
1454
|
+
if (activity.filesRead.size > 0) {
|
|
1455
|
+
parts.push(`Read ${activity.filesRead.size} files.`);
|
|
1456
|
+
}
|
|
1457
|
+
if (activity.currentTool) {
|
|
1458
|
+
parts.push(`Current: ${activity.currentTool}`);
|
|
1459
|
+
}
|
|
1460
|
+
if (activity.lastText && !activity.currentTool) {
|
|
1461
|
+
parts.push(activity.lastText.slice(0, 80));
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
const eventSummary = parts.join(' ');
|
|
1465
|
+
|
|
1466
|
+
log(`Task #${displayNumber}: stream progress (${eventSummary})`);
|
|
1467
|
+
|
|
1468
|
+
// Also update the existing heartbeat timestamp to prevent duplicate generic heartbeats
|
|
1469
|
+
taskLastHeartbeat.set(displayNumber, now);
|
|
1470
|
+
|
|
1471
|
+
apiRequest('update-task-execution', {
|
|
1472
|
+
method: 'PATCH',
|
|
1473
|
+
body: JSON.stringify({
|
|
1474
|
+
todoId: taskId,
|
|
1475
|
+
displayNumber,
|
|
1476
|
+
event: {
|
|
1477
|
+
type: 'progress',
|
|
1478
|
+
timestamp: new Date().toISOString(),
|
|
1479
|
+
machineName: getMachineName() || undefined,
|
|
1480
|
+
summary: eventSummary,
|
|
1481
|
+
filesEdited: [...activity.filesEdited].slice(-10),
|
|
1482
|
+
filesRead: activity.filesRead.size
|
|
1483
|
+
}
|
|
1484
|
+
})
|
|
1485
|
+
}).catch(err => {
|
|
1486
|
+
log(`Task #${displayNumber}: stream progress failed (non-fatal): ${err.message}`);
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1330
1490
|
function monitorTaskStdout(displayNumber, proc) {
|
|
1331
1491
|
if (!proc.stdout) return;
|
|
1332
1492
|
|
|
@@ -1424,7 +1584,23 @@ async function sendProgressHeartbeats() {
|
|
|
1424
1584
|
|
|
1425
1585
|
const activityDesc = idleSec < 60 ? 'active' : `idle ${idleMin}m`;
|
|
1426
1586
|
const phase = info.phase || 'executing';
|
|
1427
|
-
|
|
1587
|
+
|
|
1588
|
+
// Enrich heartbeat with stream activity data when available
|
|
1589
|
+
const activity = taskActivityState.get(displayNumber);
|
|
1590
|
+
const parts = [`Running for ${elapsedMin}m. Last activity: ${activityDesc}. Phase: ${phase}.`];
|
|
1591
|
+
const eventExtra = {};
|
|
1592
|
+
if (activity) {
|
|
1593
|
+
if (activity.filesEdited.size > 0) {
|
|
1594
|
+
const editedList = [...activity.filesEdited].map(f => f.split('/').pop()).slice(-5);
|
|
1595
|
+
parts.push(`Edited: ${editedList.join(', ')}`);
|
|
1596
|
+
eventExtra.filesEdited = [...activity.filesEdited].slice(-10);
|
|
1597
|
+
}
|
|
1598
|
+
if (activity.filesRead.size > 0) {
|
|
1599
|
+
parts.push(`Read ${activity.filesRead.size} files.`);
|
|
1600
|
+
eventExtra.filesRead = activity.filesRead.size;
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
const eventSummary = parts.join(' ');
|
|
1428
1604
|
|
|
1429
1605
|
log(`Task #${displayNumber}: sending progress heartbeat (${eventSummary})`);
|
|
1430
1606
|
|
|
@@ -1442,7 +1618,8 @@ async function sendProgressHeartbeats() {
|
|
|
1442
1618
|
type: 'progress',
|
|
1443
1619
|
timestamp: new Date().toISOString(),
|
|
1444
1620
|
machineName: getMachineName() || undefined,
|
|
1445
|
-
summary: eventSummary
|
|
1621
|
+
summary: eventSummary,
|
|
1622
|
+
...eventExtra
|
|
1446
1623
|
}
|
|
1447
1624
|
// No status field — event-only update, won't change execution_status
|
|
1448
1625
|
})
|
|
@@ -1538,6 +1715,9 @@ async function killIdleTasks() {
|
|
|
1538
1715
|
taskStderrBuffer.delete(displayNumber);
|
|
1539
1716
|
taskProjectPaths.delete(displayNumber);
|
|
1540
1717
|
taskLastHeartbeat.delete(displayNumber);
|
|
1718
|
+
taskStreamLineBuffer.delete(displayNumber);
|
|
1719
|
+
taskActivityState.delete(displayNumber);
|
|
1720
|
+
taskLastStreamProgress.delete(displayNumber);
|
|
1541
1721
|
}
|
|
1542
1722
|
|
|
1543
1723
|
if (idleTimedOut.length > 0) {
|
|
@@ -1547,7 +1727,14 @@ async function killIdleTasks() {
|
|
|
1547
1727
|
|
|
1548
1728
|
// ==================== Session ID Extraction ====================
|
|
1549
1729
|
|
|
1550
|
-
function extractSessionIdFromStdout(proc, buffer) {
|
|
1730
|
+
function extractSessionIdFromStdout(proc, buffer, displayNumber) {
|
|
1731
|
+
// First: check stream activity state (populated by stream-json parsing)
|
|
1732
|
+
if (displayNumber != null) {
|
|
1733
|
+
const activity = taskActivityState.get(displayNumber);
|
|
1734
|
+
if (activity?.sessionId) return activity.sessionId;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
// Fallback: drain remaining stdout and scan for session_id in JSON lines
|
|
1551
1738
|
let remaining = '';
|
|
1552
1739
|
if (proc.stdout) {
|
|
1553
1740
|
try {
|
|
@@ -1557,7 +1744,6 @@ function extractSessionIdFromStdout(proc, buffer) {
|
|
|
1557
1744
|
|
|
1558
1745
|
const allOutput = buffer.join('\n') + '\n' + remaining;
|
|
1559
1746
|
|
|
1560
|
-
// Try to parse JSON output
|
|
1561
1747
|
for (const line of allOutput.split('\n')) {
|
|
1562
1748
|
const trimmed = line.trim();
|
|
1563
1749
|
if (trimmed.startsWith('{') && trimmed.includes('session_id')) {
|
|
@@ -1822,14 +2008,14 @@ async function executeTask(task) {
|
|
|
1822
2008
|
'--continue', previousSessionId,
|
|
1823
2009
|
'-p', prompt,
|
|
1824
2010
|
'--allowedTools', allowedTools,
|
|
1825
|
-
'--output-format', 'json',
|
|
2011
|
+
'--output-format', 'stream-json',
|
|
1826
2012
|
'--permission-mode', 'bypassPermissions',
|
|
1827
2013
|
'--session-id', preAssignedSessionId
|
|
1828
2014
|
]
|
|
1829
2015
|
: [
|
|
1830
2016
|
'-p', prompt,
|
|
1831
2017
|
'--allowedTools', allowedTools,
|
|
1832
|
-
'--output-format', 'json',
|
|
2018
|
+
'--output-format', 'stream-json',
|
|
1833
2019
|
'--permission-mode', 'bypassPermissions',
|
|
1834
2020
|
'--session-id', preAssignedSessionId
|
|
1835
2021
|
];
|
|
@@ -1878,25 +2064,44 @@ async function executeTask(task) {
|
|
|
1878
2064
|
}
|
|
1879
2065
|
});
|
|
1880
2066
|
|
|
1881
|
-
// Monitor stdout
|
|
2067
|
+
// Monitor stdout (stream-json NDJSON parsing)
|
|
2068
|
+
taskStreamLineBuffer.set(displayNumber, '');
|
|
2069
|
+
taskActivityState.set(displayNumber, {
|
|
2070
|
+
filesRead: new Set(), filesEdited: new Set(),
|
|
2071
|
+
currentTool: null, lastText: '', sessionId: null
|
|
2072
|
+
});
|
|
2073
|
+
taskLastStreamProgress.set(displayNumber, Date.now());
|
|
2074
|
+
|
|
1882
2075
|
child.stdout.on('data', (data) => {
|
|
1883
|
-
|
|
2076
|
+
taskLastOutput.set(displayNumber, Date.now());
|
|
2077
|
+
|
|
2078
|
+
// NDJSON line buffering: handle chunks that split across line boundaries
|
|
2079
|
+
const pending = (taskStreamLineBuffer.get(displayNumber) || '') + data.toString();
|
|
2080
|
+
const lines = pending.split('\n');
|
|
2081
|
+
// Last element is either empty (chunk ended with \n) or a partial line
|
|
2082
|
+
taskStreamLineBuffer.set(displayNumber, lines.pop() || '');
|
|
2083
|
+
|
|
1884
2084
|
for (const line of lines) {
|
|
1885
|
-
if (line.trim())
|
|
1886
|
-
taskLastOutput.set(displayNumber, Date.now());
|
|
1887
|
-
const buffer = taskStdoutBuffer.get(displayNumber) || [];
|
|
1888
|
-
buffer.push(line);
|
|
1889
|
-
if (buffer.length > 20) buffer.shift();
|
|
1890
|
-
taskStdoutBuffer.set(displayNumber, buffer);
|
|
2085
|
+
if (!line.trim()) continue;
|
|
1891
2086
|
|
|
1892
|
-
|
|
2087
|
+
// Keep raw lines in circular buffer for debugging/fallback
|
|
2088
|
+
const buffer = taskStdoutBuffer.get(displayNumber) || [];
|
|
2089
|
+
buffer.push(line);
|
|
2090
|
+
if (buffer.length > 50) buffer.shift();
|
|
2091
|
+
taskStdoutBuffer.set(displayNumber, buffer);
|
|
2092
|
+
|
|
2093
|
+
// Parse as NDJSON stream event
|
|
2094
|
+
const event = parseStreamJsonLine(line);
|
|
2095
|
+
if (event) {
|
|
2096
|
+
processStreamEvent(displayNumber, event);
|
|
2097
|
+
} else {
|
|
2098
|
+
// Fallback: line isn't valid JSON — run legacy text checks
|
|
1893
2099
|
if (line.includes('[push-confirm] Waiting for')) {
|
|
1894
2100
|
updateTaskDetail(displayNumber, {
|
|
1895
2101
|
phase: 'awaiting_confirmation',
|
|
1896
2102
|
detail: 'Waiting for user confirmation on iPhone'
|
|
1897
2103
|
});
|
|
1898
2104
|
}
|
|
1899
|
-
|
|
1900
2105
|
const stuckReason = checkStuckPatterns(displayNumber, line);
|
|
1901
2106
|
if (stuckReason) {
|
|
1902
2107
|
log(`Task #${displayNumber} may be stuck: ${stuckReason}`);
|
|
@@ -1920,6 +2125,9 @@ async function executeTask(task) {
|
|
|
1920
2125
|
await updateTaskStatus(displayNumber, 'failed', { error: error.message }, taskId);
|
|
1921
2126
|
taskDetails.delete(displayNumber);
|
|
1922
2127
|
taskLastHeartbeat.delete(displayNumber);
|
|
2128
|
+
taskStreamLineBuffer.delete(displayNumber);
|
|
2129
|
+
taskActivityState.delete(displayNumber);
|
|
2130
|
+
taskLastStreamProgress.delete(displayNumber);
|
|
1923
2131
|
updateStatusFile();
|
|
1924
2132
|
});
|
|
1925
2133
|
|
|
@@ -1936,6 +2144,9 @@ async function executeTask(task) {
|
|
|
1936
2144
|
logError(`Error starting Claude for task #${displayNumber}: ${error.message}`);
|
|
1937
2145
|
await updateTaskStatus(displayNumber, 'failed', { error: error.message }, taskId);
|
|
1938
2146
|
taskDetails.delete(displayNumber);
|
|
2147
|
+
taskStreamLineBuffer.delete(displayNumber);
|
|
2148
|
+
taskActivityState.delete(displayNumber);
|
|
2149
|
+
taskLastStreamProgress.delete(displayNumber);
|
|
1939
2150
|
return null;
|
|
1940
2151
|
}
|
|
1941
2152
|
}
|
|
@@ -1954,7 +2165,7 @@ async function handleTaskCompletion(displayNumber, exitCode) {
|
|
|
1954
2165
|
log(`Task #${displayNumber} completed with code ${exitCode} (${duration}s)`);
|
|
1955
2166
|
|
|
1956
2167
|
// Session ID: prefer pre-assigned ID (reliable), fall back to stdout extraction
|
|
1957
|
-
const sessionId = taskInfo.sessionId || extractSessionIdFromStdout(taskInfo.process, taskStdoutBuffer.get(displayNumber) || []);
|
|
2168
|
+
const sessionId = taskInfo.sessionId || extractSessionIdFromStdout(taskInfo.process, taskStdoutBuffer.get(displayNumber) || [], displayNumber);
|
|
1958
2169
|
const worktreePath = getWorktreePath(displayNumber, projectPath);
|
|
1959
2170
|
const durationStr = duration < 60 ? `${duration}s` : `${Math.floor(duration / 60)}m ${duration % 60}s`;
|
|
1960
2171
|
const machineName = getMachineName() || 'Mac';
|
|
@@ -2296,6 +2507,9 @@ async function checkTimeouts() {
|
|
|
2296
2507
|
taskStderrBuffer.delete(displayNumber);
|
|
2297
2508
|
taskProjectPaths.delete(displayNumber);
|
|
2298
2509
|
taskLastHeartbeat.delete(displayNumber);
|
|
2510
|
+
taskStreamLineBuffer.delete(displayNumber);
|
|
2511
|
+
taskActivityState.delete(displayNumber);
|
|
2512
|
+
taskLastStreamProgress.delete(displayNumber);
|
|
2299
2513
|
cleanupWorktree(displayNumber, projectPath);
|
|
2300
2514
|
}
|
|
2301
2515
|
|
|
@@ -2355,6 +2569,55 @@ function checkAndApplyUpdate() {
|
|
|
2355
2569
|
}
|
|
2356
2570
|
}
|
|
2357
2571
|
|
|
2572
|
+
// ==================== Agent Auto-Update ====================
|
|
2573
|
+
|
|
2574
|
+
let pendingAgentUpdates = null; // { agentType: targetVersion, ... }
|
|
2575
|
+
|
|
2576
|
+
function checkAndApplyAgentUpdates() {
|
|
2577
|
+
// Check for updates (throttled internally to once per hour)
|
|
2578
|
+
if (!pendingAgentUpdates) {
|
|
2579
|
+
const results = checkAllAgentUpdates();
|
|
2580
|
+
if (!results) return; // throttled
|
|
2581
|
+
|
|
2582
|
+
const available = {};
|
|
2583
|
+
for (const [agentType, info] of Object.entries(results)) {
|
|
2584
|
+
if (info.available) {
|
|
2585
|
+
available[agentType] = info.latest;
|
|
2586
|
+
log(`Agent update available: ${agentType} v${info.current} -> v${info.latest}`);
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
if (Object.keys(available).length > 0) {
|
|
2590
|
+
pendingAgentUpdates = available;
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
// Only apply when no tasks are running
|
|
2595
|
+
if (pendingAgentUpdates && runningTasks.size === 0) {
|
|
2596
|
+
for (const [agentType, targetVersion] of Object.entries(pendingAgentUpdates)) {
|
|
2597
|
+
log(`Updating ${agentType} to v${targetVersion}...`);
|
|
2598
|
+
const success = performAgentUpdate(agentType, targetVersion);
|
|
2599
|
+
if (success) {
|
|
2600
|
+
log(`${agentType} updated to v${targetVersion}`);
|
|
2601
|
+
sendMacNotification(
|
|
2602
|
+
'Push: Agent Updated',
|
|
2603
|
+
`${agentType} updated to v${targetVersion}`,
|
|
2604
|
+
'Glass'
|
|
2605
|
+
);
|
|
2606
|
+
} else {
|
|
2607
|
+
logError(`${agentType} update to v${targetVersion} failed`);
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
pendingAgentUpdates = null;
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
function logVersionParityWarnings() {
|
|
2615
|
+
const warnings = checkVersionParity();
|
|
2616
|
+
for (const w of warnings) {
|
|
2617
|
+
log(`WARNING: ${w.agentType} v${w.installed} is below minimum v${w.required} — some features may not work`);
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2358
2621
|
// ==================== Main Loop ====================
|
|
2359
2622
|
|
|
2360
2623
|
async function pollAndExecute() {
|
|
@@ -2457,10 +2720,12 @@ async function mainLoop() {
|
|
|
2457
2720
|
log(`Max concurrent tasks: ${MAX_CONCURRENT_TASKS}`);
|
|
2458
2721
|
log(`E2EE: ${e2eeAvailable ? 'Available' : 'Not available'}`);
|
|
2459
2722
|
log(`Auto-update: ${getAutoUpdateEnabled() ? 'Enabled' : 'Disabled'}`);
|
|
2723
|
+
log(`Auto-update-agents: ${getAutoUpdateAgentsEnabled() ? 'Enabled' : 'Disabled'}`);
|
|
2460
2724
|
const caps = getCapabilities();
|
|
2461
2725
|
log(`Capabilities: gh=${caps.gh_cli}, auto-merge=${caps.auto_merge}, auto-complete=${caps.auto_complete}`);
|
|
2462
2726
|
const agentVersions = getAgentVersions({ force: true });
|
|
2463
2727
|
log(`Agent versions: ${formatAgentVersionSummary(agentVersions)}`);
|
|
2728
|
+
logVersionParityWarnings();
|
|
2464
2729
|
log(`Log file: ${LOG_FILE}`);
|
|
2465
2730
|
|
|
2466
2731
|
// Show registered projects
|
|
@@ -2559,6 +2824,15 @@ async function mainLoop() {
|
|
|
2559
2824
|
if (getAutoUpdateEnabled()) {
|
|
2560
2825
|
checkAndApplyUpdate();
|
|
2561
2826
|
}
|
|
2827
|
+
|
|
2828
|
+
// Agent CLI auto-update (throttled to once per hour, only applies when idle)
|
|
2829
|
+
if (getAutoUpdateAgentsEnabled()) {
|
|
2830
|
+
try {
|
|
2831
|
+
checkAndApplyAgentUpdates();
|
|
2832
|
+
} catch (error) {
|
|
2833
|
+
logError(`Agent auto-update error: ${error.message}`);
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2562
2836
|
} catch (error) {
|
|
2563
2837
|
logError(`Poll error: ${error.message}`);
|
|
2564
2838
|
}
|
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();
|