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.
Files changed (118) hide show
  1. package/.pantheon/code-mode/README.md +93 -0
  2. package/.pantheon/code-mode/checkpoint-session.py +7 -0
  3. package/.pantheon/code-mode/checkpoint-session.sh +11 -0
  4. package/.pantheon/code-mode/checkpoint_session.py +481 -0
  5. package/.pantheon/code-mode/compress-inline.py +450 -0
  6. package/.pantheon/code-mode/eval-llm-judge.py +255 -0
  7. package/.pantheon/code-mode/eval-monte-carlo.py +368 -0
  8. package/.pantheon/code-mode/eval-run.py +197 -0
  9. package/.pantheon/code-mode/eval-static.py +321 -0
  10. package/.pantheon/code-mode/example-sync.sh +5 -0
  11. package/.pantheon/code-mode/manifest.json +21 -0
  12. package/.pantheon/code-mode/session-end-save.py +186 -0
  13. package/.pantheon/code-mode/session-end-save.sh +18 -0
  14. package/.pantheon/code-mode/test-allowed.sh +5 -0
  15. package/.pantheon/code-mode/test-auto-continue-checkpoint.sh +179 -0
  16. package/.pantheon/code-mode/test-env.sh +7 -0
  17. package/.pantheon/code-mode/test-prlimit.sh +9 -0
  18. package/.pantheon/code-mode/test-timeout.sh +6 -0
  19. package/CHANGELOG.md +1026 -0
  20. package/README.md +292 -1080
  21. package/README.pt-BR.md +308 -0
  22. package/bin/pantheon-init.mjs +207 -31
  23. package/opencode.json +1 -0
  24. package/package.json +34 -14
  25. package/plugin.json +15 -0
  26. package/pyproject.toml +65 -0
  27. package/scripts/_pantheon_paths.py +48 -7
  28. package/scripts/ci-validate-yaml.py +7 -2
  29. package/scripts/code_mode_server.py +451 -44
  30. package/scripts/cost.mjs +30 -14
  31. package/scripts/doctor.mjs +306 -129
  32. package/scripts/generate-preset-docs.mjs +127 -66
  33. package/scripts/generate-prompts.sh +2 -2
  34. package/scripts/install/cli-ui.mjs +23 -10
  35. package/scripts/install/config-migration.mjs +399 -0
  36. package/scripts/install/health-check.mjs +3 -3
  37. package/scripts/install/model-picker.mjs +72 -23
  38. package/scripts/install/opencode-version.mjs +47 -0
  39. package/scripts/install/opencode.mjs +396 -121
  40. package/scripts/install/plugin.mjs +89 -49
  41. package/scripts/install/shared.mjs +55 -159
  42. package/scripts/install/strings.mjs +129 -0
  43. package/scripts/install-mcp.mjs +45 -232
  44. package/scripts/manifest-inventory.mjs +410 -0
  45. package/scripts/mcp_persistence_server.py +798 -60
  46. package/scripts/mcp_resources_server.py +144 -40
  47. package/scripts/memory_mcp_server.py +2 -2
  48. package/scripts/package-evidence.mjs +242 -0
  49. package/scripts/probe-context-rehydrate.mjs +240 -0
  50. package/scripts/probe-pantheon-cost.mjs +313 -0
  51. package/scripts/recover-zenodo-deposition.mjs +146 -0
  52. package/scripts/release-beta-version.mjs +56 -60
  53. package/scripts/sync-artifacts.mjs +169 -0
  54. package/scripts/sync-tui.mjs +135 -42
  55. package/scripts/test-opencode-v1-v2-sandbox.sh +862 -0
  56. package/scripts/uninstall.mjs +298 -954
  57. package/scripts/validate-zenodo-release.mjs +154 -0
  58. package/scripts/validation-policy.mjs +7 -7
  59. package/scripts/version-check.mjs +113 -108
  60. package/scripts/versioning.mjs +230 -86
  61. package/scripts/zenodo-artifact.mjs +21 -0
  62. package/scripts/zenodo-release-state.mjs +53 -0
  63. package/src/agents/mnemosyne.md +1 -1
  64. package/src/agents/zeus.md +27 -16
  65. package/src/mcp/_pantheon_paths.py +48 -7
  66. package/src/mcp/code_mode_server.py +451 -44
  67. package/src/mcp/eval_store.py +1 -1
  68. package/src/mcp/mcp_persistence_server.py +798 -60
  69. package/src/mcp/mcp_resources_server.py +144 -40
  70. package/src/mcp/pantheon_vision_server.py +1 -1
  71. package/src/mcp/toon_codec.py +491 -0
  72. package/src/pantheon/auto-wake.ts +1 -5
  73. package/src/pantheon/background-job-board.ts +110 -2
  74. package/src/pantheon/compaction-handoff.ts +304 -0
  75. package/src/pantheon/cost-command.ts +356 -69
  76. package/src/pantheon/delegate-manager.ts +766 -0
  77. package/src/pantheon/delegation-classifier.ts +307 -0
  78. package/src/pantheon/delegation-enforce.ts +13 -12
  79. package/src/pantheon/delegation-finalize.ts +406 -0
  80. package/src/pantheon/delegation-notify.ts +212 -0
  81. package/src/pantheon/delegation-toast.ts +73 -0
  82. package/src/pantheon/delegation.ts +1337 -0
  83. package/src/pantheon/file-persistence-salvage.ts +64 -0
  84. package/src/pantheon/file-persistence.ts +194 -45
  85. package/src/pantheon/goal-loop.ts +2 -2
  86. package/src/pantheon/goal-store.ts +1 -1
  87. package/src/pantheon/hashline/tool.ts +2 -2
  88. package/src/pantheon/model-command.ts +2 -1
  89. package/src/pantheon/native-probe.ts +234 -0
  90. package/src/pantheon/native-task-status.ts +139 -0
  91. package/src/pantheon/permission-eval.ts +284 -0
  92. package/src/pantheon/provider-capabilities.ts +221 -0
  93. package/src/pantheon/provider-stream-tracker.ts +182 -0
  94. package/src/pantheon/shared-board.ts +37 -0
  95. package/src/pantheon/step-cap.ts +110 -0
  96. package/src/pantheon/task-result-guard.ts +107 -0
  97. package/src/pantheon/token-opt.ts +689 -0
  98. package/src/pantheon/tool-ceiling.ts +159 -0
  99. package/src/pantheon/toon-codec.ts +481 -0
  100. package/src/pantheon/tui-version-check.ts +1 -3
  101. package/src/pantheon/v2-bridge.ts +129 -0
  102. package/src/pantheon/v2-events.ts +148 -0
  103. package/src/pantheon/v2-hooks.ts +287 -0
  104. package/src/pantheon/v2-tool-definitions.ts +331 -0
  105. package/src/pantheon/zeus-delegate-with-retry.ts +406 -0
  106. package/src/plugin-v2/index.ts +21 -0
  107. package/src/plugin-v2.ts +824 -0
  108. package/src/plugin.ts +261 -51
  109. package/src/plugins/pantheon-hooks.ts +51 -0
  110. package/src/plugins/tui/dist/tui.d.ts +116 -24
  111. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  112. package/src/plugins/tui/dist/tui.js +416 -149
  113. package/src/plugins/tui/dist/tui.tsx +456 -87
  114. package/src/plugins/tui/package-lock.json +51 -51
  115. package/src/plugins/tui/package.json +7 -7
  116. package/src/plugins/tui/src/index.tsx +462 -95
  117. package/src/skills/README.md +0 -2
  118. package/src/pantheon/tool-context.ts +0 -41
