opencode-navigator 0.17.1 → 0.17.2

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "opencode-navigator",
4
- "version": "0.17.1",
4
+ "version": "0.17.2",
5
5
  "description": "Search, navigate, and manage OpenCode sessions from the TUI",
6
6
  "type": "module",
7
7
  "packageManager": "bun@1.4.2",
@@ -29,6 +29,7 @@
29
29
  "dist",
30
30
  "assets",
31
31
  "scripts/install-font.mjs",
32
+ "scripts/update-local-snapshot.mjs",
32
33
  "README.md",
33
34
  "CHANGELOG.md",
34
35
  "LICENSE"
@@ -0,0 +1,152 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { randomUUID } from 'node:crypto'
3
+ import { existsSync } from 'node:fs'
4
+ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
5
+ import { basename, dirname, join, resolve } from 'node:path'
6
+ import { pathToFileURL } from 'node:url'
7
+
8
+ function command(program, args) {
9
+ return new Promise((resolveCommand, reject) => {
10
+ const child = spawn(program, args, { stdio: ['ignore', 'ignore', 'pipe'] })
11
+ let error = ''
12
+
13
+ child.stderr.setEncoding('utf8')
14
+ child.stderr.on('data', (chunk) => (error += chunk))
15
+ child.on('error', reject)
16
+ child.on('close', (code) => {
17
+ if (code === 0) resolveCommand()
18
+ else reject(new Error(error.trim() || `${program} exited with code ${code}`))
19
+ })
20
+ })
21
+ }
22
+
23
+ export function pinnedSnapshot(wrapper, read = readFile) {
24
+ if (basename(wrapper) !== 'tui.js' || basename(dirname(wrapper)) !== 'opencode-navigator')
25
+ throw new Error('This local Navigator source is not a recognized pinned snapshot')
26
+
27
+ return read(wrapper, 'utf8').then((source) => {
28
+ const match = /^export \{ default \} from "\.\/releases\/v(\d+\.\d+\.\d+)\/dist\/tui\.js";\s*$/.exec(source)
29
+
30
+ if (!match) throw new Error('This local Navigator source is not a recognized pinned snapshot')
31
+
32
+ return { root: dirname(wrapper), version: match[1], source }
33
+ })
34
+ }
35
+
36
+ async function validate(directory, version) {
37
+ const metadata = JSON.parse(await readFile(join(directory, 'package.json'), 'utf8'))
38
+
39
+ if (
40
+ metadata.version !== version ||
41
+ !existsSync(join(directory, 'dist', 'tui.js')) ||
42
+ !existsSync(join(directory, 'node_modules', '@opentui', 'core')) ||
43
+ !existsSync(join(directory, 'node_modules', '@opentui', 'solid')) ||
44
+ !existsSync(join(directory, 'node_modules', 'solid-js'))
45
+ )
46
+ throw new Error(`Snapshot ${version} is incomplete`)
47
+
48
+ const changelog = await readFile(join(directory, 'CHANGELOG.md'), 'utf8')
49
+
50
+ if (!changelog.includes(`## [${version}]`)) throw new Error(`Snapshot ${version} has no bundled changelog entry`)
51
+ }
52
+
53
+ export async function updateLocalSnapshot(wrapper, target, operations = {}) {
54
+ if (!/^\d+\.\d+\.\d+$/.test(target)) throw new Error('Invalid Navigator update version')
55
+
56
+ const { root, version, source } = await pinnedSnapshot(resolve(wrapper))
57
+ const previous = join(root, 'releases', `v${version}`)
58
+ const next = join(root, 'releases', `v${target}`)
59
+
60
+ if (target === version) return
61
+
62
+ await validate(previous, version)
63
+ const dependencies = JSON.parse(await readFile(join(previous, 'package.json'), 'utf8')).dependencies
64
+
65
+ if (!dependencies?.['@opentui/core'] || !dependencies['@opentui/solid'] || !dependencies['solid-js'])
66
+ throw new Error('Pinned Navigator runtime dependencies are missing')
67
+
68
+ const staging = join(root, 'releases', `.v${target}-${randomUUID()}.tmp`)
69
+ const replacement = join(root, `.tui-${randomUUID()}.tmp`)
70
+
71
+ try {
72
+ if (existsSync(next)) await validate(next, target)
73
+ else {
74
+ await mkdir(staging)
75
+
76
+ if (operations.stage) await operations.stage(staging, target)
77
+ else {
78
+ await command('npm', ['pack', `opencode-navigator@${target}`, '--pack-destination', staging, '--silent'])
79
+ await command('tar', [
80
+ '-xzf',
81
+ join(staging, `opencode-navigator-${target}.tgz`),
82
+ '-C',
83
+ staging,
84
+ '--strip-components=1',
85
+ ])
86
+ await rm(join(staging, `opencode-navigator-${target}.tgz`))
87
+ }
88
+
89
+ const metadata = JSON.parse(await readFile(join(staging, 'package.json'), 'utf8'))
90
+
91
+ if (metadata.name !== 'opencode-navigator' || metadata.version !== target)
92
+ throw new Error('Downloaded Navigator package does not match the requested version')
93
+
94
+ await writeFile(
95
+ join(staging, 'package.json'),
96
+ JSON.stringify({
97
+ name: 'opencode-navigator-local-snapshot',
98
+ version: target,
99
+ private: true,
100
+ type: 'module',
101
+ dependencies,
102
+ }),
103
+ )
104
+
105
+ if (operations.install) await operations.install(staging)
106
+ else
107
+ await command('npm', [
108
+ 'install',
109
+ '--prefix',
110
+ staging,
111
+ '--omit=dev',
112
+ '--ignore-scripts',
113
+ '--no-package-lock',
114
+ '--legacy-peer-deps',
115
+ ])
116
+
117
+ await validate(staging, target)
118
+
119
+ if (operations.verify) await operations.verify(staging)
120
+ else {
121
+ const bundleURL = pathToFileURL(join(staging, 'dist', 'tui.js')).href
122
+ const importURL = JSON.stringify(bundleURL)
123
+
124
+ await command('node', [
125
+ '--input-type=module',
126
+ '--eval',
127
+ `const plugin=(await import(${importURL})).default;if(plugin?.id!=='opencode-navigator')throw Error('Invalid Navigator plugin bundle')`,
128
+ ])
129
+ }
130
+
131
+ await rename(staging, next)
132
+ }
133
+
134
+ if ((await readFile(wrapper, 'utf8')) !== source)
135
+ throw new Error('Navigator source changed during the update; restart OpenCode and retry')
136
+
137
+ await writeFile(replacement, `export { default } from "./releases/v${target}/dist/tui.js";\n`)
138
+ await rename(replacement, wrapper)
139
+ } finally {
140
+ await rm(staging, { recursive: true, force: true })
141
+ await rm(replacement, { force: true })
142
+ }
143
+ }
144
+
145
+ if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
146
+ try {
147
+ await updateLocalSnapshot(process.argv[2], process.argv[3])
148
+ } catch (error) {
149
+ console.error(error instanceof Error ? error.message : String(error))
150
+ process.exitCode = 1
151
+ }
152
+ }