pantheon-opencode 1.4.2 → 1.5.0-beta.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/.pantheon/code-mode/.gitkeep +0 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-1.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-10.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-11.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-2.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-3.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-4.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-5.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-6.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-7.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-8.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-9.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/heartbeat.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/session.json +11 -0
- package/.pantheon/code-mode/README.md +93 -0
- package/.pantheon/code-mode/checkpoint-session.py +7 -0
- package/.pantheon/code-mode/checkpoint-session.sh +11 -0
- package/.pantheon/code-mode/checkpoint_session.py +481 -0
- package/.pantheon/code-mode/compress-inline.py +450 -0
- package/.pantheon/code-mode/eval-llm-judge.py +255 -0
- package/.pantheon/code-mode/eval-monte-carlo.py +368 -0
- package/.pantheon/code-mode/eval-run.py +197 -0
- package/.pantheon/code-mode/eval-static.py +321 -0
- package/.pantheon/code-mode/example-sync.sh +5 -0
- package/.pantheon/code-mode/session-end-save.py +186 -0
- package/.pantheon/code-mode/session-end-save.sh +18 -0
- package/.pantheon/code-mode/test-allowed.sh +5 -0
- package/.pantheon/code-mode/test-auto-continue-checkpoint.sh +179 -0
- package/.pantheon/code-mode/test-env.sh +7 -0
- package/.pantheon/code-mode/test-prlimit.sh +9 -0
- package/.pantheon/code-mode/test-timeout.sh +6 -0
- package/README.md +158 -1189
- package/README.pt-BR.md +222 -0
- package/bin/pantheon-init.mjs +30 -11
- package/opencode.json +1 -0
- package/package.json +14 -3
- package/plugin.json +15 -0
- package/scripts/_pantheon_paths.py +48 -7
- package/scripts/ci-validate-yaml.py +7 -2
- package/scripts/code_mode_server.py +33 -2
- package/scripts/doctor.mjs +135 -127
- package/scripts/generate-preset-docs.mjs +127 -66
- package/scripts/generate-prompts.sh +2 -2
- package/scripts/install/cli-ui.mjs +8 -1
- package/scripts/install/config-migration.mjs +401 -0
- package/scripts/install/health-check.mjs +3 -3
- package/scripts/install/model-picker.mjs +72 -23
- package/scripts/install/opencode-version.mjs +47 -0
- package/scripts/install/opencode.mjs +261 -88
- package/scripts/install/plugin.mjs +112 -42
- package/scripts/install/shared.mjs +13 -158
- package/scripts/install-mcp.mjs +44 -232
- package/scripts/mcp_persistence_server.py +344 -15
- package/scripts/mcp_resources_server.py +144 -40
- package/scripts/memory_mcp_server.py +2 -2
- package/scripts/recover-zenodo-deposition.mjs +146 -0
- package/scripts/sync-tui.mjs +32 -7
- package/scripts/test-opencode-v1-v2-sandbox.sh +591 -0
- package/scripts/uninstall.mjs +298 -954
- package/scripts/validate-zenodo-release.mjs +154 -0
- package/scripts/zenodo-artifact.mjs +21 -0
- package/scripts/zenodo-release-state.mjs +53 -0
- package/src/mcp/_pantheon_paths.py +48 -7
- package/src/mcp/code_mode_server.py +33 -2
- package/src/mcp/eval_store.py +1 -1
- package/src/mcp/mcp_persistence_server.py +344 -15
- package/src/mcp/mcp_resources_server.py +144 -40
- package/src/mcp/pantheon_vision_server.py +1 -1
- package/src/mcp/toon_codec.py +491 -0
- package/src/pantheon/background-job-board.ts +67 -1
- package/src/pantheon/cost-command.ts +6 -2
- package/src/pantheon/delegate-manager.ts +384 -0
- package/src/pantheon/delegate-task-adapter.ts +444 -0
- package/src/pantheon/delegation-notify.ts +35 -20
- package/src/pantheon/model-command.ts +86 -32
- package/src/pantheon/token-opt.ts +555 -0
- package/src/pantheon/toon-codec.ts +481 -0
- package/src/pantheon/tui-version-check.ts +1 -3
- package/src/pantheon/v2-bridge.ts +129 -0
- package/src/pantheon/v2-events.ts +131 -0
- package/src/pantheon/v2-hooks.ts +287 -0
- package/src/pantheon/v2-tool-definitions.ts +331 -0
- package/src/plugin-v2/index.ts +21 -0
- package/src/plugin-v2.ts +836 -0
- package/src/plugin.ts +57 -10
- package/src/plugins/tui/dist/server.d.ts +1 -1
- package/src/plugins/tui/dist/server.d.ts.map +1 -1
- package/src/plugins/tui/dist/server.js +1 -1
- package/src/plugins/tui/dist/server.js.map +1 -1
- package/src/plugins/tui/dist/tui.d.ts +10 -7
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +11 -8
- package/src/plugins/tui/dist/tui.js.map +1 -1
- package/src/plugins/tui/dist/tui.tsx +22 -16
- package/src/plugins/tui/package-lock.json +51 -51
- package/src/plugins/tui/package.json +7 -7
- package/src/plugins/tui/src/index.tsx +22 -16
- package/src/skills/README.md +0 -2
|
@@ -2,20 +2,19 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* opencode.mjs — OpenCode platform installer
|
|
4
4
|
*
|
|
5
|
-
* Dual-version install (Phase 3):
|
|
6
|
-
*
|
|
7
|
-
* v0.0.0-next-17444)
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* label; state isolation is handled at runtime via OPENCODE_DB.
|
|
5
|
+
* Dual-version install (Phase 3): V1 and V2 receive the schema each runtime
|
|
6
|
+
* actually consumes. V1 (`opencode` 1.18.x) uses the singular `plugin` key;
|
|
7
|
+
* V2 (`opencode2` v0.0.0-next-17444) converts that list to `plugins` before
|
|
8
|
+
* writing its config. Known V2 beta gaps handled here: top-level
|
|
9
|
+
* `subagent_depth` is migrated to `experimental.subagent_depth`, and the
|
|
10
|
+
* instruction content is consolidated into AGENTS.md, which both versions
|
|
11
|
+
* load. Pass --version v2 to pantheon-init for an informational label; state
|
|
12
|
+
* isolation is handled at runtime via OPENCODE_DB.
|
|
14
13
|
*/
|
|
15
14
|
|
|
16
15
|
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:fs'
|
|
17
16
|
import { homedir } from 'node:os'
|
|
18
|
-
import {
|
|
17
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path'
|
|
19
18
|
import {
|
|
20
19
|
bullet,
|
|
21
20
|
colors,
|
|
@@ -29,8 +28,10 @@ import {
|
|
|
29
28
|
success,
|
|
30
29
|
warning,
|
|
31
30
|
} from './cli-ui.mjs'
|
|
31
|
+
import { migrateV1toV2 } from './config-migration.mjs'
|
|
32
32
|
import { healthCheck } from './health-check.mjs'
|
|
33
33
|
import { detectVersion, runMigrations } from './migrate.mjs'
|
|
34
|
+
import { resolveOpenCodeVersion } from './opencode-version.mjs'
|
|
34
35
|
import { copyPluginFiles, installPlugin, registerPlugin, unregisterPlugin } from './plugin.mjs'
|
|
35
36
|
import {
|
|
36
37
|
collectSkillNames,
|
|
@@ -101,24 +102,134 @@ function mergeMissing(target, source) {
|
|
|
101
102
|
* Resolve a plugin ref from the packaged opencode.json to a path INSIDE the
|
|
102
103
|
* installed package.
|
|
103
104
|
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* from ROOT — never hardcoded), so both dev and global installs resolve to a
|
|
109
|
-
* path that actually exists.
|
|
105
|
+
* Only exact Pantheon plugin identities are resolved into the installed
|
|
106
|
+
* package. In particular, a third-party absolute path ending in
|
|
107
|
+
* `src/plugin.ts` or `src/plugins/pantheon-hooks.ts` is not evidence that the
|
|
108
|
+
* plugin belongs to Pantheon and must be preserved verbatim.
|
|
110
109
|
*/
|
|
111
110
|
export function resolveInstalledPlugin(plugin) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
if (
|
|
118
|
-
const
|
|
119
|
-
|
|
111
|
+
if (typeof plugin !== 'string') return plugin
|
|
112
|
+
const normalized = normalizePluginRef(plugin)
|
|
113
|
+
if (PANTHEON_PLUGIN_IDENTITIES.has(normalized)) return join(ROOT, normalized)
|
|
114
|
+
// Legacy V2 file ref (pre-directory-contract): migrate to the installed
|
|
115
|
+
// plugin directory so the beta loader accepts the entry.
|
|
116
|
+
if (normalized === PANTHEON_V2_LEGACY_FILE) return join(ROOT, PANTHEON_V2_PLUGIN)
|
|
117
|
+
const identity = managedPluginIdentity(plugin)
|
|
118
|
+
if (identity !== null && isAbsolute(normalized)) {
|
|
119
|
+
return INSTALLED_PANTHEON_PLUGIN_PATHS.get(identity) ?? plugin
|
|
120
|
+
}
|
|
121
|
+
return plugin
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function normalizePluginRef(ref) {
|
|
125
|
+
return typeof ref === 'string' ? ref.replaceAll('\\', '/').replace(/\/$/, '') : ref
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const PANTHEON_V1_PLUGIN = 'src/plugin.ts'
|
|
129
|
+
const PANTHEON_V1_HOOKS = 'src/plugins/pantheon-hooks.ts'
|
|
130
|
+
// Canonical V2 entry is the plugin DIRECTORY (beta loader contract: entries
|
|
131
|
+
// must be dirs with a real index.ts — file paths warn "must be a directory",
|
|
132
|
+
// symlinks are ignored). The shim at src/plugin-v2/index.ts re-exports the
|
|
133
|
+
// real entry src/plugin-v2.ts (single source of truth).
|
|
134
|
+
const PANTHEON_V2_PLUGIN = 'src/plugin-v2'
|
|
135
|
+
// Legacy V2 file ref from before the directory contract (installer + template
|
|
136
|
+
// used to register src/plugin-v2.ts). Still recognized for migration.
|
|
137
|
+
const PANTHEON_V2_LEGACY_FILE = 'src/plugin-v2.ts'
|
|
138
|
+
const PANTHEON_V1_EXPORT = 'pantheon-opencode/plugin'
|
|
139
|
+
const PANTHEON_V2_EXPORT = 'pantheon-opencode/plugin-v2'
|
|
140
|
+
const PANTHEON_PLUGIN_IDENTITIES = new Set([
|
|
141
|
+
PANTHEON_V1_PLUGIN,
|
|
142
|
+
PANTHEON_V1_HOOKS,
|
|
143
|
+
PANTHEON_V2_PLUGIN,
|
|
144
|
+
])
|
|
145
|
+
|
|
146
|
+
const INSTALLED_PANTHEON_PLUGIN_PATHS = new Map([
|
|
147
|
+
[PANTHEON_V1_PLUGIN, resolve(ROOT, PANTHEON_V1_PLUGIN)],
|
|
148
|
+
[PANTHEON_V1_HOOKS, resolve(ROOT, PANTHEON_V1_HOOKS)],
|
|
149
|
+
[PANTHEON_V2_PLUGIN, resolve(ROOT, PANTHEON_V2_PLUGIN)],
|
|
150
|
+
])
|
|
151
|
+
|
|
152
|
+
function normalizePluginPath(ref) {
|
|
153
|
+
return ref.replaceAll('\\', '/')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function pluginMetadataReferences(ref) {
|
|
157
|
+
if (typeof ref === 'string') return [ref]
|
|
158
|
+
if (!ref || typeof ref !== 'object' || Array.isArray(ref)) return []
|
|
159
|
+
return ['package', 'id', 'name', 'path', 'source']
|
|
160
|
+
.filter((key) => typeof ref[key] === 'string')
|
|
161
|
+
.map((key) => ref[key])
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function managedPluginIdentity(ref) {
|
|
165
|
+
const normalized = normalizePluginRef(ref)
|
|
166
|
+
if (typeof normalized !== 'string') return null
|
|
167
|
+
|
|
168
|
+
if (PANTHEON_PLUGIN_IDENTITIES.has(normalized)) return normalized
|
|
169
|
+
if (normalized === PANTHEON_V2_LEGACY_FILE) return PANTHEON_V2_PLUGIN
|
|
170
|
+
if (normalized === PANTHEON_V1_EXPORT || normalized.startsWith(`${PANTHEON_V1_EXPORT}@`)) {
|
|
171
|
+
return PANTHEON_V1_PLUGIN
|
|
172
|
+
}
|
|
173
|
+
if (normalized === PANTHEON_V2_EXPORT || normalized.startsWith(`${PANTHEON_V2_EXPORT}@`)) {
|
|
174
|
+
return PANTHEON_V2_PLUGIN
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (isAbsolute(normalized)) {
|
|
178
|
+
const absolutePath = normalizePluginPath(resolve(normalized))
|
|
179
|
+
for (const [identity, installedPath] of INSTALLED_PANTHEON_PLUGIN_PATHS) {
|
|
180
|
+
if (absolutePath === normalizePluginPath(installedPath)) return identity
|
|
181
|
+
}
|
|
182
|
+
// Legacy absolute V2 file path inside OUR installed package (pre-
|
|
183
|
+
// directory-contract installs) migrates to the directory identity.
|
|
184
|
+
// ROOT-scoped: third-party paths ending in src/plugin-v2.ts stay theirs.
|
|
185
|
+
if (absolutePath === normalizePluginPath(resolve(ROOT, PANTHEON_V2_LEGACY_FILE))) {
|
|
186
|
+
return PANTHEON_V2_PLUGIN
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return null
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Return the stable identity for a Pantheon plugin, or its normalized path.
|
|
195
|
+
* Basenames are deliberately not used: unrelated user plugins may share a
|
|
196
|
+
* filename such as `plugin.ts`.
|
|
197
|
+
*/
|
|
198
|
+
export function pluginReferenceIdentity(ref) {
|
|
199
|
+
const references = pluginMetadataReferences(ref)
|
|
200
|
+
for (const reference of references) {
|
|
201
|
+
const identity = managedPluginIdentity(reference)
|
|
202
|
+
if (identity !== null) return identity
|
|
203
|
+
}
|
|
204
|
+
if (references.length > 0) return normalizePluginPath(normalizePluginRef(references[0]))
|
|
205
|
+
return ref
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function isPantheonPluginReference(ref) {
|
|
209
|
+
return PANTHEON_PLUGIN_IDENTITIES.has(pluginReferenceIdentity(ref))
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function removePantheonPluginReferences(refs) {
|
|
213
|
+
return (Array.isArray(refs) ? refs : []).filter((ref) => !isPantheonPluginReference(ref))
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function hasPluginReference(refs, candidate) {
|
|
217
|
+
const identity = pluginReferenceIdentity(candidate)
|
|
218
|
+
return refs.some((ref) => pluginReferenceIdentity(ref) === identity)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function deduplicatePluginReferences(refs) {
|
|
222
|
+
const seen = new Set()
|
|
223
|
+
return (Array.isArray(refs) ? refs : []).filter((ref) => {
|
|
224
|
+
const identity = pluginReferenceIdentity(ref)
|
|
225
|
+
if (seen.has(identity)) return false
|
|
226
|
+
seen.add(identity)
|
|
227
|
+
return true
|
|
228
|
+
})
|
|
120
229
|
}
|
|
121
230
|
|
|
231
|
+
export { parseOpenCodeVersion, resolveOpenCodeVersion } from './opencode-version.mjs'
|
|
232
|
+
|
|
122
233
|
/**
|
|
123
234
|
* Detect whether `target` is the user's global OpenCode installation directory
|
|
124
235
|
* (~/.opencode or $XDG_CONFIG_HOME/opencode).
|
|
@@ -217,10 +328,15 @@ export function syncTuiRegistration(target, { isGlobal = false, dryRun = false }
|
|
|
217
328
|
|
|
218
329
|
// 2. Clean stale Pantheon TUI refs from every location OpenCode reads.
|
|
219
330
|
// unregisterPlugin covers the bare relative id plus the extended stale
|
|
220
|
-
// patterns (dist files, in-package
|
|
221
|
-
//
|
|
331
|
+
// patterns (dist files, identifiable in-package paths, owned copied
|
|
332
|
+
// directories, relative ids, and npx package identities).
|
|
222
333
|
for (const tuiConfigPath of tuiConfigLocations(target)) {
|
|
223
|
-
|
|
334
|
+
const configDirForPath = dirname(tuiConfigPath)
|
|
335
|
+
const ownedCopy = resolveTuiCopyTarget(configDirForPath)
|
|
336
|
+
unregisterPlugin(tuiConfigPath, 'plugins/pantheon-tui', {
|
|
337
|
+
dryRun,
|
|
338
|
+
knownPantheonPaths: [ownedCopy, tuiCopyDir],
|
|
339
|
+
})
|
|
224
340
|
}
|
|
225
341
|
|
|
226
342
|
// 3. Register the copied directory in the selected target only.
|
|
@@ -242,13 +358,12 @@ export async function installOpenCode(
|
|
|
242
358
|
const componentSet = new Set(components)
|
|
243
359
|
const stats = summary.opencode
|
|
244
360
|
|
|
245
|
-
// V1/V2 dual-version awareness (Phase 3): the
|
|
246
|
-
//
|
|
247
|
-
// V2 normalizes V1 fields in memory (no rewrite) and
|
|
248
|
-
// via OPENCODE_DB (~/.local/share/opencode/opencode-v2.db)
|
|
249
|
-
// service port (49375 vs V1's 49374).
|
|
250
|
-
|
|
251
|
-
const version = opts.version === 'v2' ? 'v2' : 'v1'
|
|
361
|
+
// V1/V2 dual-version awareness (Phase 3): the installer builds a unified
|
|
362
|
+
// config, then applies V2-native migration (migrateV1toV2) when
|
|
363
|
+
// --opencode-version=v2. V2 normalizes V1 fields in memory (no rewrite) and
|
|
364
|
+
// isolates its state DB via OPENCODE_DB (~/.local/share/opencode/opencode-v2.db)
|
|
365
|
+
// with a distinct service port (49375 vs V1's 49374).
|
|
366
|
+
const version = resolveOpenCodeVersion(opts.version ?? 'v1')
|
|
252
367
|
if (version === 'v2') {
|
|
253
368
|
info(
|
|
254
369
|
'OpenCode V2 target — shared config; state isolated via OPENCODE_DB ' +
|
|
@@ -445,11 +560,20 @@ export async function installOpenCode(
|
|
|
445
560
|
}
|
|
446
561
|
|
|
447
562
|
// -----------------------------------------------------------------------
|
|
448
|
-
// 2.9 Create/update tui.json with plugin registration
|
|
563
|
+
// 2.9 Create/update tui.json with plugin registration (--components plugins)
|
|
449
564
|
// -----------------------------------------------------------------------
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
565
|
+
if (componentSet.has('plugins')) {
|
|
566
|
+
let tuiStatus
|
|
567
|
+
try {
|
|
568
|
+
tuiStatus = syncTuiRegistration(target, { isGlobal, dryRun })
|
|
569
|
+
} catch (cause) {
|
|
570
|
+
error(`TUI plugin setup failed: ${cause.message}`)
|
|
571
|
+
stats.errors++
|
|
572
|
+
throw new Error('TUI plugin setup failed; installation was not completed', { cause })
|
|
573
|
+
}
|
|
574
|
+
if (tuiStatus === 'created') stats.created++
|
|
575
|
+
else stats.skipped++
|
|
576
|
+
}
|
|
453
577
|
|
|
454
578
|
// -----------------------------------------------------------------------
|
|
455
579
|
// 2.10 Install runtime infrastructure (--components runtime)
|
|
@@ -531,6 +655,14 @@ export async function installOpenCode(
|
|
|
531
655
|
const cmResult = syncDir(srcCodeModeDir, dstCodeModeDir, dryRun, clean)
|
|
532
656
|
stats.created += cmResult.created
|
|
533
657
|
stats.skipped += cmResult.skipped
|
|
658
|
+
} else {
|
|
659
|
+
// The code-mode payload ships inside the published tarball
|
|
660
|
+
// (package.json `files` includes .pantheon/code-mode/**). When it is
|
|
661
|
+
// absent, still create the runtime dir so the code-mode MCP server has
|
|
662
|
+
// a stable home, and warn loudly instead of silently skipping.
|
|
663
|
+
warning(`Code-mode source not found: ${srcCodeModeDir} — creating empty ${dstCodeModeDir}`)
|
|
664
|
+
if (!dryRun) mkdirSync(dstCodeModeDir, { recursive: true })
|
|
665
|
+
stats.created++
|
|
534
666
|
}
|
|
535
667
|
|
|
536
668
|
// ── tiers.json ──
|
|
@@ -547,7 +679,7 @@ export async function installOpenCode(
|
|
|
547
679
|
// -----------------------------------------------------------------------
|
|
548
680
|
// 3. Create/update opencode.json (always runs)
|
|
549
681
|
// Reads TARGET's existing config first, then merges Pantheon settings
|
|
550
|
-
// on top. Preserves user's MCP, provider,
|
|
682
|
+
// on top. Preserves user's MCP, provider, plugins, compaction, theme.
|
|
551
683
|
// -----------------------------------------------------------------------
|
|
552
684
|
// --------------------------------------------------------------------
|
|
553
685
|
// A. Parse canonical agent config from agents/*.agent.md frontmatter
|
|
@@ -635,9 +767,8 @@ export async function installOpenCode(
|
|
|
635
767
|
existing[field] = JSON.parse(JSON.stringify(agentCfg[field]))
|
|
636
768
|
}
|
|
637
769
|
}
|
|
638
|
-
//
|
|
639
|
-
|
|
640
|
-
delete existing.small_model
|
|
770
|
+
// Existing agent fields belong to the user; never remove them during
|
|
771
|
+
// an install. Canonical values are merged only when absent.
|
|
641
772
|
} else {
|
|
642
773
|
// ── New agent ──
|
|
643
774
|
const newAgent = {}
|
|
@@ -663,17 +794,7 @@ export async function installOpenCode(
|
|
|
663
794
|
}
|
|
664
795
|
}
|
|
665
796
|
|
|
666
|
-
//
|
|
667
|
-
// Only removes agents whose source is managed (starts with our prefix)
|
|
668
|
-
// so user-defined agents with different source paths are preserved.
|
|
669
|
-
const canonicalNames = new Set(Object.keys(canonicalAgentConfig))
|
|
670
|
-
for (const [agentName, agentCfg] of Object.entries(config.agent)) {
|
|
671
|
-
const source = agentCfg?.source || ''
|
|
672
|
-
if (source.startsWith(agentPrefix) && !canonicalNames.has(agentName)) {
|
|
673
|
-
delete config.agent[agentName]
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
if (Object.keys(config.agent).length === 0) delete config.agent
|
|
797
|
+
// Do not remove stale or user-defined agents. Installation is additive.
|
|
677
798
|
}
|
|
678
799
|
|
|
679
800
|
// --------------------------------------------------------------------
|
|
@@ -738,39 +859,70 @@ export async function installOpenCode(
|
|
|
738
859
|
// The source opencode.json keeps its dev path for local development; the
|
|
739
860
|
// transform happens at install/sync time only.
|
|
740
861
|
|
|
741
|
-
if (
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
if (Array.isArray(
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
862
|
+
if (version === 'v1') {
|
|
863
|
+
// V1 loads the singular `plugin` list. Remove every Pantheon V2 entry from
|
|
864
|
+
// both config shapes before restoring the two V1 plugins. User plugins in
|
|
865
|
+
// either list are retained verbatim.
|
|
866
|
+
if (Array.isArray(config.plugins)) {
|
|
867
|
+
config.plugins = removePantheonPluginReferences(config.plugins)
|
|
868
|
+
}
|
|
869
|
+
if (config.plugin === undefined) config.plugin = []
|
|
870
|
+
if (Array.isArray(config.plugin)) {
|
|
871
|
+
config.plugin = removePantheonPluginReferences(config.plugin)
|
|
872
|
+
if (Array.isArray(pantheonConfig.plugin)) {
|
|
873
|
+
for (const plugin of pantheonConfig.plugin) {
|
|
874
|
+
if (!isPantheonPluginReference(plugin) && !hasPluginReference(config.plugin, plugin)) {
|
|
875
|
+
config.plugin.push(deepClone(plugin))
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const ensurePantheonPlugin = (ref) => {
|
|
881
|
+
const resolved = resolveInstalledPlugin(ref)
|
|
882
|
+
config.plugin = config.plugin.filter(
|
|
883
|
+
(plugin) => pluginReferenceIdentity(plugin) !== pluginReferenceIdentity(resolved),
|
|
884
|
+
)
|
|
752
885
|
config.plugin.push(resolved)
|
|
753
886
|
}
|
|
887
|
+
// Root-level delegation plugin remains a V1-only registration.
|
|
888
|
+
ensurePantheonPlugin('src/plugin.ts')
|
|
889
|
+
ensurePantheonPlugin('src/plugins/pantheon-hooks.ts')
|
|
754
890
|
}
|
|
891
|
+
}
|
|
755
892
|
|
|
756
|
-
|
|
757
|
-
//
|
|
758
|
-
//
|
|
759
|
-
//
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
|
|
893
|
+
if (version === 'v2') {
|
|
894
|
+
// V2 loads the plural `plugins` list. Keep third-party V1 entries in the
|
|
895
|
+
// legacy list for users who still use them, but never leave a Pantheon V1
|
|
896
|
+
// entry configured alongside the V2 package export.
|
|
897
|
+
if (Array.isArray(config.plugin)) {
|
|
898
|
+
config.plugin = removePantheonPluginReferences(config.plugin)
|
|
899
|
+
}
|
|
900
|
+
if (!Array.isArray(config.plugins)) config.plugins = []
|
|
901
|
+
config.plugins = deduplicatePluginReferences(removePantheonPluginReferences(config.plugins))
|
|
902
|
+
if (Array.isArray(pantheonConfig.plugins)) {
|
|
903
|
+
for (const plugin of pantheonConfig.plugins) {
|
|
904
|
+
if (!isPantheonPluginReference(plugin) && !hasPluginReference(config.plugins, plugin)) {
|
|
905
|
+
config.plugins.push(deepClone(plugin))
|
|
906
|
+
}
|
|
907
|
+
}
|
|
769
908
|
}
|
|
770
|
-
//
|
|
771
|
-
|
|
772
|
-
//
|
|
773
|
-
|
|
909
|
+
// The beta resolves every `plugins` entry through npm install, so an
|
|
910
|
+
// `a/b` shorthand (e.g. the published `pantheon-opencode/plugin-v2`
|
|
911
|
+
// export) is treated as a GitHub repo and dies with NpmInstallFailedError
|
|
912
|
+
// (beta log evidence). The beta loader additionally requires each entry
|
|
913
|
+
// to be a DIRECTORY with a real index.js/index.ts — a file path warns
|
|
914
|
+
// "must be a directory" and a symlink is ignored (sandbox probe
|
|
915
|
+
// evidence). Register the V2 plugin DIRECTORY inside the INSTALLED
|
|
916
|
+
// package — exactly like V1's ensurePantheonPlugin, but pointing at
|
|
917
|
+
// src/plugin-v2 whose index.ts shim re-exports the real entry.
|
|
918
|
+
// Legacy refs (npm shorthand, the old src/plugin-v2.ts file path) are
|
|
919
|
+
// recognized via managedPluginIdentity and already stripped above, so
|
|
920
|
+
// they migrate here.
|
|
921
|
+
const resolvedV2 = resolveInstalledPlugin(PANTHEON_V2_PLUGIN)
|
|
922
|
+
config.plugins = config.plugins.filter(
|
|
923
|
+
(plugin) => pluginReferenceIdentity(plugin) !== pluginReferenceIdentity(resolvedV2),
|
|
924
|
+
)
|
|
925
|
+
config.plugins.push(resolvedV2)
|
|
774
926
|
}
|
|
775
927
|
|
|
776
928
|
if (config.provider === undefined && pantheonConfig.provider !== undefined) {
|
|
@@ -854,11 +1006,12 @@ export async function installOpenCode(
|
|
|
854
1006
|
}
|
|
855
1007
|
|
|
856
1008
|
// --------------------------------------------------------------------
|
|
857
|
-
// F. Compatibility cleanup (OpenCode >=
|
|
1009
|
+
// F. Compatibility cleanup (OpenCode V1 >= 1.15.7)
|
|
858
1010
|
// --------------------------------------------------------------------
|
|
859
|
-
//
|
|
860
|
-
//
|
|
861
|
-
|
|
1011
|
+
// Recent V1 releases reject the legacy `todoContinuation` setting. Remove
|
|
1012
|
+
// it from the merged V1 config, while leaving the V2-shaped configuration
|
|
1013
|
+
// untouched.
|
|
1014
|
+
if (version === 'v1' && Object.hasOwn(config, 'todoContinuation')) {
|
|
862
1015
|
delete config.todoContinuation
|
|
863
1016
|
}
|
|
864
1017
|
|
|
@@ -867,6 +1020,10 @@ export async function installOpenCode(
|
|
|
867
1020
|
// --------------------------------------------------------------------
|
|
868
1021
|
if (componentSet.has('runtime')) {
|
|
869
1022
|
config.mcp = config.mcp || {}
|
|
1023
|
+
// MCP stays a flat named-server map for BOTH V1 and V2: OpenCode 1.18.x
|
|
1024
|
+
// has no `mcp.servers` wrapper and rejects one with
|
|
1025
|
+
// "Missing key mcp.servers.enabled". config-migration.mjs normalizes any
|
|
1026
|
+
// legacy wrapper-shaped entries back to the flat map.
|
|
870
1027
|
// P1-3: derive the MCP python from the SAME venv layout setupVenv
|
|
871
1028
|
// creates (<target>/.venv via venvPythonPath). For project installs the
|
|
872
1029
|
// runtime payload lives under <target>/.opencode (runtimeTarget), but the
|
|
@@ -940,7 +1097,16 @@ export async function installOpenCode(
|
|
|
940
1097
|
}
|
|
941
1098
|
}
|
|
942
1099
|
|
|
943
|
-
|
|
1100
|
+
// ── V2 native migration ──
|
|
1101
|
+
// When targeting V2, convert the merged V1-shaped config to native V2 format
|
|
1102
|
+
// before writing. V1 path is untouched (zero regression).
|
|
1103
|
+
let configToWrite = config
|
|
1104
|
+
if (version === 'v2') {
|
|
1105
|
+
info('Migrating config to V2 native format...')
|
|
1106
|
+
configToWrite = migrateV1toV2(config)
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
const configContent = `${JSON.stringify(configToWrite, null, 2)}\n`
|
|
944
1110
|
const status = writeIfChanged(targetConfigPath, configContent, dryRun)
|
|
945
1111
|
if (status === 'created') stats.created++
|
|
946
1112
|
else stats.skipped++
|
|
@@ -969,7 +1135,8 @@ export async function installOpenCode(
|
|
|
969
1135
|
const venvSpinner = spinner('Setting up Python virtual environment')
|
|
970
1136
|
setupVenv(target, { dryRun, force: clean })
|
|
971
1137
|
venvSpinner(true)
|
|
972
|
-
const
|
|
1138
|
+
const runtimeTarget = isGlobal ? target : join(target, '.opencode')
|
|
1139
|
+
const health = healthCheck(runtimeTarget, { dryRun, pythonTarget: target })
|
|
973
1140
|
|
|
974
1141
|
// Print health summary
|
|
975
1142
|
section('\uD83D\uDD0D Health Check')
|
|
@@ -997,7 +1164,13 @@ export async function installOpenCode(
|
|
|
997
1164
|
const errors = stats.errors
|
|
998
1165
|
|
|
999
1166
|
process.stdout.write('\n')
|
|
1000
|
-
process.stdout.write(
|
|
1167
|
+
process.stdout.write(
|
|
1168
|
+
` ${colors.bold(
|
|
1169
|
+
errors > 0
|
|
1170
|
+
? colors.yellow('Installation completed with errors')
|
|
1171
|
+
: colors.green('Installation complete'),
|
|
1172
|
+
)}\n`,
|
|
1173
|
+
)
|
|
1001
1174
|
process.stdout.write(
|
|
1002
1175
|
` ${colors.dim('\u2500'.repeat(Math.min(process.stdout.columns || 60, 60)))}\n`,
|
|
1003
1176
|
)
|