opencode-navigator 0.27.3 → 0.27.5

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.
@@ -1,339 +1,689 @@
1
- import {PLUGIN_ID
2
- } from "./chunk-dm1y8x49.js"
3
- import {parseChangelog2,readBundledChangelog2
4
- } from "./chunk-rrvwmnyh.js"
5
- import {isNewerVersion,isSameMajorVersion
6
- } from "./chunk-fd7394y1.js";
7
- import {createSignal} from "solid-js"
8
- var NAVIGATOR_PACKAGE_URL="https://registry.npmjs.org/opencode-navigator/latest"
9
- var REQUEST_TIMEOUT_MS=5000
10
- async function readNavigatorVersion(request,signal){const response=await request(NAVIGATOR_PACKAGE_URL,{headers:{accept:"application/json"},signal})
11
- if(!response.ok)
12
- throw new Error("Update source unavailable")
13
- const metadata=await response.json()
14
- if(typeof metadata.version!=="string"||!/^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/.test(metadata.version))
15
- throw new Error("Invalid update version")
16
- return metadata.version
1
+ // @bun
2
+ import {
3
+ PLUGIN_ID
4
+ } from "./chunk-dm1y8x49.js";
5
+ import {
6
+ parseTree,
7
+ findNodeAtLocation,
8
+ getNodeValue
9
+ } from "./chunk-v6qckzat.js";
10
+ import {
11
+ readBundledChangelog2
12
+ } from "./chunk-sm9ej5ac.js";
13
+ import {
14
+ isNewerVersion,
15
+ isSameMajorVersion,
16
+ parseChangelog2
17
+ } from "./chunk-4rh7rb74.js";
18
+
19
+ // src/features/version-footer/model/version-status.ts
20
+ import { createSignal } from "solid-js";
21
+ var NAVIGATOR_PACKAGE_URL = "https://registry.npmjs.org/opencode-navigator/latest";
22
+ var REQUEST_TIMEOUT_MS = 5000;
23
+ async function readNavigatorVersion(request, signal) {
24
+ const response = await request(NAVIGATOR_PACKAGE_URL, { headers: { accept: "application/json" }, signal });
25
+ if (!response.ok)
26
+ throw new Error("Update source unavailable");
27
+ const metadata = await response.json();
28
+ if (typeof metadata.version !== "string" || !/^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/.test(metadata.version))
29
+ throw new Error("Invalid update version");
30
+ return metadata.version;
17
31
  }
18
- function createVersionStatus(api,navigatorVersion,request=fetch,timeoutMs=REQUEST_TIMEOUT_MS){const [openCodeUpdate,setOpenCodeUpdate]=createSignal()
19
- const [navigatorUpdate,setNavigatorUpdate]=createSignal()
20
- const [navigatorCheckState,setNavigatorCheckState]=createSignal({status:"idle"})
21
- let controller
22
- let pending
23
- let disposed=false
24
- const unsubscribe=api.event.on("installation.update-available",(event)=>{const version=event.properties.version
25
- setOpenCodeUpdate(isSameMajorVersion(version,api.app.version)&&isNewerVersion(version,api.app.version)?version:undefined)
26
- })
27
- api.lifecycle.onDispose(()=>{disposed=true
28
- controller?.abort()
29
- unsubscribe()
30
- })
31
- function refreshNavigator(){if(disposed)
32
- return Promise.resolve("unavailable")
33
- if(pending)
34
- return pending
35
- setNavigatorCheckState((previous)=>({...previous,status:"checking"}))
36
- const active=new AbortController
37
- controller=active
38
- const timeout=setTimeout(()=>active.abort(),timeoutMs)
39
- let abort
40
- const cancelled=new Promise((_,reject)=>{abort=()=>reject(new Error("Update check interrupted"))
41
- active.signal.addEventListener("abort",abort,{once:true})
42
- })
43
- const reading=Promise.race([readNavigatorVersion(request,active.signal),cancelled]).then((version)=>{if(disposed||active.signal.aborted)
44
- return "unavailable"
45
- const newer=isNewerVersion(version,navigatorVersion)
46
- setNavigatorUpdate(newer?version:undefined)
47
- setNavigatorCheckState({status:newer?"available":"current",latest:version,checkedAt:Date.now()})
48
- return newer?"update":"current"
49
- }).catch(()=>{if(!disposed)
50
- setNavigatorCheckState((previous)=>({...previous,status:"error",checkedAt:Date.now()}))
51
- return "unavailable"
52
- }).finally(()=>{clearTimeout(timeout)
53
- active.signal.removeEventListener("abort",abort)
54
- if(controller===active)
55
- controller=undefined
56
- if(pending===reading)
57
- pending=undefined
58
- })
59
- pending=reading
60
- return reading
32
+ function createVersionStatus(api, navigatorVersion, request = fetch, timeoutMs = REQUEST_TIMEOUT_MS) {
33
+ const [openCodeUpdate, setOpenCodeUpdate] = createSignal();
34
+ const [navigatorUpdate, setNavigatorUpdate] = createSignal();
35
+ const [navigatorCheckState, setNavigatorCheckState] = createSignal({ status: "idle" });
36
+ let controller;
37
+ let pending;
38
+ let disposed = false;
39
+ const unsubscribe = api.event.on("installation.update-available", (event) => {
40
+ const version = event.properties.version;
41
+ setOpenCodeUpdate(isSameMajorVersion(version, api.app.version) && isNewerVersion(version, api.app.version) ? version : undefined);
42
+ });
43
+ api.lifecycle.onDispose(() => {
44
+ disposed = true;
45
+ controller?.abort();
46
+ unsubscribe();
47
+ });
48
+ function refreshNavigator() {
49
+ if (disposed)
50
+ return Promise.resolve("unavailable");
51
+ if (pending)
52
+ return pending;
53
+ setNavigatorCheckState((previous) => ({ ...previous, status: "checking" }));
54
+ const active = new AbortController;
55
+ controller = active;
56
+ const timeout = setTimeout(() => active.abort(), timeoutMs);
57
+ let abort;
58
+ const cancelled = new Promise((_, reject) => {
59
+ abort = () => reject(new Error("Update check interrupted"));
60
+ active.signal.addEventListener("abort", abort, { once: true });
61
+ });
62
+ const reading = Promise.race([readNavigatorVersion(request, active.signal), cancelled]).then((version) => {
63
+ if (disposed || active.signal.aborted)
64
+ return "unavailable";
65
+ const newer = isNewerVersion(version, navigatorVersion);
66
+ setNavigatorUpdate(newer ? version : undefined);
67
+ setNavigatorCheckState({ status: newer ? "available" : "current", latest: version, checkedAt: Date.now() });
68
+ return newer ? "update" : "current";
69
+ }).catch(() => {
70
+ if (!disposed)
71
+ setNavigatorCheckState((previous) => ({ ...previous, status: "error", checkedAt: Date.now() }));
72
+ return "unavailable";
73
+ }).finally(() => {
74
+ clearTimeout(timeout);
75
+ active.signal.removeEventListener("abort", abort);
76
+ if (controller === active)
77
+ controller = undefined;
78
+ if (pending === reading)
79
+ pending = undefined;
80
+ });
81
+ pending = reading;
82
+ return reading;
83
+ }
84
+ refreshNavigator();
85
+ return { openCodeUpdate, navigatorUpdate, refreshNavigator, navigatorCheckState };
61
86
  }
62
- refreshNavigator()
63
- return{openCodeUpdate,navigatorUpdate,refreshNavigator,navigatorCheckState}}
64
- import {effect as _$effect4} from "@opentui/solid"
65
- import {insertNode as _$insertNode4} from "@opentui/solid"
66
- import {createComponent as _$createComponent3} from "@opentui/solid"
67
- import {insert as _$insert4} from "@opentui/solid"
68
- import {setProp as _$setProp4} from "@opentui/solid"
69
- import {createElement as _$createElement4} from "@opentui/solid"
70
- import {createTextNode as _$createTextNode3} from "@opentui/solid"
71
- import {insertNode as _$insertNode3} from "@opentui/solid"
72
- import {setProp as _$setProp3} from "@opentui/solid"
73
- import {effect as _$effect3} from "@opentui/solid"
74
- import {insert as _$insert3} from "@opentui/solid"
75
- import {memo as _$memo2} from "@opentui/solid"
76
- import {createComponent as _$createComponent2} from "@opentui/solid"
77
- import {createElement as _$createElement3} from "@opentui/solid"
78
- import {effect as _$effect2} from "@opentui/solid"
79
- import {createTextNode as _$createTextNode2} from "@opentui/solid"
80
- import {insertNode as _$insertNode2} from "@opentui/solid"
81
- import {createComponent as _$createComponent} from "@opentui/solid"
82
- import {insert as _$insert2} from "@opentui/solid"
83
- import {memo as _$memo} from "@opentui/solid"
84
- import {setProp as _$setProp2} from "@opentui/solid"
85
- import {createElement as _$createElement2} from "@opentui/solid"
86
- import {useIcons} from "./tui.js"
87
- import {setProp as _$setProp} from "@opentui/solid"
88
- import {effect as _$effect} from "@opentui/solid"
89
- import {createTextNode as _$createTextNode} from "@opentui/solid"
90
- import {insertNode as _$insertNode} from "@opentui/solid"
91
- import {insert as _$insert} from "@opentui/solid"
92
- import {createElement as _$createElement} from "@opentui/solid"
93
- import {createEffect,createSignal as createSignal2,onCleanup} from "solid-js"
94
- import {createComponent as _$createComponent5} from "@opentui/solid"
95
- import {createSignal as createSignal3} from "solid-js"
96
- function createManualUpdateCheck(api,status,current,onUpdate){let checking=false
97
- async function read(){try{return await status.refreshNavigator?.()??"unavailable"
98
- } catch{return "unavailable"
99
- }}
100
- return{checking:()=>checking,async run(offerUpdate=false){if(!current()||checking)
101
- return
102
- checking=true
103
- api.ui.toast({variant:"info",title:"Navigator update",message:"Checking for updates\u2026"})
104
- try{const result=await read()
105
- if(!current())
106
- return
107
- if(result==="update"&&offerUpdate){onUpdate()
108
- return
87
+ // src/features/version-footer/ui/version-footer.tsx
88
+ import { effect as _$effect4 } from "@opentui/solid";
89
+ import { insertNode as _$insertNode4 } from "@opentui/solid";
90
+ import { createComponent as _$createComponent3 } from "@opentui/solid";
91
+ import { insert as _$insert4 } from "@opentui/solid";
92
+ import { setProp as _$setProp4 } from "@opentui/solid";
93
+ import { createElement as _$createElement4 } from "@opentui/solid";
94
+
95
+ // src/features/version-footer/ui/version-summary.tsx
96
+ import { createTextNode as _$createTextNode3 } from "@opentui/solid";
97
+ import { insertNode as _$insertNode3 } from "@opentui/solid";
98
+ import { setProp as _$setProp3 } from "@opentui/solid";
99
+ import { effect as _$effect3 } from "@opentui/solid";
100
+ import { insert as _$insert3 } from "@opentui/solid";
101
+ import { memo as _$memo2 } from "@opentui/solid";
102
+ import { createComponent as _$createComponent2 } from "@opentui/solid";
103
+ import { createElement as _$createElement3 } from "@opentui/solid";
104
+
105
+ // src/features/version-footer/ui/version-control.tsx
106
+ import { effect as _$effect2 } from "@opentui/solid";
107
+ import { createTextNode as _$createTextNode2 } from "@opentui/solid";
108
+ import { insertNode as _$insertNode2 } from "@opentui/solid";
109
+ import { createComponent as _$createComponent } from "@opentui/solid";
110
+ import { insert as _$insert2 } from "@opentui/solid";
111
+ import { memo as _$memo } from "@opentui/solid";
112
+ import { setProp as _$setProp2 } from "@opentui/solid";
113
+ import { createElement as _$createElement2 } from "@opentui/solid";
114
+ import { useIcons } from "./tui.js";
115
+
116
+ // src/features/version-footer/ui/install-progress.tsx
117
+ import { setProp as _$setProp } from "@opentui/solid";
118
+ import { effect as _$effect } from "@opentui/solid";
119
+ import { createTextNode as _$createTextNode } from "@opentui/solid";
120
+ import { insertNode as _$insertNode } from "@opentui/solid";
121
+ import { insert as _$insert } from "@opentui/solid";
122
+ import { createElement as _$createElement } from "@opentui/solid";
123
+ import { createEffect, createSignal as createSignal2, onCleanup } from "solid-js";
124
+ // src/features/version-footer/ui/version-update.tsx
125
+ import { createComponent as _$createComponent5 } from "@opentui/solid";
126
+ import { createSignal as createSignal3 } from "solid-js";
127
+
128
+ // src/features/version-footer/model/manual-update-check.ts
129
+ function createManualUpdateCheck(api, status, current, onUpdate) {
130
+ let checking = false;
131
+ async function read() {
132
+ try {
133
+ return await status.refreshNavigator?.() ?? "unavailable";
134
+ } catch {
135
+ return "unavailable";
136
+ }
137
+ }
138
+ return {
139
+ checking: () => checking,
140
+ async run(offerUpdate = false) {
141
+ if (!current() || checking)
142
+ return;
143
+ checking = true;
144
+ api.ui.toast({ variant: "info", title: "Navigator update", message: "Checking for updates\u2026" });
145
+ try {
146
+ const result = await read();
147
+ if (!current())
148
+ return;
149
+ if (result === "update" && offerUpdate) {
150
+ onUpdate();
151
+ return;
152
+ }
153
+ api.ui.toast({
154
+ variant: result === "unavailable" ? "warning" : "info",
155
+ title: "Navigator update",
156
+ message: {
157
+ update: `Navigator ${status.navigatorUpdate()} is available. Use u in Navigator shortcuts to update.`,
158
+ current: "Navigator is up to date.",
159
+ unavailable: "Could not check Navigator updates. Try again."
160
+ }[result]
161
+ });
162
+ } finally {
163
+ checking = false;
164
+ }
165
+ }
166
+ };
109
167
  }
110
- api.ui.toast({variant:result==="unavailable"?"warning":"info",title:"Navigator update",message:{update:`Navigator ${status.navigatorUpdate()} is available. Use u in Navigator shortcuts to update.`,current:"Navigator is up to date.",unavailable:"Could not check Navigator updates. Try again."
111
- }[result]
112
- })
113
- } finally{checking=false
114
- }}}}
115
- import {createComponent as _$createComponent4} from "@opentui/solid"
116
- import {onCleanup as onCleanup2} from "solid-js"
117
- import {GuardedConfirmation,SelectionMenu} from "./tui.js"
118
- function NestedVersionConfirmation(props){onCleanup2(props.dismiss)
119
- return _$createComponent4(GuardedConfirmation,{get api(){return props.api
120
- },get current(){return props.current
121
- },get title(){return `Update ${props.name}`
122
- },get message(){return `Install ${props.version} using the current installation method?`
123
- },get onConfirm(){return props.confirm
124
- },render:({confirm
125
- })=>_$createComponent4(SelectionMenu,{get api(){return props.api
126
- },get title(){return `Update ${props.name}`
127
- },get options(){return [{title:"Cancel",value:"cancel",description:"Return without installing an update"
128
- },{title:`Install ${props.name} ${props.version}`,value:"install",description:"Keep the current installation method and scope; restart OpenCode afterward"
129
- }]
130
- },onSelect:(option)=>option.value==="install"?confirm():props.cancel()
131
- })
132
- })
168
+
169
+ // src/features/version-footer/ui/nested-version-confirmation.tsx
170
+ import { createComponent as _$createComponent4 } from "@opentui/solid";
171
+ import { onCleanup as onCleanup2 } from "solid-js";
172
+ import { GuardedConfirmation, SelectionMenu } from "./tui.js";
173
+ function NestedVersionConfirmation(props) {
174
+ onCleanup2(props.dismiss);
175
+ return _$createComponent4(GuardedConfirmation, {
176
+ get api() {
177
+ return props.api;
178
+ },
179
+ get current() {
180
+ return props.current;
181
+ },
182
+ get title() {
183
+ return `Update ${props.name}`;
184
+ },
185
+ get message() {
186
+ return `Install ${props.version} using the current installation method?`;
187
+ },
188
+ get onConfirm() {
189
+ return props.confirm;
190
+ },
191
+ render: ({
192
+ confirm
193
+ }) => _$createComponent4(SelectionMenu, {
194
+ get api() {
195
+ return props.api;
196
+ },
197
+ get title() {
198
+ return `Update ${props.name}`;
199
+ },
200
+ get options() {
201
+ return [{
202
+ title: "Cancel",
203
+ value: "cancel",
204
+ description: "Return without installing an update"
205
+ }, {
206
+ title: `Install ${props.name} ${props.version}`,
207
+ value: "install",
208
+ description: "Keep the current installation method and scope; restart OpenCode afterward"
209
+ }];
210
+ },
211
+ onSelect: (option) => option.value === "install" ? confirm() : props.cancel()
212
+ })
213
+ });
133
214
  }
134
- function createVersionUpdateActions(api,status,updater,rememberNavigatorVersion){const busy=new Set
135
- const [navigatorUpdating,setNavigatorUpdating]=createSignal3(false)
136
- const [navigatorInstalled,setNavigatorInstalled]=createSignal3()
137
- const [progress,setProgress]=createSignal3({})
138
- const installed={}
139
- let disposed=false
140
- let confirmation=0
141
- function target(kind){return kind==="openCode"?status.openCodeUpdate():status.navigatorUpdate()
215
+
216
+ // src/features/version-footer/ui/version-update.tsx
217
+ function createVersionUpdateActions(api, status, updater, rememberNavigatorVersion) {
218
+ const busy = new Set;
219
+ const [navigatorUpdating, setNavigatorUpdating] = createSignal3(false);
220
+ const [navigatorInstalled, setNavigatorInstalled] = createSignal3();
221
+ const [navigatorError, setNavigatorError] = createSignal3();
222
+ const [progress, setProgress] = createSignal3({});
223
+ const installed = {};
224
+ let disposed = false;
225
+ let confirmation = 0;
226
+ function target(kind) {
227
+ return kind === "openCode" ? status.openCodeUpdate() : status.navigatorUpdate();
228
+ }
229
+ function label(kind) {
230
+ return kind === "openCode" ? "OpenCode" : "Navigator";
231
+ }
232
+ function update(kind, version, navigation) {
233
+ if (disposed || busy.has(kind))
234
+ return;
235
+ if (version !== target(kind)) {
236
+ api.ui.toast({
237
+ variant: "warning",
238
+ title: "Update changed",
239
+ message: "Check for updates again before confirming."
240
+ });
241
+ return;
242
+ }
243
+ if (navigation)
244
+ navigation.back();
245
+ else
246
+ api.ui.dialog.clear();
247
+ busy.add(kind);
248
+ setProgress((previous) => ({
249
+ ...previous,
250
+ [kind]: {
251
+ phase: "preparing"
252
+ }
253
+ }));
254
+ if (kind === "navigator") {
255
+ setNavigatorUpdating(true);
256
+ setNavigatorError(undefined);
257
+ }
258
+ api.ui.toast({
259
+ variant: "info",
260
+ title: `Updating ${label(kind)}`,
261
+ message: `Installing ${version}...`
262
+ });
263
+ (async () => {
264
+ if (kind === "navigator")
265
+ await rememberNavigatorVersion?.();
266
+ if (disposed || version !== target(kind))
267
+ throw new Error("Update target changed; check again");
268
+ const report = (value) => {
269
+ if (!disposed && busy.has(kind))
270
+ setProgress((previous) => ({
271
+ ...previous,
272
+ [kind]: value
273
+ }));
274
+ };
275
+ report({
276
+ phase: "installing"
277
+ });
278
+ await updater[kind](version, report);
279
+ })().then(() => {
280
+ installed[kind] = version;
281
+ if (kind === "navigator")
282
+ setNavigatorInstalled(version);
283
+ if (disposed)
284
+ return;
285
+ api.ui.toast({
286
+ variant: "success",
287
+ title: `${label(kind)} updated`,
288
+ message: "Restart OpenCode to use the new version.",
289
+ duration: 6000
290
+ });
291
+ }).catch((error) => {
292
+ if (disposed)
293
+ return;
294
+ if (kind === "navigator")
295
+ setNavigatorError(error instanceof Error ? error.message : "Update failed");
296
+ api.ui.toast({
297
+ variant: "error",
298
+ title: `Could not update ${label(kind)}`,
299
+ message: error instanceof Error ? error.message : "Update failed",
300
+ duration: 6000
301
+ });
302
+ }).finally(() => {
303
+ busy.delete(kind);
304
+ setProgress((previous) => ({
305
+ ...previous,
306
+ [kind]: undefined
307
+ }));
308
+ if (kind === "navigator")
309
+ setNavigatorUpdating(false);
310
+ });
311
+ }
312
+ function open(kind, version = target(kind), navigation) {
313
+ if (disposed || !version || version !== target(kind) || busy.has(kind))
314
+ return;
315
+ if (installed[kind] === version) {
316
+ api.ui.toast({
317
+ variant: "info",
318
+ title: `${label(kind)} updated`,
319
+ message: "Restart OpenCode to use the installed update."
320
+ });
321
+ return;
322
+ }
323
+ const id = ++confirmation;
324
+ const onConfirm = () => {
325
+ if (id !== confirmation)
326
+ return;
327
+ confirmation++;
328
+ update(kind, version, navigation);
329
+ };
330
+ if (navigation) {
331
+ navigation.open(() => _$createComponent5(NestedVersionConfirmation, {
332
+ api,
333
+ get name() {
334
+ return label(kind);
335
+ },
336
+ version,
337
+ current: () => !disposed && id === confirmation && version === target(kind),
338
+ confirm: onConfirm,
339
+ cancel: () => {
340
+ if (id !== confirmation)
341
+ return;
342
+ confirmation++;
343
+ navigation.back();
344
+ },
345
+ dismiss: () => {
346
+ if (id === confirmation)
347
+ confirmation++;
348
+ }
349
+ }));
350
+ return;
351
+ }
352
+ api.ui.dialog.replace(() => _$createComponent5(api.ui.DialogConfirm, {
353
+ get title() {
354
+ return `Update ${label(kind)}`;
355
+ },
356
+ message: `Update to ${version} using the current installation method? OpenCode must be restarted afterward.`,
357
+ onConfirm: () => {
358
+ if (id !== confirmation)
359
+ return;
360
+ confirmation++;
361
+ update(kind, version);
362
+ },
363
+ onCancel: () => {
364
+ if (id !== confirmation)
365
+ return;
366
+ confirmation++;
367
+ api.ui.dialog.clear();
368
+ }
369
+ }));
370
+ }
371
+ const manual = createManualUpdateCheck(api, status, () => !disposed && !busy.has("navigator"), () => open("navigator"));
372
+ const unregister = api.keymap.registerLayer({
373
+ commands: [{
374
+ name: `${PLUGIN_ID}.check-update`,
375
+ title: "Check Navigator updates",
376
+ category: "Navigator",
377
+ namespace: "palette",
378
+ enabled: () => !manual.checking() && !busy.has("navigator"),
379
+ run: () => void manual.run()
380
+ }, {
381
+ name: `${PLUGIN_ID}.update-opencode`,
382
+ title: "Update OpenCode",
383
+ category: "Navigator",
384
+ namespace: "palette",
385
+ enabled: () => Boolean(target("openCode")) && !busy.has("openCode"),
386
+ run: () => open("openCode")
387
+ }, {
388
+ name: `${PLUGIN_ID}.update-navigator`,
389
+ title: "Update Navigator",
390
+ category: "Navigator",
391
+ namespace: "palette",
392
+ enabled: () => !manual.checking() && !busy.has("navigator"),
393
+ run: () => void manual.run(true)
394
+ }]
395
+ });
396
+ api.lifecycle.onDispose(() => {
397
+ disposed = true;
398
+ unregister();
399
+ });
400
+ return {
401
+ openCode: (version) => open("openCode", version),
402
+ navigator: (version, navigation) => open("navigator", version, navigation),
403
+ navigatorUpdating,
404
+ navigatorInstalled,
405
+ navigatorError,
406
+ navigatorProgress: () => progress().navigator,
407
+ openCodeProgress: () => progress().openCode
408
+ };
142
409
  }
143
- function label(kind){return kind==="openCode"?"OpenCode":"Navigator"
410
+ // src/app/version-updaters.ts
411
+ import { homedir as homedir2 } from "os";
412
+ import { basename, dirname, isAbsolute, join as join2 } from "path";
413
+ import { fileURLToPath } from "url";
414
+
415
+ // src/app/cli-pin.ts
416
+ import { randomUUID } from "crypto";
417
+ import { readFile, realpath, rename, stat, unlink, writeFile } from "fs/promises";
418
+ import { homedir } from "os";
419
+ import { join } from "path";
420
+
421
+ // src/shared/lib/server-file/index.ts
422
+ function serverFileTarget(path) {
423
+ const windows = /^[a-z]:[\\/]|^\\\\/iu.test(path);
424
+ const absolute = windows ? /^[a-z]:[\\/]|^\\\\[^\\/]+[\\/][^\\/]+[\\/]/iu.test(path) : path.startsWith("/");
425
+ const pieces = path.split(windows ? /[\\/]/u : /\//u);
426
+ const index = windows ? Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\")) : path.lastIndexOf("/");
427
+ const file = path.slice(index + 1);
428
+ if (!absolute || path.includes("\x00") || !file || pieces.some((piece) => piece === "." || piece === ".."))
429
+ throw new Error("OpenCode returned an unsafe or non-absolute configuration path.");
430
+ let directory = path.slice(0, index);
431
+ if (index === 0)
432
+ directory = "/";
433
+ else if (windows && index === 2)
434
+ directory = path.slice(0, 3);
435
+ return { location: { directory }, path: file };
144
436
  }
145
- function update(kind,version){if(disposed||busy.has(kind))
146
- return
147
- if(version!==target(kind)){api.ui.toast({variant:"warning",title:"Update changed",message:"Check for updates again before confirming."
148
- })
149
- return
437
+
438
+ // src/app/plugin-pin.ts
439
+ function exactNavigatorPin(target) {
440
+ return /^opencode-navigator@(\d+\.\d+\.\d+)$/.exec(target)?.[1];
150
441
  }
151
- api.ui.dialog.clear()
152
- busy.add(kind)
153
- setProgress((previous)=>({...previous,[kind]:{phase:"preparing"
154
- }}))
155
- if(kind==="navigator")
156
- setNavigatorUpdating(true)
157
- api.ui.toast({variant:"info",title:`Updating ${label(kind)}`,message:`Installing ${version}...`
158
- });
159
- (async ()=>{if(kind==="navigator")
160
- await rememberNavigatorVersion?.()
161
- if(disposed||version!==target(kind))
162
- throw new Error("Update target changed; check again")
163
- const report=(value)=>{if(!disposed&&busy.has(kind))
164
- setProgress((previous)=>({...previous,[kind]:value
165
- }))
442
+ function pluginPinEdit(text, target, version) {
443
+ const old = exactNavigatorPin(target);
444
+ if (!old || !isNewerVersion(version, old))
445
+ throw new Error("Invalid pinned Navigator update target");
446
+ const errors = [];
447
+ const root = parseTree(text, errors, { allowTrailingComma: true });
448
+ if (!root || errors.length > 0)
449
+ throw new Error("Navigator configuration is invalid; no changes were made");
450
+ const paths = [];
451
+ for (const key of ["plugins", "plugin"]) {
452
+ const list = findNodeAtLocation(root, [key]);
453
+ const nodes = list?.type === "array" ? list.children ?? [] : [];
454
+ for (const [index, node] of nodes.entries()) {
455
+ const value = getNodeValue(node);
456
+ const path = [key, index];
457
+ if (value === target)
458
+ paths.push(path);
459
+ else if (Array.isArray(value) && value[0] === target)
460
+ paths.push([...path, 0]);
461
+ else if (value?.package === target)
462
+ paths.push([...path, "package"]);
463
+ }
464
+ }
465
+ if (paths.length === 0)
466
+ return;
467
+ if (paths.length !== 1)
468
+ throw new Error("Navigator pin is ambiguous; no changes were made");
469
+ const node = findNodeAtLocation(root, paths[0]);
470
+ return text.slice(0, node.offset) + JSON.stringify(`opencode-navigator@${version}`) + text.slice(node.offset + node.length);
166
471
  }
167
- report({phase:"installing"
168
- })
169
- await updater[kind](version,report)
170
- })().then(()=>{installed[kind]=version
171
- if(kind==="navigator")
172
- setNavigatorInstalled(version)
173
- if(disposed)
174
- return
175
- api.ui.toast({variant:"success",title:`${label(kind)} updated`,message:"Restart OpenCode to use the new version.",duration:6000
176
- })
177
- }).catch((error)=>{if(disposed)
178
- return
179
- api.ui.toast({variant:"error",title:`Could not update ${label(kind)}`,message:error instanceof Error?error.message:"Update failed",duration:6000
180
- })
181
- }).finally(()=>{busy.delete(kind)
182
- setProgress((previous)=>({...previous,[kind]:undefined
183
- }))
184
- if(kind==="navigator")
185
- setNavigatorUpdating(false)
186
- })
472
+ async function advanceServerPin(client, location, target, version, signal, valid = () => true) {
473
+ const entries = await client.config.get(location, { signal });
474
+ const candidates = [];
475
+ for (const entry of entries) {
476
+ if (entry.type !== "document" || !entry.path)
477
+ continue;
478
+ const file = serverFileTarget(entry.path);
479
+ const text = new TextDecoder().decode(await client.file.read(file, { signal }));
480
+ const next = pluginPinEdit(text, target, version);
481
+ if (next)
482
+ candidates.push({ file, text, next });
483
+ }
484
+ if (candidates.length !== 1)
485
+ throw new Error("Cannot identify one owning Navigator configuration; no changes were made");
486
+ const selected = candidates[0];
487
+ const current = new TextDecoder().decode(await client.file.read(selected.file, { signal }));
488
+ if (current !== selected.text)
489
+ throw new Error("Navigator configuration changed; check again before updating");
490
+ const active = await client.plugin.list(location, { signal });
491
+ if (!valid() || active.data.every((item) => !(item.source.type === "package" && item.source.target === target)))
492
+ throw new Error("Navigator installation changed; check again before updating");
493
+ if (new TextDecoder().decode(await client.file.read(selected.file, { signal })) !== selected.text || !valid())
494
+ throw new Error("Navigator configuration changed; check again before updating");
495
+ signal.throwIfAborted();
496
+ await client.experimental.file.write({ ...selected.file, payload: new TextEncoder().encode(selected.next) }, { signal });
497
+ if (new TextDecoder().decode(await client.file.read(selected.file, { signal })) !== selected.next)
498
+ throw new Error("Could not verify the Navigator configuration update; inspect it before retrying");
187
499
  }
188
- function open(kind,version=target(kind),navigation){if(disposed||!version||version!==target(kind)||busy.has(kind))
189
- return
190
- if(installed[kind]===version){api.ui.toast({variant:"info",title:`${label(kind)} updated`,message:"Restart OpenCode to use the installed update."
191
- })
192
- return
500
+
501
+ // src/app/cli-pin.ts
502
+ async function advanceCliPin(target, version, path = join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "opencode", "cli.json"), valid = () => true) {
503
+ if (process.env.OPENCODE_CLI_CONFIG_CONTENT !== undefined)
504
+ throw new Error("Inline CLI configuration is active; update its Navigator pin explicitly");
505
+ let resolved;
506
+ try {
507
+ resolved = await realpath(path);
508
+ } catch (error) {
509
+ if (error.code === "ENOENT")
510
+ return false;
511
+ throw error;
512
+ }
513
+ const original = await readFile(resolved, "utf8");
514
+ const next = pluginPinEdit(original, target, version);
515
+ if (!next)
516
+ return false;
517
+ const temporary = `${resolved}.navigator-${randomUUID()}`;
518
+ const metadata = await stat(resolved);
519
+ const mode = metadata.mode;
520
+ try {
521
+ await writeFile(temporary, next, { flag: "wx", mode });
522
+ if (await readFile(resolved, "utf8") !== original || await realpath(path) !== resolved || !valid())
523
+ throw new Error("CLI configuration changed; check again before updating");
524
+ await rename(temporary, resolved);
525
+ if (await readFile(resolved, "utf8") !== next)
526
+ throw new Error("Could not verify the Navigator configuration update");
527
+ } finally {
528
+ await unlink(temporary).catch(() => {});
529
+ }
530
+ return true;
193
531
  }
194
- const id=++confirmation
195
- const onConfirm=()=>{if(id!==confirmation)
196
- return
197
- confirmation++
198
- update(kind,version)
532
+
533
+ // src/app/snapshot-update-process.ts
534
+ var prefix = "navigator-update-progress:";
535
+ function reportLine(line, report) {
536
+ if (!line.startsWith(prefix) || line.length > 1024)
537
+ return;
538
+ try {
539
+ const data = JSON.parse(line.slice(prefix.length));
540
+ if (["preparing", "downloading", "installing", "verifying"].includes(data.phase ?? ""))
541
+ report({ phase: data.phase });
542
+ } catch {}
199
543
  }
200
- if(navigation){navigation.open(()=>_$createComponent5(NestedVersionConfirmation,{api,get name(){return label(kind)
201
- },version,current:()=>!disposed&&id===confirmation&&version===target(kind),confirm:onConfirm,cancel:()=>{if(id!==confirmation)
202
- return
203
- confirmation++
204
- navigation.back()
205
- },dismiss:()=>{if(id===confirmation)
206
- confirmation++
207
- }}))
208
- return
544
+ async function runSnapshotUpdate(command, report) {
545
+ const child = Bun.spawn([...command], { stdout: "pipe", stderr: "pipe" });
546
+ const error = new Response(child.stderr).text();
547
+ const reader = child.stdout.getReader();
548
+ const decoder = new TextDecoder;
549
+ let buffer = "";
550
+ const reading = (async () => {
551
+ while (true) {
552
+ const { done, value } = await reader.read();
553
+ if (done)
554
+ return;
555
+ buffer += decoder.decode(value, { stream: true });
556
+ const lines = buffer.split(`
557
+ `);
558
+ buffer = (lines.pop() ?? "").slice(-1024);
559
+ for (const line of lines)
560
+ reportLine(line, report);
561
+ }
562
+ })().catch(() => {});
563
+ const exit = await child.exited;
564
+ await reader.cancel().catch(() => {});
565
+ await reading;
566
+ const message = await error;
567
+ if (exit !== 0)
568
+ throw new Error(message.trim() || `Snapshot installer failed (${exit})`);
209
569
  }
210
- api.ui.dialog.replace(()=>_$createComponent5(api.ui.DialogConfirm,{get title(){return `Update ${label(kind)}`
211
- },message:`Update to ${version} using the current installation method? OpenCode must be restarted afterward.`,onConfirm:()=>{if(id!==confirmation)
212
- return
213
- confirmation++
214
- update(kind,version)
215
- },onCancel:()=>{if(id!==confirmation)
216
- return
217
- confirmation++
218
- api.ui.dialog.clear()
219
- }}))
570
+
571
+ // src/app/version-updaters.ts
572
+ async function upgradeHost(current, target, execute) {
573
+ if (!isSameMajorVersion(current, target))
574
+ throw new Error("OpenCode updates must stay on the current major version");
575
+ await execute([process.execPath, "upgrade", target]);
220
576
  }
221
- const manual=createManualUpdateCheck(api,status,()=>!disposed&&!busy.has("navigator"),()=>open("navigator"))
222
- const unregister=api.keymap.registerLayer({commands:[{name:`${PLUGIN_ID}.check-update`,title:"Check Navigator updates",category:"Navigator",namespace:"palette",enabled:()=>!manual.checking()&&!busy.has("navigator"),run:()=>void manual.run()
223
- },{name:`${PLUGIN_ID}.update-opencode`,title:"Update OpenCode",category:"Navigator",namespace:"palette",enabled:()=>Boolean(target("openCode"))&&!busy.has("openCode"),run:()=>open("openCode")
224
- },{name:`${PLUGIN_ID}.update-navigator`,title:"Update Navigator",category:"Navigator",namespace:"palette",enabled:()=>!manual.checking()&&!busy.has("navigator"),run:()=>void manual.run(true)
225
- }]
226
- })
227
- api.lifecycle.onDispose(()=>{disposed=true
228
- unregister()
229
- })
230
- return{openCode:(version)=>open("openCode",version),navigator:(version,navigation)=>open("navigator",version,navigation),navigatorUpdating,navigatorInstalled,navigatorProgress:()=>progress().navigator,openCodeProgress:()=>progress().openCode
231
- }}
232
- import {homedir} from "os"
233
- import {basename,dirname,isAbsolute,join} from "path"
234
- import {fileURLToPath} from "url"
235
- var prefix="navigator-update-progress:"
236
- function reportLine(line,report){if(!line.startsWith(prefix)||line.length>1024)
237
- return
238
- try{const data=JSON.parse(line.slice(prefix.length))
239
- if(["preparing","downloading","installing","verifying"].includes(data.phase??""))
240
- report({phase:data.phase})
241
- } catch{}}
242
- async function runSnapshotUpdate(command,report){const child=Bun.spawn([...command],{stdout:"pipe",stderr:"pipe"})
243
- const error=new Response(child.stderr).text()
244
- const reader=child.stdout.getReader()
245
- const decoder=new TextDecoder
246
- let buffer=""
247
- const reading=(async ()=>{while(true){const{done,value}=await reader.read()
248
- if(done)
249
- return
250
- buffer += decoder.decode(value,{stream:true})
251
- const lines=buffer.split(`
252
- `)
253
- buffer=(lines.pop()??"").slice(-1024)
254
- for(const line of lines)
255
- reportLine(line,report)
256
- }})().catch(()=>{})
257
- const exit=await child.exited
258
- await reader.cancel().catch(()=>{})
259
- await reading
260
- const message=await error
261
- if(exit!==0)
262
- throw new Error(message.trim()||`Snapshot installer failed (${exit})`)
577
+ var run = async (command, progress) => {
578
+ if (progress && command[1]?.endsWith("/update-local-snapshot.mjs"))
579
+ return runSnapshotUpdate(command, progress);
580
+ const child = Bun.spawn([...command], { stdout: "ignore", stderr: "pipe" });
581
+ const error = new Response(child.stderr).text();
582
+ const exitCode = await child.exited;
583
+ if (exitCode === 0)
584
+ return;
585
+ const message = await error;
586
+ throw new Error(message.trim() || `Command failed with exit code ${child.exitCode}`);
587
+ };
588
+ var readConfig = async (path) => {
589
+ const file = Bun.file(path);
590
+ return await file.exists() ? Bun.JSONC.parse(await file.text()) : undefined;
591
+ };
592
+ function pinnedUpdate(wrapper, target, execute, progress) {
593
+ if (basename(wrapper) !== "tui.js" || basename(dirname(wrapper)) !== "opencode-navigator")
594
+ throw new Error("Unrecognized local Navigator source");
595
+ const installer = fileURLToPath(new URL("../scripts/update-local-snapshot.mjs", import.meta.url));
596
+ const command = ["node", installer, wrapper, target];
597
+ if (progress)
598
+ return execute([...command, "--progress-json"], progress);
599
+ return execute(command);
263
600
  }
264
- async function upgradeHost(current,target,execute){if(!isSameMajorVersion(current,target))
265
- throw new Error("OpenCode updates must stay on the current major version")
266
- await execute([process.execPath,"upgrade",target])
601
+ function configHasPlugin(value, spec) {
602
+ if (!value || typeof value !== "object")
603
+ return false;
604
+ const plugins = value.plugin;
605
+ return Array.isArray(plugins) && plugins.some((item) => {
606
+ let configured = item;
607
+ if (Array.isArray(item))
608
+ configured = item[0];
609
+ else if (item && typeof item === "object")
610
+ configured = item.package;
611
+ return configured === spec;
612
+ });
267
613
  }
268
- var run=async (command,progress)=>{if(progress&&command[1]?.endsWith("/update-local-snapshot.mjs"))
269
- return runSnapshotUpdate(command,progress)
270
- const child=Bun.spawn([...command],{stdout:"ignore",stderr:"pipe"})
271
- const error=new Response(child.stderr).text()
272
- const exitCode=await child.exited
273
- if(exitCode===0)
274
- return
275
- const message=await error
276
- throw new Error(message.trim()||`Command failed with exit code ${child.exitCode}`)
614
+ async function v1Global(api, spec, read) {
615
+ const configRoot = process.env.OPENCODE_CONFIG_DIR ?? join2(process.env.XDG_CONFIG_HOME ?? join2(homedir2(), ".config"), "opencode");
616
+ const global = join2(configRoot, "tui.json");
617
+ const configured = api.state.path.config.endsWith(".json") ? api.state.path.config : join2(api.state.path.config, "tui.json");
618
+ const local = new Set([configured, join2(api.state.path.worktree, ".opencode", "tui.json")]);
619
+ local.delete(global);
620
+ for (const path of local)
621
+ if (configHasPlugin(await read(path), spec))
622
+ return false;
623
+ return configHasPlugin(await read(global), spec);
277
624
  }
278
- var readConfig=async (path)=>{const file=Bun.file(path)
279
- return await file.exists()?Bun.JSONC.parse(await file.text()):undefined
625
+ function createOpenCodeV1VersionUpdater(api, meta, execute = run, read = readConfig) {
626
+ return {
627
+ openCode: (target) => upgradeHost(api.app.version, target, execute),
628
+ async navigator(target, progress) {
629
+ if (meta.source === "file") {
630
+ const wrapper = meta.spec.startsWith("file://") ? fileURLToPath(meta.spec) : meta.spec;
631
+ return pinnedUpdate(wrapper, target, execute, progress);
632
+ }
633
+ if (meta.source !== "npm")
634
+ throw new Error("Unrecognized local Navigator source");
635
+ const result = await api.plugins.install(`opencode-navigator@${target}`, {
636
+ global: await v1Global(api, meta.spec, read)
637
+ });
638
+ if (!result.ok)
639
+ throw new Error(result.message);
640
+ }
641
+ };
280
642
  }
281
- function pinnedUpdate(wrapper,target,execute,progress){if(basename(wrapper)!=="tui.js"||basename(dirname(wrapper))!=="opencode-navigator")
282
- throw new Error("Unrecognized local Navigator source")
283
- const installer=fileURLToPath(new URL("../scripts/update-local-snapshot.mjs",import.meta.url))
284
- const command=["node",installer,wrapper,target]
285
- if(progress)
286
- return execute([...command,"--progress-json"],progress)
287
- return execute(command)
643
+ function createOpenCodeV2VersionUpdater(context, execute = run) {
644
+ return {
645
+ openCode: (target) => upgradeHost(context.app.version, target, execute),
646
+ async navigator(target, progress) {
647
+ progress?.({ phase: "preparing" });
648
+ const client = context.client;
649
+ const location = JSON.stringify(context.location);
650
+ const response = await client.plugin.list(context.location ? { location: context.location } : {});
651
+ const item = response.data.find((plugin) => plugin.id === "opencode-navigator.todo-server" || plugin.id === PLUGIN_ID);
652
+ if (!item)
653
+ throw new Error("Navigator is not in the active plugin list");
654
+ if (item.source.type === "local")
655
+ return pinnedUpdate(basename(item.source.path) === "tui.js" ? item.source.path : join2(item.source.path, "tui.js"), target, execute, progress);
656
+ if (item.source.type !== "package")
657
+ throw new Error("Unrecognized local Navigator source");
658
+ if (isAbsolute(item.source.target))
659
+ return pinnedUpdate(join2(item.source.target, "tui.js"), target, execute, progress);
660
+ if (item.source.target.startsWith("file://"))
661
+ return pinnedUpdate(join2(fileURLToPath(item.source.target), "tui.js"), target, execute, progress);
662
+ if (item.source.target.startsWith("."))
663
+ throw new Error("Unrecognized local Navigator source");
664
+ if (exactNavigatorPin(item.source.target)) {
665
+ if (context.client !== client || JSON.stringify(context.location) !== location)
666
+ throw new Error("Navigator installation changed; check again");
667
+ const loadedPin = typeof __NAVIGATOR_VERSION__ === "string" ? `opencode-navigator@${__NAVIGATOR_VERSION__}` : item.source.target;
668
+ if (exactNavigatorPin(loadedPin) !== target && await advanceCliPin(loadedPin, target, undefined, () => context.client === client && JSON.stringify(context.location) === location)) {
669
+ progress?.({ phase: "verifying" });
670
+ return;
671
+ }
672
+ if (exactNavigatorPin(item.source.target) !== target)
673
+ await advanceServerPin(client, { location: response.location }, item.source.target, target, AbortSignal.timeout(120000), () => context.client === client && JSON.stringify(context.location) === location);
674
+ progress?.({ phase: "verifying" });
675
+ return;
676
+ }
677
+ progress?.({ phase: "installing" });
678
+ await context.client.plugin.update({ location: response.location, targets: [item.source.target] });
679
+ }
680
+ };
288
681
  }
289
- function configHasPlugin(value,spec){if(!value||typeof value!=="object")
290
- return false
291
- const plugins=value.plugin
292
- return Array.isArray(plugins)&&plugins.some((item)=>{let configured=item
293
- if(Array.isArray(item))
294
- configured=item[0]
295
- else if(item&&typeof item==="object")
296
- configured=item.package
297
- return configured===spec
298
- })
299
- }
300
- async function v1Global(api,spec,read){const configRoot=process.env.OPENCODE_CONFIG_DIR??join(process.env.XDG_CONFIG_HOME??join(homedir(),".config"),"opencode")
301
- const global=join(configRoot,"tui.json")
302
- const configured=api.state.path.config.endsWith(".json")?api.state.path.config:join(api.state.path.config,"tui.json")
303
- const local=new Set([configured,join(api.state.path.worktree,".opencode","tui.json")])
304
- local.delete(global)
305
- for(const path of local)
306
- if(configHasPlugin(await read(path),spec))
307
- return false
308
- return configHasPlugin(await read(global),spec)
309
- }
310
- function createOpenCodeV1VersionUpdater(api,meta,execute=run,read=readConfig){return{openCode:(target)=>upgradeHost(api.app.version,target,execute),async navigator(target,progress){if(meta.source==="file"){const wrapper=meta.spec.startsWith("file://")?fileURLToPath(meta.spec):meta.spec
311
- return pinnedUpdate(wrapper,target,execute,progress)
312
- }
313
- if(meta.source!=="npm")
314
- throw new Error("Unrecognized local Navigator source")
315
- const result=await api.plugins.install(`opencode-navigator@${target}`,{global:await v1Global(api,meta.spec,read)
316
- })
317
- if(!result.ok)
318
- throw new Error(result.message)
319
- }}}
320
- function createOpenCodeV2VersionUpdater(context,execute=run){return{openCode:(target)=>upgradeHost(context.app.version,target,execute),async navigator(target,progress){progress?.({phase:"preparing"})
321
- const response=await context.client.plugin.list(context.location?{location:context.location}:{})
322
- const item=response.data.find((plugin)=>plugin.id===PLUGIN_ID)
323
- if(!item)
324
- throw new Error("Navigator is not in the active plugin list")
325
- if(item.source.type==="local")
326
- return pinnedUpdate(basename(item.source.path)==="tui.js"?item.source.path:join(item.source.path,"tui.js"),target,execute,progress)
327
- if(item.source.type!=="package")
328
- throw new Error("Unrecognized local Navigator source")
329
- if(isAbsolute(item.source.target))
330
- return pinnedUpdate(join(item.source.target,"tui.js"),target,execute,progress)
331
- if(item.source.target.startsWith("file://"))
332
- return pinnedUpdate(join(fileURLToPath(item.source.target),"tui.js"),target,execute,progress)
333
- if(item.source.target.startsWith("."))
334
- throw new Error("Unrecognized local Navigator source")
335
- progress?.({phase:"installing"})
336
- await context.client.plugin.update({location:response.location,targets:[item.source.target]})
337
- }}}
338
- export{createOpenCodeV1VersionUpdater,createOpenCodeV2VersionUpdater,createVersionStatus,createVersionUpdateActions,parseChangelog2 as parseChangelog,readBundledChangelog2 as readBundledChangelog
339
- }
682
+ export {
683
+ createOpenCodeV1VersionUpdater,
684
+ createOpenCodeV2VersionUpdater,
685
+ createVersionStatus,
686
+ createVersionUpdateActions,
687
+ parseChangelog2 as parseChangelog,
688
+ readBundledChangelog2 as readBundledChangelog
689
+ };