pantheon-opencode 1.2.0 → 1.2.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/AGENTS.md +9 -0
- package/README.md +112 -15
- package/bin/pantheon-init.mjs +93 -0
- package/commands/pantheon-vision.md +67 -0
- package/opencode.json +14 -10
- package/package.json +20 -4
- package/scripts/_pantheon_paths.py +0 -2
- package/scripts/changelog-extract.mjs +112 -0
- package/scripts/doctor.mjs +307 -15
- package/scripts/hooks/audit-imports.sh +43 -0
- package/scripts/hooks/format-multi-language.sh +96 -0
- package/scripts/hooks/log-session-start.sh +24 -0
- package/scripts/hooks/on-subagent-delegation-start.sh +91 -0
- package/scripts/hooks/on-subagent-delegation-stop.sh +37 -0
- package/scripts/hooks/run-type-check.sh +21 -0
- package/scripts/hooks/scan-secrets.sh +55 -0
- package/scripts/hooks/validate-post-conditions.sh +21 -0
- package/scripts/hooks/validate-talos-scope.sh +43 -0
- package/scripts/hooks/validate-tool-safety.sh +46 -0
- package/scripts/install/health-check.mjs +101 -1
- package/scripts/install/model-picker.mjs +118 -0
- package/scripts/install/opencode.mjs +155 -43
- package/scripts/install/plugin.mjs +4 -2
- package/scripts/install/venv.mjs +25 -2
- package/scripts/install-mcp.mjs +170 -37
- package/scripts/mcp_persistence_server.py +2 -1
- package/scripts/prune.mjs +359 -0
- package/scripts/secret-scan.mjs +88 -0
- package/scripts/uninstall.mjs +3 -3
- package/scripts/validate-routing.mjs +15 -0
- package/scripts/version-check.mjs +146 -0
- package/scripts/versioning.mjs +13 -14
- package/src/agents/hermes.md +2 -2
- package/src/agents/mnemosyne.md +1 -1
- package/src/agents/zeus.md +1 -1
- package/src/mcp/_pantheon_paths.py +0 -2
- package/src/mcp/install-mcp.mjs +6 -913
- package/src/mcp/mcp_persistence_server.py +2 -1
- package/src/mcp/pantheon_vision_server.py +495 -0
- package/src/mcp/requirements-mcp.txt +8 -5
- package/src/mcp/requirements-vision.txt +3 -0
- package/src/pantheon/auto-wake.ts +91 -0
- package/src/pantheon/background-job-board.ts +568 -0
- package/src/pantheon/file-persistence.ts +95 -0
- package/src/pantheon/presets.d.mts +108 -0
- package/src/pantheon/presets.mjs +554 -0
- package/src/pantheon/vision.ts +1437 -0
- package/src/plugin.ts +69 -25
- package/src/plugins/hook-runner.ts +186 -0
- package/src/plugins/pantheon-hooks.ts +153 -0
- package/src/plugins/tui/dist/tui.d.ts +1 -1
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +812 -223
- package/src/plugins/tui/dist/tui.js.map +1 -1
- package/src/plugins/tui/dist/tui.tsx +881 -121
- package/src/plugins/tui/package.json +3 -3
- package/src/plugins/tui/src/index.tsx +881 -121
- package/src/routing.yml +148 -0
- package/src/skills/context-compression/SKILL.md +1 -1
- package/src/skills/loop-engineering/SKILL.md +1 -1
- package/src/skills/orchestration-workflow/SKILL.md +11 -11
- package/scripts/scrub_secrets.py +0 -281
- package/src/mcp/requirements-mcp-core.txt +0 -5
|
@@ -3,12 +3,33 @@
|
|
|
3
3
|
* opencode.mjs — OpenCode platform installer
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
chmodSync,
|
|
8
|
+
existsSync,
|
|
9
|
+
mkdirSync,
|
|
10
|
+
readdirSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
rmSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
} from 'node:fs'
|
|
7
15
|
import { homedir } from 'node:os'
|
|
8
|
-
import { join, resolve } from 'node:path'
|
|
16
|
+
import { basename, join, resolve } from 'node:path'
|
|
17
|
+
import {
|
|
18
|
+
bullet,
|
|
19
|
+
colors,
|
|
20
|
+
configure,
|
|
21
|
+
error,
|
|
22
|
+
info,
|
|
23
|
+
printSummary,
|
|
24
|
+
section,
|
|
25
|
+
spinner,
|
|
26
|
+
step,
|
|
27
|
+
success,
|
|
28
|
+
warning,
|
|
29
|
+
} from './cli-ui.mjs'
|
|
9
30
|
import { healthCheck } from './health-check.mjs'
|
|
10
31
|
import { detectVersion, runMigrations } from './migrate.mjs'
|
|
11
|
-
import {
|
|
32
|
+
import { installPlugin, registerPlugin, unregisterPlugin } from './plugin.mjs'
|
|
12
33
|
import {
|
|
13
34
|
collectSkillNames,
|
|
14
35
|
copyFiles,
|
|
@@ -20,11 +41,17 @@ import {
|
|
|
20
41
|
syncDir,
|
|
21
42
|
writeIfChanged,
|
|
22
43
|
} from './shared.mjs'
|
|
23
|
-
import { setupVenv } from './venv.mjs'
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
44
|
+
import { setupVenv, venvPythonPath } from './venv.mjs'
|
|
45
|
+
|
|
46
|
+
const COMPONENT_NAMES = [
|
|
47
|
+
'agents',
|
|
48
|
+
'skills',
|
|
49
|
+
'instructions',
|
|
50
|
+
'prompts',
|
|
51
|
+
'commands',
|
|
52
|
+
'plugins',
|
|
53
|
+
'runtime',
|
|
54
|
+
]
|
|
28
55
|
|
|
29
56
|
function deepClone(value) {
|
|
30
57
|
return JSON.parse(JSON.stringify(value))
|
|
@@ -63,7 +90,7 @@ function mergeMissing(target, source) {
|
|
|
63
90
|
* Global installs use a flat layout: agents/ skills/ commands/ plugins/
|
|
64
91
|
* Project installs use the .opencode/ sub-directory layout.
|
|
65
92
|
*/
|
|
66
|
-
function isGlobalConfigDir(target) {
|
|
93
|
+
export function isGlobalConfigDir(target) {
|
|
67
94
|
// Primary: ~/.opencode (actual OpenCode installation)
|
|
68
95
|
const homeDir = resolve(join(homedir(), '.opencode'))
|
|
69
96
|
if (resolve(target) === homeDir) return true
|
|
@@ -166,8 +193,9 @@ export async function installOpenCode(
|
|
|
166
193
|
const srcRouting = join(ROOT, 'src', 'routing.yml')
|
|
167
194
|
const dstRouting = join(target, 'routing.yml')
|
|
168
195
|
if (existsSync(srcRouting)) {
|
|
169
|
-
writeIfChanged(dstRouting, readFileSync(srcRouting, 'utf8'), dryRun)
|
|
170
|
-
stats.created++
|
|
196
|
+
const routingStatus = writeIfChanged(dstRouting, readFileSync(srcRouting, 'utf8'), dryRun)
|
|
197
|
+
if (routingStatus === 'created') stats.created++
|
|
198
|
+
else stats.skipped++
|
|
171
199
|
}
|
|
172
200
|
}
|
|
173
201
|
|
|
@@ -253,24 +281,12 @@ export async function installOpenCode(
|
|
|
253
281
|
|
|
254
282
|
// -----------------------------------------------------------------------
|
|
255
283
|
// -----------------------------------------------------------------------
|
|
256
|
-
// 2.7
|
|
284
|
+
// 2.7 subagent_depth is merged into opencode.json in the config section
|
|
285
|
+
// below (always runs) — see "Ensure critical top-level config sections".
|
|
286
|
+
// Previously this was a separate block gated on the config file already
|
|
287
|
+
// existing, so a FRESH install only gained subagent_depth on the SECOND
|
|
288
|
+
// init run (idempotence bug, issue #19).
|
|
257
289
|
// -----------------------------------------------------------------------
|
|
258
|
-
if (componentSet.has('agents')) {
|
|
259
|
-
const cfgPath = join(target, 'opencode.json')
|
|
260
|
-
if (existsSync(cfgPath)) {
|
|
261
|
-
try {
|
|
262
|
-
const cfg = JSON.parse(readFileSync(cfgPath, 'utf8'))
|
|
263
|
-
let changed = false
|
|
264
|
-
if (cfg.subagent_depth === undefined) { cfg.subagent_depth = 2; changed = true }
|
|
265
|
-
if (changed && !dryRun) {
|
|
266
|
-
writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n')
|
|
267
|
-
success('subagent_depth: 2 added to opencode.json')
|
|
268
|
-
}
|
|
269
|
-
} catch (e) {
|
|
270
|
-
warning(`Could not update opencode.json: ${e.message}`)
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
290
|
|
|
275
291
|
// 2.8 Install TUI plugins (--components plugins)
|
|
276
292
|
// -----------------------------------------------------------------------
|
|
@@ -292,9 +308,16 @@ export async function installOpenCode(
|
|
|
292
308
|
const targetTuiConfigPath = isGlobal
|
|
293
309
|
? join(target, 'tui.json')
|
|
294
310
|
: join(target, '.opencode', 'tui.json')
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
311
|
+
// Hermetic TUI plugin registration: opencode's tui loader treats relative
|
|
312
|
+
// entries ("plugins/pantheon-tui") as npm/github package specs and fails
|
|
313
|
+
// with NpmInstallFailedError ("Repository not found"). Register the plugin
|
|
314
|
+
// by the ABSOLUTE path of the built dist inside the installed package
|
|
315
|
+
// (derived from ROOT — never hardcoded), mirroring the loader's
|
|
316
|
+
// absolute-path support (same pattern as the hooks plugin fix).
|
|
317
|
+
const tuiPluginRef = join(ROOT, 'src', 'plugins', 'tui', 'dist', 'tui.tsx')
|
|
318
|
+
// Clean up the old broken relative ref (and stale dist refs) on upgrade.
|
|
319
|
+
unregisterPlugin(targetTuiConfigPath, 'plugins/pantheon-tui', { dryRun })
|
|
320
|
+
const tuiStatus = registerPlugin(targetTuiConfigPath, tuiPluginRef, { dryRun })
|
|
298
321
|
if (tuiStatus === 'created') stats.created++
|
|
299
322
|
else stats.skipped++
|
|
300
323
|
|
|
@@ -314,12 +337,26 @@ export async function installOpenCode(
|
|
|
314
337
|
'scrub-secrets.py',
|
|
315
338
|
'_pantheon_paths.py',
|
|
316
339
|
'mcp_persistence_server.py',
|
|
340
|
+
'pantheon_vision_server.py',
|
|
317
341
|
]
|
|
318
342
|
const srcScriptsDir = join(ROOT, 'scripts')
|
|
343
|
+
// Canonical MCP server sources live in src/mcp/. Map them explicitly so the
|
|
344
|
+
// sync always copies from the canonical source, never from the stale root
|
|
345
|
+
// scripts/ copies (which previously propagated bugs like the missing
|
|
346
|
+
// `import uuid` in mcp_persistence_server.py). scrub-secrets.py is not in
|
|
347
|
+
// src/mcp/ and intentionally falls back to join(ROOT, 'scripts').
|
|
348
|
+
const canonicalMcpScripts = {
|
|
349
|
+
'mcp_resources_server.py': join(ROOT, 'src', 'mcp', 'mcp_resources_server.py'),
|
|
350
|
+
'code_mode_server.py': join(ROOT, 'src', 'mcp', 'code_mode_server.py'),
|
|
351
|
+
'memory_mcp_server.py': join(ROOT, 'src', 'mcp', 'memory_mcp_server.py'),
|
|
352
|
+
'_pantheon_paths.py': join(ROOT, 'src', 'mcp', '_pantheon_paths.py'),
|
|
353
|
+
'mcp_persistence_server.py': join(ROOT, 'src', 'mcp', 'mcp_persistence_server.py'),
|
|
354
|
+
'pantheon_vision_server.py': join(ROOT, 'src', 'mcp', 'pantheon_vision_server.py'),
|
|
355
|
+
}
|
|
319
356
|
const dstScriptsDir = join(runtimeTarget, 'scripts')
|
|
320
357
|
if (!dryRun) mkdirSync(dstScriptsDir, { recursive: true })
|
|
321
358
|
for (const script of mcpScripts) {
|
|
322
|
-
const src = join(srcScriptsDir, script)
|
|
359
|
+
const src = canonicalMcpScripts[script] || join(srcScriptsDir, script)
|
|
323
360
|
if (!existsSync(src)) {
|
|
324
361
|
warning(`Script not found: ${src}`)
|
|
325
362
|
stats.errors++
|
|
@@ -341,6 +378,18 @@ export async function installOpenCode(
|
|
|
341
378
|
} else stats.skipped++
|
|
342
379
|
}
|
|
343
380
|
|
|
381
|
+
// Keep Vision's dependency contract available to the installed runtime.
|
|
382
|
+
const visionRequirements = join(ROOT, 'src', 'mcp', 'requirements-vision.txt')
|
|
383
|
+
if (existsSync(visionRequirements)) {
|
|
384
|
+
const destination = join(runtimeTarget, 'requirements-vision.txt')
|
|
385
|
+
const status = writeIfChanged(destination, readFileSync(visionRequirements, 'utf8'), dryRun)
|
|
386
|
+
if (status === 'created') stats.created++
|
|
387
|
+
else stats.skipped++
|
|
388
|
+
} else {
|
|
389
|
+
warning(`Requirements file not found: ${visionRequirements}`)
|
|
390
|
+
stats.errors++
|
|
391
|
+
}
|
|
392
|
+
|
|
344
393
|
// ── Code-mode scripts ──
|
|
345
394
|
const srcCodeModeDir = join(ROOT, '.pantheon', 'code-mode')
|
|
346
395
|
const dstCodeModeDir = join(runtimeTarget, '.pantheon', 'code-mode')
|
|
@@ -519,15 +568,41 @@ export async function installOpenCode(
|
|
|
519
568
|
if (!config.default_agent && pantheonConfig.default_agent) {
|
|
520
569
|
config.default_agent = pantheonConfig.default_agent
|
|
521
570
|
}
|
|
571
|
+
// Merge subagent_depth HERE (the config merge always runs, including on a
|
|
572
|
+
// fresh install) instead of a file-existence-gated block, so run #1 already
|
|
573
|
+
// produces the final config — run #2 must be a byte-identical no-op (#19).
|
|
574
|
+
if (config.subagent_depth === undefined) {
|
|
575
|
+
config.subagent_depth = 2
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// --------------------------------------------------------------------
|
|
579
|
+
// B.5.1 Hermetic plugin resolution (packaging fix)
|
|
580
|
+
// --------------------------------------------------------------------
|
|
581
|
+
// The packaged opencode.json may reference the pantheon-hooks plugin via a
|
|
582
|
+
// developer-machine absolute path (e.g. /home/<dev>/pantheon/src/plugins/...).
|
|
583
|
+
// Copying that verbatim into the user's config would break every global
|
|
584
|
+
// install on any other machine. Option A: rewrite the entry to the plugin
|
|
585
|
+
// file inside the INSTALLED package (derived from ROOT — never hardcoded),
|
|
586
|
+
// so both dev and global installs resolve to a path that actually exists.
|
|
587
|
+
// The source opencode.json keeps its dev path for local development; the
|
|
588
|
+
// transform happens at install/sync time only.
|
|
589
|
+
function resolveInstalledPlugin(plugin) {
|
|
590
|
+
const file = basename(plugin)
|
|
591
|
+
const packaged = join(ROOT, 'src', 'plugins', file)
|
|
592
|
+
return existsSync(packaged) ? packaged : plugin
|
|
593
|
+
}
|
|
522
594
|
|
|
523
595
|
if (!Array.isArray(config.plugin)) {
|
|
524
596
|
config.plugin = []
|
|
525
597
|
}
|
|
526
598
|
if (Array.isArray(pantheonConfig.plugin)) {
|
|
527
599
|
for (const plugin of pantheonConfig.plugin) {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
600
|
+
const resolved = resolveInstalledPlugin(plugin)
|
|
601
|
+
const file = basename(resolved)
|
|
602
|
+
// Replace any pre-existing entry for the same plugin file (e.g. a stale
|
|
603
|
+
// dev-machine path from an earlier install) so upgrades stay hermetic.
|
|
604
|
+
config.plugin = config.plugin.filter((p) => basename(p) !== file)
|
|
605
|
+
config.plugin.push(resolved)
|
|
531
606
|
}
|
|
532
607
|
}
|
|
533
608
|
|
|
@@ -604,10 +679,16 @@ export async function installOpenCode(
|
|
|
604
679
|
// --------------------------------------------------------------------
|
|
605
680
|
if (componentSet.has('runtime')) {
|
|
606
681
|
config.mcp = config.mcp || {}
|
|
682
|
+
// P1-3: derive the MCP python from the SAME venv layout setupVenv
|
|
683
|
+
// creates (<target>/.venv via venvPythonPath). For project installs the
|
|
684
|
+
// runtime payload lives under <target>/.opencode (runtimeTarget), but the
|
|
685
|
+
// venv is created at <target>/.venv — pointing MCP commands at
|
|
686
|
+
// runtimeTarget/.venv would reference an executable that never exists.
|
|
607
687
|
const runtimeTarget = isGlobal ? target : join(target, '.opencode')
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
688
|
+
const venvPython = venvPythonPath(target)
|
|
689
|
+
// Point the config at the venv even on a first install; setupVenv runs
|
|
690
|
+
// later in this function and creates this path before OpenCode starts.
|
|
691
|
+
const memoryPython = venvPython
|
|
611
692
|
|
|
612
693
|
// Only add if not already configured by user
|
|
613
694
|
if (!config.mcp['pantheon-resources']) {
|
|
@@ -642,6 +723,14 @@ export async function installOpenCode(
|
|
|
642
723
|
enabled: true,
|
|
643
724
|
}
|
|
644
725
|
}
|
|
726
|
+
if (!config.mcp['pantheon-vision']) {
|
|
727
|
+
config.mcp['pantheon-vision'] = {
|
|
728
|
+
type: 'local',
|
|
729
|
+
cwd: runtimeTarget,
|
|
730
|
+
command: [memoryPython, 'scripts/pantheon_vision_server.py'],
|
|
731
|
+
enabled: true,
|
|
732
|
+
}
|
|
733
|
+
}
|
|
645
734
|
|
|
646
735
|
// Default MCP permissions
|
|
647
736
|
config.permission = config.permission || {}
|
|
@@ -658,6 +747,9 @@ export async function installOpenCode(
|
|
|
658
747
|
if (!config.permission.mcp['pantheon-persistence']) {
|
|
659
748
|
config.permission.mcp['pantheon-persistence'] = 'allow'
|
|
660
749
|
}
|
|
750
|
+
if (!config.permission.mcp['pantheon-vision']) {
|
|
751
|
+
config.permission.mcp['pantheon-vision'] = 'ask'
|
|
752
|
+
}
|
|
661
753
|
}
|
|
662
754
|
|
|
663
755
|
const configContent = `${JSON.stringify(config, null, 2)}\n`
|
|
@@ -707,7 +799,7 @@ export async function installOpenCode(
|
|
|
707
799
|
}
|
|
708
800
|
} catch (err) {
|
|
709
801
|
error(`Setup failed: ${err.message}`)
|
|
710
|
-
throw err
|
|
802
|
+
throw err // Fatal — abort installation
|
|
711
803
|
}
|
|
712
804
|
}
|
|
713
805
|
|
|
@@ -718,18 +810,38 @@ export async function installOpenCode(
|
|
|
718
810
|
|
|
719
811
|
process.stdout.write('\n')
|
|
720
812
|
process.stdout.write(` ${colors.bold(colors.green('Installation complete'))}\n`)
|
|
721
|
-
process.stdout.write(
|
|
813
|
+
process.stdout.write(
|
|
814
|
+
` ${colors.dim('\u2500'.repeat(Math.min(process.stdout.columns || 60, 60)))}\n`,
|
|
815
|
+
)
|
|
722
816
|
process.stdout.write(` ${colors.green('\u2713')} ${created} component(s) installed\n`)
|
|
723
|
-
if (skipped > 0)
|
|
724
|
-
|
|
817
|
+
if (skipped > 0)
|
|
818
|
+
process.stdout.write(` ${colors.dim('\u2014')} ${skipped} already up-to-date\n`)
|
|
819
|
+
if (errors > 0)
|
|
820
|
+
process.stdout.write(` ${colors.yellow('\u26a0')} ${errors} error(s) encountered\n`)
|
|
725
821
|
process.stdout.write('\n')
|
|
726
822
|
process.stdout.write(` ${colors.bold('Next steps:')}\n`)
|
|
727
823
|
process.stdout.write(` ${colors.dim('\u2022')} Configure agents in opencode.json\n`)
|
|
728
824
|
process.stdout.write(` ${colors.dim('\u2022')} Add MCP servers in mcp.json\n`)
|
|
729
825
|
process.stdout.write(` ${colors.dim('\u2022')} Run 'opencode doctor' to verify\n`)
|
|
730
|
-
process.stdout.write(
|
|
826
|
+
process.stdout.write(
|
|
827
|
+
` ${colors.dim('\u2022')} Run 'opencode' to start using Pantheon agents\n`,
|
|
828
|
+
)
|
|
731
829
|
process.stdout.write('\n')
|
|
732
830
|
} else {
|
|
733
831
|
printSummary(target, ['opencode'], stats)
|
|
734
832
|
}
|
|
833
|
+
|
|
834
|
+
// -----------------------------------------------------------------------
|
|
835
|
+
// 5. Model preset selection (--components agents). Interactive picker runs
|
|
836
|
+
// unless autoYes (use defaults) or an explicit --preset was given.
|
|
837
|
+
// -----------------------------------------------------------------------
|
|
838
|
+
if (interactive && !dryRun && !autoYes && !opts.preset) {
|
|
839
|
+
const { runModelPicker } = await import('./model-picker.mjs')
|
|
840
|
+
await runModelPicker({ presetDir: target, logger: console })
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
if (opts.preset && !dryRun) {
|
|
844
|
+
const { writeActivePreset } = await import('./model-picker.mjs')
|
|
845
|
+
writeActivePreset(target, opts.preset, { source: 'cli' })
|
|
846
|
+
}
|
|
735
847
|
}
|
|
@@ -128,8 +128,10 @@ export function unregisterPlugin(tuiJsonPath, pluginId, { dryRun = false } = {})
|
|
|
128
128
|
if (!Array.isArray(tuiConfig.plugin)) {
|
|
129
129
|
tuiConfig.plugin = []
|
|
130
130
|
}
|
|
131
|
-
// Remove stale
|
|
132
|
-
|
|
131
|
+
// Remove stale refs: the bare relative id (openCode < 1.19 wrote
|
|
132
|
+
// "plugins/pantheon-tui", which its tui loader misreads as an npm/github
|
|
133
|
+
// spec → NpmInstallFailedError) and the old dist paths.
|
|
134
|
+
const staleRefs = [pluginId, `${pluginId}/dist/tui.tsx`, `${pluginId}/dist/tui.js`]
|
|
133
135
|
for (const stale of staleRefs) {
|
|
134
136
|
const idx = tuiConfig.plugin.indexOf(stale)
|
|
135
137
|
if (idx !== -1) tuiConfig.plugin.splice(idx, 1)
|
package/scripts/install/venv.mjs
CHANGED
|
@@ -17,6 +17,25 @@ import { fileURLToPath } from 'node:url'
|
|
|
17
17
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
18
18
|
const ROOT = join(__dirname, '..', '..')
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the python executable of the venv that setupVenv creates for a
|
|
22
|
+
* target. Single source of truth for the venv layout: the venv always lives
|
|
23
|
+
* at <target>/.venv — even for PROJECT installs, where the runtime payload
|
|
24
|
+
* goes to <target>/.opencode. MCP commands and health checks MUST derive
|
|
25
|
+
* their python from here (never from a runtime sub-directory), or a project
|
|
26
|
+
* install would point at <target>/.opencode/.venv/bin/python3, which
|
|
27
|
+
* setupVenv never creates (P1-3).
|
|
28
|
+
*
|
|
29
|
+
* @param {string} target - target installation directory
|
|
30
|
+
* @returns {string} absolute path to the venv python executable
|
|
31
|
+
*/
|
|
32
|
+
export function venvPythonPath(target) {
|
|
33
|
+
const venvPath = join(target, '.venv')
|
|
34
|
+
return process.platform === 'win32'
|
|
35
|
+
? join(venvPath, 'Scripts', 'python.exe')
|
|
36
|
+
: join(venvPath, 'bin', 'python3')
|
|
37
|
+
}
|
|
38
|
+
|
|
20
39
|
/**
|
|
21
40
|
* Set up a Python virtual environment and install MCP dependencies.
|
|
22
41
|
*
|
|
@@ -26,7 +45,7 @@ const ROOT = join(__dirname, '..', '..')
|
|
|
26
45
|
*/
|
|
27
46
|
export function setupVenv(target, { dryRun = false, skipInstall = false, force = false } = {}) {
|
|
28
47
|
const venvPath = join(target, '.venv')
|
|
29
|
-
const pythonBin =
|
|
48
|
+
const pythonBin = venvPythonPath(target)
|
|
30
49
|
|
|
31
50
|
// Step 1: Force recreate venv if --force
|
|
32
51
|
if (force && existsSync(venvPath)) {
|
|
@@ -94,7 +113,11 @@ export function setupVenv(target, { dryRun = false, skipInstall = false, force =
|
|
|
94
113
|
if (r2.status !== 0) {
|
|
95
114
|
throw new Error(
|
|
96
115
|
'Failed to install MCP dependencies. ' +
|
|
97
|
-
|
|
116
|
+
'Try: PIP_USER=0 ' +
|
|
117
|
+
pip +
|
|
118
|
+
' install -r ' +
|
|
119
|
+
reqFile +
|
|
120
|
+
' --break-system-packages',
|
|
98
121
|
)
|
|
99
122
|
}
|
|
100
123
|
}
|