@@ -4,13 +4,12 @@
4
4
 
5
5
  import { execSync } from 'node:child_process'
6
6
  import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:fs'
7
- import { join } from 'node:path'
8
- import { warning } from './cli-ui.mjs'
9
- import { writeIfChanged } from './shared.mjs'
7
+ import { isAbsolute, join, resolve } from 'node:path'
8
+ import { ROOT, writeIfChanged } from './shared.mjs'
10
9
 
11
10
  /**
12
11
  * Copy a plugin's runtime payload (src/index.tsx → index.tsx, dist/*,
13
- * package.json) from source to destination. Pure file copy — no npm install.
12
+ * package.json and package-lock.json) from source to destination.
14
13
  * @param {string} srcDir - Source plugin directory (e.g. ROOT/src/plugins/tui)
15
14
  * @param {string} dstDir - Destination plugin directory
16
15
  * @param {object} [options]
@@ -58,6 +57,19 @@ export function copyPluginFiles(srcDir, dstDir, { dryRun = false } = {}) {
58
57
  else result.skipped++
59
58
  }
60
59
 
60
+ // Copy the lockfile so dependency installation is reproducible. A missing
61
+ // lockfile is intentionally not synthesized by the installer.
62
+ const lockSrc = join(srcDir, 'package-lock.json')
63
+ if (existsSync(lockSrc)) {
64
+ const lockStatus = writeIfChanged(
65
+ join(dstDir, 'package-lock.json'),
66
+ readFileSync(lockSrc, 'utf8'),
67
+ dryRun,
68
+ )
69
+ if (lockStatus === 'created') result.created++
70
+ else result.skipped++
71
+ }
72
+
61
73
  return result
62
74
  }
63
75
 
@@ -81,14 +93,10 @@ export function installPlugin(srcDir, dstDir, { dryRun = false, clean = false }
81
93
  result.created += copyResult.created
82
94
  result.skipped += copyResult.skipped
83
95
 
84
- // Install plugin dependencies (@opentui/core, @opentui/solid, solid-js)
96
+ // Install plugin dependencies (@opentui/core, @opentui/solid, solid-js).
97
+ // Lockfile installation is the only accepted dependency path.
85
98
  if (!dryRun) {
86
- try {
87
- execSync('npm install --omit=dev --no-audit', { cwd: dstDir, stdio: 'pipe' })
88
- } catch (e) {
89
- warning(`Failed to install TUI plugin dependencies: ${e.message}`)
90
- result.errors++
91
- }
99
+ execSync('npm ci --omit=dev --no-audit --no-fund', { cwd: dstDir, stdio: 'pipe' })
92
100
  }
93
101
 
94
102
  return result
@@ -102,11 +110,7 @@ export function installPlugin(srcDir, dstDir, { dryRun = false, clean = false }
102
110
  */
