pantheon-opencode 1.5.0-beta.1 → 1.5.0-beta.11
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/manifest.json +21 -0
- package/CHANGELOG.md +1054 -0
- package/README.md +171 -16
- package/README.pt-BR.md +99 -13
- package/bin/pantheon-init.mjs +178 -21
- package/package.json +23 -15
- package/plugin.json +1 -1
- package/pyproject.toml +65 -0
- package/scripts/code_mode_server.py +431 -55
- package/scripts/cost.mjs +30 -14
- package/scripts/dev-tui.sh +48 -0
- package/scripts/doctor.mjs +207 -9
- package/scripts/install/cli-ui.mjs +17 -11
- package/scripts/install/config-migration.mjs +4 -6
- package/scripts/install/opencode.mjs +137 -35
- package/scripts/install/plugin.mjs +3 -33
- package/scripts/install/shared.mjs +43 -2
- package/scripts/install/strings.mjs +129 -0
- package/scripts/install-mcp.mjs +1 -0
- package/scripts/manifest-inventory.mjs +410 -0
- package/scripts/mcp_persistence_server.py +537 -128
- 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/release-beta-version.mjs +56 -60
- package/scripts/sync-artifacts.mjs +169 -0
- package/scripts/sync-tui.mjs +121 -53
- package/scripts/test-opencode-v1-v2-sandbox.sh +367 -96
- package/scripts/validation-policy.mjs +7 -7
- package/scripts/version-check.mjs +113 -108
- package/scripts/versioning.mjs +230 -86
- package/src/agents/mnemosyne.md +1 -1
- package/src/agents/zeus.md +2 -2
- package/src/mcp/code_mode_server.py +431 -55
- package/src/mcp/mcp_persistence_server.py +537 -128
- package/src/pantheon/auto-wake.ts +1 -5
- package/src/pantheon/background-job-board.ts +43 -1
- package/src/pantheon/compaction-handoff.ts +304 -0
- package/src/pantheon/cost-command.ts +345 -62
- package/src/pantheon/delegate-manager.ts +390 -8
- package/src/pantheon/delegation-finalize.ts +32 -0
- package/src/pantheon/delegation.ts +235 -18
- package/src/pantheon/file-persistence-salvage.ts +64 -0
- package/src/pantheon/file-persistence.ts +194 -45
- package/src/pantheon/native-probe.ts +234 -0
- package/src/pantheon/native-task-status.ts +139 -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/task-result-guard.ts +107 -0
- package/src/pantheon/token-opt.ts +146 -12
- package/src/pantheon/tool-ceiling.ts +159 -0
- package/src/pantheon/v2-events.ts +18 -1
- package/src/plugin-v2.ts +2 -14
- package/src/plugin.ts +28 -17
- package/src/plugins/pantheon-hooks.ts +51 -0
- 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 +137 -27
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +459 -163
- package/src/plugins/tui/dist/tui.js.map +1 -1
- package/src/plugins/tui/dist/tui.tsx +506 -103
- package/src/plugins/tui/package-lock.json +2 -2
- package/src/plugins/tui/package.json +1 -1
- package/src/plugins/tui/src/index.tsx +506 -103
- package/.pantheon/code-mode/.gitkeep +0 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-1.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-10.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-11.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-2.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-3.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-4.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-5.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-6.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-7.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-8.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-9.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/heartbeat.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/session.json +0 -11
- package/src/pantheon/delegate-task-adapter.ts +0 -444
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* isolation is handled at runtime via OPENCODE_DB.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { createHash } from 'node:crypto'
|
|
15
16
|
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:fs'
|
|
16
17
|
import { homedir } from 'node:os'
|
|
17
18
|
import { dirname, isAbsolute, join, resolve } from 'node:path'
|
|
@@ -34,6 +35,7 @@ import { detectVersion, runMigrations } from './migrate.mjs'
|
|
|
34
35
|
import { resolveOpenCodeVersion } from './opencode-version.mjs'
|
|
35
36
|
import { copyPluginFiles, installPlugin, registerPlugin, unregisterPlugin } from './plugin.mjs'
|
|
36
37
|
import {
|
|
38
|
+
checkRuntimePrerequisites,
|
|
37
39
|
collectSkillNames,
|
|
38
40
|
copyFiles,
|
|
39
41
|
installSkills,
|
|
@@ -42,8 +44,10 @@ import {
|
|
|
42
44
|
sourceDirValid,
|
|
43
45
|
summary,
|
|
44
46
|
syncDir,
|
|
47
|
+
writeConfigWithBackup,
|
|
45
48
|
writeIfChanged,
|
|
46
49
|
} from './shared.mjs'
|
|
50
|
+
import { strings } from './strings.mjs'
|
|
47
51
|
|
|
48
52
|
import { setupVenv, venvPythonPath } from './venv.mjs'
|
|
49
53
|
|
|
@@ -61,6 +65,34 @@ function deepClone(value) {
|
|
|
61
65
|
return JSON.parse(JSON.stringify(value))
|
|
62
66
|
}
|
|
63
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
|
+
|
|
64
96
|
function readJsonConfig(filePath) {
|
|
65
97
|
if (!existsSync(filePath)) return {}
|
|
66
98
|
|
|
@@ -185,6 +217,21 @@ function managedPluginIdentity(ref) {
|
|
|
185
217
|
if (absolutePath === normalizePluginPath(resolve(ROOT, PANTHEON_V2_LEGACY_FILE))) {
|
|
186
218
|
return PANTHEON_V2_PLUGIN
|
|
187
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
|
+
}
|
|
188
235
|
}
|
|
189
236
|
|
|
190
237
|
return null
|
|
@@ -343,6 +390,45 @@ export function syncTuiRegistration(target, { isGlobal = false, dryRun = false }
|
|
|
343
390
|
return registerPlugin(targetTuiConfigPath, tuiCopyDir, { dryRun })
|
|
344
391
|
}
|
|
345
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
|
+
|
|
346
432
|
export async function installOpenCode(
|
|
347
433
|
target,
|
|
348
434
|
dryRun = false,
|
|
@@ -357,6 +443,7 @@ export async function installOpenCode(
|
|
|
357
443
|
|
|
358
444
|
const componentSet = new Set(components)
|
|
359
445
|
const stats = summary.opencode
|
|
446
|
+
const S = strings()
|
|
360
447
|
|
|
361
448
|
// V1/V2 dual-version awareness (Phase 3): the installer builds a unified
|
|
362
449
|
// config, then applies V2-native migration (migrateV1toV2) when
|
|
@@ -405,6 +492,17 @@ export async function installOpenCode(
|
|
|
405
492
|
const config = readJsonConfig(targetConfigPath)
|
|
406
493
|
const pantheonConfig = readJsonConfig(pantheonConfigPath)
|
|
407
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
|
+
|
|
408
506
|
// Determine layout based on install scope.
|
|
409
507
|
// Global config dir (~/.opencode or ~/.config/opencode) uses a flat layout:
|
|
410
508
|
// agents/ skills/ commands/ plugins/
|
|
@@ -417,7 +515,7 @@ export async function installOpenCode(
|
|
|
417
515
|
configure({ dryRun })
|
|
418
516
|
|
|
419
517
|
if (isGlobal) {
|
|
420
|
-
info(
|
|
518
|
+
info(S.globalLayout)
|
|
421
519
|
}
|
|
422
520
|
|
|
423
521
|
// -----------------------------------------------------------------------
|
|
@@ -430,7 +528,7 @@ export async function installOpenCode(
|
|
|
430
528
|
warning(`Agent source directory not found: ${srcDir}`)
|
|
431
529
|
stats.errors++
|
|
432
530
|
} else {
|
|
433
|
-
const agentStep = step(
|
|
531
|
+
const agentStep = step(S.installingAgents)
|
|
434
532
|
const dstDir = isGlobal ? join(target, 'agents') : join(target, '.opencode', 'agents')
|
|
435
533
|
if (!dryRun) mkdirSync(dstDir, { recursive: true })
|
|
436
534
|
if (clean && existsSync(dstDir) && !dryRun) {
|
|
@@ -665,6 +763,13 @@ export async function installOpenCode(
|
|
|
665
763
|
stats.created++
|
|
666
764
|
}
|
|
667
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`)
|
|
771
|
+
}
|
|
772
|
+
|
|
668
773
|
// ── tiers.json ──
|
|
669
774
|
const srcTiers = join(ROOT, '.pantheon', 'tiers.json')
|
|
670
775
|
const dstTiers = join(runtimeTarget, '.pantheon', 'tiers.json')
|
|
@@ -676,6 +781,17 @@ export async function installOpenCode(
|
|
|
676
781
|
}
|
|
677
782
|
}
|
|
678
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
|
+
|
|
679
795
|
// -----------------------------------------------------------------------
|
|
680
796
|
// 3. Create/update opencode.json (always runs)
|
|
681
797
|
// Reads TARGET's existing config first, then merges Pantheon settings
|
|
@@ -1017,8 +1133,10 @@ export async function installOpenCode(
|
|
|
1017
1133
|
|
|
1018
1134
|
// --------------------------------------------------------------------
|
|
1019
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.
|
|
1020
1138
|
// --------------------------------------------------------------------
|
|
1021
|
-
if (componentSet.has('runtime')) {
|
|
1139
|
+
if (componentSet.has('runtime') && runtimeHealthy) {
|
|
1022
1140
|
config.mcp = config.mcp || {}
|
|
1023
1141
|
// MCP stays a flat named-server map for BOTH V1 and V2: OpenCode 1.18.x
|
|
1024
1142
|
// has no `mcp.servers` wrapper and rejects one with
|
|
@@ -1102,12 +1220,12 @@ export async function installOpenCode(
|
|
|
1102
1220
|
// before writing. V1 path is untouched (zero regression).
|
|
1103
1221
|
let configToWrite = config
|
|
1104
1222
|
if (version === 'v2') {
|
|
1105
|
-
info(
|
|
1223
|
+
info(S.v2Migrating)
|
|
1106
1224
|
configToWrite = migrateV1toV2(config)
|
|
1107
1225
|
}
|
|
1108
1226
|
|
|
1109
1227
|
const configContent = `${JSON.stringify(configToWrite, null, 2)}\n`
|
|
1110
|
-
const status =
|
|
1228
|
+
const status = writeConfigWithBackup(targetConfigPath, configContent, dryRun)
|
|
1111
1229
|
if (status === 'created') stats.created++
|
|
1112
1230
|
else stats.skipped++
|
|
1113
1231
|
|
|
@@ -1128,40 +1246,15 @@ export async function installOpenCode(
|
|
|
1128
1246
|
}
|
|
1129
1247
|
|
|
1130
1248
|
// -----------------------------------------------------------------------
|
|
1131
|
-
// 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)
|
|
1132
1251
|
// -----------------------------------------------------------------------
|
|
1133
|
-
if (componentSet.has('runtime')) {
|
|
1134
|
-
try {
|
|
1135
|
-
const venvSpinner = spinner('Setting up Python virtual environment')
|
|
1136
|
-
setupVenv(target, { dryRun, force: clean })
|
|
1137
|
-
venvSpinner(true)
|
|
1138
|
-
const runtimeTarget = isGlobal ? target : join(target, '.opencode')
|
|
1139
|
-
const health = healthCheck(runtimeTarget, { dryRun, pythonTarget: target })
|
|
1140
|
-
|
|
1141
|
-
// Print health summary
|
|
1142
|
-
section('\uD83D\uDD0D Health Check')
|
|
1143
|
-
const healthStep = step('Running health checks')
|
|
1144
|
-
for (const p of health.passed) success(`${p.check}: ${p.detail}`)
|
|
1145
|
-
for (const w of health.warnings) warning(`${w.check}: ${w.detail}`)
|
|
1146
|
-
for (const f of health.failed) error(`${f.check}: ${f.detail}`)
|
|
1147
|
-
|
|
1148
|
-
if (health.failed.length > 0) {
|
|
1149
|
-
healthStep(false)
|
|
1150
|
-
warning(`${health.failed.length} check(s) failed — review above`)
|
|
1151
|
-
stats.errors += health.failed.length
|
|
1152
|
-
} else {
|
|
1153
|
-
healthStep(true)
|
|
1154
|
-
}
|
|
1155
|
-
} catch (err) {
|
|
1156
|
-
error(`Setup failed: ${err.message}`)
|
|
1157
|
-
throw err // Fatal — abort installation
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
1252
|
|
|
1161
1253
|
if (interactive && !dryRun) {
|
|
1162
1254
|
const created = stats.created
|
|
1163
1255
|
const skipped = stats.skipped
|
|
1164
1256
|
const errors = stats.errors
|
|
1257
|
+
const warnings = stats.warnings ?? 0
|
|
1165
1258
|
|
|
1166
1259
|
process.stdout.write('\n')
|
|
1167
1260
|
process.stdout.write(
|
|
@@ -1179,11 +1272,14 @@ export async function installOpenCode(
|
|
|
1179
1272
|
process.stdout.write(` ${colors.dim('\u2014')} ${skipped} already up-to-date\n`)
|
|
1180
1273
|
if (errors > 0)
|
|
1181
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
|
+
)
|
|
1182
1279
|
process.stdout.write('\n')
|
|
1183
1280
|
process.stdout.write(` ${colors.bold('Next steps:')}\n`)
|
|
1184
1281
|
process.stdout.write(` ${colors.dim('\u2022')} Configure agents in opencode.json\n`)
|
|
1185
|
-
process.stdout.write(` ${colors.dim('\u2022')}
|
|
1186
|
-
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`)
|
|
1187
1283
|
process.stdout.write(
|
|
1188
1284
|
` ${colors.dim('\u2022')} Run 'opencode' to start using Pantheon agents\n`,
|
|
1189
1285
|
)
|
|
@@ -1207,4 +1303,10 @@ export async function installOpenCode(
|
|
|
1207
1303
|
const { writeActivePreset } = await import('./model-picker.mjs')
|
|
1208
1304
|
writeActivePreset(target, opts.preset, { source: 'cli' })
|
|
1209
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
|
+
}
|
|
1210
1312
|
}
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
import { execSync } from 'node:child_process'
|
|
6
6
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:fs'
|
|
7
7
|
import { isAbsolute, join, resolve } from 'node:path'
|
|
8
|
-
import { warning } from './cli-ui.mjs'
|
|
9
8
|
import { ROOT, writeIfChanged } from './shared.mjs'
|
|
10
9
|
|
|
11
10
|
/**
|
|
@@ -95,31 +94,9 @@ export function installPlugin(srcDir, dstDir, { dryRun = false, clean = false }
|
|
|
95
94
|
result.skipped += copyResult.skipped
|
|
96
95
|
|
|
97
96
|
// Install plugin dependencies (@opentui/core, @opentui/solid, solid-js).
|
|
98
|
-
//
|
|
99
|
-
// fallback is opt-in because npm install may rewrite the dependency graph.
|
|
97
|
+
// Lockfile installation is the only accepted dependency path.
|
|
100
98
|
if (!dryRun) {
|
|
101
|
-
|
|
102
|
-
execSync('npm ci --omit=dev --no-audit --no-fund', { cwd: dstDir, stdio: 'pipe' })
|
|
103
|
-
} catch (e) {
|
|
104
|
-
if (process.env.PANTHEON_ALLOW_NPM_INSTALL_FALLBACK === '1') {
|
|
105
|
-
warning(`npm ci failed; explicit fallback enabled: ${e.message}`)
|
|
106
|
-
try {
|
|
107
|
-
execSync('npm install --omit=dev --no-audit --no-fund --package-lock=false', {
|
|
108
|
-
cwd: dstDir,
|
|
109
|
-
stdio: 'pipe',
|
|
110
|
-
})
|
|
111
|
-
} catch (fallbackError) {
|
|
112
|
-
warning(`Failed to install TUI plugin dependencies: ${fallbackError.message}`)
|
|
113
|
-
result.errors++
|
|
114
|
-
}
|
|
115
|
-
} else {
|
|
116
|
-
warning(
|
|
117
|
-
`npm ci failed; dependencies were not changed. Set ` +
|
|
118
|
-
`PANTHEON_ALLOW_NPM_INSTALL_FALLBACK=1 to explicitly allow npm install: ${e.message}`,
|
|
119
|
-
)
|
|
120
|
-
result.errors++
|
|
121
|
-
}
|
|
122
|
-
}
|
|
99
|
+
execSync('npm ci --omit=dev --no-audit --no-fund', { cwd: dstDir, stdio: 'pipe' })
|
|
123
100
|
}
|
|
124
101
|
|
|
125
102
|
return result
|
|
@@ -133,14 +110,7 @@ export function installPlugin(srcDir, dstDir, { dryRun = false, clean = false }
|
|
|
133
110
|
*/
|
|
134
111
|
export function installPluginDependencies(pluginDir, { dryRun = false } = {}) {
|
|
135
112
|
if (dryRun) return
|
|
136
|
-
|
|
137
|
-
execSync('npm ci --omit=dev --no-audit --no-fund', { cwd: pluginDir, stdio: 'pipe' })
|
|
138
|
-
} catch (e) {
|
|
139
|
-
warning(
|
|
140
|
-
`npm ci failed; dependencies were not changed. Set ` +
|
|
141
|
-
`PANTHEON_ALLOW_NPM_INSTALL_FALLBACK=1 to explicitly allow npm install: ${e.message}`,
|
|
142
|
-
)
|
|
143
|
-
}
|
|
113
|
+
execSync('npm ci --omit=dev --no-audit --no-fund', { cwd: pluginDir, stdio: 'pipe' })
|
|
144
114
|
}
|
|
145
115
|
|
|
146
116
|
/**
|
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
* shared.mjs — Shared utilities for the OpenCode installer
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { spawnSync } from 'node:child_process'
|
|
5
6
|
import {
|
|
7
|
+
copyFileSync,
|
|
6
8
|
cpSync,
|
|
7
9
|
existsSync,
|
|
8
10
|
mkdirSync,
|
|
9
11
|
readdirSync,
|
|
10
12
|
readFileSync,
|
|
13
|
+
renameSync,
|
|
11
14
|
rmSync,
|
|
12
15
|
statSync,
|
|
13
16
|
writeFileSync,
|
|
@@ -41,7 +44,7 @@ export function getAgentNames() {
|
|
|
41
44
|
// Cached constant for backward compatibility
|
|
42
45
|
export const AGENT_NAMES = getAgentNames()
|
|
43
46
|
|
|
44
|
-
export const summary = { opencode: { created: 0, skipped: 0, errors: 0 } }
|
|
47
|
+
export const summary = { opencode: { created: 0, skipped: 0, errors: 0, warnings: 0 } }
|
|
45
48
|
|
|
46
49
|
export function showHelp() {
|
|
47
50
|
console.log(`
|
|
@@ -260,11 +263,49 @@ export function writeIfChanged(filePath, content, dryRun) {
|
|
|
260
263
|
return 'skipped'
|
|
261
264
|
}
|
|
262
265
|
if (!dryRun) {
|
|
263
|
-
|
|
266
|
+
// Atomic write: write a temp sibling and rename over the target so a
|
|
267
|
+
// crash mid-write can never leave a truncated file behind (rename is
|
|
268
|
+
// atomic within the same directory).
|
|
269
|
+
const tmpPath = `${filePath}.tmp-${process.pid}`
|
|
270
|
+
writeFileSync(tmpPath, content, 'utf8')
|
|
271
|
+
renameSync(tmpPath, filePath)
|
|
264
272
|
}
|
|
265
273
|
return 'created'
|
|
266
274
|
}
|
|
267
275
|
|
|
276
|
+
/**
|
|
277
|
+
* beta.5: write the user's opencode.json with a same-directory backup of the
|
|
278
|
+
* previous content. The installer rewrites the whole config; a `.bak` gives
|
|
279
|
+
* the user a one-step recovery if a merge ever goes wrong.
|
|
280
|
+
*/
|
|
281
|
+
export function writeConfigWithBackup(filePath, content, dryRun) {
|
|
282
|
+
if (!dryRun && existsSync(filePath)) {
|
|
283
|
+
copyFileSync(filePath, `${filePath}.bak`)
|
|
284
|
+
}
|
|
285
|
+
return writeIfChanged(filePath, content, dryRun)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* beta.5 preflight: verify the toolchain pieces the runtime component needs
|
|
290
|
+
* BEFORE any files are written, so a missing python3/npm fails in seconds
|
|
291
|
+
* with a clear message instead of aborting a half-done install.
|
|
292
|
+
* Returns a list of human-readable problems (empty = all good).
|
|
293
|
+
*/
|
|
294
|
+
export function checkRuntimePrerequisites(env = process.env) {
|
|
295
|
+
const problems = []
|
|
296
|
+
const checks = [
|
|
297
|
+
{ cmd: 'python3', hint: 'install Python 3.10+ (https://www.python.org/downloads/)' },
|
|
298
|
+
{ cmd: 'npm', hint: 'install Node.js 22+ (https://nodejs.org) — npm ships with it' },
|
|
299
|
+
]
|
|
300
|
+
for (const { cmd, hint } of checks) {
|
|
301
|
+
const probe = spawnSync(cmd, ['--version'], { encoding: 'utf8', timeout: 10_000, env })
|
|
302
|
+
if (probe.error || probe.status !== 0) {
|
|
303
|
+
problems.push(`${cmd} not found or not runnable — ${hint}`)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return problems
|
|
307
|
+
}
|
|
308
|
+
|
|
268
309
|
export function collectSkillNames() {
|
|
269
310
|
const skillsDir = join(ROOT, 'src', 'skills')
|
|
270
311
|
if (!existsSync(skillsDir)) return []
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* strings.mjs — installer strings with locale auto-detection.
|
|
3
|
+
*
|
|
4
|
+
* LANG/LC_ALL/LC_MESSAGES starting with "pt" selects the PT-BR table;
|
|
5
|
+
* everything else (and the absence of a locale) selects EN. A plain string
|
|
6
|
+
* table — deliberately no i18n framework (YAGNI).
|
|
7
|
+
*
|
|
8
|
+
* Scope: the user journey (banner → install steps → health → summary →
|
|
9
|
+
* next steps). Deep diagnostics (per-file copy detail, doctor output) and
|
|
10
|
+
* the model-picker wizard remain single-language by design.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const TABLE = {
|
|
14
|
+
en: {
|
|
15
|
+
installing: 'Installing...',
|
|
16
|
+
dryRun: 'DRY RUN',
|
|
17
|
+
canceled: 'Installation canceled — nothing was broken; run init again anytime.',
|
|
18
|
+
canceledShort: 'Installation canceled.',
|
|
19
|
+
installFailed: (msg) => `❌ Installation failed: ${msg}`,
|
|
20
|
+
failHintNoMcp:
|
|
21
|
+
' Run with --no-mcp to skip Python dependencies:\n npx pantheon-opencode init --no-mcp',
|
|
22
|
+
failHintForce:
|
|
23
|
+
' Or retry with --force to recreate the venv:\n npx pantheon-opencode init --force',
|
|
24
|
+
runningHealthCheck: 'Running health check...',
|
|
25
|
+
installedTitle: (v) => `✅ Pantheon OpenCode v${v} installed!`,
|
|
26
|
+
nextSteps: 'Next steps:',
|
|
27
|
+
nextVerify: '1. Verify installation:\n npx pantheon-opencode doctor',
|
|
28
|
+
nextLaunch: '2. Launch OpenCode',
|
|
29
|
+
nextAgents: '3. Invoke agents with @agent-name in chat',
|
|
30
|
+
nextProject: '4. For project-local install:\n npx pantheon-opencode init --project',
|
|
31
|
+
// install steps
|
|
32
|
+
globalLayout:
|
|
33
|
+
'Global config directory detected — using flat layout (agents/, skills/, commands/)',
|
|
34
|
+
installingAgents: 'Installing agents',
|
|
35
|
+
installingSkills: 'Installing skills',
|
|
36
|
+
installingInstructions: 'Installing instructions',
|
|
37
|
+
installingPrompts: 'Installing prompts',
|
|
38
|
+
installingCommands: 'Installing commands',
|
|
39
|
+
installingPlugins: 'Installing plugins',
|
|
40
|
+
settingUpRuntime: 'Setting up Python virtual environment',
|
|
41
|
+
healthSection: 'Health Check',
|
|
42
|
+
runningHealthChecks: 'Running health checks',
|
|
43
|
+
runtimeFailed: (msg) => `Python runtime setup failed: ${msg}`,
|
|
44
|
+
runtimeSkippedMcp:
|
|
45
|
+
'MCP servers were NOT configured because the Python runtime is unavailable. ' +
|
|
46
|
+
'Fix python3 (or free disk space) and re-run init to add them; everything else was installed.',
|
|
47
|
+
prereqMissing:
|
|
48
|
+
'runtime prerequisites missing — install the tools above, or re-run with --no-mcp to install without Python MCP servers',
|
|
49
|
+
v2Migrating: 'Migrating config to V2 native format...',
|
|
50
|
+
// summary (printSummary)
|
|
51
|
+
summaryInstalled: (target) => `OpenCode installed in ${target}`,
|
|
52
|
+
summaryWithErrors: (target) => `Installation finished WITH ERRORS in ${target}`,
|
|
53
|
+
summaryComponents: (created, skipped) =>
|
|
54
|
+
` Components:\t${created} installed, ${skipped} skipped`,
|
|
55
|
+
summaryErrors: (n) => ` ${n} error(s) found`,
|
|
56
|
+
summaryWarnings: (n) => ` ${n} warning(s) — run 'doctor' for details`,
|
|
57
|
+
summaryNext: ' Next steps:',
|
|
58
|
+
summaryNextAgents: ' • Configure your agents in opencode.json',
|
|
59
|
+
summaryNextMcp: ' • Add MCP servers in opencode.json (mcp section)',
|
|
60
|
+
summaryNextDoctor: " • Run 'opencode doctor' to verify the installation",
|
|
61
|
+
},
|
|
62
|
+
pt: {
|
|
63
|
+
installing: 'Instalando...',
|
|
64
|
+
dryRun: 'SIMULAÇÃO',
|
|
65
|
+
canceled: 'Instalação cancelada — nada foi quebrado; rode init de novo quando quiser.',
|
|
66
|
+
canceledShort: 'Instalação cancelada.',
|
|
67
|
+
installFailed: (msg) => `❌ Instalação falhou: ${msg}`,
|
|
68
|
+
failHintNoMcp:
|
|
69
|
+
' Rode com --no-mcp para pular dependências Python:\n npx pantheon-opencode init --no-mcp',
|
|
70
|
+
failHintForce:
|
|
71
|
+
' Ou tente de novo com --force para recriar a venv:\n npx pantheon-opencode init --force',
|
|
72
|
+
runningHealthCheck: 'Rodando verificação de saúde...',
|
|
73
|
+
installedTitle: (v) => `✅ Pantheon OpenCode v${v} instalado!`,
|
|
74
|
+
nextSteps: 'Próximos passos:',
|
|
75
|
+
nextVerify: '1. Verifique a instalação:\n npx pantheon-opencode doctor',
|
|
76
|
+
nextLaunch: '2. Abra o OpenCode',
|
|
77
|
+
nextAgents: '3. Invoc agentes com @nome-do-agente no chat',
|
|
78
|
+
nextProject: '4. Para instalar no projeto:\n npx pantheon-opencode init --project',
|
|
79
|
+
// install steps
|
|
80
|
+
globalLayout:
|
|
81
|
+
'Diretório de config global detectado — layout plano (agents/, skills/, commands/)',
|
|
82
|
+
installingAgents: 'Instalando agentes',
|
|
83
|
+
installingSkills: 'Instalando skills',
|
|
84
|
+
installingInstructions: 'Instalando instruções',
|
|
85
|
+
installingPrompts: 'Instalando prompts',
|
|
86
|
+
installingCommands: 'Instalando comandos',
|
|
87
|
+
installingPlugins: 'Instalando plugins',
|
|
88
|
+
settingUpRuntime: 'Configurando ambiente virtual Python',
|
|
89
|
+
healthSection: 'Verificação de saúde',
|
|
90
|
+
runningHealthChecks: 'Rodando verificações de saúde',
|
|
91
|
+
runtimeFailed: (msg) => `Falha ao configurar o runtime Python: ${msg}`,
|
|
92
|
+
runtimeSkippedMcp:
|
|
93
|
+
'Os servidores MCP NÃO foram configurados porque o runtime Python está indisponível. ' +
|
|
94
|
+
'Corrija o python3 (ou libere espaço em disco) e rode init de novo para adicioná-los; todo o resto foi instalado.',
|
|
95
|
+
prereqMissing:
|
|
96
|
+
'pré-requisitos do runtime ausentes — instale as ferramentas acima, ou rode de novo com --no-mcp para instalar sem os MCPs Python',
|
|
97
|
+
v2Migrating: 'Migrando config para o formato nativo V2...',
|
|
98
|
+
// summary (printSummary)
|
|
99
|
+
summaryInstalled: (target) => `OpenCode instalado em ${target}`,
|
|
100
|
+
summaryWithErrors: (target) => `Instalação concluída COM ERROS em ${target}`,
|
|
101
|
+
summaryComponents: (created, skipped) =>
|
|
102
|
+
` Componentes:\t${created} instalados, ${skipped} pulados`,
|
|
103
|
+
summaryErrors: (n) => ` ${n} erro(s) encontrados`,
|
|
104
|
+
summaryWarnings: (n) => ` ${n} aviso(s) — rode 'doctor' para detalhes`,
|
|
105
|
+
summaryNext: ' Próximos passos:',
|
|
106
|
+
summaryNextAgents: ' • Configure seus agentes em opencode.json',
|
|
107
|
+
summaryNextMcp: ' • Adicione MCP servers em opencode.json (seção mcp)',
|
|
108
|
+
summaryNextDoctor: " • Rode 'opencode doctor' para verificar a instalação",
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Pure locale resolution (exported for tests). */
|
|
113
|
+
export function detectLocale(env = process.env) {
|
|
114
|
+
const raw = env.LC_ALL ?? env.LC_MESSAGES ?? env.LANG ?? ''
|
|
115
|
+
return /^pt/i.test(raw) ? 'pt' : 'en'
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Pure table access (exported for tests and explicit-locale callers). */
|
|
119
|
+
export function stringsFor(locale) {
|
|
120
|
+
return TABLE[locale] ?? TABLE.en
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let cached = null
|
|
124
|
+
|
|
125
|
+
/** Locale-resolved string table for the current process. */
|
|
126
|
+
export function strings(env = process.env) {
|
|
127
|
+
if (cached === null) cached = stringsFor(detectLocale(env))
|
|
128
|
+
return cached
|
|
129
|
+
}
|