opencode-worktree 0.3.2 → 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/bin/opencode-worktree +0 -0
- package/package.json +2 -4
- package/src/ui.ts +50 -9
- package/src/update-check.ts +163 -0
package/bin/opencode-worktree
CHANGED
|
File without changes
|
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
|
}
|
|
@@ -1180,6 +1180,17 @@ class WorktreeSelector {
|
|
|
1180
1180
|
this.confirmSelect = null;
|
|
1181
1181
|
}
|
|
1182
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
|
+
|
|
1183
1194
|
this.selectElement.visible = true;
|
|
1184
1195
|
this.instructions.content =
|
|
1185
1196
|
"↑/↓ navigate • Enter open • o folder • d delete • n new • c config • q quit";
|
|
@@ -1271,8 +1282,21 @@ class WorktreeSelector {
|
|
|
1271
1282
|
this.isSelectingForDelete = true;
|
|
1272
1283
|
this.selectedForDelete.clear();
|
|
1273
1284
|
|
|
1274
|
-
//
|
|
1275
|
-
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);
|
|
1276
1300
|
this.instructions.content =
|
|
1277
1301
|
"Enter toggle selection • d confirm delete • Esc cancel";
|
|
1278
1302
|
this.setStatus("Select worktrees to delete, then press 'd' to confirm.", "info");
|
|
@@ -1282,6 +1306,20 @@ class WorktreeSelector {
|
|
|
1282
1306
|
private exitSelectMode(): void {
|
|
1283
1307
|
this.isSelectingForDelete = false;
|
|
1284
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
|
+
|
|
1285
1323
|
this.loadWorktrees();
|
|
1286
1324
|
this.instructions.content =
|
|
1287
1325
|
"↑/↓ navigate • Enter open • o folder • d delete • n new • c config • q quit";
|
|
@@ -1308,10 +1346,13 @@ class WorktreeSelector {
|
|
|
1308
1346
|
this.selectedForDelete.add(worktree.path);
|
|
1309
1347
|
}
|
|
1310
1348
|
|
|
1311
|
-
// Rebuild options to update checkboxes
|
|
1349
|
+
// Rebuild options to update checkboxes (only deletable worktrees)
|
|
1312
1350
|
if (this.repoRoot) {
|
|
1313
1351
|
const worktrees = listWorktrees(this.repoRoot);
|
|
1314
|
-
|
|
1352
|
+
const deletableWorktrees = worktrees.filter(
|
|
1353
|
+
(wt) => !isMainWorktree(this.repoRoot!, wt.path)
|
|
1354
|
+
);
|
|
1355
|
+
this.selectElement.options = this.buildOptions(deletableWorktrees);
|
|
1315
1356
|
// Restore selection index
|
|
1316
1357
|
this.selectElement.setSelectedIndex(selectedIndex);
|
|
1317
1358
|
}
|
|
@@ -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
|
+
}
|