103
111
  export function installPluginDependencies(pluginDir, { dryRun = false } = {}) {
104
112
  if (dryRun) return
105
- try {
106
- execSync('npm install --omit=dev --no-audit', { cwd: pluginDir, stdio: 'pipe' })
107
- } catch (e) {
108
- warning(`Failed to install plugin dependencies: ${e.message}`)
109
- }
113
+ execSync('npm ci --omit=dev --no-audit --no-fund', { cwd: pluginDir, stdio: 'pipe' })
110
114
  }
111
115
 
112
116
  /**
@@ -137,42 +141,70 @@ export function registerPlugin(tuiJsonPath, pluginId, { dryRun = false } = {}) {
137
141
  }
138
142
 
139
143
  /**
140
- * Suffix patterns that identify a Pantheon TUI registration as STALE. Every
141
- * reference shape ever written by previous installers must be covered:
142
- * - dist file refs (both the old in-package src/plugins/tui target and the
143
- * copied plugins/pantheon-tui target, tsx and js eras)
144
- * - the in-package source dir (src/plugins/tui) — a registration pointing
145
- * into the installed package breaks on reinstall/upgrade (the dir is
146
- * replaced under a live registration)
147
- * - bare copied dirs (plugins/pantheon-tui) from any OLD install target —
148
- * indistinguishable from the current copied dir without knowing the
149
- * install target, so they are flagged too; syncTuiRegistration keeps the
150
- * current ref via unregister-then-register
151
- * - node_modules copies (npx cache installs of the vendored plugin)
144
+ * Exact relative markers written by previous installers. These are intentionally
145
+ * complete references, not suffixes: a user-owned path may contain
146
+ * `pantheon-opencode` or end in the same filename without being ours.
152
147
  */
