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
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# dev-tui.sh — load the Pantheon TUI from the repo (src/plugins/tui) WITHOUT publishing.
|
|
3
|
+
#
|
|
4
|
+
# Flow: build the bundle → register the repo's absolute path in
|
|
5
|
+
# .opencode/tui.json (read by the OpenCode TUI loader) → restart hint.
|
|
6
|
+
# It deliberately does NOT touch opencode.json: that file is packaged and
|
|
7
|
+
# merged into user configs by the installer, so a repo-relative path would
|
|
8
|
+
# leak into third-party installs and break the CI package gates.
|
|
9
|
+
#
|
|
10
|
+
# Usage: scripts/dev-tui.sh [--test]
|
|
11
|
+
# --test also run the full repo test suite (npm run test:all) after build.
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
|
|
14
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
15
|
+
TUI_DIR="$ROOT/src/plugins/tui"
|
|
16
|
+
TUI_JSON="$ROOT/.opencode/tui.json"
|
|
17
|
+
|
|
18
|
+
command -v npm >/dev/null 2>&1 || { echo "error: npm not found in PATH" >&2; exit 1; }
|
|
19
|
+
[ -d "$TUI_DIR" ] || { echo "error: TUI source not found: $TUI_DIR" >&2; exit 1; }
|
|
20
|
+
|
|
21
|
+
echo "==> TUI dev source: $TUI_DIR"
|
|
22
|
+
|
|
23
|
+
# 1. Install pinned deps + build dist/tui.js (dist/tui.tsx is copied by build).
|
|
24
|
+
echo "==> Installing TUI dev dependencies (npm ci)"
|
|
25
|
+
npm ci --prefix "$TUI_DIR" --ignore-scripts --no-audit --no-fund
|
|
26
|
+
|
|
27
|
+
echo "==> Building TUI plugin"
|
|
28
|
+
npm run build --prefix "$TUI_DIR"
|
|
29
|
+
|
|
30
|
+
# 2. Optional: full suite to match the CI gate before opening a PR.
|
|
31
|
+
if [[ "${1:-}" == "--test" ]]; then
|
|
32
|
+
echo "==> Running full test suite (npm run test:all)"
|
|
33
|
+
npm run test:all
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# 3. Register the repo path in the project tui.json (absolute, gitignored).
|
|
37
|
+
# Reuses the installer's registerPlugin so the array is merged, not clobbered.
|
|
38
|
+
echo "==> Registering repo TUI path in $TUI_JSON"
|
|
39
|
+
ROOT="$ROOT" TUI_JSON="$TUI_JSON" TUI_DIR="$TUI_DIR" node --input-type=module <<'NODE'
|
|
40
|
+
const { registerPlugin } = await import(`${process.env.ROOT}/scripts/install/plugin.mjs`)
|
|
41
|
+
const result = registerPlugin(process.env.TUI_JSON, process.env.TUI_DIR)
|
|
42
|
+
console.log(`${result}: ${process.env.TUI_DIR}`)
|
|
43
|
+
NODE
|
|
44
|
+
|
|
45
|
+
# 4. Restart hint + dev-mode caveat.
|
|
46
|
+
echo "==> Done. Restart OpenCode to load the rebuilt TUI plugin."
|
|
47
|
+
echo " Dev mode caveat: do NOT run 'init' in this repo while iterating — it"
|
|
48
|
+
echo " removes repo path refs as stale. If you do, re-run scripts/dev-tui.sh."
|
package/scripts/doctor.mjs
CHANGED
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import { spawn as spawnAsync, spawnSync } from 'node:child_process'
|
|
25
|
-
import {
|
|
25
|
+
import { createHash } from 'node:crypto'
|
|
26
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
26
27
|
import { homedir } from 'node:os'
|
|
27
28
|
import { basename, dirname, join, resolve } from 'node:path'
|
|
28
29
|
import { fileURLToPath } from 'node:url'
|
|
@@ -389,7 +390,7 @@ function checkMcpConfig(args) {
|
|
|
389
390
|
// ENOENT-on-spawn failure class (dead hardcoded path, ambiguous relative
|
|
390
391
|
// path) before opencode tries to spawn the server.
|
|
391
392
|
for (const [name, mcpEntry] of Object.entries(mcp)) {
|
|
392
|
-
if (
|
|
393
|
+
if (mcpEntry?.type !== 'local') continue
|
|
393
394
|
const cmd = mcpEntry.command
|
|
394
395
|
if (!Array.isArray(cmd) || cmd.length === 0) {
|
|
395
396
|
warn(`${cfg.label}: MCP "${name}" has no command array`)
|
|
@@ -530,7 +531,7 @@ async function checkMcpRuntimeSmoke(args) {
|
|
|
530
531
|
const local = []
|
|
531
532
|
for (const cfg of collectMcpConfigs(args)) {
|
|
532
533
|
for (const [name, entry] of Object.entries(cfg.data.mcp ?? {})) {
|
|
533
|
-
if (
|
|
534
|
+
if (entry?.type !== 'local') continue
|
|
534
535
|
if (!Array.isArray(entry.command) || entry.command.length === 0) continue
|
|
535
536
|
const cwd = resolveMcpCwd(entry, cfg.path)
|
|
536
537
|
const command = entry.command.map((part, index) =>
|
|
@@ -958,6 +959,35 @@ function checkVenvLayer(args) {
|
|
|
958
959
|
// Check F.2: Code-Mode Scripts Directory
|
|
959
960
|
// ---------------------------------------------------------------------------
|
|
960
961
|
|
|
962
|
+
/**
|
|
963
|
+
* Return whether *dir* contains an executable code-mode target.
|
|
964
|
+
*
|
|
965
|
+
* Mirrors `_has_usable_scripts` in `scripts/code_mode_server.py`: a directory
|
|
966
|
+
* is only a valid resolver candidate when it is a real (non-symlink) directory
|
|
967
|
+
* containing a regular `.py`/`.sh` direct child. This prevents an empty project
|
|
968
|
+
* overlay from masking a lower-priority installation that actually ships
|
|
969
|
+
* scripts. Keep both implementations in sync (see tests/test_doctor_layers.mjs).
|
|
970
|
+
*/
|
|
971
|
+
function hasUsableScripts(dir) {
|
|
972
|
+
try {
|
|
973
|
+
if (!existsSync(dir)) return false
|
|
974
|
+
const stat = lstatSync(dir)
|
|
975
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) return false
|
|
976
|
+
return readdirSync(dir).some((name) => {
|
|
977
|
+
if (name.startsWith('.')) return false
|
|
978
|
+
const lower = name.toLowerCase()
|
|
979
|
+
if (!lower.endsWith('.py') && !lower.endsWith('.sh')) return false
|
|
980
|
+
try {
|
|
981
|
+
return statSync(join(dir, name)).isFile()
|
|
982
|
+
} catch {
|
|
983
|
+
return false
|
|
984
|
+
}
|
|
985
|
+
})
|
|
986
|
+
} catch {
|
|
987
|
+
return false
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
961
991
|
/**
|
|
962
992
|
* Verify the code-mode scripts directory exists for the resolved runtime
|
|
963
993
|
* root (project → <target>/.opencode, otherwise the global config dir).
|
|
@@ -966,12 +996,20 @@ function checkVenvLayer(args) {
|
|
|
966
996
|
*/
|
|
967
997
|
export function checkCodeModeDir(args) {
|
|
968
998
|
section('F.2 Code-Mode Scripts')
|
|
969
|
-
const
|
|
970
|
-
const
|
|
971
|
-
const
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
999
|
+
const env = args.env ?? process.env
|
|
1000
|
+
const target = resolve(env.PANTHEON_PROJECT ?? args.target)
|
|
1001
|
+
const projectCandidates = [
|
|
1002
|
+
join(target, '.opencode', '.pantheon', 'code-mode'),
|
|
1003
|
+
join(target, '.pantheon', 'code-mode'),
|
|
1004
|
+
]
|
|
1005
|
+
// Seeding semantics: an existing project candidate wins; a project
|
|
1006
|
+
// install (.opencode present) gets its runtime dir created when missing.
|
|
1007
|
+
// Only targets with no project layout fall back to the global config dir.
|
|
1008
|
+
const codeModeDir =
|
|
1009
|
+
projectCandidates.find(hasUsableScripts) ??
|
|
1010
|
+
(existsSync(join(target, '.opencode'))
|
|
1011
|
+
? projectCandidates[0]
|
|
1012
|
+
: join(resolveOpenCodeConfigDir(env), '.pantheon', 'code-mode'))
|
|
975
1013
|
if (existsSync(codeModeDir)) {
|
|
976
1014
|
pass(`Code-mode scripts directory exists: ${codeModeDir}`)
|
|
977
1015
|
return codeModeDir
|
|
@@ -986,6 +1024,83 @@ export function checkCodeModeDir(args) {
|
|
|
986
1024
|
return codeModeDir
|
|
987
1025
|
}
|
|
988
1026
|
|
|
1027
|
+
/** Resolve code-mode using the same project-first rule as the MCP server. */
|
|
1028
|
+
export function resolveCodeModeDir(args) {
|
|
1029
|
+
const env = args.env ?? process.env
|
|
1030
|
+
const target = resolve(env.PANTHEON_PROJECT ?? args.target)
|
|
1031
|
+
const projectCandidates = [
|
|
1032
|
+
join(target, '.opencode', '.pantheon', 'code-mode'),
|
|
1033
|
+
join(target, '.pantheon', 'code-mode'),
|
|
1034
|
+
]
|
|
1035
|
+
const projectDir = projectCandidates.find(hasUsableScripts)
|
|
1036
|
+
if (projectDir) return projectDir
|
|
1037
|
+
return join(resolveOpenCodeConfigDir(env), '.pantheon', 'code-mode')
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/** Validate a code-mode manifest without modifying it. */
|
|
1041
|
+
export function validateCodeModeManifest(codeModeDir) {
|
|
1042
|
+
const manifestPath = join(codeModeDir, 'manifest.json')
|
|
1043
|
+
if (!existsSync(manifestPath)) {
|
|
1044
|
+
return { ok: false, message: `Code-mode manifest missing: ${manifestPath}` }
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
let data
|
|
1048
|
+
try {
|
|
1049
|
+
data = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
|
1050
|
+
} catch {
|
|
1051
|
+
return { ok: false, message: `Code-mode manifest malformed: ${manifestPath}` }
|
|
1052
|
+
}
|
|
1053
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
1054
|
+
return { ok: false, message: 'Code-mode manifest malformed: root must be an object' }
|
|
1055
|
+
}
|
|
1056
|
+
if (data.version !== 1) {
|
|
1057
|
+
return { ok: false, message: 'Code-mode manifest malformed: version must be 1' }
|
|
1058
|
+
}
|
|
1059
|
+
if (!data.scripts || typeof data.scripts !== 'object' || Array.isArray(data.scripts)) {
|
|
1060
|
+
return { ok: false, message: 'Code-mode manifest malformed: scripts must be an object' }
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
const listed = Object.keys(data.scripts)
|
|
1064
|
+
const invalid = listed.filter(
|
|
1065
|
+
(name) => typeof data.scripts[name] !== 'string' || !/^[a-f0-9]{64}$/i.test(data.scripts[name]),
|
|
1066
|
+
)
|
|
1067
|
+
if (invalid.length) {
|
|
1068
|
+
return { ok: false, message: `Code-mode manifest invalid digest: ${invalid.join(', ')}` }
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const actualScripts = readdirSync(codeModeDir).filter(
|
|
1072
|
+
(name) =>
|
|
1073
|
+
(name.endsWith('.py') || name.endsWith('.sh')) && statSync(join(codeModeDir, name)).isFile(),
|
|
1074
|
+
)
|
|
1075
|
+
const missing = listed.filter((name) => !existsSync(join(codeModeDir, name)))
|
|
1076
|
+
const extra = actualScripts.filter((name) => !Object.hasOwn(data.scripts, name))
|
|
1077
|
+
const differing = listed.filter((name) => {
|
|
1078
|
+
const path = join(codeModeDir, name)
|
|
1079
|
+
if (!existsSync(path)) return false
|
|
1080
|
+
const digest = createHash('sha256').update(readFileSync(path)).digest('hex')
|
|
1081
|
+
return digest !== data.scripts[name].toLowerCase()
|
|
1082
|
+
})
|
|
1083
|
+
if (missing.length || extra.length || differing.length) {
|
|
1084
|
+
const names = [...new Set([...missing, ...extra, ...differing])]
|
|
1085
|
+
return {
|
|
1086
|
+
ok: false,
|
|
1087
|
+
message: `Code-mode manifest mismatch: ${differing.length + missing.length + extra.length}/${listed.length} scripts differ — ${names.join(', ')}`,
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
return { ok: true, count: listed.length, total: actualScripts.length }
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/** Run the diagnostic manifest check after directory validation. */
|
|
1094
|
+
export function checkCodeModeManifest(args) {
|
|
1095
|
+
const codeModeDir = resolveCodeModeDir(args)
|
|
1096
|
+
const result = validateCodeModeManifest(codeModeDir)
|
|
1097
|
+
if (!result.ok) {
|
|
1098
|
+
error(result.message)
|
|
1099
|
+
return
|
|
1100
|
+
}
|
|
1101
|
+
pass(`✅ Code-mode manifest valid: ${result.count}/${result.total} scripts verified`)
|
|
1102
|
+
}
|
|
1103
|
+
|
|
989
1104
|
/** Resolve the venv created by init for either a project or global install. */
|
|
990
1105
|
export function resolveRuntimePython(args) {
|
|
991
1106
|
const target = resolve(args.target)
|
|
@@ -1018,6 +1133,87 @@ export function resolveMcpCwd(entry, configPath = '') {
|
|
|
1018
1133
|
return dirname(configPath)
|
|
1019
1134
|
}
|
|
1020
1135
|
|
|
1136
|
+
// ---------------------------------------------------------------------------
|
|
1137
|
+
// Check H2: installed-version drift (beta.5)
|
|
1138
|
+
// ---------------------------------------------------------------------------
|
|
1139
|
+
|
|
1140
|
+
/**
|
|
1141
|
+
* Prerelease-aware comparison for pantheon versions (X.Y.Z or X.Y.Z-beta.N).
|
|
1142
|
+
* A stable release outranks any beta of the same core version. Returns
|
|
1143
|
+
* <0, 0, >0 (a - b). The shared compareVersions/parseVersion above ignores
|
|
1144
|
+
* the `-beta.N` suffix (it only reads 4 numeric groups), which would make
|
|
1145
|
+
* every beta pair compare equal.
|
|
1146
|
+
*/
|
|
1147
|
+
function comparePantheonVersions(a, b) {
|
|
1148
|
+
const parse = (v) => {
|
|
1149
|
+
const m = String(v)
|
|
1150
|
+
.trim()
|
|
1151
|
+
.match(/^(\d+)\.(\d+)\.(\d+)(?:-beta\.(\d+))?$/)
|
|
1152
|
+
if (!m) return null
|
|
1153
|
+
return {
|
|
1154
|
+
core: [+m[1], +m[2], +m[3]],
|
|
1155
|
+
beta: m[4] === undefined ? Number.POSITIVE_INFINITY : +m[4],
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
const pa = parse(a)
|
|
1159
|
+
const pb = parse(b)
|
|
1160
|
+
if (!pa || !pb) return 0
|
|
1161
|
+
for (let i = 0; i < 3; i++) {
|
|
1162
|
+
if (pa.core[i] !== pb.core[i]) return pa.core[i] - pb.core[i]
|
|
1163
|
+
}
|
|
1164
|
+
return pa.beta - pb.beta
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
/**
|
|
1168
|
+
* Compare the version recorded in the installation marker
|
|
1169
|
+
* (<config>/.pantheon/install-state.json, written by the postinstall sync)
|
|
1170
|
+
* against this package's version. When the package is newer, the config
|
|
1171
|
+
* merge/venv/MCP wiring may be stale — point the user at `update`.
|
|
1172
|
+
* Advisory only: a missing marker (pre-beta.5 install) is not an error.
|
|
1173
|
+
*/
|
|
1174
|
+
export function checkInstallVersionDrift(args) {
|
|
1175
|
+
section('H2 Installed Version')
|
|
1176
|
+
const env = args.env ?? process.env
|
|
1177
|
+
let pkgVersion = null
|
|
1178
|
+
try {
|
|
1179
|
+
pkgVersion = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')).version
|
|
1180
|
+
} catch {
|
|
1181
|
+
return 'no-marker'
|
|
1182
|
+
}
|
|
1183
|
+
if (!pkgVersion) return 'no-marker'
|
|
1184
|
+
|
|
1185
|
+
const statePath = join(resolveOpenCodeConfigDir(env), '.pantheon', 'install-state.json')
|
|
1186
|
+
let installed = null
|
|
1187
|
+
if (existsSync(statePath)) {
|
|
1188
|
+
try {
|
|
1189
|
+
installed = JSON.parse(readFileSync(statePath, 'utf8')).pantheon_version ?? null
|
|
1190
|
+
} catch {
|
|
1191
|
+
installed = null
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
if (!installed) {
|
|
1195
|
+
info('No installation version marker found (pre-beta.5 install) — skipping drift check')
|
|
1196
|
+
return 'no-marker'
|
|
1197
|
+
}
|
|
1198
|
+
const drift = comparePantheonVersions(pkgVersion, installed)
|
|
1199
|
+
if (drift > 0) {
|
|
1200
|
+
warn(
|
|
1201
|
+
`Package v${pkgVersion} is newer than the last-synced installation (v${installed}) — ` +
|
|
1202
|
+
'run `npx pantheon-opencode update` (or re-run init) to refresh config, venv and MCP entries',
|
|
1203
|
+
)
|
|
1204
|
+
return 'package-newer'
|
|
1205
|
+
}
|
|
1206
|
+
if (drift < 0) {
|
|
1207
|
+
warn(
|
|
1208
|
+
`Installation marker (v${installed}) is newer than this package (v${pkgVersion}) — ` +
|
|
1209
|
+
'the doctor is running from a different install than the last sync',
|
|
1210
|
+
)
|
|
1211
|
+
return 'install-newer'
|
|
1212
|
+
}
|
|
1213
|
+
pass(`Installation in sync with package v${pkgVersion}`)
|
|
1214
|
+
return 'sync'
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1021
1217
|
// ---------------------------------------------------------------------------
|
|
1022
1218
|
// Check G: AGENTS.md freshness
|
|
1023
1219
|
// ---------------------------------------------------------------------------
|
|
@@ -1314,6 +1510,8 @@ async function main() {
|
|
|
1314
1510
|
checkMcpConfig(args)
|
|
1315
1511
|
checkVenvLayer(args)
|
|
1316
1512
|
checkCodeModeDir(args)
|
|
1513
|
+
checkCodeModeManifest(args)
|
|
1514
|
+
checkInstallVersionDrift(args)
|
|
1317
1515
|
await checkMcpRuntimeSmoke(args)
|
|
1318
1516
|
checkPermissionMismatches(args)
|
|
1319
1517
|
checkSyncStatus(args)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { strings } from './strings.mjs'
|
|
2
|
+
|
|
1
3
|
export const icons = {
|
|
2
4
|
success: '\u2705',
|
|
3
5
|
warning: '\u26a0\ufe0f ',
|
|
@@ -8,9 +10,9 @@ export const icons = {
|
|
|
8
10
|
arrow: '\u2192',
|
|
9
11
|
bullet: '\u2022',
|
|
10
12
|
star: '\u2b50',
|
|
11
|
-
wrench: '\
|
|
12
|
-
rocket: '\
|
|
13
|
-
package: '\
|
|
13
|
+
wrench: '\u{1F527}',
|
|
14
|
+
rocket: '\u{1F680}',
|
|
15
|
+
package: '\u{1F4E6}',
|
|
14
16
|
gear: '\u2699\ufe0f',
|
|
15
17
|
}
|
|
16
18
|
|
|
@@ -160,28 +162,32 @@ export function spinner(message) {
|
|
|
160
162
|
export function printSummary(target, platforms, stats) {
|
|
161
163
|
if (_quiet) return
|
|
162
164
|
|
|
165
|
+
const S = strings()
|
|
163
166
|
const line = '\u2500'.repeat(Math.min(process.stdout.columns || 60, 60))
|
|
164
167
|
const hasErrors = stats && stats.errors > 0
|
|
165
168
|
console.log(
|
|
166
169
|
tag(
|
|
167
170
|
hasErrors
|
|
168
|
-
? `${icons.warning}
|
|
169
|
-
: `${icons.rocket}
|
|
171
|
+
? `${icons.warning} ${S.summaryWithErrors(target)}`
|
|
172
|
+
: `${icons.rocket} ${S.summaryInstalled(target)}`,
|
|
170
173
|
),
|
|
171
174
|
)
|
|
172
175
|
console.log(tag(line))
|
|
173
176
|
console.log('')
|
|
174
177
|
|
|
175
178
|
if (stats) {
|
|
176
|
-
console.log(tag(
|
|
179
|
+
console.log(tag(S.summaryComponents(stats.created, stats.skipped)))
|
|
177
180
|
if (stats.errors > 0) {
|
|
178
|
-
console.warn(tag(
|
|
181
|
+
console.warn(tag(S.summaryErrors(stats.errors)))
|
|
182
|
+
}
|
|
183
|
+
if (stats.warnings > 0) {
|
|
184
|
+
console.warn(tag(S.summaryWarnings(stats.warnings)))
|
|
179
185
|
}
|
|
180
186
|
console.log('')
|
|
181
187
|
}
|
|
182
188
|
|
|
183
|
-
console.log(tag(` ${icons.star}
|
|
184
|
-
console.log(tag(
|
|
185
|
-
console.log(tag(
|
|
186
|
-
console.log(tag(
|
|
189
|
+
console.log(tag(` ${icons.star} ${S.summaryNext.trim()}`))
|
|
190
|
+
console.log(tag(S.summaryNextAgents))
|
|
191
|
+
console.log(tag(S.summaryNextMcp))
|
|
192
|
+
console.log(tag(S.summaryNextDoctor))
|
|
187
193
|
}
|
|
@@ -73,7 +73,7 @@ function convertPermissionsV2toV1(permArray) {
|
|
|
73
73
|
const result = {}
|
|
74
74
|
// Reverse action mapping: V2 action → V1 key
|
|
75
75
|
const ACTION_TO_V1 = Object.fromEntries(
|
|
76
|
-
Object.entries(V1_PERMISSION_TO_ACTION).map(([k, v]) => [v, k])
|
|
76
|
+
Object.entries(V1_PERMISSION_TO_ACTION).map(([k, v]) => [v, k]),
|
|
77
77
|
)
|
|
78
78
|
|
|
79
79
|
for (const { action, resource, effect } of permArray) {
|
|
@@ -104,7 +104,7 @@ const PROVIDER_RENAME = {
|
|
|
104
104
|
|
|
105
105
|
/** Reverse: V2 → V1 */
|
|
106
106
|
const PROVIDER_RENAME_REVERSE = Object.fromEntries(
|
|
107
|
-
Object.entries(PROVIDER_RENAME).map(([k, v]) => [v, k])
|
|
107
|
+
Object.entries(PROVIDER_RENAME).map(([k, v]) => [v, k]),
|
|
108
108
|
)
|
|
109
109
|
|
|
110
110
|
/**
|
|
@@ -176,9 +176,7 @@ function convertProviderV2toV1(name, config) {
|
|
|
176
176
|
|
|
177
177
|
// package → npm (strip aisdk: prefix)
|
|
178
178
|
if (result.package !== undefined) {
|
|
179
|
-
result.npm = result.package.startsWith('aisdk:')
|
|
180
|
-
? result.package.slice(6)
|
|
181
|
-
: result.package
|
|
179
|
+
result.npm = result.package.startsWith('aisdk:') ? result.package.slice(6) : result.package
|
|
182
180
|
delete result.package
|
|
183
181
|
}
|
|
184
182
|
|
|
@@ -303,7 +301,7 @@ const V1_TO_V2_RENAMES = {
|
|
|
303
301
|
|
|
304
302
|
/** Build reverse map */
|
|
305
303
|
const V2_TO_V1_RENAMES = Object.fromEntries(
|
|
306
|
-
Object.entries(V1_TO_V2_RENAMES).map(([k, v]) => [v, k])
|
|
304
|
+
Object.entries(V1_TO_V2_RENAMES).map(([k, v]) => [v, k]),
|
|
307
305
|
)
|
|
308
306
|
|
|
309
307
|
// ---------------------------------------------------------------------------
|