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.
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-worktree",
3
- "version": "0.3.2",
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 updateNotifier from "update-notifier";
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 notifier = updateNotifier({ pkg: this.pkg });
131
+ const updateInfo = checkForUpdate(this.pkg);
132
132
 
133
133
  let noticeContent: string;
134
134
  let noticeColor: string;
135
135
 
136
- if (notifier.update) {
136
+ if (updateInfo?.hasUpdate) {
137
137
  // Update available
138
- noticeContent = `Update: ${notifier.update.current} → ${notifier.update.latest} (npm i -g)`;
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
- // Rebuild options to show checkboxes
1275
- this.selectElement.options = this.buildOptions(worktrees);
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
- this.selectElement.options = this.buildOptions(worktrees);
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
+ }