153
148
  export const TUI_STALE_SUFFIXES = [
154
- '/src/plugins/tui/dist/tui.tsx',
155
- '/src/plugins/tui/dist/tui.js',
156
- '/src/plugins/tui',
157
- '/plugins/pantheon-tui/dist/tui.tsx',
158
- '/plugins/pantheon-tui/dist/tui.js',
159
- '/plugins/pantheon-tui',
160
- '/node_modules/pantheon-tui',
149
+ 'plugins/pantheon-tui',
150
+ 'plugins/pantheon-tui/dist/tui.tsx',
151
+ 'plugins/pantheon-tui/dist/tui.js',
152
+ 'npx pantheon-tui',
153
+ 'npx -y pantheon-tui',
154
+ 'npx:pantheon-tui',
155
+ ]
156
+
157
+ const PACKAGE_TUI_PATHS = [
158
+ join(ROOT, 'src', 'plugins', 'tui'),
159
+ join(ROOT, 'src', 'plugins', 'tui', 'dist', 'tui.tsx'),
160
+ join(ROOT, 'src', 'plugins', 'tui', 'dist', 'tui.js'),
161
161
  ]
162
162
 
163
+ function normalizePluginPath(ref) {
164
+ return ref.replaceAll('\\', '/')
165
+ }
166
+
167
+ function exactManagedPaths(knownPantheonPaths) {
168
+ return new Set(
169
+ [...PACKAGE_TUI_PATHS, ...knownPantheonPaths]
170
+ .filter((path) => typeof path === 'string')
171
+ .map((path) => normalizePluginPath(path)),
172
+ )
173
+ }
174
+
175
+ function isExactNpxMarker(ref) {
176
+ return (
177
+ /^npx(?:\s+-y)?\s+pantheon-tui(?:@[A-Za-z0-9._-]+)?$/i.test(ref) ||
178
+ /^npx:pantheon-tui(?:@[A-Za-z0-9._-]+)?$/i.test(ref) ||
179
+ /(?:^|\/)\.npm\/_npx\/[^/]+\/node_modules\/pantheon-tui(?:\/|$)/i.test(ref)
180
+ )
181
+ }
182
+
163
183
  /**
164
- * Whether a single tui.json plugin entry is a stale Pantheon TUI reference.
184
+ * Return whether a registration is unambiguously one of our old TUI refs.
185
+ *
165
186
  * @param {unknown} ref - tui.json plugin entry
187
+ * @param {string[]} [knownPantheonPaths=[]] - paths owned by this installer
166
188
  * @returns {boolean}
167
189
  */
