clementine-agent 1.1.21 → 1.1.22
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/dist/cli/index.js +199 -8
- package/dist/cli/version-check.d.ts +35 -0
- package/dist/cli/version-check.js +147 -0
- package/dist/index.js +8 -2
- package/dist/types.d.ts +4 -0
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -428,17 +428,22 @@ async function cmdRestart(options) {
|
|
|
428
428
|
}
|
|
429
429
|
}
|
|
430
430
|
function cmdStatus() {
|
|
431
|
+
const DIM = '\x1b[0;90m';
|
|
432
|
+
const RESET = '\x1b[0m';
|
|
431
433
|
const pid = readPid();
|
|
432
434
|
const name = getAssistantName();
|
|
435
|
+
const localVersion = readPkgVersion(PACKAGE_ROOT);
|
|
433
436
|
if (!pid) {
|
|
434
|
-
console.log(` ${name} is not running (no PID file).`);
|
|
437
|
+
console.log(` ${name} is not running ${DIM}(no PID file, v${localVersion})${RESET}.`);
|
|
438
|
+
surfaceUpdateNudge(localVersion);
|
|
435
439
|
return;
|
|
436
440
|
}
|
|
437
441
|
if (!isProcessAlive(pid)) {
|
|
438
|
-
console.log(` ${name} is not running (stale PID ${pid}).`);
|
|
442
|
+
console.log(` ${name} is not running ${DIM}(stale PID ${pid}, v${localVersion})${RESET}.`);
|
|
443
|
+
surfaceUpdateNudge(localVersion);
|
|
439
444
|
return;
|
|
440
445
|
}
|
|
441
|
-
console.log(` ${name} is running (PID ${pid})`);
|
|
446
|
+
console.log(` ${name} is running ${DIM}(PID ${pid}, v${localVersion})${RESET}`);
|
|
442
447
|
// Show uptime from PID file mtime
|
|
443
448
|
try {
|
|
444
449
|
const { mtimeMs } = statSync(getPidFilePath());
|
|
@@ -468,6 +473,36 @@ function cmdStatus() {
|
|
|
468
473
|
if (channels.length > 0) {
|
|
469
474
|
console.log(` Channels: ${channels.join(', ')}`);
|
|
470
475
|
}
|
|
476
|
+
surfaceUpdateNudge(localVersion);
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Print a one-line nudge if a newer version is on npm. Reads the cached
|
|
480
|
+
* result synchronously (no network on the hot path) and fires off an async
|
|
481
|
+
* refresh in the background so the next call has fresh data.
|
|
482
|
+
*/
|
|
483
|
+
function surfaceUpdateNudge(localVersion) {
|
|
484
|
+
const DIM = '\x1b[0;90m';
|
|
485
|
+
const BOLD = '\x1b[1m';
|
|
486
|
+
const YELLOW = '\x1b[1;33m';
|
|
487
|
+
const RESET = '\x1b[0m';
|
|
488
|
+
try {
|
|
489
|
+
const cached = (() => {
|
|
490
|
+
// Lazy require to avoid pulling https/network into trivial CLI calls
|
|
491
|
+
// when the cache module isn't needed.
|
|
492
|
+
const { readCachedUpdateCheck } = require('./version-check.js');
|
|
493
|
+
return readCachedUpdateCheck(BASE_DIR, localVersion);
|
|
494
|
+
})();
|
|
495
|
+
if (cached?.updateAvailable && cached.latestVersion) {
|
|
496
|
+
console.log(` ${YELLOW}⬆${RESET} Update available: ${BOLD}v${cached.latestVersion}${RESET} ${DIM}(you're on v${localVersion})${RESET}`);
|
|
497
|
+
console.log(` ${DIM}Run: ${BOLD}clementine update restart${RESET}`);
|
|
498
|
+
}
|
|
499
|
+
// Fire-and-forget background refresh — never blocks status output.
|
|
500
|
+
const { checkForUpdate } = require('./version-check.js');
|
|
501
|
+
void checkForUpdate(BASE_DIR, localVersion).catch(() => { });
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
// version-check failed to load — degrade silently
|
|
505
|
+
}
|
|
471
506
|
}
|
|
472
507
|
function cmdDoctor(opts = {}) {
|
|
473
508
|
const DIM = '\x1b[0;90m';
|
|
@@ -1991,11 +2026,16 @@ program
|
|
|
1991
2026
|
});
|
|
1992
2027
|
program
|
|
1993
2028
|
.command('update')
|
|
1994
|
-
.description('Pull latest code, rebuild, and reinstall (preserves config)')
|
|
1995
|
-
.argument('[action]', 'Optional: "restart"
|
|
2029
|
+
.description('Pull latest code, rebuild, and reinstall (preserves config). Pass "history" to show recent updates.')
|
|
2030
|
+
.argument('[action]', 'Optional: "restart" = restart daemon after update; "history" = show update log')
|
|
1996
2031
|
.option('--restart', 'Restart daemon after update')
|
|
1997
2032
|
.option('--dry-run', 'Preview what would happen without making changes')
|
|
2033
|
+
.option('-n, --limit <n>', 'For history mode: max entries to show', '10')
|
|
1998
2034
|
.action((action, options) => {
|
|
2035
|
+
if (action === 'history') {
|
|
2036
|
+
cmdUpdateHistory(parseInt(options.limit ?? '10', 10));
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
1999
2039
|
if (action === 'restart')
|
|
2000
2040
|
options.restart = true;
|
|
2001
2041
|
cmdUpdate(options).catch((err) => {
|
|
@@ -2583,14 +2623,113 @@ projectsCmd
|
|
|
2583
2623
|
}
|
|
2584
2624
|
});
|
|
2585
2625
|
// ── Update command ──────────────────────────────────────────────────
|
|
2626
|
+
/** Print the last N entries from update-history.jsonl. */
|
|
2627
|
+
function cmdUpdateHistory(limit) {
|
|
2628
|
+
const BOLD = '\x1b[1m';
|
|
2629
|
+
const DIM = '\x1b[0;90m';
|
|
2630
|
+
const GREEN = '\x1b[0;32m';
|
|
2631
|
+
const RED = '\x1b[0;31m';
|
|
2632
|
+
const RESET = '\x1b[0m';
|
|
2633
|
+
const historyPath = path.join(BASE_DIR, 'update-history.jsonl');
|
|
2634
|
+
if (!existsSync(historyPath)) {
|
|
2635
|
+
console.log();
|
|
2636
|
+
console.log(` ${DIM}No update history yet (${historyPath} doesn't exist).${RESET}`);
|
|
2637
|
+
console.log(` Run ${BOLD}clementine update${RESET} once to start the log.`);
|
|
2638
|
+
console.log();
|
|
2639
|
+
return;
|
|
2640
|
+
}
|
|
2641
|
+
const lines = readFileSync(historyPath, 'utf-8').split('\n').filter(Boolean);
|
|
2642
|
+
const entries = lines
|
|
2643
|
+
.map(l => { try {
|
|
2644
|
+
return JSON.parse(l);
|
|
2645
|
+
}
|
|
2646
|
+
catch {
|
|
2647
|
+
return null;
|
|
2648
|
+
} })
|
|
2649
|
+
.filter((e) => e !== null)
|
|
2650
|
+
.slice(-Math.max(1, limit))
|
|
2651
|
+
.reverse();
|
|
2652
|
+
if (entries.length === 0) {
|
|
2653
|
+
console.log(` ${DIM}History file exists but is empty or unparseable.${RESET}`);
|
|
2654
|
+
return;
|
|
2655
|
+
}
|
|
2656
|
+
console.log();
|
|
2657
|
+
console.log(` ${BOLD}Update history${RESET} ${DIM}(${historyPath})${RESET}`);
|
|
2658
|
+
console.log();
|
|
2659
|
+
for (const e of entries) {
|
|
2660
|
+
const ts = String(e.timestamp ?? '').slice(0, 19).replace('T', ' ');
|
|
2661
|
+
const from = String(e.fromVersion ?? '?');
|
|
2662
|
+
const to = String(e.toVersion ?? '?');
|
|
2663
|
+
const flavor = String(e.flavor ?? 'git');
|
|
2664
|
+
const failed = e.failed === true;
|
|
2665
|
+
const arrow = from === to ? '=' : '→';
|
|
2666
|
+
const verLabel = failed
|
|
2667
|
+
? `${RED}v${from} ${arrow} v${to} FAILED${RESET}`
|
|
2668
|
+
: (from === to ? `${DIM}v${from}${RESET}` : `v${from} ${arrow} ${BOLD}v${to}${RESET}`);
|
|
2669
|
+
const dur = typeof e.durationMs === 'number' ? ` ${DIM}(${Math.round(e.durationMs / 1000)}s)${RESET}` : '';
|
|
2670
|
+
console.log(` ${DIM}${ts}${RESET} ${verLabel} ${DIM}[${flavor}]${RESET}${dur}`);
|
|
2671
|
+
if (typeof e.commitHash === 'string' && e.commitHash) {
|
|
2672
|
+
console.log(` ${DIM}commit ${e.commitHash}${e.commitDate ? ` (${e.commitDate})` : ''}, ${e.commitsPulled ?? 0} commit${e.commitsPulled === 1 ? '' : 's'} pulled${RESET}`);
|
|
2673
|
+
}
|
|
2674
|
+
if (typeof e.summary === 'string' && e.summary) {
|
|
2675
|
+
const trimmed = e.summary.length > 100 ? e.summary.slice(0, 100) + '…' : e.summary;
|
|
2676
|
+
console.log(` ${DIM}${trimmed}${RESET}`);
|
|
2677
|
+
}
|
|
2678
|
+
if (failed && typeof e.error === 'string') {
|
|
2679
|
+
console.log(` ${RED}error: ${e.error.slice(0, 120)}${RESET}`);
|
|
2680
|
+
}
|
|
2681
|
+
const modSummary = [];
|
|
2682
|
+
if (typeof e.modsReapplied === 'number' && e.modsReapplied > 0)
|
|
2683
|
+
modSummary.push(`${e.modsReapplied} re-applied`);
|
|
2684
|
+
if (typeof e.modsSuperseded === 'number' && e.modsSuperseded > 0)
|
|
2685
|
+
modSummary.push(`${e.modsSuperseded} superseded`);
|
|
2686
|
+
if (typeof e.modsNeedReconciliation === 'number' && e.modsNeedReconciliation > 0)
|
|
2687
|
+
modSummary.push(`${e.modsNeedReconciliation} need attention`);
|
|
2688
|
+
if (typeof e.modsFailed === 'number' && e.modsFailed > 0)
|
|
2689
|
+
modSummary.push(`${e.modsFailed} failed`);
|
|
2690
|
+
if (modSummary.length > 0) {
|
|
2691
|
+
console.log(` ${DIM}source mods: ${modSummary.join(', ')}${RESET}`);
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
console.log();
|
|
2695
|
+
console.log(` ${GREEN}Showing ${entries.length}${RESET}${DIM} of ${lines.length} total entries.${RESET}`);
|
|
2696
|
+
console.log();
|
|
2697
|
+
}
|
|
2698
|
+
/** Read the npm version from a package.json (returns 'unknown' on failure). */
|
|
2699
|
+
function readPkgVersion(packageRoot) {
|
|
2700
|
+
try {
|
|
2701
|
+
const pkgPath = path.join(packageRoot, 'package.json');
|
|
2702
|
+
if (!existsSync(pkgPath))
|
|
2703
|
+
return 'unknown';
|
|
2704
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
2705
|
+
return pkg.version ?? 'unknown';
|
|
2706
|
+
}
|
|
2707
|
+
catch {
|
|
2708
|
+
return 'unknown';
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
/** Append one line to the update-history log. Append-only, never throws. */
|
|
2712
|
+
function appendUpdateHistory(entry) {
|
|
2713
|
+
try {
|
|
2714
|
+
const historyPath = path.join(BASE_DIR, 'update-history.jsonl');
|
|
2715
|
+
const line = JSON.stringify({ timestamp: new Date().toISOString(), ...entry }) + '\n';
|
|
2716
|
+
require('node:fs').appendFileSync(historyPath, line, { mode: 0o600 });
|
|
2717
|
+
}
|
|
2718
|
+
catch {
|
|
2719
|
+
// Non-fatal — history is observability, not critical state.
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2586
2722
|
async function cmdUpdate(options) {
|
|
2587
2723
|
const DIM = '\x1b[0;90m';
|
|
2724
|
+
const BOLD = '\x1b[1m';
|
|
2588
2725
|
const GREEN = '\x1b[0;32m';
|
|
2589
2726
|
const YELLOW = '\x1b[1;33m';
|
|
2590
2727
|
const RED = '\x1b[0;31m';
|
|
2591
2728
|
const RESET = '\x1b[0m';
|
|
2729
|
+
const updateStartedAt = Date.now();
|
|
2730
|
+
const previousVersion = readPkgVersion(PACKAGE_ROOT);
|
|
2592
2731
|
console.log();
|
|
2593
|
-
console.log(` ${DIM}Updating ${getAssistantName()}...${RESET}`);
|
|
2732
|
+
console.log(` ${DIM}Updating ${getAssistantName()} (current: v${previousVersion})...${RESET}`);
|
|
2594
2733
|
console.log();
|
|
2595
2734
|
// 1. Detect install flavor. Two valid paths:
|
|
2596
2735
|
// - git-clone install (PACKAGE_ROOT has .git) → pull + rebuild path below
|
|
@@ -2608,12 +2747,36 @@ async function cmdUpdate(options) {
|
|
|
2608
2747
|
console.log();
|
|
2609
2748
|
try {
|
|
2610
2749
|
execSync('npm install -g clementine-agent@latest', { stdio: 'inherit' });
|
|
2750
|
+
const newVersion = readPkgVersion(PACKAGE_ROOT);
|
|
2611
2751
|
console.log();
|
|
2612
|
-
|
|
2752
|
+
if (previousVersion !== 'unknown' && newVersion !== 'unknown' && previousVersion !== newVersion) {
|
|
2753
|
+
console.log(` ${GREEN}OK${RESET} Updated v${previousVersion} → ${BOLD}v${newVersion}${RESET}`);
|
|
2754
|
+
}
|
|
2755
|
+
else if (previousVersion === newVersion) {
|
|
2756
|
+
console.log(` ${GREEN}OK${RESET} Already on latest (v${newVersion})`);
|
|
2757
|
+
}
|
|
2758
|
+
else {
|
|
2759
|
+
console.log(` ${GREEN}OK${RESET} Updated via npm`);
|
|
2760
|
+
}
|
|
2761
|
+
appendUpdateHistory({
|
|
2762
|
+
flavor: 'npm-global',
|
|
2763
|
+
fromVersion: previousVersion,
|
|
2764
|
+
toVersion: newVersion,
|
|
2765
|
+
durationMs: Date.now() - updateStartedAt,
|
|
2766
|
+
restartRequested: !!options.restart,
|
|
2767
|
+
});
|
|
2613
2768
|
}
|
|
2614
2769
|
catch (err) {
|
|
2615
2770
|
console.error(` ${RED}FAIL${RESET} npm update failed: ${String(err).slice(0, 200)}`);
|
|
2616
2771
|
console.error(` ${YELLOW}Hint${RESET} If you see EACCES, see README "Troubleshooting" for npm prefix setup.`);
|
|
2772
|
+
appendUpdateHistory({
|
|
2773
|
+
flavor: 'npm-global',
|
|
2774
|
+
fromVersion: previousVersion,
|
|
2775
|
+
toVersion: previousVersion,
|
|
2776
|
+
durationMs: Date.now() - updateStartedAt,
|
|
2777
|
+
failed: true,
|
|
2778
|
+
error: String(err).slice(0, 300),
|
|
2779
|
+
});
|
|
2617
2780
|
process.exit(1);
|
|
2618
2781
|
}
|
|
2619
2782
|
if (options.restart) {
|
|
@@ -3152,6 +3315,26 @@ async function cmdUpdate(options) {
|
|
|
3152
3315
|
}).trim().slice(0, 10);
|
|
3153
3316
|
}
|
|
3154
3317
|
catch { /* best effort */ }
|
|
3318
|
+
// Capture the new version once the build is verified — package.json on
|
|
3319
|
+
// disk is now authoritative for the version we're about to run.
|
|
3320
|
+
const newVersion = readPkgVersion(PACKAGE_ROOT);
|
|
3321
|
+
// Persist update history before the restart (in case daemon restart fails,
|
|
3322
|
+
// we still have the record of what was attempted).
|
|
3323
|
+
appendUpdateHistory({
|
|
3324
|
+
flavor: 'git',
|
|
3325
|
+
fromVersion: previousVersion,
|
|
3326
|
+
toVersion: newVersion,
|
|
3327
|
+
commitHash,
|
|
3328
|
+
commitDate,
|
|
3329
|
+
commitsPulled,
|
|
3330
|
+
summary: pullSummary.split('\n').slice(0, 5).join('; '),
|
|
3331
|
+
modsReapplied: reconcileResult?.reapplied.length ?? 0,
|
|
3332
|
+
modsSuperseded: reconcileResult?.superseded.length ?? 0,
|
|
3333
|
+
modsNeedReconciliation: reconcileResult?.needsReconciliation.length ?? 0,
|
|
3334
|
+
modsFailed: reconcileResult?.failed.length ?? 0,
|
|
3335
|
+
durationMs: Date.now() - updateStartedAt,
|
|
3336
|
+
restartRequested: !!(options.restart || wasRunning),
|
|
3337
|
+
});
|
|
3155
3338
|
if (options.restart || wasRunning) {
|
|
3156
3339
|
const sentinelPath = path.join(BASE_DIR, '.restart-sentinel.json');
|
|
3157
3340
|
const sentinel = {
|
|
@@ -3159,6 +3342,8 @@ async function cmdUpdate(options) {
|
|
|
3159
3342
|
restartedAt: new Date().toISOString(),
|
|
3160
3343
|
reason: 'update',
|
|
3161
3344
|
updateDetails: {
|
|
3345
|
+
previousVersion,
|
|
3346
|
+
newVersion,
|
|
3162
3347
|
commitHash,
|
|
3163
3348
|
commitDate,
|
|
3164
3349
|
commitsBehind: commitsPulled,
|
|
@@ -3249,7 +3434,13 @@ async function cmdUpdate(options) {
|
|
|
3249
3434
|
}
|
|
3250
3435
|
// 14. Show current version
|
|
3251
3436
|
console.log();
|
|
3252
|
-
if (
|
|
3437
|
+
if (previousVersion !== 'unknown' && newVersion !== 'unknown' && previousVersion !== newVersion) {
|
|
3438
|
+
console.log(` ${GREEN}Updated v${previousVersion} → ${BOLD}v${newVersion}${RESET}${commitHash ? ` ${DIM}(${commitHash})${RESET}` : ''}`);
|
|
3439
|
+
}
|
|
3440
|
+
else if (previousVersion === newVersion && previousVersion !== 'unknown') {
|
|
3441
|
+
console.log(` ${GREEN}Already on latest (v${newVersion})${RESET}${commitHash ? ` ${DIM}(${commitHash})${RESET}` : ''}`);
|
|
3442
|
+
}
|
|
3443
|
+
else if (commitHash) {
|
|
3253
3444
|
console.log(` ${GREEN}Updated to ${commitHash} (${commitDate})${RESET}`);
|
|
3254
3445
|
}
|
|
3255
3446
|
else {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background "is there a newer version on npm?" check.
|
|
3
|
+
*
|
|
4
|
+
* Polls the public npm registry once per `CACHE_TTL_MS` (default 24h) and
|
|
5
|
+
* caches the result on disk so subsequent calls are instant. Surfaced in
|
|
6
|
+
* `clementine status` and the dashboard header so the user discovers
|
|
7
|
+
* updates without remembering to run `clementine update`.
|
|
8
|
+
*
|
|
9
|
+
* Pure read-only — never installs anything. Network failures are silent
|
|
10
|
+
* (offline → no nudge, not an error).
|
|
11
|
+
*/
|
|
12
|
+
export interface VersionCheckResult {
|
|
13
|
+
localVersion: string;
|
|
14
|
+
latestVersion: string | null;
|
|
15
|
+
/** True when latestVersion is strictly greater than localVersion. */
|
|
16
|
+
updateAvailable: boolean;
|
|
17
|
+
/** ISO of last successful registry check. null = never checked or fetch failed. */
|
|
18
|
+
checkedAt: string | null;
|
|
19
|
+
/** True when the cache was used (no network call this invocation). */
|
|
20
|
+
fromCache: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Check whether a newer version is available. Uses the cache when fresh.
|
|
24
|
+
* Pass `force = true` to bypass the cache (e.g. from a "check now" CLI flag).
|
|
25
|
+
*/
|
|
26
|
+
export declare function checkForUpdate(baseDir: string, localVersion: string, opts?: {
|
|
27
|
+
force?: boolean;
|
|
28
|
+
}): Promise<VersionCheckResult>;
|
|
29
|
+
/**
|
|
30
|
+
* Synchronous read of the cached result — used in fast paths like
|
|
31
|
+
* `clementine status` so we never block on a network call. Returns null
|
|
32
|
+
* when there's no cache yet.
|
|
33
|
+
*/
|
|
34
|
+
export declare function readCachedUpdateCheck(baseDir: string, localVersion: string): VersionCheckResult | null;
|
|
35
|
+
//# sourceMappingURL=version-check.d.ts.map
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background "is there a newer version on npm?" check.
|
|
3
|
+
*
|
|
4
|
+
* Polls the public npm registry once per `CACHE_TTL_MS` (default 24h) and
|
|
5
|
+
* caches the result on disk so subsequent calls are instant. Surfaced in
|
|
6
|
+
* `clementine status` and the dashboard header so the user discovers
|
|
7
|
+
* updates without remembering to run `clementine update`.
|
|
8
|
+
*
|
|
9
|
+
* Pure read-only — never installs anything. Network failures are silent
|
|
10
|
+
* (offline → no nudge, not an error).
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import https from 'node:https';
|
|
15
|
+
const PACKAGE_NAME = 'clementine-agent';
|
|
16
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
17
|
+
function cachePath(baseDir) {
|
|
18
|
+
return path.join(baseDir, '.update-check.json');
|
|
19
|
+
}
|
|
20
|
+
function readCache(baseDir) {
|
|
21
|
+
try {
|
|
22
|
+
const p = cachePath(baseDir);
|
|
23
|
+
if (!existsSync(p))
|
|
24
|
+
return null;
|
|
25
|
+
return JSON.parse(readFileSync(p, 'utf-8'));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function writeCache(baseDir, entry) {
|
|
32
|
+
try {
|
|
33
|
+
writeFileSync(cachePath(baseDir), JSON.stringify(entry, null, 2), { mode: 0o600 });
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Non-fatal — cache is an optimization, not state.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Fetch the latest published version of the package from npm. Resolves null
|
|
41
|
+
* on any network/parse error so callers can degrade silently.
|
|
42
|
+
*/
|
|
43
|
+
function fetchLatestFromNpm(timeoutMs = 5000) {
|
|
44
|
+
return new Promise(resolve => {
|
|
45
|
+
const req = https.get(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, { timeout: timeoutMs, headers: { Accept: 'application/json' } }, res => {
|
|
46
|
+
if (res.statusCode !== 200) {
|
|
47
|
+
res.resume();
|
|
48
|
+
resolve(null);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
let body = '';
|
|
52
|
+
res.setEncoding('utf-8');
|
|
53
|
+
res.on('data', chunk => { body += chunk; });
|
|
54
|
+
res.on('end', () => {
|
|
55
|
+
try {
|
|
56
|
+
const parsed = JSON.parse(body);
|
|
57
|
+
resolve(parsed.version ?? null);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
resolve(null);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
req.on('error', () => resolve(null));
|
|
65
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Compare two semver strings lexicographically by parts. Returns positive
|
|
70
|
+
* when `a` > `b`, negative when `a` < `b`, zero when equal. Tolerates
|
|
71
|
+
* pre-release suffixes by ignoring them (we only care about released bumps).
|
|
72
|
+
*/
|
|
73
|
+
function compareSemver(a, b) {
|
|
74
|
+
if (a === b)
|
|
75
|
+
return 0;
|
|
76
|
+
const partsA = a.replace(/[-+].*$/, '').split('.').map(n => parseInt(n, 10) || 0);
|
|
77
|
+
const partsB = b.replace(/[-+].*$/, '').split('.').map(n => parseInt(n, 10) || 0);
|
|
78
|
+
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
|
|
79
|
+
const av = partsA[i] ?? 0;
|
|
80
|
+
const bv = partsB[i] ?? 0;
|
|
81
|
+
if (av !== bv)
|
|
82
|
+
return av - bv;
|
|
83
|
+
}
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Check whether a newer version is available. Uses the cache when fresh.
|
|
88
|
+
* Pass `force = true` to bypass the cache (e.g. from a "check now" CLI flag).
|
|
89
|
+
*/
|
|
90
|
+
export async function checkForUpdate(baseDir, localVersion, opts = {}) {
|
|
91
|
+
const cache = readCache(baseDir);
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
const cacheFresh = !!cache && (now - new Date(cache.checkedAt).getTime() < CACHE_TTL_MS);
|
|
94
|
+
if (cache && cacheFresh && !opts.force) {
|
|
95
|
+
return {
|
|
96
|
+
localVersion,
|
|
97
|
+
latestVersion: cache.latestVersion,
|
|
98
|
+
updateAvailable: compareSemver(cache.latestVersion, localVersion) > 0,
|
|
99
|
+
checkedAt: cache.checkedAt,
|
|
100
|
+
fromCache: true,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const latest = await fetchLatestFromNpm();
|
|
104
|
+
if (!latest) {
|
|
105
|
+
// Couldn't reach the registry — fall back to stale cache if we have one.
|
|
106
|
+
if (cache) {
|
|
107
|
+
return {
|
|
108
|
+
localVersion,
|
|
109
|
+
latestVersion: cache.latestVersion,
|
|
110
|
+
updateAvailable: compareSemver(cache.latestVersion, localVersion) > 0,
|
|
111
|
+
checkedAt: cache.checkedAt,
|
|
112
|
+
fromCache: true,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return { localVersion, latestVersion: null, updateAvailable: false, checkedAt: null, fromCache: false };
|
|
116
|
+
}
|
|
117
|
+
writeCache(baseDir, {
|
|
118
|
+
checkedAt: new Date().toISOString(),
|
|
119
|
+
latestVersion: latest,
|
|
120
|
+
observedLocalVersion: localVersion,
|
|
121
|
+
});
|
|
122
|
+
return {
|
|
123
|
+
localVersion,
|
|
124
|
+
latestVersion: latest,
|
|
125
|
+
updateAvailable: compareSemver(latest, localVersion) > 0,
|
|
126
|
+
checkedAt: new Date().toISOString(),
|
|
127
|
+
fromCache: false,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Synchronous read of the cached result — used in fast paths like
|
|
132
|
+
* `clementine status` so we never block on a network call. Returns null
|
|
133
|
+
* when there's no cache yet.
|
|
134
|
+
*/
|
|
135
|
+
export function readCachedUpdateCheck(baseDir, localVersion) {
|
|
136
|
+
const cache = readCache(baseDir);
|
|
137
|
+
if (!cache)
|
|
138
|
+
return null;
|
|
139
|
+
return {
|
|
140
|
+
localVersion,
|
|
141
|
+
latestVersion: cache.latestVersion,
|
|
142
|
+
updateAvailable: compareSemver(cache.latestVersion, localVersion) > 0,
|
|
143
|
+
checkedAt: cache.checkedAt,
|
|
144
|
+
fromCache: true,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=version-check.js.map
|
package/dist/index.js
CHANGED
|
@@ -888,8 +888,14 @@ async function asyncMain() {
|
|
|
888
888
|
else if (sentinel.reason === 'update' && sentinel.updateDetails) {
|
|
889
889
|
const d = sentinel.updateDetails;
|
|
890
890
|
const parts = [];
|
|
891
|
-
// Version info
|
|
892
|
-
if (d.
|
|
891
|
+
// Version info — prefer semver transition over commit hash for human readability.
|
|
892
|
+
if (d.previousVersion && d.newVersion && d.previousVersion !== d.newVersion) {
|
|
893
|
+
parts.push(`Updated v${d.previousVersion} → v${d.newVersion}`);
|
|
894
|
+
}
|
|
895
|
+
else if (d.newVersion) {
|
|
896
|
+
parts.push(`Now on v${d.newVersion}`);
|
|
897
|
+
}
|
|
898
|
+
else if (d.commitHash) {
|
|
893
899
|
parts.push(`Updated to ${d.commitHash}${d.commitDate ? ` (${d.commitDate})` : ''}`);
|
|
894
900
|
}
|
|
895
901
|
else {
|
package/dist/types.d.ts
CHANGED
|
@@ -573,6 +573,10 @@ export interface RestartSentinel {
|
|
|
573
573
|
sessionKey?: string;
|
|
574
574
|
changedFiles?: string[];
|
|
575
575
|
updateDetails?: {
|
|
576
|
+
/** Semver before the update — read from package.json prior to git pull. */
|
|
577
|
+
previousVersion?: string;
|
|
578
|
+
/** Semver after the update — read from package.json after build. */
|
|
579
|
+
newVersion?: string;
|
|
576
580
|
commitHash?: string;
|
|
577
581
|
commitDate?: string;
|
|
578
582
|
commitsBehind?: number;
|