opencode-worktree 0.3.3 → 0.3.4
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/package.json +2 -4
- package/src/ui.ts +88 -71
- package/src/update-check.ts +163 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-worktree",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "TUI for managing git worktrees with opencode integration.",
|
|
@@ -42,13 +42,11 @@
|
|
|
42
42
|
"node": ">=18"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@opentui/core": "^0.1.75"
|
|
46
|
-
"update-notifier": "^7.3.1"
|
|
45
|
+
"@opentui/core": "^0.1.75"
|
|
47
46
|
},
|
|
48
47
|
"devDependencies": {
|
|
49
48
|
"@types/bun": "^1.2.0",
|
|
50
49
|
"@types/node": "^24.2.0",
|
|
51
|
-
"@types/update-notifier": "^6.0.8",
|
|
52
50
|
"typescript": "^5.9.3"
|
|
53
51
|
}
|
|
54
52
|
}
|
package/src/ui.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
type KeyEvent,
|
|
11
11
|
type SelectOption,
|
|
12
12
|
} from "@opentui/core";
|
|
13
|
-
import
|
|
13
|
+
import { checkForUpdate } from "./update-check.js";
|
|
14
14
|
import { basename } from "node:path";
|
|
15
15
|
import {
|
|
16
16
|
createWorktree,
|
|
@@ -128,17 +128,17 @@ class WorktreeSelector {
|
|
|
128
128
|
|
|
129
129
|
// Display version or update notification in title line
|
|
130
130
|
if (this.pkg) {
|
|
131
|
-
const
|
|
131
|
+
const updateInfo = checkForUpdate(this.pkg);
|
|
132
132
|
|
|
133
133
|
let noticeContent: string;
|
|
134
134
|
let noticeColor: string;
|
|
135
135
|
|
|
136
|
-
if (
|
|
136
|
+
if (updateInfo?.hasUpdate) {
|
|
137
137
|
// Update available
|
|
138
|
-
noticeContent = `Update: ${
|
|
138
|
+
noticeContent = `Update: ${updateInfo.current} → ${updateInfo.latest} (npm i -g)`;
|
|
139
139
|
noticeColor = "#F59E0B"; // Amber
|
|
140
140
|
} else {
|
|
141
|
-
// On latest version
|
|
141
|
+
// On latest version (or no cache yet)
|
|
142
142
|
noticeContent = `v${this.pkg.version}`;
|
|
143
143
|
noticeColor = "#64748B"; // Subtle gray
|
|
144
144
|
}
|
|
@@ -1063,7 +1063,7 @@ class WorktreeSelector {
|
|
|
1063
1063
|
const branchDisplay = worktree.branch || basename(worktree.path);
|
|
1064
1064
|
|
|
1065
1065
|
// Build dialog title
|
|
1066
|
-
const title =
|
|
1066
|
+
const title = `Remove: ${branchDisplay}`;
|
|
1067
1067
|
|
|
1068
1068
|
this.confirmContainer = new BoxRenderable(this.renderer, {
|
|
1069
1069
|
id: "confirm-container",
|
|
@@ -1071,30 +1071,18 @@ class WorktreeSelector {
|
|
|
1071
1071
|
left: 2,
|
|
1072
1072
|
top: 3,
|
|
1073
1073
|
width: 76,
|
|
1074
|
-
height: isDirty ?
|
|
1074
|
+
height: isDirty ? 10 : 8,
|
|
1075
1075
|
borderStyle: "single",
|
|
1076
|
-
borderColor: "#
|
|
1076
|
+
borderColor: "#F59E0B",
|
|
1077
1077
|
title,
|
|
1078
1078
|
titleAlignment: "center",
|
|
1079
|
-
backgroundColor: "#
|
|
1079
|
+
backgroundColor: "#0F172A",
|
|
1080
1080
|
border: true,
|
|
1081
1081
|
});
|
|
1082
1082
|
this.renderer.root.add(this.confirmContainer);
|
|
1083
1083
|
|
|
1084
|
-
// Branch name prominently displayed
|
|
1085
|
-
let yOffset = 1;
|
|
1086
|
-
const branchHeader = new TextRenderable(this.renderer, {
|
|
1087
|
-
id: "confirm-branch",
|
|
1088
|
-
position: "absolute",
|
|
1089
|
-
left: 1,
|
|
1090
|
-
top: yOffset,
|
|
1091
|
-
content: `Branch: ${branchDisplay}`,
|
|
1092
|
-
fg: "#FBBF24",
|
|
1093
|
-
});
|
|
1094
|
-
this.confirmContainer.add(branchHeader);
|
|
1095
|
-
yOffset += 2;
|
|
1096
|
-
|
|
1097
1084
|
// Warning for dirty worktree
|
|
1085
|
+
let yOffset = 1;
|
|
1098
1086
|
if (isDirty) {
|
|
1099
1087
|
const warningText = new TextRenderable(this.renderer, {
|
|
1100
1088
|
id: "confirm-warning",
|
|
@@ -1102,7 +1090,7 @@ class WorktreeSelector {
|
|
|
1102
1090
|
left: 1,
|
|
1103
1091
|
top: yOffset,
|
|
1104
1092
|
content: "⚠ This worktree has uncommitted changes!",
|
|
1105
|
-
fg: "#
|
|
1093
|
+
fg: "#F59E0B",
|
|
1106
1094
|
});
|
|
1107
1095
|
this.confirmContainer.add(warningText);
|
|
1108
1096
|
yOffset += 2;
|
|
@@ -1114,7 +1102,7 @@ class WorktreeSelector {
|
|
|
1114
1102
|
left: 1,
|
|
1115
1103
|
top: yOffset,
|
|
1116
1104
|
content: `Path: ${worktree.path}`,
|
|
1117
|
-
fg: "#
|
|
1105
|
+
fg: "#94A3B8",
|
|
1118
1106
|
});
|
|
1119
1107
|
this.confirmContainer.add(pathText);
|
|
1120
1108
|
yOffset += 2;
|
|
@@ -1146,13 +1134,13 @@ class WorktreeSelector {
|
|
|
1146
1134
|
width: 72,
|
|
1147
1135
|
height: 4,
|
|
1148
1136
|
options,
|
|
1149
|
-
backgroundColor: "#
|
|
1150
|
-
focusedBackgroundColor: "#
|
|
1151
|
-
selectedBackgroundColor: "#
|
|
1152
|
-
textColor: "#
|
|
1153
|
-
selectedTextColor: "#
|
|
1154
|
-
descriptionColor: "#
|
|
1155
|
-
selectedDescriptionColor: "#
|
|
1137
|
+
backgroundColor: "#0F172A",
|
|
1138
|
+
focusedBackgroundColor: "#1E293B",
|
|
1139
|
+
selectedBackgroundColor: "#1E3A5F",
|
|
1140
|
+
textColor: "#E2E8F0",
|
|
1141
|
+
selectedTextColor: "#38BDF8",
|
|
1142
|
+
descriptionColor: "#94A3B8",
|
|
1143
|
+
selectedDescriptionColor: "#E2E8F0",
|
|
1156
1144
|
showDescription: true,
|
|
1157
1145
|
wrapSelection: true,
|
|
1158
1146
|
});
|
|
@@ -1192,6 +1180,17 @@ class WorktreeSelector {
|
|
|
1192
1180
|
this.confirmSelect = null;
|
|
1193
1181
|
}
|
|
1194
1182
|
|
|
1183
|
+
// Restore original title and colors (in case we came from delete mode)
|
|
1184
|
+
this.title.content = "OPENCODE WORKTREES";
|
|
1185
|
+
this.title.fg = "#E2E8F0";
|
|
1186
|
+
this.selectElement.backgroundColor = "#0F172A";
|
|
1187
|
+
this.selectElement.focusedBackgroundColor = "#1E293B";
|
|
1188
|
+
this.selectElement.selectedBackgroundColor = "#1E3A5F";
|
|
1189
|
+
this.selectElement.textColor = "#E2E8F0";
|
|
1190
|
+
this.selectElement.selectedTextColor = "#38BDF8";
|
|
1191
|
+
this.selectElement.descriptionColor = "#94A3B8";
|
|
1192
|
+
this.selectElement.selectedDescriptionColor = "#E2E8F0";
|
|
1193
|
+
|
|
1195
1194
|
this.selectElement.visible = true;
|
|
1196
1195
|
this.instructions.content =
|
|
1197
1196
|
"↑/↓ navigate • Enter open • o folder • d delete • n new • c config • q quit";
|
|
@@ -1283,8 +1282,21 @@ class WorktreeSelector {
|
|
|
1283
1282
|
this.isSelectingForDelete = true;
|
|
1284
1283
|
this.selectedForDelete.clear();
|
|
1285
1284
|
|
|
1286
|
-
//
|
|
1287
|
-
this.
|
|
1285
|
+
// Change title to indicate delete mode
|
|
1286
|
+
this.title.content = "DELETE WORKTREES";
|
|
1287
|
+
this.title.fg = "#EF4444"; // Red
|
|
1288
|
+
|
|
1289
|
+
// Change select element colors to danger theme
|
|
1290
|
+
this.selectElement.backgroundColor = "#1C1917";
|
|
1291
|
+
this.selectElement.focusedBackgroundColor = "#292524";
|
|
1292
|
+
this.selectElement.selectedBackgroundColor = "#44403C";
|
|
1293
|
+
this.selectElement.textColor = "#E7E5E4";
|
|
1294
|
+
this.selectElement.selectedTextColor = "#F87171";
|
|
1295
|
+
this.selectElement.descriptionColor = "#A8A29E";
|
|
1296
|
+
this.selectElement.selectedDescriptionColor = "#E7E5E4";
|
|
1297
|
+
|
|
1298
|
+
// Rebuild options to show checkboxes (only deletable worktrees)
|
|
1299
|
+
this.selectElement.options = this.buildOptions(deletableWorktrees);
|
|
1288
1300
|
this.instructions.content =
|
|
1289
1301
|
"Enter toggle selection • d confirm delete • Esc cancel";
|
|
1290
1302
|
this.setStatus("Select worktrees to delete, then press 'd' to confirm.", "info");
|
|
@@ -1294,6 +1306,20 @@ class WorktreeSelector {
|
|
|
1294
1306
|
private exitSelectMode(): void {
|
|
1295
1307
|
this.isSelectingForDelete = false;
|
|
1296
1308
|
this.selectedForDelete.clear();
|
|
1309
|
+
|
|
1310
|
+
// Restore original title
|
|
1311
|
+
this.title.content = "OPENCODE WORKTREES";
|
|
1312
|
+
this.title.fg = "#E2E8F0";
|
|
1313
|
+
|
|
1314
|
+
// Restore original select element colors
|
|
1315
|
+
this.selectElement.backgroundColor = "#0F172A";
|
|
1316
|
+
this.selectElement.focusedBackgroundColor = "#1E293B";
|
|
1317
|
+
this.selectElement.selectedBackgroundColor = "#1E3A5F";
|
|
1318
|
+
this.selectElement.textColor = "#E2E8F0";
|
|
1319
|
+
this.selectElement.selectedTextColor = "#38BDF8";
|
|
1320
|
+
this.selectElement.descriptionColor = "#94A3B8";
|
|
1321
|
+
this.selectElement.selectedDescriptionColor = "#E2E8F0";
|
|
1322
|
+
|
|
1297
1323
|
this.loadWorktrees();
|
|
1298
1324
|
this.instructions.content =
|
|
1299
1325
|
"↑/↓ navigate • Enter open • o folder • d delete • n new • c config • q quit";
|
|
@@ -1320,10 +1346,13 @@ class WorktreeSelector {
|
|
|
1320
1346
|
this.selectedForDelete.add(worktree.path);
|
|
1321
1347
|
}
|
|
1322
1348
|
|
|
1323
|
-
// Rebuild options to update checkboxes
|
|
1349
|
+
// Rebuild options to update checkboxes (only deletable worktrees)
|
|
1324
1350
|
if (this.repoRoot) {
|
|
1325
1351
|
const worktrees = listWorktrees(this.repoRoot);
|
|
1326
|
-
|
|
1352
|
+
const deletableWorktrees = worktrees.filter(
|
|
1353
|
+
(wt) => !isMainWorktree(this.repoRoot!, wt.path)
|
|
1354
|
+
);
|
|
1355
|
+
this.selectElement.options = this.buildOptions(deletableWorktrees);
|
|
1327
1356
|
// Restore selection index
|
|
1328
1357
|
this.selectElement.setSelectedIndex(selectedIndex);
|
|
1329
1358
|
}
|
|
@@ -1369,7 +1398,7 @@ class WorktreeSelector {
|
|
|
1369
1398
|
const hasDirty = dirtyWorktrees.length > 0;
|
|
1370
1399
|
|
|
1371
1400
|
const count = worktrees.length;
|
|
1372
|
-
const title = `
|
|
1401
|
+
const title = `Delete ${count} worktree${count === 1 ? "" : "s"}`;
|
|
1373
1402
|
|
|
1374
1403
|
this.confirmContainer = new BoxRenderable(this.renderer, {
|
|
1375
1404
|
id: "confirm-container",
|
|
@@ -1377,36 +1406,18 @@ class WorktreeSelector {
|
|
|
1377
1406
|
left: 2,
|
|
1378
1407
|
top: 3,
|
|
1379
1408
|
width: 76,
|
|
1380
|
-
height: hasDirty ?
|
|
1409
|
+
height: hasDirty ? 12 : 10,
|
|
1381
1410
|
borderStyle: "single",
|
|
1382
|
-
borderColor: "#
|
|
1411
|
+
borderColor: "#F59E0B",
|
|
1383
1412
|
title,
|
|
1384
1413
|
titleAlignment: "center",
|
|
1385
|
-
backgroundColor: "#
|
|
1414
|
+
backgroundColor: "#0F172A",
|
|
1386
1415
|
border: true,
|
|
1387
1416
|
});
|
|
1388
1417
|
this.renderer.root.add(this.confirmContainer);
|
|
1389
1418
|
|
|
1390
1419
|
let yOffset = 1;
|
|
1391
1420
|
|
|
1392
|
-
// List branches to be deleted prominently
|
|
1393
|
-
const branchNames = worktrees
|
|
1394
|
-
.map((wt) => wt.branch || basename(wt.path))
|
|
1395
|
-
.slice(0, 3);
|
|
1396
|
-
const displayList =
|
|
1397
|
-
branchNames.join(", ") + (worktrees.length > 3 ? `, +${worktrees.length - 3} more` : "");
|
|
1398
|
-
|
|
1399
|
-
const branchHeader = new TextRenderable(this.renderer, {
|
|
1400
|
-
id: "confirm-branches",
|
|
1401
|
-
position: "absolute",
|
|
1402
|
-
left: 1,
|
|
1403
|
-
top: yOffset,
|
|
1404
|
-
content: `Branches: ${displayList}`,
|
|
1405
|
-
fg: "#FBBF24",
|
|
1406
|
-
});
|
|
1407
|
-
this.confirmContainer.add(branchHeader);
|
|
1408
|
-
yOffset += 2;
|
|
1409
|
-
|
|
1410
1421
|
// Warning for dirty worktrees
|
|
1411
1422
|
if (hasDirty) {
|
|
1412
1423
|
const warningText = new TextRenderable(this.renderer, {
|
|
@@ -1415,22 +1426,28 @@ class WorktreeSelector {
|
|
|
1415
1426
|
left: 1,
|
|
1416
1427
|
top: yOffset,
|
|
1417
1428
|
content: `⚠ ${dirtyWorktrees.length} worktree${dirtyWorktrees.length === 1 ? " has" : "s have"} uncommitted changes!`,
|
|
1418
|
-
fg: "#
|
|
1429
|
+
fg: "#F59E0B",
|
|
1419
1430
|
});
|
|
1420
1431
|
this.confirmContainer.add(warningText);
|
|
1421
1432
|
yOffset += 2;
|
|
1422
1433
|
}
|
|
1423
1434
|
|
|
1424
|
-
//
|
|
1425
|
-
const
|
|
1426
|
-
|
|
1435
|
+
// List worktrees to be deleted
|
|
1436
|
+
const branchNames = worktrees
|
|
1437
|
+
.map((wt) => wt.branch || basename(wt.path))
|
|
1438
|
+
.slice(0, 3);
|
|
1439
|
+
const displayList =
|
|
1440
|
+
branchNames.join(", ") + (worktrees.length > 3 ? `, +${worktrees.length - 3} more` : "");
|
|
1441
|
+
|
|
1442
|
+
const listText = new TextRenderable(this.renderer, {
|
|
1443
|
+
id: "confirm-list",
|
|
1427
1444
|
position: "absolute",
|
|
1428
1445
|
left: 1,
|
|
1429
1446
|
top: yOffset,
|
|
1430
|
-
content:
|
|
1431
|
-
fg: "#
|
|
1447
|
+
content: `Worktrees: ${displayList}`,
|
|
1448
|
+
fg: "#94A3B8",
|
|
1432
1449
|
});
|
|
1433
|
-
this.confirmContainer.add(
|
|
1450
|
+
this.confirmContainer.add(listText);
|
|
1434
1451
|
yOffset += 2;
|
|
1435
1452
|
|
|
1436
1453
|
// Build options
|
|
@@ -1460,13 +1477,13 @@ class WorktreeSelector {
|
|
|
1460
1477
|
width: 72,
|
|
1461
1478
|
height: 4,
|
|
1462
1479
|
options,
|
|
1463
|
-
backgroundColor: "#
|
|
1464
|
-
focusedBackgroundColor: "#
|
|
1465
|
-
selectedBackgroundColor: "#
|
|
1466
|
-
textColor: "#
|
|
1467
|
-
selectedTextColor: "#
|
|
1468
|
-
descriptionColor: "#
|
|
1469
|
-
selectedDescriptionColor: "#
|
|
1480
|
+
backgroundColor: "#0F172A",
|
|
1481
|
+
focusedBackgroundColor: "#1E293B",
|
|
1482
|
+
selectedBackgroundColor: "#1E3A5F",
|
|
1483
|
+
textColor: "#E2E8F0",
|
|
1484
|
+
selectedTextColor: "#38BDF8",
|
|
1485
|
+
descriptionColor: "#94A3B8",
|
|
1486
|
+
selectedDescriptionColor: "#E2E8F0",
|
|
1470
1487
|
showDescription: true,
|
|
1471
1488
|
wrapSelection: true,
|
|
1472
1489
|
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
|
|
6
|
+
export type UpdateInfo = {
|
|
7
|
+
hasUpdate: boolean;
|
|
8
|
+
current: string;
|
|
9
|
+
latest: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type UpdateCache = {
|
|
13
|
+
name: string; // Package name to verify cache matches
|
|
14
|
+
latest: string;
|
|
15
|
+
lastCheck: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const CACHE_DIR = path.join(os.homedir(), ".config", "configstore");
|
|
19
|
+
const CACHE_FILE = path.join(CACHE_DIR, "opencode-worktree-update.json");
|
|
20
|
+
|
|
21
|
+
// Check interval: 1 hour in milliseconds
|
|
22
|
+
const CHECK_INTERVAL_MS = 60 * 60 * 1000;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Read the cached update info from disk
|
|
26
|
+
* Returns null if cache doesn't exist or package name doesn't match
|
|
27
|
+
*/
|
|
28
|
+
function readCache(packageName: string): UpdateCache | null {
|
|
29
|
+
try {
|
|
30
|
+
if (!fs.existsSync(CACHE_FILE)) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const content = fs.readFileSync(CACHE_FILE, "utf-8");
|
|
34
|
+
const cache = JSON.parse(content) as UpdateCache;
|
|
35
|
+
|
|
36
|
+
// Verify the cache is for the correct package
|
|
37
|
+
if (cache.name !== packageName) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return cache;
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Compare two semver version strings
|
|
49
|
+
* Returns true if latest > current
|
|
50
|
+
*/
|
|
51
|
+
function isNewerVersion(current: string, latest: string): boolean {
|
|
52
|
+
// Handle dev versions
|
|
53
|
+
if (current === "dev" || current === "0.0.0") {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const parseVersion = (v: string): number[] => {
|
|
58
|
+
return v
|
|
59
|
+
.replace(/^v/, "")
|
|
60
|
+
.split(".")
|
|
61
|
+
.map((n) => parseInt(n, 10) || 0);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const currentParts = parseVersion(current);
|
|
65
|
+
const latestParts = parseVersion(latest);
|
|
66
|
+
|
|
67
|
+
for (let i = 0; i < 3; i++) {
|
|
68
|
+
const c = currentParts[i] || 0;
|
|
69
|
+
const l = latestParts[i] || 0;
|
|
70
|
+
if (l > c) return true;
|
|
71
|
+
if (l < c) return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Spawn a detached background process to fetch the latest version from npm
|
|
79
|
+
* and write it to the cache file. This doesn't block the main process.
|
|
80
|
+
*/
|
|
81
|
+
function fetchLatestInBackground(packageName: string): void {
|
|
82
|
+
// Create an inline script that fetches from npm and writes to cache
|
|
83
|
+
const script = `
|
|
84
|
+
const https = require('https');
|
|
85
|
+
const fs = require('fs');
|
|
86
|
+
const path = require('path');
|
|
87
|
+
const os = require('os');
|
|
88
|
+
|
|
89
|
+
const cacheDir = path.join(os.homedir(), '.config', 'configstore');
|
|
90
|
+
const cacheFile = path.join(cacheDir, 'opencode-worktree-update.json');
|
|
91
|
+
|
|
92
|
+
const url = 'https://registry.npmjs.org/${packageName}/latest';
|
|
93
|
+
|
|
94
|
+
https.get(url, { headers: { 'Accept': 'application/json' } }, (res) => {
|
|
95
|
+
let data = '';
|
|
96
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
97
|
+
res.on('end', () => {
|
|
98
|
+
try {
|
|
99
|
+
const pkg = JSON.parse(data);
|
|
100
|
+
if (pkg.version) {
|
|
101
|
+
if (!fs.existsSync(cacheDir)) {
|
|
102
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
103
|
+
}
|
|
104
|
+
fs.writeFileSync(cacheFile, JSON.stringify({
|
|
105
|
+
name: '${packageName}',
|
|
106
|
+
latest: pkg.version,
|
|
107
|
+
lastCheck: Date.now()
|
|
108
|
+
}, null, 2));
|
|
109
|
+
}
|
|
110
|
+
} catch {}
|
|
111
|
+
});
|
|
112
|
+
}).on('error', () => {});
|
|
113
|
+
`;
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
// Spawn node to run the script
|
|
117
|
+
const child = spawn(process.execPath, ["-e", script], {
|
|
118
|
+
detached: true,
|
|
119
|
+
stdio: "ignore",
|
|
120
|
+
env: { ...process.env },
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Unref so the parent process can exit independently
|
|
124
|
+
child.unref();
|
|
125
|
+
} catch {
|
|
126
|
+
// Silently fail - background fetch is best-effort
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Check for updates. This is non-blocking:
|
|
132
|
+
* 1. Reads the cached latest version (if available)
|
|
133
|
+
* 2. Compares against current version
|
|
134
|
+
* 3. Spawns a background process to refresh the cache for next time
|
|
135
|
+
*
|
|
136
|
+
* Returns null if no cache exists yet (first run)
|
|
137
|
+
*/
|
|
138
|
+
export function checkForUpdate(pkg: {
|
|
139
|
+
name: string;
|
|
140
|
+
version: string;
|
|
141
|
+
}): UpdateInfo | null {
|
|
142
|
+
const cache = readCache(pkg.name);
|
|
143
|
+
const now = Date.now();
|
|
144
|
+
|
|
145
|
+
// Spawn background fetch if cache is stale or doesn't exist
|
|
146
|
+
const shouldFetch =
|
|
147
|
+
!cache || now - cache.lastCheck > CHECK_INTERVAL_MS;
|
|
148
|
+
|
|
149
|
+
if (shouldFetch) {
|
|
150
|
+
fetchLatestInBackground(pkg.name);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// If no cache, we can't determine if there's an update yet
|
|
154
|
+
if (!cache) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
hasUpdate: isNewerVersion(pkg.version, cache.latest),
|
|
160
|
+
current: pkg.version,
|
|
161
|
+
latest: cache.latest,
|
|
162
|
+
};
|
|
163
|
+
}
|