pantheon-opencode 1.5.0-beta.3 → 1.5.0-beta.5
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 +871 -0
- package/README.md +101 -1
- package/README.pt-BR.md +55 -1
- package/bin/pantheon-init.mjs +178 -21
- package/package.json +14 -10
- package/plugin.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/code_mode_server.py +339 -28
- package/scripts/doctor.mjs +178 -9
- package/scripts/install/cli-ui.mjs +17 -11
- package/scripts/install/config-migration.mjs +4 -6
- package/scripts/install/opencode.mjs +116 -35
- package/scripts/install/shared.mjs +43 -2
- package/scripts/install/strings.mjs +129 -0
- package/scripts/install-mcp.mjs +1 -0
- package/scripts/probe-pantheon-cost.mjs +10 -1
- package/scripts/release-beta-version.mjs +2 -7
- package/scripts/sync-artifacts.mjs +169 -0
- package/scripts/sync-tui.mjs +124 -34
- package/src/agents/zeus.md +2 -2
- package/src/mcp/code_mode_server.py +339 -28
- package/src/pantheon/auto-wake.ts +1 -5
- package/src/pantheon/background-job-board.ts +14 -0
- package/src/pantheon/cost-command.ts +62 -42
- package/src/pantheon/delegate-manager.ts +109 -12
- package/src/pantheon/native-probe.ts +234 -0
- package/src/pantheon/native-task-status.ts +139 -0
- package/src/pantheon/shared-board.ts +37 -0
- package/src/pantheon/task-result-guard.ts +107 -0
- package/src/pantheon/token-opt.ts +18 -110
- package/src/pantheon/tool-ceiling.ts +159 -0
- package/src/pantheon/v2-events.ts +10 -1
- package/src/plugin.ts +25 -9
- 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 +1 -1
- package/src/plugins/tui/dist/tui.js +1 -1
- package/src/plugins/tui/dist/tui.js.map +1 -1
- package/src/plugins/tui/package-lock.json +2 -2
- package/src/plugins/tui/package.json +1 -1
|
@@ -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
|
+
}
|
package/scripts/install-mcp.mjs
CHANGED
|
@@ -28,6 +28,15 @@ function emit(status, detail, checks = []) {
|
|
|
28
28
|
if (status === 'FAIL') process.exitCode = 1
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
function diagnostic(error) {
|
|
32
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
33
|
+
const stderr =
|
|
34
|
+
error && typeof error === 'object' && typeof error.stderr === 'string'
|
|
35
|
+
? error.stderr.trim()
|
|
36
|
+
: ''
|
|
37
|
+
return stderr === '' ? message : `${message}; stderr: ${stderr}`
|
|
38
|
+
}
|
|
39
|
+
|
|
31
40
|
function ambiental(detail) {
|
|
32
41
|
emit('AMBIENTAL', detail)
|
|
33
42
|
}
|
|
@@ -295,7 +304,7 @@ async function main() {
|
|
|
295
304
|
checks.push('incompatible schema: PASS')
|
|
296
305
|
emit('PASS', 'offline synthetic DB checks passed; no real DB was read', checks)
|
|
297
306
|
} catch (error) {
|
|
298
|
-
emit('FAIL',
|
|
307
|
+
emit('FAIL', diagnostic(error), checks)
|
|
299
308
|
} finally {
|
|
300
309
|
rmSync(root, { recursive: true, force: true })
|
|
301
310
|
}
|
|
@@ -14,18 +14,13 @@
|
|
|
14
14
|
* compatibility only; it is no longer generated.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import {
|
|
18
|
-
SEMVER_PATTERN,
|
|
19
|
-
validateInventory,
|
|
20
|
-
writeInventoryVersion,
|
|
21
|
-
} from './manifest-inventory.mjs'
|
|
17
|
+
import { SEMVER_PATTERN, validateInventory, writeInventoryVersion } from './manifest-inventory.mjs'
|
|
22
18
|
|
|
23
19
|
const STABLE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/
|
|
24
20
|
// Current committed sequential beta line.
|
|
25
21
|
const BETA_LINE = /^(\d+\.\d+\.\d+)-beta\.(\d+)$/
|
|
26
22
|
// Legacy PR+SHA beta scheme, accepted for recovery only.
|
|
27
|
-
const LEGACY_BETA =
|
|
28
|
-
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-beta\.[1-9]\d*\.[0-9a-f]{7}$/
|
|
23
|
+
const LEGACY_BETA = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-beta\.[1-9]\d*\.[0-9a-f]{7}$/
|
|
29
24
|
const BETA_RELEASE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-beta\.[1-9]\d*$/
|
|
30
25
|
|
|
31
26
|
export function nextStableVersion(latest, intent = 'patch') {
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sync-artifacts.mjs — postinstall freshness sync for copy-only artifacts.
|
|
3
|
+
*
|
|
4
|
+
* Guarantees that an `npm install -g pantheon-opencode` (or npx cache
|
|
5
|
+
* refresh) brings an EXISTING installation up to date for everything that is
|
|
6
|
+
* a pure byte-compare copy: agents + routing.yml, skills, AGENTS.md,
|
|
7
|
+
* commands, the runtime MCP scripts, the code-mode payload and tiers.json.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately NOT done here (they belong to `pantheon-opencode init` /
|
|
10
|
+
* `update`, which the doctor points at when versions drift):
|
|
11
|
+
* - opencode.json merge (config keys, plugin registration, MCP entries)
|
|
12
|
+
* - venv/pip setup
|
|
13
|
+
* - tui.json registration changes
|
|
14
|
+
*
|
|
15
|
+
* The user's opencode.json already references the installed package root for
|
|
16
|
+
* plugins, so after a package update those references hit the NEW code
|
|
17
|
+
* automatically; this sync refreshes the COPIES that would otherwise stay
|
|
18
|
+
* stale until the next manual init.
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs'
|
|
21
|
+
import { join } from 'node:path'
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
collectSkillNames,
|
|
25
|
+
copyFiles,
|
|
26
|
+
installSkills,
|
|
27
|
+
ROOT,
|
|
28
|
+
syncDir,
|
|
29
|
+
writeIfChanged,
|
|
30
|
+
} from './install/shared.mjs'
|
|
31
|
+
|
|
32
|
+
/** Canonical MCP script sources — mirrors installOpenCode phase 2.10. */
|
|
33
|
+
const MCP_SCRIPTS = {
|
|
34
|
+
'mcp_resources_server.py': () => join(ROOT, 'src', 'mcp', 'mcp_resources_server.py'),
|
|
35
|
+
'code_mode_server.py': () => join(ROOT, 'src', 'mcp', 'code_mode_server.py'),
|
|
36
|
+
'memory_mcp_server.py': () => join(ROOT, 'src', 'mcp', 'memory_mcp_server.py'),
|
|
37
|
+
'mcp_persistence_server.py': () => join(ROOT, 'src', 'mcp', 'mcp_persistence_server.py'),
|
|
38
|
+
'_pantheon_paths.py': () => join(ROOT, 'src', 'mcp', '_pantheon_paths.py'),
|
|
39
|
+
'mcp_codemap_module.py': () => join(ROOT, 'src', 'mcp', 'mcp_codemap_module.py'),
|
|
40
|
+
'pantheon_vision_server.py': () => join(ROOT, 'src', 'mcp', 'pantheon_vision_server.py'),
|
|
41
|
+
'eval_store.py': () => join(ROOT, 'src', 'mcp', 'eval_store.py'),
|
|
42
|
+
'scrub-secrets.py': () => join(ROOT, 'scripts', 'scrub-secrets.py'),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Sync all copy-only artifacts from the installed package into configDir
|
|
47
|
+
* (the GLOBAL OpenCode config dir). Byte-compare idempotent; never throws.
|
|
48
|
+
* @returns {{ created: number, skipped: number, errors: string[] }}
|
|
49
|
+
*/
|
|
50
|
+
export function syncCopyArtifacts(configDir) {
|
|
51
|
+
const result = { created: 0, skipped: 0, errors: [] }
|
|
52
|
+
const bump = (r) => {
|
|
53
|
+
result.created += r.created
|
|
54
|
+
result.skipped += r.skipped
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── agents + routing.yml ──
|
|
58
|
+
try {
|
|
59
|
+
const dstAgents = join(configDir, 'agents')
|
|
60
|
+
if (existsSync(join(ROOT, 'src', 'agents'))) {
|
|
61
|
+
mkdirSync(dstAgents, { recursive: true })
|
|
62
|
+
bump(copyFiles(join(ROOT, 'src', 'agents'), dstAgents, false))
|
|
63
|
+
const routing = join(ROOT, 'src', 'routing.yml')
|
|
64
|
+
if (existsSync(routing)) {
|
|
65
|
+
if (
|
|
66
|
+
writeIfChanged(join(configDir, 'routing.yml'), readFileSync(routing, 'utf8'), false) ===
|
|
67
|
+
'created'
|
|
68
|
+
) {
|
|
69
|
+
result.created++
|
|
70
|
+
} else result.skipped++
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} catch (err) {
|
|
74
|
+
result.errors.push(`agents: ${err.message}`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── skills ──
|
|
78
|
+
try {
|
|
79
|
+
const names = collectSkillNames()
|
|
80
|
+
if (names.length > 0) bump(installSkills(names, configDir, false, ''))
|
|
81
|
+
} catch (err) {
|
|
82
|
+
result.errors.push(`skills: ${err.message}`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── instructions (AGENTS.md) ──
|
|
86
|
+
try {
|
|
87
|
+
const src = join(ROOT, 'AGENTS.md')
|
|
88
|
+
if (existsSync(src)) {
|
|
89
|
+
if (
|
|
90
|
+
writeIfChanged(join(configDir, 'AGENTS.md'), readFileSync(src, 'utf8'), false) === 'created'
|
|
91
|
+
) {
|
|
92
|
+
result.created++
|
|
93
|
+
} else result.skipped++
|
|
94
|
+
}
|
|
95
|
+
} catch (err) {
|
|
96
|
+
result.errors.push(`instructions: ${err.message}`)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── commands (*.md) ──
|
|
100
|
+
try {
|
|
101
|
+
const srcCmds = join(ROOT, 'commands')
|
|
102
|
+
if (existsSync(srcCmds)) {
|
|
103
|
+
bump(syncDir(srcCmds, join(configDir, 'commands'), false, false, (f) => f.endsWith('.md')))
|
|
104
|
+
}
|
|
105
|
+
} catch (err) {
|
|
106
|
+
result.errors.push(`commands: ${err.message}`)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── runtime payload (MCP scripts, code-mode, tiers) ──
|
|
110
|
+
try {
|
|
111
|
+
const dstScripts = join(configDir, 'scripts')
|
|
112
|
+
mkdirSync(dstScripts, { recursive: true })
|
|
113
|
+
for (const [name, srcFn] of Object.entries(MCP_SCRIPTS)) {
|
|
114
|
+
const src = srcFn()
|
|
115
|
+
if (!existsSync(src)) continue
|
|
116
|
+
const content = readFileSync(src, 'utf8')
|
|
117
|
+
const dst = join(dstScripts, name)
|
|
118
|
+
if (writeIfChanged(dst, content, false) === 'created') {
|
|
119
|
+
statSync(dst)
|
|
120
|
+
result.created++
|
|
121
|
+
} else result.skipped++
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const req = join(ROOT, 'src', 'mcp', 'requirements-vision.txt')
|
|
125
|
+
if (existsSync(req)) {
|
|
126
|
+
if (
|
|
127
|
+
writeIfChanged(
|
|
128
|
+
join(configDir, 'requirements-vision.txt'),
|
|
129
|
+
readFileSync(req, 'utf8'),
|
|
130
|
+
false,
|
|
131
|
+
) === 'created'
|
|
132
|
+
) {
|
|
133
|
+
result.created++
|
|
134
|
+
} else result.skipped++
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const srcCodeMode = join(ROOT, '.pantheon', 'code-mode')
|
|
138
|
+
const dstCodeMode = join(configDir, '.pantheon', 'code-mode')
|
|
139
|
+
if (existsSync(srcCodeMode)) {
|
|
140
|
+
bump(syncDir(srcCodeMode, dstCodeMode, false, false))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const srcTiers = join(ROOT, '.pantheon', 'tiers.json')
|
|
144
|
+
const dstTiers = join(configDir, '.pantheon', 'tiers.json')
|
|
145
|
+
if (existsSync(srcTiers)) {
|
|
146
|
+
if (writeIfChanged(dstTiers, readFileSync(srcTiers, 'utf8'), false) === 'created')
|
|
147
|
+
result.created++
|
|
148
|
+
else result.skipped++
|
|
149
|
+
}
|
|
150
|
+
} catch (err) {
|
|
151
|
+
result.errors.push(`runtime: ${err.message}`)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return result
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Drop stale entries in dst that no longer exist in src (agents/skills dirs). */
|
|
158
|
+
export function pruneMissing(srcDir, dstDir) {
|
|
159
|
+
if (!existsSync(srcDir) || !existsSync(dstDir)) return 0
|
|
160
|
+
const src = new Set(readdirSync(srcDir))
|
|
161
|
+
let removed = 0
|
|
162
|
+
for (const entry of readdirSync(dstDir)) {
|
|
163
|
+
if (!src.has(entry)) {
|
|
164
|
+
rmSync(join(dstDir, entry), { recursive: true, force: true })
|
|
165
|
+
removed++
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return removed
|
|
169
|
+
}
|
package/scripts/sync-tui.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import { execSync } from 'node:child_process'
|
|
|
13
13
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
14
14
|
import { homedir } from 'node:os'
|
|
15
15
|
import { join } from 'node:path'
|
|
16
|
-
import { fileURLToPath } from 'node:url'
|
|
16
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
17
17
|
|
|
18
18
|
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
|
19
19
|
const ROOT = join(__dirname, '..')
|
|
@@ -106,46 +106,136 @@ function copyPluginFiles(srcDir, dstDir) {
|
|
|
106
106
|
return result
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
// ──
|
|
109
|
+
// ── npm env hygiene ────────────────────────────────────────────────────
|
|
110
110
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
111
|
+
/**
|
|
112
|
+
* npm lifecycle env vars that put a nested `npm ci` into global context and
|
|
113
|
+
* make it abort with ECIGLOBAL ("`npm ci` does not work for global packages").
|
|
114
|
+
* When the package itself is installed with `npm install -g`, npm injects these
|
|
115
|
+
* into the postinstall environment; the TUI dependency sync is a local
|
|
116
|
+
* operation and must never inherit them.
|
|
117
|
+
*/
|
|
118
|
+
const GLOBAL_NPM_ENV_KEYS = new Set([
|
|
119
|
+
'npm_config_global',
|
|
120
|
+
'npm_config_globalconfig',
|
|
121
|
+
'npm_config_prefix',
|
|
122
|
+
'npm_config_location',
|
|
123
|
+
])
|
|
117
124
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Return a copy of `env` with global-context npm config vars removed.
|
|
127
|
+
* Matching is case-insensitive so Windows-style uppercase keys are handled.
|
|
128
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
129
|
+
* @returns {NodeJS.ProcessEnv}
|
|
130
|
+
*/
|
|
131
|
+
export function sanitizeNpmEnv(env = process.env) {
|
|
132
|
+
const clean = { ...env }
|
|
133
|
+
for (const key of Object.keys(clean)) {
|
|
134
|
+
if (GLOBAL_NPM_ENV_KEYS.has(key.toLowerCase())) delete clean[key]
|
|
122
135
|
}
|
|
136
|
+
return clean
|
|
137
|
+
}
|
|
123
138
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
139
|
+
// ── Main ───────────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
async function main() {
|
|
142
|
+
try {
|
|
143
|
+
const configDir = resolveConfigDir()
|
|
144
|
+
if (!configDir) {
|
|
145
|
+
// User hasn't run init yet — silent exit, no error
|
|
146
|
+
process.exit(0)
|
|
147
|
+
}
|
|
129
148
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
149
|
+
const tuiCopyDir = join(configDir, 'plugins', 'pantheon-tui')
|
|
150
|
+
if (!existsSync(tuiCopyDir)) {
|
|
151
|
+
// beta.5: no TUI install (plugins component omitted) — skip the TUI
|
|
152
|
+
// sync but STILL refresh the copy-only artifacts below.
|
|
153
|
+
const { syncCopyArtifacts } = await import('./sync-artifacts.mjs')
|
|
154
|
+
const synced = syncCopyArtifacts(configDir)
|
|
155
|
+
if (synced.errors.length > 0) {
|
|
156
|
+
for (const e of synced.errors) console.error(` ⚠️ artifact sync: ${e}`)
|
|
157
|
+
}
|
|
158
|
+
console.log(
|
|
159
|
+
` Artifacts synced: ${synced.created} updated, ${synced.skipped} already current`,
|
|
160
|
+
)
|
|
161
|
+
return
|
|
162
|
+
}
|
|
133
163
|
|
|
134
|
-
|
|
135
|
-
|
|
164
|
+
// Source: TUI plugin inside the installed package
|
|
165
|
+
const tuiSrcDir = join(ROOT, 'src', 'plugins', 'tui')
|
|
166
|
+
if (!existsSync(tuiSrcDir)) {
|
|
167
|
+
throw new Error(`TUI plugin source not found: ${tuiSrcDir}`)
|
|
168
|
+
}
|
|
136
169
|
|
|
137
|
-
|
|
138
|
-
|
|
170
|
+
// Compare versions before copy
|
|
171
|
+
const installedVersion = readVersion(join(tuiCopyDir, 'package.json'))
|
|
172
|
+
const sourceVersion = readVersion(join(tuiSrcDir, 'package.json'))
|
|
173
|
+
|
|
174
|
+
// Copy fresh files
|
|
175
|
+
const { created } = copyPluginFiles(tuiSrcDir, tuiCopyDir)
|
|
176
|
+
|
|
177
|
+
// Lockfile installation is the only accepted dependency path. Strip
|
|
178
|
+
// global-context npm vars so a global postinstall can't trigger ECIGLOBAL.
|
|
179
|
+
execSync('npm ci --omit=dev --no-audit --no-fund', {
|
|
180
|
+
cwd: tuiCopyDir,
|
|
181
|
+
stdio: 'pipe',
|
|
182
|
+
env: sanitizeNpmEnv(),
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
// Log result
|
|
186
|
+
if (installedVersion && sourceVersion && installedVersion === sourceVersion && created === 0) {
|
|
187
|
+
console.log(` TUI plugin already up to date (v${sourceVersion})`)
|
|
188
|
+
} else {
|
|
189
|
+
console.log(
|
|
190
|
+
` TUI plugin updated to v${sourceVersion || 'latest'}${installedVersion ? ` (was v${installedVersion})` : ''}`,
|
|
191
|
+
)
|
|
192
|
+
}
|
|
139
193
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
)
|
|
194
|
+
// ── beta.5 freshness guarantee: refresh ALL copy-only artifacts ──
|
|
195
|
+
// Agents, skills, AGENTS.md, commands, MCP scripts, code-mode payload.
|
|
196
|
+
// The plugin references in opencode.json already point INTO this package,
|
|
197
|
+
// so only the copies would go stale after a package update.
|
|
198
|
+
const { syncCopyArtifacts } = await import('./sync-artifacts.mjs')
|
|
199
|
+
const { readState, writeState } = await import('./install/state.mjs')
|
|
200
|
+
const pkgVersion = readVersion(join(ROOT, 'package.json')) ?? 'unknown'
|
|
201
|
+
const synced = syncCopyArtifacts(configDir)
|
|
202
|
+
if (synced.errors.length > 0) {
|
|
203
|
+
for (const e of synced.errors) console.error(` ⚠️ artifact sync: ${e}`)
|
|
204
|
+
}
|
|
205
|
+
console.log(` Artifacts synced: ${synced.created} updated, ${synced.skipped} already current`)
|
|
206
|
+
|
|
207
|
+
// Version marker so doctor can detect a stale installation and point the
|
|
208
|
+
// user at `pantheon-opencode update` for the full refresh (config merge,
|
|
209
|
+
// venv, MCP entries).
|
|
210
|
+
try {
|
|
211
|
+
const previous = readState(configDir)
|
|
212
|
+
writeState(configDir, {
|
|
213
|
+
pantheon_version: pkgVersion,
|
|
214
|
+
previous_version: previous?.pantheon_version ?? null,
|
|
215
|
+
})
|
|
216
|
+
if (previous?.pantheon_version && previous.pantheon_version !== pkgVersion) {
|
|
217
|
+
console.log(
|
|
218
|
+
` ⚠️ Installation moved ${previous.pantheon_version} → ${pkgVersion}. ` +
|
|
219
|
+
'Run `npx pantheon-opencode init` to refresh config/venv/MCP entries.',
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
} catch {
|
|
223
|
+
// Marker is best-effort — never fail the postinstall over it.
|
|
224
|
+
}
|
|
225
|
+
} catch (err) {
|
|
226
|
+
console.error(`❌ TUI sync failed: ${err.message}`)
|
|
227
|
+
process.exitCode = 1
|
|
147
228
|
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Only run when invoked directly (`node scripts/sync-tui.mjs`). Importing this
|
|
232
|
+
// module for unit tests must not trigger the side-effecting postinstall sync.
|
|
233
|
+
const isDirectRun =
|
|
234
|
+
Boolean(process.argv[1]) && pathToFileURL(process.argv[1]).href === import.meta.url
|
|
235
|
+
|
|
236
|
+
if (isDirectRun) {
|
|
237
|
+
main().catch((err) => {
|
|
238
|
+
console.error(`❌ TUI sync failed: ${err.message}`)
|
|
239
|
+
process.exitCode = 1
|
|
240
|
+
})
|
|
151
241
|
}
|
package/src/agents/zeus.md
CHANGED
|
@@ -329,8 +329,8 @@ nunca redispara. Apos 1 retry vazio → escalate (mesma regra do TODO Enforcer).
|
|
|
329
329
|
> Uso baixo-nível: `createZeusRetryHelper().executeWithRetry(first, () => secondRead())` — classifica empty e decide retry/escalate (mesmo cap 1).
|
|
330
330
|
|
|
331
331
|
### /cost — pantheon_cost tool (WIRED no plugin)
|
|
332
|
-
`pantheon_cost({ days?: number })` le o `opencode.db` READ-ONLY
|
|
333
|
-
|
|
332
|
+
`pantheon_cost({ days?: number })` le o `opencode.db` READ-ONLY usando o backend
|
|
333
|
+
unico `node:sqlite`; se indisponivel, retorna `UNSUPPORTED`, sem fallback. Devolve tabela markdown de custo + tokens por
|
|
334
334
|
agente nos ultimos N dias (default 7). Uso: quando Nyx pedir visibilidade de
|
|
335
335
|
custo, ou antes de escalar tier — decida se o batch valeu o preco.
|
|
336
336
|
|