168
- export function isStaleTuiRef(ref) {
190
+ export function isPantheonTuiRef(ref, knownPantheonPaths = []) {
169
191
  if (typeof ref !== 'string') return false
170
- if (ref === 'plugins/pantheon-tui') return true
171
- const normalized = ref.replaceAll('\\', '/')
172
- // npx spec strings ("npx pantheon-tui", "npx -y pantheon-tui",
173
- // "npx:pantheon-tui") and npx cache paths end with /node_modules/pantheon-tui.
174
- if (/^npx[\s:]/i.test(normalized) && normalized.includes('pantheon-tui')) return true
175
- return TUI_STALE_SUFFIXES.some((suffix) => normalized.endsWith(suffix))
192
+ const normalized = normalizePluginPath(ref)
193
+ if (TUI_STALE_SUFFIXES.includes(normalized)) return true
194
+ if (isExactNpxMarker(normalized)) return true
195
+
196
+ const knownPaths = exactManagedPaths(knownPantheonPaths)
197
+ if (knownPaths.has(normalized)) return true
198
+ return isAbsolute(normalized) && knownPaths.has(normalizePluginPath(resolve(normalized)))
199
+ }
200
+
201
+ /**
202
+ * Whether a single tui.json plugin entry is a stale Pantheon TUI reference.
203
+ * @param {unknown} ref - tui.json plugin entry
204
+ * @returns {boolean}
205
+ */
206
+ export function isStaleTuiRef(ref, knownPantheonPaths = []) {
207
+ return isPantheonTuiRef(ref, knownPantheonPaths)
176
208
  }
177
209
 
178
210
  /**
@@ -181,8 +213,10 @@ export function isStaleTuiRef(ref) {
181
213
  * @param {unknown[]} pluginArray - tui.json plugin array
182
214
  * @returns {string[]}
183
215
  */
184
- export function staleTuiRefs(pluginArray) {
185
- return (Array.isArray(pluginArray) ? pluginArray : []).filter(isStaleTuiRef)
216
+ export function staleTuiRefs(pluginArray, knownPantheonPaths = []) {
217
+ return (Array.isArray(pluginArray) ? pluginArray : []).filter((ref) =>
218
+ isStaleTuiRef(ref, knownPantheonPaths),
219
+ )
186
220
  }
187
221
 
188
222
  /**
@@ -193,7 +227,11 @@ export function staleTuiRefs(pluginArray) {
193
227
  * @param {boolean} [options.dryRun=false]
194
228
  * @returns {'created'|'skipped'}
195
229
  */
196
- export function unregisterPlugin(tuiJsonPath, pluginId, { dryRun = false } = {}) {
230
+ export function unregisterPlugin(
231
+ tuiJsonPath,
232
+ pluginId,
233
+ { dryRun = false, knownPantheonPaths = [] } = {},
234
+ ) {
197
235
  if (!existsSync(tuiJsonPath)) {
198
236
  // Nothing to clean — and never create an empty tui.json in a location
199
237
  // OpenCode reads (the cross-location cleanup also touches global config
@@ -215,10 +253,12 @@ export function unregisterPlugin(tuiJsonPath, pluginId, { dryRun = false } = {})
215
253
  // bare copy dirs from previous install targets, and npx cache copies.
216
254
  // Without removing the absolute paths, each init run accumulates another
217
255
  // TUI plugin and OpenCode loads stale copies.
218
- const exactStale = [pluginId, `${pluginId}/dist/tui.tsx`, `${pluginId}/dist/tui.js`]
256
+ const exactStale = new Set(
257
+ [pluginId, `${pluginId}/dist/tui.tsx`, `${pluginId}/dist/tui.js`].map(normalizePluginPath),
258
+ )
219
259
  tuiConfig.plugin = tuiConfig.plugin.filter((ref) => {
220
- if (exactStale.includes(ref)) return false
221
- return !isStaleTuiRef(ref)
260
+ if (typeof ref === 'string' && exactStale.has(normalizePluginPath(ref))) return false
261
+ return !isPantheonTuiRef(ref, knownPantheonPaths)
222
262
  })
223
263
  const tuiContent = `${JSON.stringify(tuiConfig, null, 2)}\n`
224
264
  return writeIfChanged(tuiJsonPath, tuiContent, dryRun)
@@ -1,13 +1,16 @@
1
1
  /**
2
- * shared.mjs — Shared utilities for Pantheon platform installers
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,
@@ -26,8 +29,6 @@ import yaml from 'js-yaml'
26
29
  export const __dirname = dirname(fileURLToPath(import.meta.url))
27
30
  export const ROOT = join(__dirname, '..', '..')
28
31
  export const AGENTS_DIR = join(ROOT, 'src', 'agents')
29
- // PLATFORM_DIR is deprecated — platform/ was removed in v1.0
30
- export const PLATFORM_DIR = join(ROOT, 'platform')
31
32
 
32
33
  // Auto-detect agent names from agents/ directory
33
34
  export function getAgentNames() {
@@ -43,33 +44,7 @@ export function getAgentNames() {
43
44
  // Cached constant for backward compatibility
44
45
  export const AGENT_NAMES = getAgentNames()
45
46
 
46
- // DEPRECATED: OpenCode-only since v1.0
47
- export const PLATFORM_DETECTORS = {
48
- opencode: (target) =>
49
- existsSync(join(target, 'opencode.json')) ||
50
- existsSync(join(target, 'platform', 'opencode', 'opencode.json')),
51
- claude: (target) => existsSync(join(target, '.claude')) || existsSync(join(target, 'CLAUDE.md')),
52
- cursor: (target) =>
53
- existsSync(join(target, '.cursor')) || existsSync(join(target, '.cursorrules')),
54
- windsurf: (target) =>
55
- existsSync(join(target, '.windsurf')) || existsSync(join(target, '.windsurfrules')),
56
- copilot: (target) =>
57
- existsSync(join(target, '.github', 'copilot-instructions.md')) ||
58
- existsSync(join(target, '.vscode')),
59
- continue: (target) =>
60
- existsSync(join(target, '.continue', 'config.yaml')) || existsSync(join(target, '.continue')),
61
- cline: (target) => existsSync(join(target, '.clinerules')),
62
- }
63
-
64
- export const summary = {
65
- opencode: { created: 0, skipped: 0, errors: 0 },
66
- claude: { created: 0, skipped: 0, errors: 0 },
67
- cursor: { created: 0, skipped: 0, errors: 0 },
68
- windsurf: { created: 0, skipped: 0, errors: 0 },
69
- copilot: { created: 0, skipped: 0, errors: 0 },
70
- continue: { created: 0, skipped: 0, errors: 0 },
71
- cline: { created: 0, skipped: 0, errors: 0 },
72
- }
47
+ export const summary = { opencode: { created: 0, skipped: 0, errors: 0, warnings: 0 } }
73
48
 
74
49
  export function showHelp() {
75
50
  console.log(`
@@ -78,9 +53,7 @@ pantheon-init.mjs — Pantheon OpenCode installer
78
53
  Usage:
79
54
  npx pantheon-opencode init auto-detect, cwd
80
55
  npx pantheon-opencode init --project auto-detect, project
81
- npx pantheon-opencode init --project --platforms opencode,claude specific platforms
82
- npx pantheon-opencode init --project --platforms all all platforms
83
- npx pantheon-opencode init --detect detect platforms without installing
56
+ npx pantheon-opencode init --project install in a project
84
57
  npx pantheon-opencode init --project --dry-run preview without writing
85
58
  npx pantheon-opencode init --project --backup create timestamped backup before writing
86
59
  npx pantheon-opencode init --project --clean wipe + fresh install (all components)
@@ -101,19 +74,8 @@ Clean mode (--clean):
101
74
  re-installs fresh from source. Useful after removing/renaming agents or skills.
102
75
  OFF by default — without --clean only copies new/changed files (never deletes).
103
76
 
104
- Platforms:
77
+ Platform:
105
78
  opencode → .opencode/agents/ + opencode.json
106
- claude → .claude/agents/ + CLAUDE.md + settings.json
107
- cursor → .cursor/rules/ (renamed .mdc files)
108
- windsurf → .windsurf/agents/ + .windsurfrules
109
- copilot → .github/agents/ symlinks + .vscode/settings.json check
110
- continue → .continue/rules/ + .continue/config.yaml
111
- cline → .clinerules/ (no extension, plain markdown)
112
- all → install every platform
113
-
114
- When --platforms is omitted, the script auto-detects which platforms
115
- the target project already supports (based on config files present).
116
- If none are detected, ALL platforms are installed.
117
79
  `)
118
80
  }
119
81
 
@@ -206,63 +168,25 @@ export function createBackup(target) {
206
168
  }
207
169
 
208
170
  export function detectAndReport(target) {
209
- const PLATFORM_LABELS = {
210
- opencode: 'OpenCode',
211
- claude: 'Claude Code',
212
- cursor: 'Cursor',
213
- windsurf: 'Windsurf',
214
- copilot: 'VS Code / Copilot',
215
- continue: 'Continue.dev',
216
- cline: 'Cline',
217
- }
218
-
219
- const CONFIG_FILES = {
220
- opencode: () => 'opencode.json',
221
- claude: () => '.claude/ or CLAUDE.md',
222
- cursor: () => '.cursor/ or .cursorrules',
223
- windsurf: () => '.windsurf/ or .windsurfrules',
224
- copilot: () => '.github/copilot-instructions.md or .vscode/',
225
- continue: () => '.continue/config.yaml or .continue/',
226
- cline: () => '.clinerules',
227
- }
171
+ const found = detectPlatforms(target).length > 0
228
172
 
229
173
  console.log(`\n 🔍 Pantheon Platform Detection`)
230
174
  console.log(` Target: ${target}\n`)
231
175
 
232
- const results = []
233
- for (const [platform, detector] of Object.entries(PLATFORM_DETECTORS)) {
234
- const found = detector(target)
235
- results.push({ platform, found })
236
- }
237
-
238
- // Table header
239
176
  console.log(` ${'Platform'.padEnd(12)} ${'Detected?'.padEnd(12)} Config file`)
240
177
  console.log(` ${'─'.repeat(55)}`)
241
-
242
- const detected = results.filter((r) => r.found)
243
- for (const r of results) {
244
- const status = r.found ? '✅ YES' : '❌ no'
245
- const configFile = r.found ? CONFIG_FILES[r.platform]() : '(not found)'
246
- console.log(` ${PLATFORM_LABELS[r.platform].padEnd(12)} ${status.padEnd(12)} ${configFile}`)
247
- }
248
-
249
- console.log(`\n 📊 ${detected.length} of ${results.length} platforms detected.`)
250
- if (detected.length > 0) {
251
- const names = detected.map((r) => r.platform).join(',')
252
- console.log(` → Install with: npx pantheon-opencode init --platforms ${names}`)
178
+ const status = (found ? '✅ YES' : '❌ no').padEnd(12)
179
+ console.log(` ${'OpenCode'.padEnd(12)} ${status} ${found ? 'opencode.json' : '(not found)'}`)
180
+ console.log(`\n 📊 ${found ? 1 : 0} of 1 platforms detected.`)
181
+ if (found) {
182
+ console.log(' → Install with: npx pantheon-opencode init')
253
183
  }
254
184
 
255
- return detected.map((r) => r.platform)
185
+ return found ? ['opencode'] : []
256
186
  }
257
187
 
258
188
  export function detectPlatforms(target) {
259
- const detected = []
260
- for (const [platform, detector] of Object.entries(PLATFORM_DETECTORS)) {
261
- if (detector(target)) {
262
- detected.push(platform)
263
- }
264
- }
265
- return detected
189
+ return existsSync(join(target, 'opencode.json')) ? ['opencode'] : []
266
190
  }
267
191
 
268
192
  export function sourceDirValid(dir) {
@@ -339,11 +263,49 @@ export function writeIfChanged(filePath, content, dryRun) {
339
263
  return 'skipped'
340
264
  }
341
265
  if (!dryRun) {
342
- writeFileSync(filePath, content, 'utf8')
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)
343
272
  }
344
273
  return 'created'
345
274
  }
346
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
+
347
309
  export function collectSkillNames() {
348
310
  const skillsDir = join(ROOT, 'src', 'skills')
349
311
  if (!existsSync(skillsDir)) return []
@@ -569,16 +531,6 @@ export function validateMcpServers(mcpServers, _agentTools = []) {
569
531
  }
570
532
 
571
533
  export function printSummary(target, platforms) {
572
- const PLATFORM_LABELS = {
573
- opencode: 'OpenCode',
574
- claude: 'Claude Code',
575
- cursor: 'Cursor',
576
- windsurf: 'Windsurf',
577
- copilot: 'VS Code / Copilot',
578
- continue: 'Continue.dev',
579
- cline: 'Cline',
580
- }
581
-
582
534
  console.log('')
583
535
  console.log('='.repeat(60))
584
536
  console.log('📋 Installation Summary')
@@ -590,7 +542,7 @@ export function printSummary(target, platforms) {
590
542
  let totalErrors = 0
591
543
 
592
544
  for (const platform of platforms) {
593
- const label = PLATFORM_LABELS[platform] ?? platform
545
+ const label = platform === 'opencode' ? 'OpenCode' : platform
594
546
  const stats = summary[platform]
595
547
  totalCreated += stats.created
596
548
  totalSkipped += stats.skipped
@@ -624,62 +576,6 @@ export function printSummary(target, platforms) {
624
576
  console.log('')
625
577
  }
626
578
 
627
- if (platforms.includes('claude')) {
628
- console.log(' Claude Code:')
629
- console.log(` - Run \`claude\` in ${target}`)
630
- console.log(' - Agents in .claude/agents/')
631
- console.log(' - Skills in .claude/skills/')
632
- console.log(' - Settings in .claude/settings.json')
633
- console.log('')
634
- }
635
-
636
- if (platforms.includes('cursor')) {
637
- console.log(' Cursor:')
638
- console.log(` - Rules in .cursor/rules/ (.mdc format)`)
639
- console.log(' - Skills in .cursor/skills/')
640
- console.log(' - AGENTS.md in project root')
641
- console.log(' - Use @agent-name in Agent mode')
642
- console.log('')
643
- }
644
-
645
- if (platforms.includes('windsurf')) {
646
- console.log(' Windsurf (Cascade):')
647
- console.log(` - Rules in .windsurf/rules/`)
648
- console.log(' - Skills in .windsurf/skills/')
649
- console.log(' - Workflows in .windsurf/workflows/ (/orchestrate, /code-review)')
650
- console.log(' - AGENTS.md in project root')
651
- console.log('')
652
- }
653
-
654
- if (platforms.includes('copilot')) {
655
- console.log(' VS Code / Copilot:')
656
- console.log(` - Agents in .github/agents/`)
657
- console.log(' - Ensure .vscode/settings.json has plugin config')
658
- console.log(' - Use @agent-name in VS Code Copilot Chat')
659
- console.log(' - Plugin manifest in plugin.json')
660
- console.log('')
661
- }
662
-
663
- if (platforms.includes('continue')) {
664
- console.log(' Continue.dev:')
665
- console.log(` - Rules in .continue/rules/`)
666
- console.log(' - Config in .continue/config.yaml')
667
- console.log(' - Rules are injected into system prompts (no @name invocation)')
668
- console.log(' - Edit config.yaml to set API keys and models')
669
- console.log(' - Use /reload to apply config changes')
670
- console.log('')
671
- }
672
-
673
- if (platforms.includes('cline')) {
674
- console.log(' Cline:')
675
- console.log(` - Agent rules in .clinerules/ (no extension, plain markdown)`)
676
- console.log(' - Skills in .clinerules/skills/')
677
- console.log(' - Commands in .clinerules/commands/')
678
- console.log(' - AGENTS.md in project root')
679
- console.log(' - Invoke agents with @agent-name in Cline chat')
680
- console.log('')
681
- }
682
-
683
579
  console.log(' 📚 Full documentation: https://github.com/ils15/pantheon')
684
580
  console.log(' 🐛 Report issues: https://github.com/ils15/pantheon/issues')
685
581
  }
@@ -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
+ }