opencode-navigator 0.21.8 → 0.23.0

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,10 +1,10 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "opencode-navigator",
4
- "version": "0.21.8",
4
+ "version": "0.23.0",
5
5
  "description": "Search, navigate, and manage OpenCode sessions from the TUI",
6
6
  "type": "module",
7
- "packageManager": "bun@1.4.2",
7
+ "packageManager": "bun@1.4.4",
8
8
  "license": "MIT",
9
9
  "author": "St1ggy",
10
10
  "repository": {
@@ -32,9 +32,12 @@
32
32
  "assets",
33
33
  "scripts/install-font.mjs",
34
34
  "scripts/update-local-snapshot.mjs",
35
+ "scripts/snapshot-runtime.mjs",
36
+ "scripts/snapshot-verifier.mjs",
35
37
  "scripts/save-project-profile.mjs",
36
38
  "README.md",
37
39
  "PROVIDERS.md",
40
+ "HOST_API.md",
38
41
  "CHANGELOG.md",
39
42
  "LICENSE"
40
43
  ],
@@ -57,7 +60,7 @@
57
60
  "access": "public"
58
61
  },
59
62
  "engines": {
60
- "opencode": ">=1.18.30"
63
+ "opencode": "1.19.5 || 2.2.0"
61
64
  },
62
65
  "scripts": {
63
66
  "build": "bun run build.ts",
@@ -80,8 +83,8 @@
80
83
  "check": "bun run test && bun run typecheck && bun run lint && bun run bundle:size && bun run format:check"
81
84
  },
82
85
  "peerDependencies": {
83
- "@opencode-ai/plugin": ">=1.18.30",
84
- "@opencode/plugin": ">=2.0.11",
86
+ "@opencode-ai/plugin": "1.19.5",
87
+ "@opencode/plugin": "2.2.0",
85
88
  "@opentui/core": ">=0.4.5",
86
89
  "@opentui/solid": ">=0.4.5",
87
90
  "solid-js": ">=1.9.12"
@@ -104,12 +107,12 @@
104
107
  }
105
108
  },
