pantheon-opencode 1.4.3 → 1.5.0-beta.10
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/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/manifest.json +21 -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/CHANGELOG.md +1026 -0
- package/README.md +292 -1080
- package/README.pt-BR.md +308 -0
- package/bin/pantheon-init.mjs +207 -31
- package/opencode.json +1 -0
- package/package.json +34 -14
- package/plugin.json +15 -0
- package/pyproject.toml +65 -0
- package/scripts/_pantheon_paths.py +48 -7
- package/scripts/ci-validate-yaml.py +7 -2
- package/scripts/code_mode_server.py +451 -44
- package/scripts/cost.mjs +30 -14
- package/scripts/doctor.mjs +306 -129
- package/scripts/generate-preset-docs.mjs +127 -66
- package/scripts/generate-prompts.sh +2 -2
- package/scripts/install/cli-ui.mjs +23 -10
- package/scripts/install/config-migration.mjs +399 -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 +396 -121
- package/scripts/install/plugin.mjs +89 -49
- package/scripts/install/shared.mjs +55 -159
- package/scripts/install/strings.mjs +129 -0
- package/scripts/install-mcp.mjs +45 -232
- package/scripts/manifest-inventory.mjs +410 -0
- package/scripts/mcp_persistence_server.py +798 -60
- package/scripts/mcp_resources_server.py +144 -40
- package/scripts/memory_mcp_server.py +2 -2
- package/scripts/package-evidence.mjs +242 -0
- package/scripts/probe-context-rehydrate.mjs +240 -0
- package/scripts/probe-pantheon-cost.mjs +313 -0
- package/scripts/recover-zenodo-deposition.mjs +146 -0
- package/scripts/release-beta-version.mjs +56 -60
- package/scripts/sync-artifacts.mjs +169 -0
- package/scripts/sync-tui.mjs +135 -42
- package/scripts/test-opencode-v1-v2-sandbox.sh +862 -0
- package/scripts/uninstall.mjs +298 -954
- package/scripts/validate-zenodo-release.mjs +154 -0
- package/scripts/validation-policy.mjs +7 -7
- package/scripts/version-check.mjs +113 -108
- package/scripts/versioning.mjs +230 -86
- package/scripts/zenodo-artifact.mjs +21 -0
- package/scripts/zenodo-release-state.mjs +53 -0
- package/src/agents/mnemosyne.md +1 -1
- package/src/agents/zeus.md +27 -16
- package/src/mcp/_pantheon_paths.py +48 -7
- package/src/mcp/code_mode_server.py +451 -44
- package/src/mcp/eval_store.py +1 -1
- package/src/mcp/mcp_persistence_server.py +798 -60
- 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/auto-wake.ts +1 -5
- package/src/pantheon/background-job-board.ts +110 -2
- package/src/pantheon/compaction-handoff.ts +304 -0
- package/src/pantheon/cost-command.ts +356 -69
- package/src/pantheon/delegate-manager.ts +766 -0
- package/src/pantheon/delegation-classifier.ts +307 -0
- package/src/pantheon/delegation-enforce.ts +13 -12
- package/src/pantheon/delegation-finalize.ts +406 -0
- package/src/pantheon/delegation-notify.ts +212 -0
- package/src/pantheon/delegation-toast.ts +73 -0
- package/src/pantheon/delegation.ts +1337 -0
- package/src/pantheon/file-persistence-salvage.ts +64 -0
- package/src/pantheon/file-persistence.ts +194 -45
- package/src/pantheon/goal-loop.ts +2 -2
- package/src/pantheon/goal-store.ts +1 -1
- package/src/pantheon/hashline/tool.ts +2 -2
- package/src/pantheon/model-command.ts +2 -1
- package/src/pantheon/native-probe.ts +234 -0
- package/src/pantheon/native-task-status.ts +139 -0
- package/src/pantheon/permission-eval.ts +284 -0
- package/src/pantheon/provider-capabilities.ts +221 -0
- package/src/pantheon/provider-stream-tracker.ts +182 -0
- package/src/pantheon/shared-board.ts +37 -0
- package/src/pantheon/step-cap.ts +110 -0
- package/src/pantheon/task-result-guard.ts +107 -0
- package/src/pantheon/token-opt.ts +689 -0
- package/src/pantheon/tool-ceiling.ts +159 -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 +148 -0
- package/src/pantheon/v2-hooks.ts +287 -0
- package/src/pantheon/v2-tool-definitions.ts +331 -0
- package/src/pantheon/zeus-delegate-with-retry.ts +406 -0
- package/src/plugin-v2/index.ts +21 -0
- package/src/plugin-v2.ts +824 -0
- package/src/plugin.ts +261 -51
- package/src/plugins/pantheon-hooks.ts +51 -0
- package/src/plugins/tui/dist/tui.d.ts +116 -24
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +416 -149
- package/src/plugins/tui/dist/tui.tsx +456 -87
- package/src/plugins/tui/package-lock.json +51 -51
- package/src/plugins/tui/package.json +7 -7
- package/src/plugins/tui/src/index.tsx +462 -95
- package/src/skills/README.md +0 -2
- package/src/pantheon/tool-context.ts +0 -41
|
@@ -2,20 +2,20 @@
|
|
|
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
|
|
|
15
|
+
import { createHash } from 'node:crypto'
|
|
16
16
|
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:fs'
|
|
17
17
|
import { homedir } from 'node:os'
|
|
18
|
-
import {
|
|
18
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path'
|
|
19
19
|
import {
|
|
20
20
|
bullet,
|
|
21
21
|
colors,
|
|
@@ -29,10 +29,13 @@ import {
|
|
|
29
29
|
success,
|
|
30
30
|
warning,
|
|
31
31
|
} from './cli-ui.mjs'
|
|
32
|
+
import { migrateV1toV2 } from './config-migration.mjs'
|
|
32
33
|
import { healthCheck } from './health-check.mjs'
|
|
33
34
|
import { detectVersion, runMigrations } from './migrate.mjs'
|
|
35
|
+
import { resolveOpenCodeVersion } from './opencode-version.mjs'
|
|
34
36
|
import { copyPluginFiles, installPlugin, registerPlugin, unregisterPlugin } from './plugin.mjs'
|
|
35
37
|
import {
|
|
38
|
+
checkRuntimePrerequisites,
|
|
36
39
|
collectSkillNames,
|
|
37
40
|
copyFiles,
|
|
38
41
|
installSkills,
|
|
@@ -41,8 +44,10 @@ import {
|
|
|
41
44
|
sourceDirValid,
|
|
42
45
|
summary,
|
|
43
46
|
syncDir,
|
|
47
|
+
writeConfigWithBackup,
|
|
44
48
|
writeIfChanged,
|
|
45
49
|
} from './shared.mjs'
|
|
50
|
+
import { strings } from './strings.mjs'
|
|
46
51
|
|
|
47
52
|
import { setupVenv, venvPythonPath } from './venv.mjs'
|
|
48
53
|
|
|
@@ -60,6 +65,34 @@ function deepClone(value) {
|
|
|
60
65
|
return JSON.parse(JSON.stringify(value))
|
|
61
66
|
}
|
|
62
67
|
|
|
68
|
+
/**
|
|
69
|
+
* (Re)generate the code-mode manifest with SHA-256 digests for every bundled
|
|
70
|
+
* script. This is the explicit installation opt-in: the MCP server only
|
|
71
|
+
* executes scripts listed here whose on-disk hash matches. Missing or empty
|
|
72
|
+
* directories approve nothing (0) — execution is never implicit.
|
|
73
|
+
*/
|
|
74
|
+
function writeCodeModeManifest(dir, dryRun) {
|
|
75
|
+
if (!existsSync(dir)) return 0
|
|
76
|
+
let names
|
|
77
|
+
try {
|
|
78
|
+
names = readdirSync(dir)
|
|
79
|
+
.filter((name) => /\.(sh|py)$/i.test(name) && !name.startsWith('.'))
|
|
80
|
+
.sort()
|
|
81
|
+
} catch {
|
|
82
|
+
return 0
|
|
83
|
+
}
|
|
84
|
+
if (names.length === 0) return 0
|
|
85
|
+
const scripts = {}
|
|
86
|
+
for (const name of names) {
|
|
87
|
+
scripts[name] = createHash('sha256')
|
|
88
|
+
.update(readFileSync(join(dir, name)))
|
|
89
|
+
.digest('hex')
|
|
90
|
+
}
|
|
91
|
+
const payload = `${JSON.stringify({ version: 1, scripts }, null, 2)}\n`
|
|
92
|
+
writeIfChanged(join(dir, 'manifest.json'), payload, dryRun)
|
|
93
|
+
return names.length
|
|
94
|
+
}
|
|
95
|
+
|
|
63
96
|
function readJsonConfig(filePath) {
|
|
64
97
|
if (!existsSync(filePath)) return {}
|
|
65
98
|
|
|
@@ -101,24 +134,149 @@ function mergeMissing(target, source) {
|
|
|
101
134
|
* Resolve a plugin ref from the packaged opencode.json to a path INSIDE the
|
|
102
135
|
* installed package.
|
|
103
136
|
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* from ROOT — never hardcoded), so both dev and global installs resolve to a
|
|
109
|
-
* path that actually exists.
|
|
137
|
+
* Only exact Pantheon plugin identities are resolved into the installed
|
|
138
|
+
* package. In particular, a third-party absolute path ending in
|
|
139
|
+
* `src/plugin.ts` or `src/plugins/pantheon-hooks.ts` is not evidence that the
|
|
140
|
+
* plugin belongs to Pantheon and must be preserved verbatim.
|
|
110
141
|
*/
|
|
111
142
|
export function resolveInstalledPlugin(plugin) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
if (
|
|
118
|
-
const
|
|
119
|
-
|
|
143
|
+
if (typeof plugin !== 'string') return plugin
|
|
144
|
+
const normalized = normalizePluginRef(plugin)
|
|
145
|
+
if (PANTHEON_PLUGIN_IDENTITIES.has(normalized)) return join(ROOT, normalized)
|
|
146
|
+
// Legacy V2 file ref (pre-directory-contract): migrate to the installed
|
|
147
|
+
// plugin directory so the beta loader accepts the entry.
|
|
148
|
+
if (normalized === PANTHEON_V2_LEGACY_FILE) return join(ROOT, PANTHEON_V2_PLUGIN)
|
|
149
|
+
const identity = managedPluginIdentity(plugin)
|
|
150
|
+
if (identity !== null && isAbsolute(normalized)) {
|
|
151
|
+
return INSTALLED_PANTHEON_PLUGIN_PATHS.get(identity) ?? plugin
|
|
152
|
+
}
|
|
153
|
+
return plugin
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function normalizePluginRef(ref) {
|
|
157
|
+
return typeof ref === 'string' ? ref.replaceAll('\\', '/').replace(/\/$/, '') : ref
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const PANTHEON_V1_PLUGIN = 'src/plugin.ts'
|
|
161
|
+
const PANTHEON_V1_HOOKS = 'src/plugins/pantheon-hooks.ts'
|
|
162
|
+
// Canonical V2 entry is the plugin DIRECTORY (beta loader contract: entries
|
|
163
|
+
// must be dirs with a real index.ts — file paths warn "must be a directory",
|
|
164
|
+
// symlinks are ignored). The shim at src/plugin-v2/index.ts re-exports the
|
|
165
|
+
// real entry src/plugin-v2.ts (single source of truth).
|
|
166
|
+
const PANTHEON_V2_PLUGIN = 'src/plugin-v2'
|
|
167
|
+
// Legacy V2 file ref from before the directory contract (installer + template
|
|
168
|
+
// used to register src/plugin-v2.ts). Still recognized for migration.
|
|
169
|
+
const PANTHEON_V2_LEGACY_FILE = 'src/plugin-v2.ts'
|
|
170
|
+
const PANTHEON_V1_EXPORT = 'pantheon-opencode/plugin'
|
|
171
|
+
const PANTHEON_V2_EXPORT = 'pantheon-opencode/plugin-v2'
|
|
172
|
+
const PANTHEON_PLUGIN_IDENTITIES = new Set([
|
|
173
|
+
PANTHEON_V1_PLUGIN,
|
|
174
|
+
PANTHEON_V1_HOOKS,
|
|
175
|
+
PANTHEON_V2_PLUGIN,
|
|
176
|
+
])
|
|
177
|
+
|
|
178
|
+
const INSTALLED_PANTHEON_PLUGIN_PATHS = new Map([
|
|
179
|
+
[PANTHEON_V1_PLUGIN, resolve(ROOT, PANTHEON_V1_PLUGIN)],
|
|
180
|
+
[PANTHEON_V1_HOOKS, resolve(ROOT, PANTHEON_V1_HOOKS)],
|
|
181
|
+
[PANTHEON_V2_PLUGIN, resolve(ROOT, PANTHEON_V2_PLUGIN)],
|
|
182
|
+
])
|
|
183
|
+
|
|
184
|
+
function normalizePluginPath(ref) {
|
|
185
|
+
return ref.replaceAll('\\', '/')
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function pluginMetadataReferences(ref) {
|
|
189
|
+
if (typeof ref === 'string') return [ref]
|
|
190
|
+
if (!ref || typeof ref !== 'object' || Array.isArray(ref)) return []
|
|
191
|
+
return ['package', 'id', 'name', 'path', 'source']
|
|
192
|
+
.filter((key) => typeof ref[key] === 'string')
|
|
193
|
+
.map((key) => ref[key])
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function managedPluginIdentity(ref) {
|
|
197
|
+
const normalized = normalizePluginRef(ref)
|
|
198
|
+
if (typeof normalized !== 'string') return null
|
|
199
|
+
|
|
200
|
+
if (PANTHEON_PLUGIN_IDENTITIES.has(normalized)) return normalized
|
|
201
|
+
if (normalized === PANTHEON_V2_LEGACY_FILE) return PANTHEON_V2_PLUGIN
|
|
202
|
+
if (normalized === PANTHEON_V1_EXPORT || normalized.startsWith(`${PANTHEON_V1_EXPORT}@`)) {
|
|
203
|
+
return PANTHEON_V1_PLUGIN
|
|
204
|
+
}
|
|
205
|
+
if (normalized === PANTHEON_V2_EXPORT || normalized.startsWith(`${PANTHEON_V2_EXPORT}@`)) {
|
|
206
|
+
return PANTHEON_V2_PLUGIN
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (isAbsolute(normalized)) {
|
|
210
|
+
const absolutePath = normalizePluginPath(resolve(normalized))
|
|
211
|
+
for (const [identity, installedPath] of INSTALLED_PANTHEON_PLUGIN_PATHS) {
|
|
212
|
+
if (absolutePath === normalizePluginPath(installedPath)) return identity
|
|
213
|
+
}
|
|
214
|
+
// Legacy absolute V2 file path inside OUR installed package (pre-
|
|
215
|
+
// directory-contract installs) migrates to the directory identity.
|
|
216
|
+
// ROOT-scoped: third-party paths ending in src/plugin-v2.ts stay theirs.
|
|
217
|
+
if (absolutePath === normalizePluginPath(resolve(ROOT, PANTHEON_V2_LEGACY_FILE))) {
|
|
218
|
+
return PANTHEON_V2_PLUGIN
|
|
219
|
+
}
|
|
220
|
+
// beta.5: stale refs from OTHER installed copies of this package (older
|
|
221
|
+
// nvm prefixes, npx cache hashes). Only paths inside
|
|
222
|
+
// `node_modules/pantheon-opencode/` count as installed copies — a dev
|
|
223
|
+
// checkout elsewhere (e.g. a vendor folder named pantheon-opencode) remains
|
|
224
|
+
// protected by the third-party rule above.
|
|
225
|
+
const installedMarker = '/node_modules/pantheon-opencode/'
|
|
226
|
+
const withSlashes = normalizePluginPath(normalized)
|
|
227
|
+
const markerIndex = withSlashes.lastIndexOf(installedMarker)
|
|
228
|
+
if (markerIndex !== -1) {
|
|
229
|
+
const suffix = withSlashes.slice(markerIndex + installedMarker.length)
|
|
230
|
+
if (suffix === PANTHEON_V1_PLUGIN) return PANTHEON_V1_PLUGIN
|
|
231
|
+
if (suffix === PANTHEON_V1_HOOKS) return PANTHEON_V1_HOOKS
|
|
232
|
+
if (suffix === PANTHEON_V2_PLUGIN) return PANTHEON_V2_PLUGIN
|
|
233
|
+
if (suffix === PANTHEON_V2_LEGACY_FILE) return PANTHEON_V2_PLUGIN
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return null
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Return the stable identity for a Pantheon plugin, or its normalized path.
|
|
242
|
+
* Basenames are deliberately not used: unrelated user plugins may share a
|
|
243
|
+
* filename such as `plugin.ts`.
|
|
244
|
+
*/
|
|
245
|
+
export function pluginReferenceIdentity(ref) {
|
|
246
|
+
const references = pluginMetadataReferences(ref)
|
|
247
|
+
for (const reference of references) {
|
|
248
|
+
const identity = managedPluginIdentity(reference)
|
|
249
|
+
if (identity !== null) return identity
|
|
250
|
+
}
|
|
251
|
+
if (references.length > 0) return normalizePluginPath(normalizePluginRef(references[0]))
|
|
252
|
+
return ref
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function isPantheonPluginReference(ref) {
|
|
256
|
+
return PANTHEON_PLUGIN_IDENTITIES.has(pluginReferenceIdentity(ref))
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function removePantheonPluginReferences(refs) {
|
|
260
|
+
return (Array.isArray(refs) ? refs : []).filter((ref) => !isPantheonPluginReference(ref))
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function hasPluginReference(refs, candidate) {
|
|
264
|
+
const identity = pluginReferenceIdentity(candidate)
|
|
265
|
+
return refs.some((ref) => pluginReferenceIdentity(ref) === identity)
|
|
120
266
|
}
|
|
121
267
|
|
|
268
|
+
export function deduplicatePluginReferences(refs) {
|
|
269
|
+
const seen = new Set()
|
|
270
|
+
return (Array.isArray(refs) ? refs : []).filter((ref) => {
|
|
271
|
+
const identity = pluginReferenceIdentity(ref)
|
|
272
|
+
if (seen.has(identity)) return false
|
|
273
|
+
seen.add(identity)
|
|
274
|
+
return true
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export { parseOpenCodeVersion, resolveOpenCodeVersion } from './opencode-version.mjs'
|
|
279
|
+
|
|
122
280
|
/**
|
|
123
281
|
* Detect whether `target` is the user's global OpenCode installation directory
|
|
124
282
|
* (~/.opencode or $XDG_CONFIG_HOME/opencode).
|
|
@@ -217,16 +375,60 @@ export function syncTuiRegistration(target, { isGlobal = false, dryRun = false }
|
|
|
217
375
|
|
|
218
376
|
// 2. Clean stale Pantheon TUI refs from every location OpenCode reads.
|
|
219
377
|
// unregisterPlugin covers the bare relative id plus the extended stale
|
|
220
|
-
// patterns (dist files, in-package
|
|
221
|
-
//
|
|
378
|
+
// patterns (dist files, identifiable in-package paths, owned copied
|
|
379
|
+
// directories, relative ids, and npx package identities).
|
|
222
380
|
for (const tuiConfigPath of tuiConfigLocations(target)) {
|
|
223
|
-
|
|
381
|
+
const configDirForPath = dirname(tuiConfigPath)
|
|
382
|
+
const ownedCopy = resolveTuiCopyTarget(configDirForPath)
|
|
383
|
+
unregisterPlugin(tuiConfigPath, 'plugins/pantheon-tui', {
|
|
384
|
+
dryRun,
|
|
385
|
+
knownPantheonPaths: [ownedCopy, tuiCopyDir],
|
|
386
|
+
})
|
|
224
387
|
}
|
|
225
388
|
|
|
226
389
|
// 3. Register the copied directory in the selected target only.
|
|
227
390
|
return registerPlugin(targetTuiConfigPath, tuiCopyDir, { dryRun })
|
|
228
391
|
}
|
|
229
392
|
|
|
393
|
+
/**
|
|
394
|
+
* beta.5: set up the Python venv + run the health check. Never throws —
|
|
395
|
+
* failures are reported as warnings and return false so the caller omits
|
|
396
|
+
* MCP entries from the config (entries pointing at a missing venv would
|
|
397
|
+
* make every MCP fail to launch). `deps` is injectable for tests.
|
|
398
|
+
*/
|
|
399
|
+
export function setupRuntimePhase(target, { dryRun, clean, isGlobal }, stats, deps = {}) {
|
|
400
|
+
const S = strings()
|
|
401
|
+
const setup = deps.setupVenv ?? setupVenv
|
|
402
|
+
const check = deps.healthCheck ?? healthCheck
|
|
403
|
+
try {
|
|
404
|
+
const venvSpinner = spinner(S.settingUpRuntime)
|
|
405
|
+
setup(target, { dryRun, force: clean })
|
|
406
|
+
venvSpinner(true)
|
|
407
|
+
const runtimeTarget = isGlobal ? target : join(target, '.opencode')
|
|
408
|
+
const health = check(runtimeTarget, { dryRun, pythonTarget: target })
|
|
409
|
+
|
|
410
|
+
section(`\uD83D\uDD0D ${S.healthSection}`)
|
|
411
|
+
const healthStep = step(S.runningHealthChecks)
|
|
412
|
+
for (const p of health.passed) success(`${p.check}: ${p.detail}`)
|
|
413
|
+
for (const w of health.warnings) warning(`${w.check}: ${w.detail}`)
|
|
414
|
+
for (const f of health.failed) error(`${f.check}: ${f.detail}`)
|
|
415
|
+
|
|
416
|
+
if (health.failed.length > 0) {
|
|
417
|
+
healthStep(false)
|
|
418
|
+
warning(`${health.failed.length} check(s) failed — review above`)
|
|
419
|
+
stats.warnings += health.failed.length
|
|
420
|
+
return false
|
|
421
|
+
}
|
|
422
|
+
healthStep(true)
|
|
423
|
+
return true
|
|
424
|
+
} catch (err) {
|
|
425
|
+
error(S.runtimeFailed(err.message))
|
|
426
|
+
warning(S.runtimeSkippedMcp)
|
|
427
|
+
stats.warnings += 1
|
|
428
|
+
return false
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
230
432
|
export async function installOpenCode(
|
|
231
433
|
target,
|
|
232
434
|
dryRun = false,
|
|
@@ -241,14 +443,14 @@ export async function installOpenCode(
|
|
|
241
443
|
|
|
242
444
|
const componentSet = new Set(components)
|
|
243
445
|
const stats = summary.opencode
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
const version = opts.version
|
|
446
|
+
const S = strings()
|
|
447
|
+
|
|
448
|
+
// V1/V2 dual-version awareness (Phase 3): the installer builds a unified
|
|
449
|
+
// config, then applies V2-native migration (migrateV1toV2) when
|
|
450
|
+
// --opencode-version=v2. V2 normalizes V1 fields in memory (no rewrite) and
|
|
451
|
+
// isolates its state DB via OPENCODE_DB (~/.local/share/opencode/opencode-v2.db)
|
|
452
|
+
// with a distinct service port (49375 vs V1's 49374).
|
|
453
|
+
const version = resolveOpenCodeVersion(opts.version ?? 'v1')
|
|
252
454
|
if (version === 'v2') {
|
|
253
455
|
info(
|
|
254
456
|
'OpenCode V2 target — shared config; state isolated via OPENCODE_DB ' +
|
|
@@ -290,6 +492,17 @@ export async function installOpenCode(
|
|
|
290
492
|
const config = readJsonConfig(targetConfigPath)
|
|
291
493
|
const pantheonConfig = readJsonConfig(pantheonConfigPath)
|
|
292
494
|
|
|
495
|
+
// beta.5 preflight: fail BEFORE any files are written when the runtime
|
|
496
|
+
// component needs toolchain pieces that are missing. Nothing has been
|
|
497
|
+
// copied yet at this point, so the failure is clean.
|
|
498
|
+
if (componentSet.has('runtime') && !dryRun) {
|
|
499
|
+
const missing = checkRuntimePrerequisites()
|
|
500
|
+
if (missing.length > 0) {
|
|
501
|
+
for (const problem of missing) error(problem)
|
|
502
|
+
throw new Error(S.prereqMissing)
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
293
506
|
// Determine layout based on install scope.
|
|
294
507
|
// Global config dir (~/.opencode or ~/.config/opencode) uses a flat layout:
|
|
295
508
|
// agents/ skills/ commands/ plugins/
|
|
@@ -302,7 +515,7 @@ export async function installOpenCode(
|
|
|
302
515
|
configure({ dryRun })
|
|
303
516
|
|
|
304
517
|
if (isGlobal) {
|
|
305
|
-
info(
|
|
518
|
+
info(S.globalLayout)
|
|
306
519
|
}
|
|
307
520
|
|
|
308
521
|
// -----------------------------------------------------------------------
|
|
@@ -315,7 +528,7 @@ export async function installOpenCode(
|
|
|
315
528
|
warning(`Agent source directory not found: ${srcDir}`)
|
|
316
529
|
stats.errors++
|
|
317
530
|
} else {
|
|
318
|
-
const agentStep = step(
|
|
531
|
+
const agentStep = step(S.installingAgents)
|
|
319
532
|
const dstDir = isGlobal ? join(target, 'agents') : join(target, '.opencode', 'agents')
|
|
320
533
|
if (!dryRun) mkdirSync(dstDir, { recursive: true })
|
|
321
534
|
if (clean && existsSync(dstDir) && !dryRun) {
|
|
@@ -445,11 +658,20 @@ export async function installOpenCode(
|
|
|
445
658
|
}
|
|
446
659
|
|
|
447
660
|
// -----------------------------------------------------------------------
|
|
448
|
-
// 2.9 Create/update tui.json with plugin registration
|
|
661
|
+
// 2.9 Create/update tui.json with plugin registration (--components plugins)
|
|
449
662
|
// -----------------------------------------------------------------------
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
663
|
+
if (componentSet.has('plugins')) {
|
|
664
|
+
let tuiStatus
|
|
665
|
+
try {
|
|
666
|
+
tuiStatus = syncTuiRegistration(target, { isGlobal, dryRun })
|
|
667
|
+
} catch (cause) {
|
|
668
|
+
error(`TUI plugin setup failed: ${cause.message}`)
|
|
669
|
+
stats.errors++
|
|
670
|
+
throw new Error('TUI plugin setup failed; installation was not completed', { cause })
|
|
671
|
+
}
|
|
672
|
+
if (tuiStatus === 'created') stats.created++
|
|
673
|
+
else stats.skipped++
|
|
674
|
+
}
|
|
453
675
|
|
|
454
676
|
// -----------------------------------------------------------------------
|
|
455
677
|
// 2.10 Install runtime infrastructure (--components runtime)
|
|
@@ -531,6 +753,21 @@ export async function installOpenCode(
|
|
|
531
753
|
const cmResult = syncDir(srcCodeModeDir, dstCodeModeDir, dryRun, clean)
|
|
532
754
|
stats.created += cmResult.created
|
|
533
755
|
stats.skipped += cmResult.skipped
|
|
756
|
+
} else {
|
|
757
|
+
// The code-mode payload ships inside the published tarball
|
|
758
|
+
// (package.json `files` includes .pantheon/code-mode/**). When it is
|
|
759
|
+
// absent, still create the runtime dir so the code-mode MCP server has
|
|
760
|
+
// a stable home, and warn loudly instead of silently skipping.
|
|
761
|
+
warning(`Code-mode source not found: ${srcCodeModeDir} — creating empty ${dstCodeModeDir}`)
|
|
762
|
+
if (!dryRun) mkdirSync(dstCodeModeDir, { recursive: true })
|
|
763
|
+
stats.created++
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// B3-08: explicit installation mode — seed the manifest with SHA-256
|
|
767
|
+
// digests so the server never executes a script implicitly.
|
|
768
|
+
const approvedScripts = writeCodeModeManifest(dstCodeModeDir, dryRun)
|
|
769
|
+
if (approvedScripts > 0) {
|
|
770
|
+
info(`Code-mode manifest: ${approvedScripts} script(s) approved`)
|
|
534
771
|
}
|
|
535
772
|
|
|
536
773
|
// ── tiers.json ──
|
|
@@ -544,10 +781,21 @@ export async function installOpenCode(
|
|
|
544
781
|
}
|
|
545
782
|
}
|
|
546
783
|
|
|
784
|
+
// -----------------------------------------------------------------------
|
|
785
|
+
// 2.11 Setup virtual environment + health check (--components runtime)
|
|
786
|
+
// beta.5: runs BEFORE the config write and is no longer fatal — on failure
|
|
787
|
+
// the install completes without MCP entries (which would point at a venv
|
|
788
|
+
// that does not exist) instead of aborting a half-done installation.
|
|
789
|
+
// -----------------------------------------------------------------------
|
|
790
|
+
let runtimeHealthy = !componentSet.has('runtime')
|
|
791
|
+
if (componentSet.has('runtime')) {
|
|
792
|
+
runtimeHealthy = setupRuntimePhase(target, { dryRun, clean, isGlobal }, stats)
|
|
793
|
+
}
|
|
794
|
+
|
|
547
795
|
// -----------------------------------------------------------------------
|
|
548
796
|
// 3. Create/update opencode.json (always runs)
|
|
549
797
|
// Reads TARGET's existing config first, then merges Pantheon settings
|
|
550
|
-
// on top. Preserves user's MCP, provider,
|
|
798
|
+
// on top. Preserves user's MCP, provider, plugins, compaction, theme.
|
|
551
799
|
// -----------------------------------------------------------------------
|
|
552
800
|
// --------------------------------------------------------------------
|
|
553
801
|
// A. Parse canonical agent config from agents/*.agent.md frontmatter
|
|
@@ -635,9 +883,8 @@ export async function installOpenCode(
|
|
|
635
883
|
existing[field] = JSON.parse(JSON.stringify(agentCfg[field]))
|
|
636
884
|
}
|
|
637
885
|
}
|
|
638
|
-
//
|
|
639
|
-
|
|
640
|
-
delete existing.small_model
|
|
886
|
+
// Existing agent fields belong to the user; never remove them during
|
|
887
|
+
// an install. Canonical values are merged only when absent.
|
|
641
888
|
} else {
|
|
642
889
|
// ── New agent ──
|
|
643
890
|
const newAgent = {}
|
|
@@ -663,17 +910,7 @@ export async function installOpenCode(
|
|
|
663
910
|
}
|
|
664
911
|
}
|
|
665
912
|
|
|
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
|
|
913
|
+
// Do not remove stale or user-defined agents. Installation is additive.
|
|
677
914
|
}
|
|
678
915
|
|
|
679
916
|
// --------------------------------------------------------------------
|
|
@@ -738,39 +975,70 @@ export async function installOpenCode(
|
|
|
738
975
|
// The source opencode.json keeps its dev path for local development; the
|
|
739
976
|
// transform happens at install/sync time only.
|
|
740
977
|
|
|
741
|
-
if (
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
if (Array.isArray(
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
978
|
+
if (version === 'v1') {
|
|
979
|
+
// V1 loads the singular `plugin` list. Remove every Pantheon V2 entry from
|
|
980
|
+
// both config shapes before restoring the two V1 plugins. User plugins in
|
|
981
|
+
// either list are retained verbatim.
|
|
982
|
+
if (Array.isArray(config.plugins)) {
|
|
983
|
+
config.plugins = removePantheonPluginReferences(config.plugins)
|
|
984
|
+
}
|
|
985
|
+
if (config.plugin === undefined) config.plugin = []
|
|
986
|
+
if (Array.isArray(config.plugin)) {
|
|
987
|
+
config.plugin = removePantheonPluginReferences(config.plugin)
|
|
988
|
+
if (Array.isArray(pantheonConfig.plugin)) {
|
|
989
|
+
for (const plugin of pantheonConfig.plugin) {
|
|
990
|
+
if (!isPantheonPluginReference(plugin) && !hasPluginReference(config.plugin, plugin)) {
|
|
991
|
+
config.plugin.push(deepClone(plugin))
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
const ensurePantheonPlugin = (ref) => {
|
|
997
|
+
const resolved = resolveInstalledPlugin(ref)
|
|
998
|
+
config.plugin = config.plugin.filter(
|
|
999
|
+
(plugin) => pluginReferenceIdentity(plugin) !== pluginReferenceIdentity(resolved),
|
|
1000
|
+
)
|
|
752
1001
|
config.plugin.push(resolved)
|
|
753
1002
|
}
|
|
1003
|
+
// Root-level delegation plugin remains a V1-only registration.
|
|
1004
|
+
ensurePantheonPlugin('src/plugin.ts')
|
|
1005
|
+
ensurePantheonPlugin('src/plugins/pantheon-hooks.ts')
|
|
754
1006
|
}
|
|
1007
|
+
}
|
|
755
1008
|
|
|
756
|
-
|
|
757
|
-
//
|
|
758
|
-
//
|
|
759
|
-
//
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
|
|
1009
|
+
if (version === 'v2') {
|
|
1010
|
+
// V2 loads the plural `plugins` list. Keep third-party V1 entries in the
|
|
1011
|
+
// legacy list for users who still use them, but never leave a Pantheon V1
|
|
1012
|
+
// entry configured alongside the V2 package export.
|
|
1013
|
+
if (Array.isArray(config.plugin)) {
|
|
1014
|
+
config.plugin = removePantheonPluginReferences(config.plugin)
|
|
1015
|
+
}
|
|
1016
|
+
if (!Array.isArray(config.plugins)) config.plugins = []
|
|
1017
|
+
config.plugins = deduplicatePluginReferences(removePantheonPluginReferences(config.plugins))
|
|
1018
|
+
if (Array.isArray(pantheonConfig.plugins)) {
|
|
1019
|
+
for (const plugin of pantheonConfig.plugins) {
|
|
1020
|
+
if (!isPantheonPluginReference(plugin) && !hasPluginReference(config.plugins, plugin)) {
|
|
1021
|
+
config.plugins.push(deepClone(plugin))
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
769
1024
|
}
|
|
770
|
-
//
|
|
771
|
-
|
|
772
|
-
//
|
|
773
|
-
|
|
1025
|
+
// The beta resolves every `plugins` entry through npm install, so an
|
|
1026
|
+
// `a/b` shorthand (e.g. the published `pantheon-opencode/plugin-v2`
|
|
1027
|
+
// export) is treated as a GitHub repo and dies with NpmInstallFailedError
|
|
1028
|
+
// (beta log evidence). The beta loader additionally requires each entry
|
|
1029
|
+
// to be a DIRECTORY with a real index.js/index.ts — a file path warns
|
|
1030
|
+
// "must be a directory" and a symlink is ignored (sandbox probe
|
|
1031
|
+
// evidence). Register the V2 plugin DIRECTORY inside the INSTALLED
|
|
1032
|
+
// package — exactly like V1's ensurePantheonPlugin, but pointing at
|
|
1033
|
+
// src/plugin-v2 whose index.ts shim re-exports the real entry.
|
|
1034
|
+
// Legacy refs (npm shorthand, the old src/plugin-v2.ts file path) are
|
|
1035
|
+
// recognized via managedPluginIdentity and already stripped above, so
|
|
1036
|
+
// they migrate here.
|
|
1037
|
+
const resolvedV2 = resolveInstalledPlugin(PANTHEON_V2_PLUGIN)
|
|
1038
|
+
config.plugins = config.plugins.filter(
|
|
1039
|
+
(plugin) => pluginReferenceIdentity(plugin) !== pluginReferenceIdentity(resolvedV2),
|
|
1040
|
+
)
|
|
1041
|
+
config.plugins.push(resolvedV2)
|
|
774
1042
|
}
|
|
775
1043
|
|
|
776
1044
|
if (config.provider === undefined && pantheonConfig.provider !== undefined) {
|
|
@@ -854,19 +1122,26 @@ export async function installOpenCode(
|
|
|
854
1122
|
}
|
|
855
1123
|
|
|
856
1124
|
// --------------------------------------------------------------------
|
|
857
|
-
// F. Compatibility cleanup (OpenCode >=
|
|
1125
|
+
// F. Compatibility cleanup (OpenCode V1 >= 1.15.7)
|
|
858
1126
|
// --------------------------------------------------------------------
|
|
859
|
-
//
|
|
860
|
-
//
|
|
861
|
-
|
|
1127
|
+
// Recent V1 releases reject the legacy `todoContinuation` setting. Remove
|
|
1128
|
+
// it from the merged V1 config, while leaving the V2-shaped configuration
|
|
1129
|
+
// untouched.
|
|
1130
|
+
if (version === 'v1' && Object.hasOwn(config, 'todoContinuation')) {
|
|
862
1131
|
delete config.todoContinuation
|
|
863
1132
|
}
|
|
864
1133
|
|
|
865
1134
|
// --------------------------------------------------------------------
|
|
866
1135
|
// F.5 MCP server entries (for runtime-deployed scripts)
|
|
1136
|
+
// beta.5: skipped when the Python runtime setup failed — entries pointing
|
|
1137
|
+
// at a venv that does not exist would make every MCP fail to launch.
|
|
867
1138
|
// --------------------------------------------------------------------
|
|
868
|
-
if (componentSet.has('runtime')) {
|
|
1139
|
+
if (componentSet.has('runtime') && runtimeHealthy) {
|
|
869
1140
|
config.mcp = config.mcp || {}
|
|
1141
|
+
// MCP stays a flat named-server map for BOTH V1 and V2: OpenCode 1.18.x
|
|
1142
|
+
// has no `mcp.servers` wrapper and rejects one with
|
|
1143
|
+
// "Missing key mcp.servers.enabled". config-migration.mjs normalizes any
|
|
1144
|
+
// legacy wrapper-shaped entries back to the flat map.
|
|
870
1145
|
// P1-3: derive the MCP python from the SAME venv layout setupVenv
|
|
871
1146
|
// creates (<target>/.venv via venvPythonPath). For project installs the
|
|
872
1147
|
// runtime payload lives under <target>/.opencode (runtimeTarget), but the
|
|
@@ -940,8 +1215,17 @@ export async function installOpenCode(
|
|
|
940
1215
|
}
|
|
941
1216
|
}
|
|
942
1217
|
|
|
943
|
-
|
|
944
|
-
|
|
1218
|
+
// ── V2 native migration ──
|
|
1219
|
+
// When targeting V2, convert the merged V1-shaped config to native V2 format
|
|
1220
|
+
// before writing. V1 path is untouched (zero regression).
|
|
1221
|
+
let configToWrite = config
|
|
1222
|
+
if (version === 'v2') {
|
|
1223
|
+
info(S.v2Migrating)
|
|
1224
|
+
configToWrite = migrateV1toV2(config)
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
const configContent = `${JSON.stringify(configToWrite, null, 2)}\n`
|
|
1228
|
+
const status = writeConfigWithBackup(targetConfigPath, configContent, dryRun)
|
|
945
1229
|
if (status === 'created') stats.created++
|
|
946
1230
|
else stats.skipped++
|
|
947
1231
|
|
|
@@ -962,42 +1246,24 @@ export async function installOpenCode(
|
|
|
962
1246
|
}
|
|
963
1247
|
|
|
964
1248
|
// -----------------------------------------------------------------------
|
|
965
|
-
// 4.
|
|
1249
|
+
// 4. (runtime setup moved to 2.11 — before the config write — so MCP
|
|
1250
|
+
// entries are only added when the Python runtime is actually healthy)
|
|
966
1251
|
// -----------------------------------------------------------------------
|
|
967
|
-
if (componentSet.has('runtime')) {
|
|
968
|
-
try {
|
|
969
|
-
const venvSpinner = spinner('Setting up Python virtual environment')
|
|
970
|
-
setupVenv(target, { dryRun, force: clean })
|
|
971
|
-
venvSpinner(true)
|
|
972
|
-
const health = healthCheck(target, { dryRun })
|
|
973
|
-
|
|
974
|
-
// Print health summary
|
|
975
|
-
section('\uD83D\uDD0D Health Check')
|
|
976
|
-
const healthStep = step('Running health checks')
|
|
977
|
-
for (const p of health.passed) success(`${p.check}: ${p.detail}`)
|
|
978
|
-
for (const w of health.warnings) warning(`${w.check}: ${w.detail}`)
|
|
979
|
-
for (const f of health.failed) error(`${f.check}: ${f.detail}`)
|
|
980
|
-
|
|
981
|
-
if (health.failed.length > 0) {
|
|
982
|
-
healthStep(false)
|
|
983
|
-
warning(`${health.failed.length} check(s) failed — review above`)
|
|
984
|
-
stats.errors += health.failed.length
|
|
985
|
-
} else {
|
|
986
|
-
healthStep(true)
|
|
987
|
-
}
|
|
988
|
-
} catch (err) {
|
|
989
|
-
error(`Setup failed: ${err.message}`)
|
|
990
|
-
throw err // Fatal — abort installation
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
1252
|
|
|
994
1253
|
if (interactive && !dryRun) {
|
|
995
1254
|
const created = stats.created
|
|
996
1255
|
const skipped = stats.skipped
|
|
997
1256
|
const errors = stats.errors
|
|
1257
|
+
const warnings = stats.warnings ?? 0
|
|
998
1258
|
|
|
999
1259
|
process.stdout.write('\n')
|
|
1000
|
-
process.stdout.write(
|
|
1260
|
+
process.stdout.write(
|
|
1261
|
+
` ${colors.bold(
|
|
1262
|
+
errors > 0
|
|
1263
|
+
? colors.yellow('Installation completed with errors')
|
|
1264
|
+
: colors.green('Installation complete'),
|
|
1265
|
+
)}\n`,
|
|
1266
|
+
)
|
|
1001
1267
|
process.stdout.write(
|
|
1002
1268
|
` ${colors.dim('\u2500'.repeat(Math.min(process.stdout.columns || 60, 60)))}\n`,
|
|
1003
1269
|
)
|
|
@@ -1006,11 +1272,14 @@ export async function installOpenCode(
|
|
|
1006
1272
|
process.stdout.write(` ${colors.dim('\u2014')} ${skipped} already up-to-date\n`)
|
|
1007
1273
|
if (errors > 0)
|
|
1008
1274
|
process.stdout.write(` ${colors.yellow('\u26a0')} ${errors} error(s) encountered\n`)
|
|
1275
|
+
if (warnings > 0)
|
|
1276
|
+
process.stdout.write(
|
|
1277
|
+
` ${colors.yellow('\u26a0')} ${warnings} warning(s) — run 'pantheon-opencode doctor' for details\n`,
|
|
1278
|
+
)
|
|
1009
1279
|
process.stdout.write('\n')
|
|
1010
1280
|
process.stdout.write(` ${colors.bold('Next steps:')}\n`)
|
|
1011
1281
|
process.stdout.write(` ${colors.dim('\u2022')} Configure agents in opencode.json\n`)
|
|
1012
|
-
process.stdout.write(` ${colors.dim('\u2022')}
|
|
1013
|
-
process.stdout.write(` ${colors.dim('\u2022')} Run 'opencode doctor' to verify\n`)
|
|
1282
|
+
process.stdout.write(` ${colors.dim('\u2022')} Run 'pantheon-opencode doctor' to verify\n`)
|
|
1014
1283
|
process.stdout.write(
|
|
1015
1284
|
` ${colors.dim('\u2022')} Run 'opencode' to start using Pantheon agents\n`,
|
|
1016
1285
|
)
|
|
@@ -1034,4 +1303,10 @@ export async function installOpenCode(
|
|
|
1034
1303
|
const { writeActivePreset } = await import('./model-picker.mjs')
|
|
1035
1304
|
writeActivePreset(target, opts.preset, { source: 'cli' })
|
|
1036
1305
|
}
|
|
1306
|
+
|
|
1307
|
+
// Fail-closed: accumulated stats.errors (bad sources, missing requirements,
|
|
1308
|
+
// failed health checks) must never masquerade as a successful installation.
|
|
1309
|
+
if (stats.errors > 0) {
|
|
1310
|
+
throw new Error(`installation completed with ${stats.errors} error(s); see output above`)
|
|
1311
|
+
}
|
|
1037
1312
|
}
|