@masslessai/push-todo 4.1.5 → 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 +66 -1
- 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);
|
|
@@ -282,6 +282,11 @@ function getAutoUpdateEnabled() {
|
|
|
282
282
|
return v.toLowerCase() === 'true' || v === '1' || v.toLowerCase() === 'yes';
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
+
function getAutoUpdateAgentsEnabled() {
|
|
286
|
+
const v = getConfigValueFromFile('AUTO_UPDATE_AGENTS', 'false');
|
|
287
|
+
return v.toLowerCase() === 'true' || v === '1' || v.toLowerCase() === 'yes';
|
|
288
|
+
}
|
|
289
|
+
|
|
285
290
|
// ==================== Capabilities Detection ====================
|
|
286
291
|
|
|
287
292
|
let cachedCapabilities = null;
|
|
@@ -2355,6 +2360,55 @@ function checkAndApplyUpdate() {
|
|
|
2355
2360
|
}
|
|
2356
2361
|
}
|
|
2357
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
|
+
|
|
2358
2412
|
// ==================== Main Loop ====================
|
|
2359
2413
|
|
|
2360
2414
|
async function pollAndExecute() {
|
|
@@ -2457,10 +2511,12 @@ async function mainLoop() {
|
|
|
2457
2511
|
log(`Max concurrent tasks: ${MAX_CONCURRENT_TASKS}`);
|
|
2458
2512
|
log(`E2EE: ${e2eeAvailable ? 'Available' : 'Not available'}`);
|
|
2459
2513
|
log(`Auto-update: ${getAutoUpdateEnabled() ? 'Enabled' : 'Disabled'}`);
|
|
2514
|
+
log(`Auto-update-agents: ${getAutoUpdateAgentsEnabled() ? 'Enabled' : 'Disabled'}`);
|
|
2460
2515
|
const caps = getCapabilities();
|
|
2461
2516
|
log(`Capabilities: gh=${caps.gh_cli}, auto-merge=${caps.auto_merge}, auto-complete=${caps.auto_complete}`);
|
|
2462
2517
|
const agentVersions = getAgentVersions({ force: true });
|
|
2463
2518
|
log(`Agent versions: ${formatAgentVersionSummary(agentVersions)}`);
|
|
2519
|
+
logVersionParityWarnings();
|
|
2464
2520
|
log(`Log file: ${LOG_FILE}`);
|
|
2465
2521
|
|
|
2466
2522
|
// Show registered projects
|
|
@@ -2559,6 +2615,15 @@ async function mainLoop() {
|
|
|
2559
2615
|
if (getAutoUpdateEnabled()) {
|
|
2560
2616
|
checkAndApplyUpdate();
|
|
2561
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
|
+
}
|
|
2562
2627
|
} catch (error) {
|
|
2563
2628
|
logError(`Poll error: ${error.message}`);
|
|
2564
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();
|