106
109
  "devDependencies": {
107
- "@opencode-ai/plugin": "1.18.34",
108
- "@opencode-ai/sdk": "1.18.34",
109
- "@opencode/plugin": "2.0.21",
110
- "@opentui/core": "0.5.12",
111
- "@opentui/keymap": "0.5.12",
112
- "@opentui/solid": "0.5.12",
110
+ "@opencode-ai/plugin": "1.19.5",
111
+ "@opencode-ai/sdk": "1.19.5",
112
+ "@opencode/plugin": "2.2.0",
113
+ "@opentui/core": "0.7.0",
114
+ "@opentui/keymap": "0.7.0",
115
+ "@opentui/solid": "0.7.0",
113
116
  "@st1ggy/linter-config": "8.0.0",
114
117
  "@types/bun": "1.4.2",
115
118
  "eslint-plugin-solid": "0.18.0",
@@ -0,0 +1,32 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { join } from 'node:path'
3
+
4
+ const PACKAGES = ['@opencode/plugin', '@opentui/core', '@opentui/keymap', '@opentui/solid', 'solid-js']
5
+
6
+ export async function snapshotRuntime(directory, fallback) {
7
+ let source
8
+
9
+ try {
10
+ source = await readFile(join(directory, 'dist', 'runtime-dependencies.json'), 'utf8')
11
+ } catch (error) {
12
+ if (error.code === 'ENOENT') return fallback
13
+
14
+ throw error
15
+ }
16
+
17
+ const manifest = JSON.parse(source)
18
+ const dependencies = manifest?.dependencies
19
+
20
+ if (
21
+ manifest?.version !== 1 ||
22
+ Object.keys(manifest).some((key) => !['version', 'dependencies'].includes(key)) ||
23
+ !dependencies ||
24
+ typeof dependencies !== 'object' ||
25
+ Array.isArray(dependencies) ||
26
+ Object.keys(dependencies).length !== PACKAGES.length ||
27
+ PACKAGES.some((name) => typeof dependencies[name] !== 'string' || !/^\d+\.\d+\.\d+$/.test(dependencies[name]))
28
+ )
29
+ throw new Error('Navigator package has an invalid runtime dependency manifest')
30
+
31
+ return Object.fromEntries(PACKAGES.map((name) => [name, dependencies[name]]))
32
+ }
@@ -0,0 +1,36 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { join } from 'node:path'
3
+
4
+ function supports(version, minimum) {
5
+ const value = version.replace(/^v/, '').split('.').map(Number)
6
+ const required = minimum.split('.').map(Number)
7
+
8
+ for (let index = 0; index < 3; index++) {
9
+ if (value[index] !== required[index]) return value[index] > required[index]
10
+ }
11
+
12
+ return true
13
+ }
14
+
15
+ export async function snapshotVerifier(directory, probe) {
16
+ const source = await readFile(join(directory, 'dist', 'runtime-requirements.json'), 'utf8').catch((error) => {
17
+ if (error.code !== 'ENOENT') throw error
18
+ })
19
+
20
+ if (!source) return 'node'
21
+
22
+ const requirements = JSON.parse(source)
23
+
24
+ if (requirements.version !== 1 || ['bun', 'node'].some((name) => !/^\d+\.\d+\.\d+$/.test(requirements[name])))
25
+ throw new Error('Navigator package has invalid runtime requirements')
26
+
27
+ for (const runtime of ['bun', 'node']) {
28
+ const version = await probe(runtime).catch(() => '')
29
+
30
+ if (/^v?\d+\.\d+\.\d+$/.test(version.trim()) && supports(version.trim(), requirements[runtime])) return runtime
31
+ }
32
+
33
+ throw new Error(
34
+ `Snapshot verification requires Bun >=${requirements.bun} or Node >=${requirements.node}; the installed snapshot was preserved`,
35
+ )
36
+ }
@@ -5,16 +5,23 @@ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
5
5
  import { basename, dirname, join, resolve } from 'node:path'
6
6
  import { pathToFileURL } from 'node:url'
7
7
 
8
- function command(program, args) {
8
+ import { snapshotRuntime } from './snapshot-runtime.mjs'
9
+ import { snapshotVerifier } from './snapshot-verifier.mjs'
10
+
11
+ function command(program, args, capture = false) {
9
12
  return new Promise((resolveCommand, reject) => {
10
- const child = spawn(program, args, { stdio: ['ignore', 'ignore', 'pipe'] })
13
+ const child = spawn(program, args, { stdio: ['ignore', capture ? 'pipe' : 'ignore', 'pipe'] })
11
14
  let error = ''
15
+ let output = ''
16
+
17
+ child.stdout?.setEncoding('utf8')
18
+ child.stdout?.on('data', (chunk) => (output += chunk))
12
19
 
13
20
  child.stderr.setEncoding('utf8')
14
21
  child.stderr.on('data', (chunk) => (error += chunk))
15
22
  child.on('error', reject)
16
23
  child.on('close', (code) => {
17
- if (code === 0) resolveCommand()
24
+ if (code === 0) resolveCommand(output.trim())
18
25
  else reject(new Error(error.trim() || `${program} exited with code ${code}`))
19
26
  })
20
27
  })
@@ -33,7 +40,7 @@ export function pinnedSnapshot(wrapper, read = readFile) {
33
40
  })
34
41
  }
35
42
 
36
- async function validate(directory, version) {
43
+ async function validate(directory, version, { checkRuntime = true } = {}) {
37
44
  const metadata = JSON.parse(await readFile(join(directory, 'package.json'), 'utf8'))
38
45
 
39
46
  if (
@@ -48,6 +55,26 @@ async function validate(directory, version) {
48
55
  const changelog = await readFile(join(directory, 'CHANGELOG.md'), 'utf8')
49
56
 
50
57
  if (!changelog.includes(`## [${version}]`)) throw new Error(`Snapshot ${version} has no bundled changelog entry`)
58
+
59
+ // Older installers inherited the predecessor's pins even when a new package
60
+ // declared a runtime manifest. Accept that running source; verify the target.
61
+ if (!checkRuntime) return
62
+
63
+ const runtime = await snapshotRuntime(directory, metadata.dependencies)
64
+
65
+ if (runtime && Object.entries(runtime).some(([name, pinned]) => metadata.dependencies?.[name] !== pinned))
66
+ throw new Error(`Snapshot ${version} has mismatched runtime dependencies`)
67
+ }
68
+
69
+ async function verifySnapshot(directory, runtime) {
70
+ const verifier = runtime ?? (await snapshotVerifier(directory, (program) => command(program, ['--version'], true)))
71
+ const importURL = JSON.stringify(pathToFileURL(join(directory, 'dist', 'tui.js')).href)
72
+
73
+ await command(verifier, [
74
+ '--input-type=module',
75
+ '--eval',
76
+ `const plugin=(await import(${importURL})).default;if(plugin?.id!=='opencode-navigator')throw Error('Invalid Navigator plugin bundle')`,
77
+ ])
51
78
  }
52
79
 
53
80
  export async function updateLocalSnapshot(wrapper, target, operations = {}) {
@@ -59,7 +86,7 @@ export async function updateLocalSnapshot(wrapper, target, operations = {}) {
59
86
 
60
87
  if (target === version) return
61
88
 
62
- await validate(previous, version)
89
+ await validate(previous, version, { checkRuntime: false })
63
90
  const previousMetadata = JSON.parse(await readFile(join(previous, 'package.json'), 'utf8'))
64
91
  const dependencies = previousMetadata.dependencies
65
92
 
@@ -70,8 +97,11 @@ export async function updateLocalSnapshot(wrapper, target, operations = {}) {
70
97
  const replacement = join(root, `.tui-${randomUUID()}.tmp`)
71
98
 
72
99
  try {
73
- if (existsSync(next)) await validate(next, target)
74
- else {
100
+ if (existsSync(next)) {
101
+ await validate(next, target)
102
+
103
+ await (operations.verify ? operations.verify(next) : verifySnapshot(next))
104
+ } else {
75
105
  await mkdir(staging)
76
106
 
77
107
  if (operations.stage) await operations.stage(staging, target)
@@ -92,6 +122,10 @@ export async function updateLocalSnapshot(wrapper, target, operations = {}) {
92
122
  if (metadata.name !== 'opencode-navigator' || metadata.version !== target)
93
123
  throw new Error('Downloaded Navigator package does not match the requested version')
94
124
 
125
+ const verifier = operations.verify
126
+ ? undefined
127
+ : await snapshotVerifier(staging, (runtime) => command(runtime, ['--version'], true))
128
+
95
129
  await writeFile(
96
130
  join(staging, 'package.json'),
97
131
  JSON.stringify({
@@ -99,7 +133,7 @@ export async function updateLocalSnapshot(wrapper, target, operations = {}) {
99
133
  version: target,
100
134
  private: true,
101
135
  type: 'module',
102
- dependencies,
136
+ dependencies: await snapshotRuntime(staging, dependencies),
103
137
  optionalDependencies: { ...previousMetadata.optionalDependencies, ...metadata.optionalDependencies },
104
138
  }),
105
139
  )
@@ -118,17 +152,7 @@ export async function updateLocalSnapshot(wrapper, target, operations = {}) {
118
152
 
119
153
  await validate(staging, target)
120
154
 
121
- if (operations.verify) await operations.verify(staging)
122
- else {
123
- const bundleURL = pathToFileURL(join(staging, 'dist', 'tui.js')).href
124
- const importURL = JSON.stringify(bundleURL)
125
-
126
- await command('node', [
127
- '--input-type=module',
128
- '--eval',
129
- `const plugin=(await import(${importURL})).default;if(plugin?.id!=='opencode-navigator')throw Error('Invalid Navigator plugin bundle')`,
130
- ])
131
- }
155
+ await (operations.verify ? operations.verify(staging) : verifySnapshot(staging, verifier))
132
156
 
133
157
  await rename(staging, next)
134
158
  }
@@ -1,8 +0,0 @@
1
- var PLUGIN_ID="opencode-navigator"
2
- var TOGGLE_COMMAND=`${PLUGIN_ID}.toggle`
3
- var FOCUS_COMMAND=`${PLUGIN_ID}.focus`
4
- var SEARCH_COMMAND=`${PLUGIN_ID}.search`;
5
- var QUICK_ACTION_IDS=["session.rename","session.timeline","session.copy","session.export","session.compact","session.list","session.new","session.fork","messages.copy","session.first","session.last","session.messages_last_user","session.message.next","session.message.previous","permission.mode"]
6
- var SECTION_DEFINITIONS=[{name:"limits",label:"Limits"},{name:"todo",label:"Todo"},{name:"subagents",label:"Subagents"},{name:"skills",label:"Skills"},{name:"quick_actions",label:"Quick actions"},{name:"lsp",label:"LSP"},{name:"mcp",label:"MCP"}];
7
- var MAX_LIMITS_REFRESH_MINUTES=Math.floor(2147483647 / 60000)
8
- export{QUICK_ACTION_IDS,SECTION_DEFINITIONS,PLUGIN_ID}