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
|
@@ -1,80 +1,76 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* release-beta-version.mjs — beta version helpers.
|
|
4
|
+
*
|
|
5
|
+
* The beta version is committed in the manifests (package.json) and never
|
|
6
|
+
* computed at release time. `nextBetaVersion` advances the committed version:
|
|
7
|
+
*
|
|
8
|
+
* X.Y.Z-beta.N -> X.Y.Z-beta.(N+1) (stay on the current beta line)
|
|
9
|
+
* X.Y.Z -> X.Y.(Z+1)-beta.1 (start a fresh beta line)
|
|
10
|
+
*
|
|
11
|
+
* `applyBetaVersion` writes an already-decided beta version into every
|
|
12
|
+
* manifest and is retained for recovery tooling. The legacy PR+SHA scheme
|
|
13
|
+
* (`X.Y.Z-beta.<pr>.<sha7>`) is accepted by validation for backward
|
|
14
|
+
* compatibility only; it is no longer generated.
|
|
15
|
+
*/
|
|
3
16
|
|
|
4
|
-
import {
|
|
5
|
-
import { join } from 'node:path'
|
|
17
|
+
import { SEMVER_PATTERN, validateInventory, writeInventoryVersion } from './manifest-inventory.mjs'
|
|
6
18
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
]
|
|
13
|
-
|
|
14
|
-
const STABLE = /^(\d+)\.(\d+)\.(\d+)$/
|
|
19
|
+
const STABLE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/
|
|
20
|
+
// Current committed sequential beta line.
|
|
21
|
+
const BETA_LINE = /^(\d+\.\d+\.\d+)-beta\.(\d+)$/
|
|
22
|
+
// Legacy PR+SHA beta scheme, accepted for recovery only.
|
|
23
|
+
const LEGACY_BETA = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-beta\.[1-9]\d*\.[0-9a-f]{7}$/
|
|
24
|
+
const BETA_RELEASE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-beta\.[1-9]\d*$/
|
|
15
25
|
|
|
16
26
|
export function nextStableVersion(latest, intent = 'patch') {
|
|
17
|
-
const match = STABLE.exec(String(latest))
|
|
18
|
-
if (!match) throw new Error(`
|
|
19
|
-
const [major, minor, patch] = match.slice(1).map(Number)
|
|
27
|
+
const match = STABLE.exec(String(latest).trim())
|
|
28
|
+
if (!match) throw new Error(`latest is not a stable semver: "${latest}"`)
|
|
20
29
|
if (!['patch', 'minor', 'major'].includes(intent)) {
|
|
21
30
|
throw new Error(`release intent must be patch, minor, or major: "${intent}"`)
|
|
22
31
|
}
|
|
32
|
+
const [major, minor, patch] = match.slice(1).map(Number)
|
|
23
33
|
if (intent === 'major') return `${major + 1}.0.0`
|
|
24
34
|
if (intent === 'minor') return `${major}.${minor + 1}.0`
|
|
25
35
|
return `${major}.${minor}.${patch + 1}`
|
|
26
36
|
}
|
|
27
37
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Next beta version derived from the committed manifest version.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} current committed version (e.g. "1.5.0-beta.2")
|
|
42
|
+
* @returns {string} the next sequential beta (e.g. "1.5.0-beta.3")
|
|
43
|
+
*/
|
|
44
|
+
export function nextBetaVersion(current) {
|
|
45
|
+
const value = String(current).trim()
|
|
46
|
+
const line = BETA_LINE.exec(value)
|
|
47
|
+
if (line) return `${line[1]}-beta.${Number(line[2]) + 1}`
|
|
34
48
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const raw = readFileSync(path, 'utf8')
|
|
38
|
-
if (kind === 'toml') {
|
|
39
|
-
const updated = raw.replace(/^(version\s*=\s*")[^"]+("\s*)$/m, `$1${version}$2`)
|
|
40
|
-
if (updated === raw) throw new Error(`no version field in ${file}`)
|
|
41
|
-
writeFileSync(path, updated)
|
|
42
|
-
return
|
|
43
|
-
}
|
|
44
|
-
const content = JSON.parse(raw)
|
|
45
|
-
content.version = version
|
|
46
|
-
writeFileSync(path, `${JSON.stringify(content, null, 2)}\n`)
|
|
47
|
-
}
|
|
49
|
+
const stable = STABLE.exec(value)
|
|
50
|
+
if (stable) return `${nextStableVersion(value, 'patch')}-beta.1`
|
|
48
51
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const lock = JSON.parse(readFileSync(lockPath, 'utf8'))
|
|
54
|
-
lock.version = version
|
|
55
|
-
if (lock.packages?.['']) lock.packages[''].version = version
|
|
56
|
-
writeFileSync(lockPath, `${JSON.stringify(lock, null, 2)}\n`)
|
|
52
|
+
// Any other prerelease (e.g. 1.5.0-rc.1): beta the next patch of its base.
|
|
53
|
+
const base = /^(\d+\.\d+\.\d+)-[0-9A-Za-z.-]+$/.exec(value)
|
|
54
|
+
if (!base) throw new Error(`cannot derive a beta version from: "${current}"`)
|
|
55
|
+
return `${nextStableVersion(base[1], 'patch')}-beta.1`
|
|
57
56
|
}
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (args.has('latest')) {
|
|
67
|
-
try {
|
|
68
|
-
const version = betaVersion(
|
|
69
|
-
args.get('latest'),
|
|
70
|
-
args.get('intent') || 'patch',
|
|
71
|
-
args.get('pr'),
|
|
72
|
-
args.get('sha'),
|
|
58
|
+
function validateReleaseVersion(version) {
|
|
59
|
+
if (typeof version !== 'string' || !SEMVER_PATTERN.test(version)) {
|
|
60
|
+
throw new Error(`invalid release semver: "${version}"`)
|
|
61
|
+
}
|
|
62
|
+
if (!BETA_RELEASE.test(version) && !LEGACY_BETA.test(version)) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`release version must be <base>-beta.<N> or legacy <base>-beta.<pr>.<7-char-sha>: "${version}"`,
|
|
73
65
|
)
|
|
74
|
-
if (args.has('apply')) applyBetaVersion(process.cwd(), version)
|
|
75
|
-
console.log(version)
|
|
76
|
-
} catch (error) {
|
|
77
|
-
console.error(`release-beta-version: ${error.message}`)
|
|
78
|
-
process.exit(1)
|
|
79
66
|
}
|
|
80
67
|
}
|
|
68
|
+
|
|
69
|
+
/** Preflight all six entries, then update metadata only. */
|
|
70
|
+
export function applyBetaVersion(root, version) {
|
|
71
|
+
validateReleaseVersion(version)
|
|
72
|
+
const inventory = validateInventory(root, { allowVersionDivergence: true })
|
|
73
|
+
writeInventoryVersion(inventory, version)
|
|
74
|
+
const after = validateInventory(root)
|
|
75
|
+
if (after.sourceVersion !== version) throw new Error('beta version post-write validation failed')
|
|
76
|
+
}
|
|
@@ -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
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
* If the user hasn't initialized yet → do nothing (silent exit).
|
|
11
11
|
*/
|
|
12
12
|
import { execSync } from 'node:child_process'
|
|
13
|
-
import { existsSync, mkdirSync,
|
|
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, '..')
|
|
@@ -58,7 +58,8 @@ function resolveConfigDir() {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
|
-
* Copy plugin runtime files (dist/*, package.json,
|
|
61
|
+
* Copy plugin runtime files (dist/*, package.json, package-lock.json,
|
|
62
|
+
* src/index.tsx) from srcDir
|
|
62
63
|
* to dstDir. Skips files that are byte-identical.
|
|
63
64
|
* @returns {{ created: number, skipped: number }}
|
|
64
65
|
*/
|
|
@@ -94,55 +95,147 @@ function copyPluginFiles(srcDir, dstDir) {
|
|
|
94
95
|
else result.skipped++
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
// Keep the copied plugin installable with deterministic npm ci.
|
|
99
|
+
const lockSrc = join(srcDir, 'package-lock.json')
|
|
100
|
+
if (existsSync(lockSrc)) {
|
|
101
|
+
const content = readFileSync(lockSrc, 'utf8')
|
|
102
|
+
if (writeIfChanged(join(dstDir, 'package-lock.json'), content)) result.created++
|
|
103
|
+
else result.skipped++
|
|
104
|
+
}
|
|
105
|
+
|
|
97
106
|
return result
|
|
98
107
|
}
|
|
99
108
|
|
|
100
|
-
// ──
|
|
109
|
+
// ── npm env hygiene ────────────────────────────────────────────────────
|
|
101
110
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
+
])
|
|
108
124
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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]
|
|
113
135
|
}
|
|
136
|
+
return clean
|
|
137
|
+
}
|
|
114
138
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
+
}
|
|
121
148
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
+
}
|
|
125
163
|
|
|
126
|
-
|
|
127
|
-
|
|
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
|
+
}
|
|
128
169
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
//
|
|
134
|
-
|
|
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
|
+
}
|
|
135
193
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
)
|
|
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
|
|
143
228
|
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
+
})
|
|
148
241
|
}
|