opencode-worktree 0.4.0 → 0.4.1

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/README.md CHANGED
@@ -155,7 +155,11 @@ Previous versions stored config in `.opencode-worktree.json` files in each repos
155
155
 
156
156
  ## Update notifications
157
157
 
158
- When a new version is published to npm, the TUI will show a non-intrusive update message in the title bar. The version check runs in the background and doesn't slow down startup.
158
+ On launch, the app performs a single in-process npm version check and stores the result in a local cache.
159
+
160
+ If a newer version is found, the warning appears on the next launch in the title bar as:
161
+
162
+ `Update: <current> -> <latest> (npm i -g)`
159
163
 
160
164
  ## Development
161
165
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-worktree",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "TUI for managing git worktrees with opencode integration.",
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 { checkForUpdate } from "./update-check.js";
13
+ import { checkForUpdatesOnLaunch, getCachedUpdateNotice } from "./update-check.js";
14
14
  import { basename } from "node:path";
15
15
  import {
16
16
  checkoutBranch,
@@ -145,21 +145,17 @@ class WorktreeSelector {
145
145
  });
146
146
  this.renderer.root.add(this.title);
147
147
 
148
- // Display version or update notification in title line
148
+ // Display version/update notice from previous launch, then refresh cache now.
149
149
  if (this.pkg) {
150
- const updateInfo = checkForUpdate(this.pkg);
150
+ const updateInfo = getCachedUpdateNotice(this.pkg);
151
+ checkForUpdatesOnLaunch(this.pkg);
151
152
 
152
- let noticeContent: string;
153
- let noticeColor: string;
153
+ let noticeContent = `v${this.pkg.version}`;
154
+ let noticeColor = "#64748B";
154
155
 
155
156
  if (updateInfo?.hasUpdate) {
156
- // Update available
157
- noticeContent = `Update: ${updateInfo.current} → ${updateInfo.latest} (npm i -g)`;
158
- noticeColor = "#F59E0B"; // Amber
159
- } else {
160
- // On latest version (or no cache yet)
161
- noticeContent = `v${this.pkg.version}`;
162
- noticeColor = "#64748B"; // Subtle gray
157
+ noticeContent = `Update: ${updateInfo.current} -> ${updateInfo.latest} (npm i -g)`;
158
+ noticeColor = "#F59E0B";
163
159
  }
164
160
 
165
161
  this.versionNotice = new TextRenderable(renderer, {
@@ -1,7 +1,7 @@
1
- import { spawn } from "node:child_process";
2
1
  import fs from "node:fs";
3
- import path from "node:path";
4
2
  import os from "node:os";
3
+ import path from "node:path";
4
+ import https from "node:https";
5
5
 
6
6
  export type UpdateInfo = {
7
7
  hasUpdate: boolean;
@@ -10,147 +10,123 @@ export type UpdateInfo = {
10
10
  };
11
11
 
12
12
  type UpdateCache = {
13
- name: string; // Package name to verify cache matches
13
+ name: string;
14
14
  latest: string;
15
15
  lastCheck: number;
16
16
  };
17
17
 
18
18
  const CACHE_DIR = path.join(os.homedir(), ".config", "configstore");
19
19
  const CACHE_FILE = path.join(CACHE_DIR, "opencode-worktree-update.json");
20
+ const REQUEST_TIMEOUT_MS = 4000;
20
21
 
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
22
  function readCache(packageName: string): UpdateCache | null {
29
23
  try {
30
24
  if (!fs.existsSync(CACHE_FILE)) {
31
25
  return null;
32
26
  }
27
+
33
28
  const content = fs.readFileSync(CACHE_FILE, "utf-8");
34
29
  const cache = JSON.parse(content) as UpdateCache;
35
-
36
- // Verify the cache is for the correct package
37
30
  if (cache.name !== packageName) {
38
31
  return null;
39
32
  }
40
-
33
+
41
34
  return cache;
42
35
  } catch {
43
36
  return null;
44
37
  }
45
38
  }
46
39
 
47
- /**
48
- * Compare two semver version strings
49
- * Returns true if latest > current
50
- */
40
+ function writeCache(packageName: string, latestVersion: string): void {
41
+ try {
42
+ if (!fs.existsSync(CACHE_DIR)) {
43
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
44
+ }
45
+
46
+ fs.writeFileSync(
47
+ CACHE_FILE,
48
+ JSON.stringify(
49
+ {
50
+ name: packageName,
51
+ latest: latestVersion,
52
+ lastCheck: Date.now(),
53
+ },
54
+ null,
55
+ 2,
56
+ ),
57
+ );
58
+ } catch {
59
+ // Best-effort cache write.
60
+ }
61
+ }
62
+
51
63
  function isNewerVersion(current: string, latest: string): boolean {
52
- // Handle dev versions
53
64
  if (current === "dev" || current === "0.0.0") {
54
65
  return false;
55
66
  }
56
67
 
57
- const parseVersion = (v: string): number[] => {
58
- return v
68
+ const parseVersion = (version: string): number[] =>
69
+ version
59
70
  .replace(/^v/, "")
60
71
  .split(".")
61
- .map((n) => parseInt(n, 10) || 0);
62
- };
72
+ .map((part) => parseInt(part, 10) || 0);
63
73
 
64
74
  const currentParts = parseVersion(current);
65
75
  const latestParts = parseVersion(latest);
66
76
 
67
77
  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;
78
+ const currentPart = currentParts[i] || 0;
79
+ const latestPart = latestParts[i] || 0;
80
+
81
+ if (latestPart > currentPart) return true;
82
+ if (latestPart < currentPart) return false;
72
83
  }
73
84
 
74
85
  return false;
75
86
  }
76
87
 
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', () => {
88
+ function fetchLatestVersion(packageName: string): void {
89
+ const url = `https://registry.npmjs.org/${packageName}/latest`;
90
+
91
+ const request = https.get(
92
+ url,
93
+ { headers: { Accept: "application/json" } },
94
+ (response) => {
95
+ if (response.statusCode !== 200) {
96
+ response.resume();
97
+ return;
98
+ }
99
+
100
+ let data = "";
101
+ response.on("data", (chunk: Buffer) => {
102
+ data += chunk.toString("utf-8");
103
+ });
104
+ response.on("end", () => {
98
105
  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));
106
+ const payload = JSON.parse(data) as { version?: string };
107
+ if (payload.version) {
108
+ writeCache(packageName, payload.version);
109
109
  }
110
- } catch {}
110
+ } catch {
111
+ // Ignore invalid response payloads.
112
+ }
111
113
  });
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
- }
114
+ },
115
+ );
116
+
117
+ request.setTimeout(REQUEST_TIMEOUT_MS, () => {
118
+ request.destroy();
119
+ });
120
+ request.on("error", () => {
121
+ // Best-effort network check.
122
+ });
128
123
  }
129
124
 
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: {
125
+ export function getCachedUpdateNotice(pkg: {
139
126
  name: string;
140
127
  version: string;
141
128
  }): UpdateInfo | null {
142
129
  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
130
  if (!cache) {
155
131
  return null;
156
132
  }
@@ -161,3 +137,7 @@ export function checkForUpdate(pkg: {
161
137
  latest: cache.latest,
162
138
  };
163
139
  }
140
+
141
+ export function checkForUpdatesOnLaunch(pkg: { name: string }): void {
142
+ fetchLatestVersion(pkg.name);
143
+ }