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 +5 -1
- package/package.json +1 -1
- package/src/ui.ts +8 -12
- package/src/update-check.ts +74 -94
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
|
-
|
|
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
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 { 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
|
|
148
|
+
// Display version/update notice from previous launch, then refresh cache now.
|
|
149
149
|
if (this.pkg) {
|
|
150
|
-
const updateInfo =
|
|
150
|
+
const updateInfo = getCachedUpdateNotice(this.pkg);
|
|
151
|
+
checkForUpdatesOnLaunch(this.pkg);
|
|
151
152
|
|
|
152
|
-
let noticeContent
|
|
153
|
-
let noticeColor
|
|
153
|
+
let noticeContent = `v${this.pkg.version}`;
|
|
154
|
+
let noticeColor = "#64748B";
|
|
154
155
|
|
|
155
156
|
if (updateInfo?.hasUpdate) {
|
|
156
|
-
|
|
157
|
-
|
|
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, {
|
package/src/update-check.ts
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
49
|
-
|
|
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 = (
|
|
58
|
-
|
|
68
|
+
const parseVersion = (version: string): number[] =>
|
|
69
|
+
version
|
|
59
70
|
.replace(/^v/, "")
|
|
60
71
|
.split(".")
|
|
61
|
-
.map((
|
|
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
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
if (
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
100
|
-
if (
|
|
101
|
-
|
|
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
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
+
}
|