pantheon-opencode 1.0.0
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/AGENTS.md +37 -0
- package/LICENSE +21 -0
- package/README.md +1013 -0
- package/bin/pantheon-init.mjs +183 -0
- package/commands/pantheon-audit.md +25 -0
- package/commands/pantheon-bg.md +10 -0
- package/commands/pantheon-consolidate.md +11 -0
- package/commands/pantheon-deepwork.md +128 -0
- package/commands/pantheon-doc.md +10 -0
- package/commands/pantheon-focus.md +9 -0
- package/commands/pantheon-forget.md +58 -0
- package/commands/pantheon-hash.md +11 -0
- package/commands/pantheon-optimize.md +79 -0
- package/commands/pantheon-remember.md +44 -0
- package/commands/pantheon-search.md +48 -0
- package/commands/pantheon-status.md +71 -0
- package/commands/pantheon-todo.md +11 -0
- package/commands/pantheon.md +49 -0
- package/docs/AGENT-MCP.md +194 -0
- package/docs/ARCHITECTURE.md +384 -0
- package/docs/BRANCH-PROTECTION.md +142 -0
- package/docs/INDEX.md +81 -0
- package/docs/INSTALLATION.md +217 -0
- package/docs/MCP.md +238 -0
- package/docs/MEMORY.md +471 -0
- package/docs/MIGRATION-MEMORY-BANK.md +139 -0
- package/docs/PLATFORMS.md +5 -0
- package/docs/QUICKSTART.md +49 -0
- package/docs/README.md +18 -0
- package/docs/RELEASING.md +256 -0
- package/docs/SETUP.md +5 -0
- package/docs/UPGRADING.md +41 -0
- package/docs/mcp-recommendations.md +439 -0
- package/docs/mcp-tools.md +156 -0
- package/docs/mcp-user-guide.md +204 -0
- package/docs/persistence-mcp.md +111 -0
- package/package.json +72 -0
- package/pantheon.schema.json +158 -0
- package/scripts/__init__.py +0 -0
- package/scripts/_pantheon_paths.py +68 -0
- package/scripts/check-version-consistency.sh +23 -0
- package/scripts/code_mode_server.py +202 -0
- package/scripts/doctor.mjs +763 -0
- package/scripts/generate-prompts.sh +222 -0
- package/scripts/generate-routing-docs.mjs +104 -0
- package/scripts/hash_verify.py +192 -0
- package/scripts/init-pantheon-mcp.sh +118 -0
- package/scripts/install/agents-md.mjs +214 -0
- package/scripts/install/health-check.mjs +196 -0
- package/scripts/install/migrate.mjs +209 -0
- package/scripts/install/opencode.mjs +645 -0
- package/scripts/install/shared.mjs +655 -0
- package/scripts/install/venv.mjs +116 -0
- package/scripts/install-mcp.mjs +885 -0
- package/scripts/install.mjs +26 -0
- package/scripts/manifest.mjs +622 -0
- package/scripts/mcp_persistence_server.py +459 -0
- package/scripts/mcp_resources_server.py +206 -0
- package/scripts/memory_cache.py +78 -0
- package/scripts/memory_mcp_server.py +605 -0
- package/scripts/paths.py +64 -0
- package/scripts/prune_context.py +72 -0
- package/scripts/release-bundle.mjs +109 -0
- package/scripts/scrub-secrets.py +282 -0
- package/scripts/scrub_secrets.py +281 -0
- package/scripts/test-context-compression.sh +166 -0
- package/scripts/themis_heuristic_scan.py +287 -0
- package/scripts/todo_enforcer.py +242 -0
- package/scripts/uninstall.mjs +1057 -0
- package/scripts/validate-routing.mjs +160 -0
- package/scripts/validate_agent_frontmatter.py +226 -0
- package/scripts/versioning.mjs +254 -0
- package/skills-lock.json +16 -0
- package/src/agents/aphrodite.md +162 -0
- package/src/agents/apollo.md +109 -0
- package/src/agents/athena.md +226 -0
- package/src/agents/demeter.md +146 -0
- package/src/agents/gaia.md +82 -0
- package/src/agents/hephaestus.md +105 -0
- package/src/agents/hermes.md +302 -0
- package/src/agents/iris.md +99 -0
- package/src/agents/mnemosyne.md +226 -0
- package/src/agents/nyx.md +87 -0
- package/src/agents/prometheus.md +199 -0
- package/src/agents/talos.md +93 -0
- package/src/agents/themis.md +187 -0
- package/src/agents/zeus.md +209 -0
- package/src/instructions/agent-return-format.instructions.md +26 -0
- package/src/instructions/backend-standards.instructions.md +45 -0
- package/src/instructions/documentation-standards.instructions.md +53 -0
- package/src/instructions/frontend-standards.instructions.md +46 -0
- package/src/instructions/memory-protocol.instructions.md +67 -0
- package/src/instructions/yagni.instructions.md +21 -0
- package/src/instructions/zeus-anti-stall.instructions.md +72 -0
- package/src/instructions/zeus-communication-rules.instructions.md +15 -0
- package/src/instructions/zeus-council-synthesis.instructions.md +105 -0
- package/src/instructions/zeus-timeout-retry.instructions.md +127 -0
- package/src/mcp/_pantheon_paths.py +67 -0
- package/src/mcp/code_mode_server.py +202 -0
- package/src/mcp/init-pantheon-mcp.sh +118 -0
- package/src/mcp/install-mcp.mjs +885 -0
- package/src/mcp/mcp_persistence_server.py +458 -0
- package/src/mcp/mcp_resources_server.py +205 -0
- package/src/mcp/memory_mcp_server.py +604 -0
- package/src/mcp/requirements-mcp-core.txt +5 -0
- package/src/mcp/requirements-mcp.txt +5 -0
- package/src/plugin.ts +19 -0
- package/src/plugins/tui/dist/tui.js +143 -0
- package/src/plugins/tui/dist/tui.tsx +144 -0
- package/src/plugins/tui/package.json +32 -0
- package/src/plugins/tui/src/index.tsx +144 -0
- package/src/plugins/tui/tsdown.config.ts +22 -0
- package/src/routing.yml +499 -0
- package/src/skills/README.md +230 -0
- package/src/skills/agent-coordination/SKILL.md +95 -0
- package/src/skills/artifact-management/SKILL.md +118 -0
- package/src/skills/auto-continue/SKILL.md +280 -0
- package/src/skills/code-review-checklist/SKILL.md +139 -0
- package/src/skills/context-compression/SKILL.md +861 -0
- package/src/skills/git-workflow-and-versioning/SKILL.md +32 -0
- package/src/skills/incremental-implementation/SKILL.md +27 -0
- package/src/skills/memory-bank/SKILL.md +165 -0
- package/src/skills/orchestration-workflow/SKILL.md +311 -0
- package/src/skills/security-hardening/SKILL.md +36 -0
- package/src/skills/session-goal/SKILL.md +138 -0
- package/src/skills/spec-driven-development/SKILL.md +23 -0
- package/src/skills/tdd-with-agents/SKILL.md +170 -0
- package/src/skills/visual-review-pipeline/SKILL.md +200 -0
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* opencode.mjs — OpenCode platform installer
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:fs'
|
|
7
|
+
import { homedir } from 'node:os'
|
|
8
|
+
import { join, resolve } from 'node:path'
|
|
9
|
+
import { healthCheck } from './health-check.mjs'
|
|
10
|
+
import { detectVersion, runMigrations } from './migrate.mjs'
|
|
11
|
+
import {
|
|
12
|
+
collectSkillNames,
|
|
13
|
+
copyFiles,
|
|
14
|
+
installSkills,
|
|
15
|
+
PLATFORM_DIR,
|
|
16
|
+
parseFrontmatter,
|
|
17
|
+
ROOT,
|
|
18
|
+
sourceDirValid,
|
|
19
|
+
summary,
|
|
20
|
+
syncDir,
|
|
21
|
+
writeIfChanged,
|
|
22
|
+
} from './shared.mjs'
|
|
23
|
+
import { setupVenv } from './venv.mjs'
|
|
24
|
+
|
|
25
|
+
function deepClone(value) {
|
|
26
|
+
return JSON.parse(JSON.stringify(value))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function mergeMissing(target, source) {
|
|
30
|
+
if (!source || typeof source !== 'object' || Array.isArray(source)) return target
|
|
31
|
+
if (!target || typeof target !== 'object' || Array.isArray(target)) return deepClone(source)
|
|
32
|
+
|
|
33
|
+
for (const [key, sourceVal] of Object.entries(source)) {
|
|
34
|
+
const targetVal = target[key]
|
|
35
|
+
|
|
36
|
+
if (targetVal === undefined) {
|
|
37
|
+
target[key] = deepClone(sourceVal)
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (
|
|
42
|
+
sourceVal &&
|
|
43
|
+
typeof sourceVal === 'object' &&
|
|
44
|
+
!Array.isArray(sourceVal) &&
|
|
45
|
+
targetVal &&
|
|
46
|
+
typeof targetVal === 'object' &&
|
|
47
|
+
!Array.isArray(targetVal)
|
|
48
|
+
) {
|
|
49
|
+
mergeMissing(targetVal, sourceVal)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return target
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Detect whether `target` is the user's global OpenCode installation directory
|
|
58
|
+
* (~/.opencode or $XDG_CONFIG_HOME/opencode).
|
|
59
|
+
* Global installs use a flat layout: agents/ skills/ commands/ plugins/
|
|
60
|
+
* Project installs use the .opencode/ sub-directory layout.
|
|
61
|
+
*/
|
|
62
|
+
function isGlobalConfigDir(target) {
|
|
63
|
+
// Primary: ~/.opencode (actual OpenCode installation)
|
|
64
|
+
const homeDir = resolve(join(homedir(), '.opencode'))
|
|
65
|
+
if (resolve(target) === homeDir) return true
|
|
66
|
+
|
|
67
|
+
// Fallback: $XDG_CONFIG_HOME/opencode (legacy/alternative)
|
|
68
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), '.config')
|
|
69
|
+
const xdgDir = resolve(join(xdgConfig, 'opencode'))
|
|
70
|
+
return resolve(target) === xdgDir
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function installOpenCode(
|
|
74
|
+
target,
|
|
75
|
+
dryRun,
|
|
76
|
+
clean = false,
|
|
77
|
+
components = ['agents', 'skills', 'instructions', 'commands', 'plugins', 'runtime'],
|
|
78
|
+
) {
|
|
79
|
+
const componentSet = new Set(components)
|
|
80
|
+
const stats = summary.opencode
|
|
81
|
+
|
|
82
|
+
// Determine layout based on install scope.
|
|
83
|
+
// Global config dir (~/.opencode or ~/.config/opencode) uses a flat layout:
|
|
84
|
+
// agents/ skills/ commands/ plugins/
|
|
85
|
+
// Project installs use the .opencode/ sub-directory layout:
|
|
86
|
+
// .opencode/agents/ .opencode/skills/ .opencode/commands/
|
|
87
|
+
const isGlobal = isGlobalConfigDir(target)
|
|
88
|
+
const _subDir = isGlobal ? '' : '.opencode'
|
|
89
|
+
const agentPrefix = isGlobal ? 'agents' : '.opencode/agents'
|
|
90
|
+
|
|
91
|
+
if (isGlobal) {
|
|
92
|
+
console.log(
|
|
93
|
+
' 🌐 Global config directory detected — using flat layout (agents/, skills/, commands/)',
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// -----------------------------------------------------------------------
|
|
98
|
+
// 1. Install agents (--components agents)
|
|
99
|
+
// -----------------------------------------------------------------------
|
|
100
|
+
if (componentSet.has('agents')) {
|
|
101
|
+
const srcDir = join(ROOT, 'src', 'agents')
|
|
102
|
+
if (!sourceDirValid(srcDir)) {
|
|
103
|
+
console.warn(` ⚠️ Agent source directory not found: ${srcDir}`)
|
|
104
|
+
stats.errors++
|
|
105
|
+
} else {
|
|
106
|
+
const dstDir = isGlobal ? join(target, 'agents') : join(target, '.opencode', 'agents')
|
|
107
|
+
if (!dryRun) mkdirSync(dstDir, { recursive: true })
|
|
108
|
+
if (clean && existsSync(dstDir) && !dryRun) {
|
|
109
|
+
const existing = readdirSync(dstDir)
|
|
110
|
+
for (const f of existing) {
|
|
111
|
+
rmSync(join(dstDir, f), { recursive: true, force: true })
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const { created, skipped } = copyFiles(srcDir, dstDir, dryRun)
|
|
115
|
+
stats.created += created
|
|
116
|
+
stats.skipped += skipped
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// -----------------------------------------------------------------------
|
|
121
|
+
// 2. Install skills (--components skills)
|
|
122
|
+
// -----------------------------------------------------------------------
|
|
123
|
+
if (componentSet.has('skills')) {
|
|
124
|
+
const skillNames = collectSkillNames()
|
|
125
|
+
if (skillNames.length > 0) {
|
|
126
|
+
console.log(` 📚 Installing ${skillNames.length} skills...`)
|
|
127
|
+
const dstSkillsDir = isGlobal ? join(target, 'skills') : join(target, '.opencode', 'skills')
|
|
128
|
+
if (clean && existsSync(dstSkillsDir) && !dryRun) {
|
|
129
|
+
const existing = readdirSync(dstSkillsDir)
|
|
130
|
+
for (const s of existing) {
|
|
131
|
+
rmSync(join(dstSkillsDir, s), { recursive: true, force: true })
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const installSubDir = isGlobal ? '' : '.opencode'
|
|
135
|
+
const { created: sCreated, skipped: sSkipped } = installSkills(
|
|
136
|
+
skillNames,
|
|
137
|
+
target,
|
|
138
|
+
dryRun,
|
|
139
|
+
installSubDir,
|
|
140
|
+
)
|
|
141
|
+
stats.created += sCreated
|
|
142
|
+
stats.skipped += sSkipped
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// -----------------------------------------------------------------------
|
|
147
|
+
// 2.5 Install instructions: AGENTS.md + instructions/ (--components instructions)
|
|
148
|
+
// -----------------------------------------------------------------------
|
|
149
|
+
if (componentSet.has('instructions')) {
|
|
150
|
+
// AGENTS.md
|
|
151
|
+
const srcAgentsMd = join(ROOT, 'AGENTS.md')
|
|
152
|
+
const dstAgentsMd = join(target, 'AGENTS.md')
|
|
153
|
+
if (existsSync(srcAgentsMd)) {
|
|
154
|
+
const content = readFileSync(srcAgentsMd, 'utf8')
|
|
155
|
+
const status = writeIfChanged(dstAgentsMd, content, dryRun)
|
|
156
|
+
if (status === 'created') stats.created++
|
|
157
|
+
else stats.skipped++
|
|
158
|
+
}
|
|
159
|
+
// instructions/ directory
|
|
160
|
+
const srcInstr = join(ROOT, 'src', 'instructions')
|
|
161
|
+
const dstInstr = join(target, 'instructions')
|
|
162
|
+
if (existsSync(srcInstr)) {
|
|
163
|
+
const instrResult = syncDir(srcInstr, dstInstr, dryRun, clean)
|
|
164
|
+
stats.created += instrResult.created
|
|
165
|
+
stats.skipped += instrResult.skipped
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// -----------------------------------------------------------------------
|
|
170
|
+
// 2.6 Install prompts (--components prompts)
|
|
171
|
+
// -----------------------------------------------------------------------
|
|
172
|
+
if (componentSet.has('prompts')) {
|
|
173
|
+
const srcPrompts = join(ROOT, 'prompts')
|
|
174
|
+
const dstPrompts = join(target, 'prompts')
|
|
175
|
+
if (existsSync(srcPrompts)) {
|
|
176
|
+
const promptsResult = syncDir(srcPrompts, dstPrompts, dryRun, clean)
|
|
177
|
+
stats.created += promptsResult.created
|
|
178
|
+
stats.skipped += promptsResult.skipped
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// -----------------------------------------------------------------------
|
|
183
|
+
// 2.7 Install commands (--components commands)
|
|
184
|
+
// -----------------------------------------------------------------------
|
|
185
|
+
if (componentSet.has('commands')) {
|
|
186
|
+
const srcCmds = join(ROOT, 'commands')
|
|
187
|
+
const dstCmds = isGlobal ? join(target, 'commands') : join(target, '.opencode', 'commands')
|
|
188
|
+
if (existsSync(srcCmds)) {
|
|
189
|
+
const cmdResult = syncDir(srcCmds, dstCmds, dryRun, clean, (f) => f.endsWith('.md'))
|
|
190
|
+
stats.created += cmdResult.created
|
|
191
|
+
stats.skipped += cmdResult.skipped
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// -----------------------------------------------------------------------
|
|
196
|
+
// 2.8 Install TUI plugins (--components plugins)
|
|
197
|
+
// -----------------------------------------------------------------------
|
|
198
|
+
if (componentSet.has('plugins')) {
|
|
199
|
+
const srcPluginDir = join(PLATFORM_DIR, 'opencode', '.opencode', 'plugins', 'pantheon-tui')
|
|
200
|
+
const dstPluginDir = isGlobal
|
|
201
|
+
? join(target, 'plugins', 'pantheon-tui')
|
|
202
|
+
: join(target, '.opencode', 'plugins', 'pantheon-tui')
|
|
203
|
+
if (!dryRun) mkdirSync(dstPluginDir, { recursive: true })
|
|
204
|
+
if (clean && existsSync(dstPluginDir) && !dryRun) {
|
|
205
|
+
rmSync(dstPluginDir, { recursive: true, force: true })
|
|
206
|
+
mkdirSync(dstPluginDir, { recursive: true })
|
|
207
|
+
}
|
|
208
|
+
const { created, skipped } = syncDir(srcPluginDir, dstPluginDir, dryRun, false)
|
|
209
|
+
stats.created += created
|
|
210
|
+
stats.skipped += skipped
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// -----------------------------------------------------------------------
|
|
214
|
+
// 2.9 Create/update tui.json with plugin registration
|
|
215
|
+
// -----------------------------------------------------------------------
|
|
216
|
+
const targetTuiConfigPath = isGlobal
|
|
217
|
+
? join(target, 'tui.json')
|
|
218
|
+
: join(target, '.opencode', 'tui.json')
|
|
219
|
+
let tuiConfig = { plugin: [] }
|
|
220
|
+
if (existsSync(targetTuiConfigPath)) {
|
|
221
|
+
try {
|
|
222
|
+
tuiConfig = JSON.parse(readFileSync(targetTuiConfigPath, 'utf8'))
|
|
223
|
+
} catch {
|
|
224
|
+
/* use default */
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (!Array.isArray(tuiConfig.plugin)) {
|
|
228
|
+
tuiConfig.plugin = []
|
|
229
|
+
}
|
|
230
|
+
// Add our plugin if not already present
|
|
231
|
+
const pluginRef = 'plugins/pantheon-tui/dist/tui.tsx'
|
|
232
|
+
if (!tuiConfig.plugin.includes(pluginRef)) {
|
|
233
|
+
tuiConfig.plugin.push(pluginRef)
|
|
234
|
+
}
|
|
235
|
+
const tuiContent = `${JSON.stringify(tuiConfig, null, 2)}\n`
|
|
236
|
+
const tuiStatus = writeIfChanged(targetTuiConfigPath, tuiContent, dryRun)
|
|
237
|
+
if (tuiStatus === 'created') stats.created++
|
|
238
|
+
else stats.skipped++
|
|
239
|
+
|
|
240
|
+
// -----------------------------------------------------------------------
|
|
241
|
+
// 2.10 Install runtime infrastructure (--components runtime)
|
|
242
|
+
// MCP server scripts, code-mode scripts, tiers.json
|
|
243
|
+
// -----------------------------------------------------------------------
|
|
244
|
+
if (componentSet.has('runtime')) {
|
|
245
|
+
const runtimeTarget = isGlobal ? target : join(target, '.opencode')
|
|
246
|
+
|
|
247
|
+
// ── MCP server scripts ──
|
|
248
|
+
const mcpScripts = [
|
|
249
|
+
'mcp_resources_server.py',
|
|
250
|
+
'code_mode_server.py',
|
|
251
|
+
'memory_mcp_server.py',
|
|
252
|
+
'scrub-secrets.py',
|
|
253
|
+
'_pantheon_paths.py',
|
|
254
|
+
'mcp_persistence_server.py',
|
|
255
|
+
]
|
|
256
|
+
const srcScriptsDir = join(ROOT, 'scripts')
|
|
257
|
+
const dstScriptsDir = join(runtimeTarget, 'scripts')
|
|
258
|
+
if (!dryRun) mkdirSync(dstScriptsDir, { recursive: true })
|
|
259
|
+
for (const script of mcpScripts) {
|
|
260
|
+
const src = join(srcScriptsDir, script)
|
|
261
|
+
if (!existsSync(src)) {
|
|
262
|
+
console.warn(` ⚠️ Script not found: ${src}`)
|
|
263
|
+
stats.errors++
|
|
264
|
+
continue
|
|
265
|
+
}
|
|
266
|
+
const content = readFileSync(src, 'utf8')
|
|
267
|
+
const dst = join(dstScriptsDir, script)
|
|
268
|
+
const status = writeIfChanged(dst, content, dryRun)
|
|
269
|
+
if (status === 'created') {
|
|
270
|
+
stats.created++
|
|
271
|
+
// Make executable
|
|
272
|
+
if (!dryRun) {
|
|
273
|
+
try {
|
|
274
|
+
chmodSync(dst, 0o755)
|
|
275
|
+
} catch {
|
|
276
|
+
/* non-critical */
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
} else stats.skipped++
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ── Code-mode scripts ──
|
|
283
|
+
const srcCodeModeDir = join(ROOT, '.pantheon', 'code-mode')
|
|
284
|
+
const dstCodeModeDir = join(runtimeTarget, '.pantheon', 'code-mode')
|
|
285
|
+
if (existsSync(srcCodeModeDir)) {
|
|
286
|
+
const cmResult = syncDir(srcCodeModeDir, dstCodeModeDir, dryRun, clean)
|
|
287
|
+
stats.created += cmResult.created
|
|
288
|
+
stats.skipped += cmResult.skipped
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ── tiers.json ──
|
|
292
|
+
const srcTiers = join(ROOT, '.pantheon', 'tiers.json')
|
|
293
|
+
const dstTiers = join(runtimeTarget, '.pantheon', 'tiers.json')
|
|
294
|
+
if (existsSync(srcTiers)) {
|
|
295
|
+
const content = readFileSync(srcTiers, 'utf8')
|
|
296
|
+
const status = writeIfChanged(dstTiers, content, dryRun)
|
|
297
|
+
if (status === 'created') stats.created++
|
|
298
|
+
else stats.skipped++
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// -----------------------------------------------------------------------
|
|
303
|
+
// 3. Create/update opencode.json (always runs)
|
|
304
|
+
// Reads TARGET's existing config first, then merges Pantheon settings
|
|
305
|
+
// on top. Preserves user's MCP, provider, plugin, compaction, theme.
|
|
306
|
+
// -----------------------------------------------------------------------
|
|
307
|
+
const pantheonConfigPath = join(ROOT, 'opencode.json')
|
|
308
|
+
const targetConfigPath = join(target, 'opencode.json')
|
|
309
|
+
|
|
310
|
+
let config = {}
|
|
311
|
+
if (existsSync(targetConfigPath)) {
|
|
312
|
+
try {
|
|
313
|
+
config = JSON.parse(readFileSync(targetConfigPath, 'utf8'))
|
|
314
|
+
} catch {
|
|
315
|
+
config = {}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
let pantheonConfig = {}
|
|
320
|
+
if (existsSync(pantheonConfigPath)) {
|
|
321
|
+
try {
|
|
322
|
+
pantheonConfig = JSON.parse(readFileSync(pantheonConfigPath, 'utf8'))
|
|
323
|
+
} catch {
|
|
324
|
+
pantheonConfig = {}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// --------------------------------------------------------------------
|
|
329
|
+
// A. Parse canonical agent config from agents/*.agent.md frontmatter
|
|
330
|
+
// and merge into opencode.json config.
|
|
331
|
+
// --------------------------------------------------------------------
|
|
332
|
+
function getAgentSources(agentPrefix) {
|
|
333
|
+
const agentsDir = join(ROOT, 'src', 'agents')
|
|
334
|
+
if (!existsSync(agentsDir)) return {}
|
|
335
|
+
const sources = {}
|
|
336
|
+
const files = readdirSync(agentsDir).filter((f) => f.endsWith('.agent.md') || f.endsWith('.md'))
|
|
337
|
+
for (const f of files) {
|
|
338
|
+
const name = f.replace(/\.(agent\.)?md$/, '')
|
|
339
|
+
sources[name] = `${agentPrefix}/${name}.md`
|
|
340
|
+
}
|
|
341
|
+
return sources
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function readAgentConfigFromCanonical() {
|
|
345
|
+
const agentsDir = join(ROOT, 'src', 'agents')
|
|
346
|
+
if (!existsSync(agentsDir)) return {}
|
|
347
|
+
const config = {}
|
|
348
|
+
const files = readdirSync(agentsDir).filter((f) => f.endsWith('.agent.md') || f.endsWith('.md'))
|
|
349
|
+
for (const f of files) {
|
|
350
|
+
const name = f.replace(/\.(agent\.)?md$/, '')
|
|
351
|
+
const content = readFileSync(join(agentsDir, f), 'utf8')
|
|
352
|
+
const parsed = parseFrontmatter(content)
|
|
353
|
+
if (!parsed) continue
|
|
354
|
+
const fm = parsed.fm
|
|
355
|
+
const agent = {}
|
|
356
|
+
|
|
357
|
+
// Extract fields from frontmatter
|
|
358
|
+
if (fm.color) agent.color = fm.color
|
|
359
|
+
if (fm.description) agent.description = fm.description
|
|
360
|
+
if (fm.mode) agent.mode = fm.mode
|
|
361
|
+
if (fm.hidden) agent.hidden = fm.hidden
|
|
362
|
+
if (fm.temperature !== undefined) agent.temperature = fm.temperature
|
|
363
|
+
if (fm.steps !== undefined) agent.steps = fm.steps
|
|
364
|
+
// Support both hyphen (YAML) and underscore (JSON) key variants
|
|
365
|
+
if (fm['disable-model-invocation'] !== undefined)
|
|
366
|
+
agent.disable_model_invocation = fm['disable-model-invocation']
|
|
367
|
+
else if (fm.disable_model_invocation !== undefined)
|
|
368
|
+
agent.disable_model_invocation = fm.disable_model_invocation
|
|
369
|
+
|
|
370
|
+
// Build permission from frontmatter
|
|
371
|
+
if (fm.permission) {
|
|
372
|
+
agent.permission = JSON.parse(JSON.stringify(fm.permission))
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
config[name] = agent
|
|
376
|
+
}
|
|
377
|
+
return config
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const canonicalAgentConfig = readAgentConfigFromCanonical()
|
|
381
|
+
const agentSources = getAgentSources(agentPrefix)
|
|
382
|
+
const MANAGED_FIELDS = [
|
|
383
|
+
'steps',
|
|
384
|
+
'temperature',
|
|
385
|
+
'color',
|
|
386
|
+
'permission',
|
|
387
|
+
'mode',
|
|
388
|
+
'hidden',
|
|
389
|
+
'disable_model_invocation',
|
|
390
|
+
]
|
|
391
|
+
|
|
392
|
+
if (!config.agent) config.agent = {}
|
|
393
|
+
|
|
394
|
+
for (const [agentName, agentCfg] of Object.entries(canonicalAgentConfig)) {
|
|
395
|
+
if (!agentCfg || typeof agentCfg !== 'object') continue
|
|
396
|
+
|
|
397
|
+
if (config.agent[agentName]) {
|
|
398
|
+
// ── Agent exists in target config ──
|
|
399
|
+
// Update framework-managed fields from canonical source
|
|
400
|
+
// Preserve user-customized fields (model, provider, mcp, etc.)
|
|
401
|
+
const existing = config.agent[agentName]
|
|
402
|
+
if (agentSources[agentName]) existing.source = agentSources[agentName]
|
|
403
|
+
for (const field of MANAGED_FIELDS) {
|
|
404
|
+
if (field in agentCfg) {
|
|
405
|
+
existing[field] = JSON.parse(JSON.stringify(agentCfg[field]))
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
// Remove stale fields that are no longer in canonical config
|
|
409
|
+
delete existing.model
|
|
410
|
+
delete existing.small_model
|
|
411
|
+
} else {
|
|
412
|
+
// ── New agent ──
|
|
413
|
+
const newAgent = {}
|
|
414
|
+
if (agentSources[agentName]) newAgent.source = agentSources[agentName]
|
|
415
|
+
if (agentCfg.description) newAgent.description = agentCfg.description
|
|
416
|
+
|
|
417
|
+
// Copy all framework-managed fields from canonical config
|
|
418
|
+
for (const field of MANAGED_FIELDS) {
|
|
419
|
+
if (field in agentCfg) {
|
|
420
|
+
newAgent[field] = JSON.parse(JSON.stringify(agentCfg[field]))
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Ensure bash permission is set from canonical (default to deny if missing)
|
|
425
|
+
if (!newAgent.permission) {
|
|
426
|
+
newAgent.permission = {}
|
|
427
|
+
}
|
|
428
|
+
if (!newAgent.permission.bash && agentCfg.permission?.bash) {
|
|
429
|
+
newAgent.permission.bash = JSON.parse(JSON.stringify(agentCfg.permission.bash))
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
config.agent[agentName] = newAgent
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Remove stale agents (exist in target config but not in canonical source)
|
|
437
|
+
// Only removes agents whose source is managed (starts with our prefix)
|
|
438
|
+
// so user-defined agents with different source paths are preserved.
|
|
439
|
+
const canonicalNames = new Set(Object.keys(canonicalAgentConfig))
|
|
440
|
+
for (const [agentName, agentCfg] of Object.entries(config.agent)) {
|
|
441
|
+
const source = agentCfg?.source || ''
|
|
442
|
+
if (source.startsWith(agentPrefix) && !canonicalNames.has(agentName)) {
|
|
443
|
+
delete config.agent[agentName]
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (Object.keys(config.agent).length === 0) delete config.agent
|
|
447
|
+
|
|
448
|
+
// --------------------------------------------------------------------
|
|
449
|
+
// B. Commands from .md frontmatter (commands.json removed)
|
|
450
|
+
// --------------------------------------------------------------------
|
|
451
|
+
// Commands are now sourced from .md frontmatter in commands/.
|
|
452
|
+
// The .md frontmatter is the canonical source — no json merge needed.
|
|
453
|
+
|
|
454
|
+
// --------------------------------------------------------------------
|
|
455
|
+
// B.5 Ensure critical top-level OpenCode config sections
|
|
456
|
+
// --------------------------------------------------------------------
|
|
457
|
+
if (!config.default_agent && pantheonConfig.default_agent) {
|
|
458
|
+
config.default_agent = pantheonConfig.default_agent
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (!Array.isArray(config.plugin)) {
|
|
462
|
+
config.plugin = []
|
|
463
|
+
}
|
|
464
|
+
if (Array.isArray(pantheonConfig.plugin)) {
|
|
465
|
+
for (const plugin of pantheonConfig.plugin) {
|
|
466
|
+
if (!config.plugin.includes(plugin)) {
|
|
467
|
+
config.plugin.push(plugin)
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (!config.provider && pantheonConfig.provider) {
|
|
473
|
+
config.provider = deepClone(pantheonConfig.provider)
|
|
474
|
+
} else if (config.provider && pantheonConfig.provider) {
|
|
475
|
+
mergeMissing(config.provider, pantheonConfig.provider)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (!config.compaction && pantheonConfig.compaction) {
|
|
479
|
+
config.compaction = deepClone(pantheonConfig.compaction)
|
|
480
|
+
} else if (config.compaction && pantheonConfig.compaction) {
|
|
481
|
+
mergeMissing(config.compaction, pantheonConfig.compaction)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// --------------------------------------------------------------------
|
|
485
|
+
// C. Merge permissions
|
|
486
|
+
// --------------------------------------------------------------------
|
|
487
|
+
if (!config.permission) config.permission = {}
|
|
488
|
+
if (pantheonConfig.permission) {
|
|
489
|
+
mergeMissing(config.permission, pantheonConfig.permission)
|
|
490
|
+
}
|
|
491
|
+
if (componentSet.has('skills')) {
|
|
492
|
+
config.permission.skill = { '*': 'allow' }
|
|
493
|
+
}
|
|
494
|
+
if (!config.permission.bash) {
|
|
495
|
+
config.permission.bash = {
|
|
496
|
+
'git *': 'allow',
|
|
497
|
+
'npm *': 'allow',
|
|
498
|
+
'npx *': 'allow',
|
|
499
|
+
'pytest *': 'allow',
|
|
500
|
+
'ruff *': 'allow',
|
|
501
|
+
'black *': 'allow',
|
|
502
|
+
'pip *': 'allow',
|
|
503
|
+
'docker *': 'allow',
|
|
504
|
+
'curl *': 'allow',
|
|
505
|
+
'gh *': 'allow',
|
|
506
|
+
'make *': 'allow',
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// --------------------------------------------------------------------
|
|
511
|
+
// D. Merge instructions paths
|
|
512
|
+
// --------------------------------------------------------------------
|
|
513
|
+
const pantheonInstructions = ['AGENTS.md', 'instructions/*.instructions.md']
|
|
514
|
+
if (!config.instructions) {
|
|
515
|
+
config.instructions = [...pantheonInstructions]
|
|
516
|
+
} else {
|
|
517
|
+
for (const instr of pantheonInstructions) {
|
|
518
|
+
if (!config.instructions.includes(instr)) {
|
|
519
|
+
config.instructions.push(instr)
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// --------------------------------------------------------------------
|
|
525
|
+
// E. Ensure $schema
|
|
526
|
+
// --------------------------------------------------------------------
|
|
527
|
+
if (!config.$schema) {
|
|
528
|
+
config.$schema = 'https://opencode.ai/config.json'
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// --------------------------------------------------------------------
|
|
532
|
+
// F. Compatibility cleanup (OpenCode >= v1.15.7)
|
|
533
|
+
// --------------------------------------------------------------------
|
|
534
|
+
// `todoContinuation` is rejected by newer OpenCode versions.
|
|
535
|
+
// Remove it from merged user config to avoid startup/config failures.
|
|
536
|
+
if (Object.hasOwn(config, 'todoContinuation')) {
|
|
537
|
+
delete config.todoContinuation
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// --------------------------------------------------------------------
|
|
541
|
+
// F.5 MCP server entries (for runtime-deployed scripts)
|
|
542
|
+
// --------------------------------------------------------------------
|
|
543
|
+
if (componentSet.has('runtime')) {
|
|
544
|
+
config.mcp = config.mcp || {}
|
|
545
|
+
const runtimeTarget = isGlobal ? target : join(target, '.opencode')
|
|
546
|
+
const pythonPath = process.platform === 'win32' ? 'python' : 'python3'
|
|
547
|
+
const venvPython = join(runtimeTarget, '.venv', 'bin', 'python3')
|
|
548
|
+
const memoryPython = existsSync(venvPython) ? venvPython : pythonPath
|
|
549
|
+
|
|
550
|
+
// Only add if not already configured by user
|
|
551
|
+
if (!config.mcp['pantheon-resources']) {
|
|
552
|
+
config.mcp['pantheon-resources'] = {
|
|
553
|
+
type: 'local',
|
|
554
|
+
cwd: runtimeTarget,
|
|
555
|
+
command: [memoryPython, 'scripts/mcp_resources_server.py'],
|
|
556
|
+
enabled: true,
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
if (!config.mcp['pantheon-code-mode']) {
|
|
560
|
+
config.mcp['pantheon-code-mode'] = {
|
|
561
|
+
type: 'local',
|
|
562
|
+
cwd: runtimeTarget,
|
|
563
|
+
command: [memoryPython, 'scripts/code_mode_server.py'],
|
|
564
|
+
enabled: true,
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (!config.mcp['pantheon-memory']) {
|
|
568
|
+
config.mcp['pantheon-memory'] = {
|
|
569
|
+
type: 'local',
|
|
570
|
+
cwd: runtimeTarget,
|
|
571
|
+
command: [memoryPython, 'scripts/memory_mcp_server.py'],
|
|
572
|
+
enabled: true,
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (!config.mcp['pantheon-persistence']) {
|
|
576
|
+
config.mcp['pantheon-persistence'] = {
|
|
577
|
+
type: 'local',
|
|
578
|
+
cwd: runtimeTarget,
|
|
579
|
+
command: [memoryPython, 'scripts/mcp_persistence_server.py'],
|
|
580
|
+
enabled: true,
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Default MCP permissions
|
|
585
|
+
config.permission = config.permission || {}
|
|
586
|
+
config.permission.mcp = config.permission.mcp || {}
|
|
587
|
+
if (!config.permission.mcp['pantheon-resources']) {
|
|
588
|
+
config.permission.mcp['pantheon-resources'] = 'allow'
|
|
589
|
+
}
|
|
590
|
+
if (!config.permission.mcp['pantheon-code-mode']) {
|
|
591
|
+
config.permission.mcp['pantheon-code-mode'] = 'ask'
|
|
592
|
+
}
|
|
593
|
+
if (!config.permission.mcp['pantheon-memory']) {
|
|
594
|
+
config.permission.mcp['pantheon-memory'] = 'allow'
|
|
595
|
+
}
|
|
596
|
+
if (!config.permission.mcp['pantheon-persistence']) {
|
|
597
|
+
config.permission.mcp['pantheon-persistence'] = 'allow'
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const configContent = `${JSON.stringify(config, null, 2)}\n`
|
|
602
|
+
const status = writeIfChanged(targetConfigPath, configContent, dryRun)
|
|
603
|
+
if (status === 'created') stats.created++
|
|
604
|
+
else stats.skipped++
|
|
605
|
+
|
|
606
|
+
// -----------------------------------------------------------------------
|
|
607
|
+
// 3.5 Run migrations (--components runtime)
|
|
608
|
+
// -----------------------------------------------------------------------
|
|
609
|
+
if (componentSet.has('runtime')) {
|
|
610
|
+
const currentVersion = detectVersion(target)
|
|
611
|
+
if (currentVersion) {
|
|
612
|
+
const migration = runMigrations(target, currentVersion, { dryRun })
|
|
613
|
+
if (migration.applied > 0) {
|
|
614
|
+
console.log(` ✅ Applied ${migration.applied} migration(s)`)
|
|
615
|
+
for (const msg of migration.messages) {
|
|
616
|
+
console.log(` • ${msg}`)
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// -----------------------------------------------------------------------
|
|
623
|
+
// 4. Setup virtual environment + health check (--components runtime)
|
|
624
|
+
// -----------------------------------------------------------------------
|
|
625
|
+
if (componentSet.has('runtime')) {
|
|
626
|
+
try {
|
|
627
|
+
setupVenv(target, { dryRun })
|
|
628
|
+
const health = healthCheck(target, { dryRun })
|
|
629
|
+
|
|
630
|
+
// Print health summary
|
|
631
|
+
console.log('\n 📊 Health Check:')
|
|
632
|
+
for (const p of health.passed) console.log(` ✅ ${p.check}: ${p.detail}`)
|
|
633
|
+
for (const w of health.warnings) console.log(` ⚠️ ${w.check}: ${w.detail}`)
|
|
634
|
+
for (const f of health.failed) console.log(` ❌ ${f.check}: ${f.detail}`)
|
|
635
|
+
|
|
636
|
+
if (health.failed.length > 0) {
|
|
637
|
+
console.log(` ⚠️ ${health.failed.length} check(s) failed — review above`)
|
|
638
|
+
stats.errors += health.failed.length
|
|
639
|
+
}
|
|
640
|
+
} catch (err) {
|
|
641
|
+
console.error(` ❌ Setup failed: ${err.message}`)
|
|
642
|
+
stats.errors++
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|