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,214 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* agents-md.mjs — Dynamic AGENTS.md generator from canonical agent frontmatter
|
|
4
|
+
*
|
|
5
|
+
* Reads all agents/*.agent.md files, extracts name + description from frontmatter,
|
|
6
|
+
* and generates AGENTS.md content dynamically. Eliminates hardcoded agent tables
|
|
7
|
+
* in platform installers (claude.mjs, cursor.mjs, windsurf.mjs, cline.mjs, etc.).
|
|
8
|
+
*
|
|
9
|
+
* Usage (CLI):
|
|
10
|
+
* node agents-md.mjs # write AGENTS.md to cwd
|
|
11
|
+
* node agents-md.mjs /target/dir # write AGENTS.md to target dir
|
|
12
|
+
* node agents-md.mjs /target/dir "Claude" # with platform name
|
|
13
|
+
*
|
|
14
|
+
* Usage (import):
|
|
15
|
+
* import { readCanonicalAgents, generateAgentsMd } from './agents-md.mjs';
|
|
16
|
+
* const agents = readCanonicalAgents();
|
|
17
|
+
* const md = generateAgentsMd(agents, 'Claude Code', extraSections);
|
|
18
|
+
* writeFileSync('/path/AGENTS.md', md);
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
22
|
+
import { dirname, join } from 'node:path'
|
|
23
|
+
import { fileURLToPath } from 'node:url'
|
|
24
|
+
import yaml from 'js-yaml'
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Paths
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
31
|
+
export const ROOT = join(__dirname, '..', '..')
|
|
32
|
+
export const AGENTS_DIR = join(ROOT, 'src', 'agents')
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Frontmatter parsing
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Parse YAML frontmatter from an agent markdown file content.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} content - Raw content of a .agent.md file
|
|
42
|
+
* @returns {{ name: string, description: string, color?: string } | null}
|
|
43
|
+
* Parsed agent metadata, or null if no valid frontmatter with a `name` field.
|
|
44
|
+
*/
|
|
45
|
+
function parseAgentFrontmatter(content) {
|
|
46
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
47
|
+
if (!match) return null
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const fm = yaml.load(match[1])
|
|
51
|
+
if (!fm || typeof fm !== 'object' || !fm.name) return null
|
|
52
|
+
return {
|
|
53
|
+
name: String(fm.name),
|
|
54
|
+
description: fm.description ? String(fm.description) : '',
|
|
55
|
+
color: fm.color ? String(fm.color) : undefined,
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Exported API
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Read all canonical agents/*.agent.md files, parse frontmatter,
|
|
68
|
+
* and return an array of agent metadata objects sorted alphabetically by name.
|
|
69
|
+
*
|
|
70
|
+
* Skips files that don't match the `*.agent.md` pattern, files without
|
|
71
|
+
* valid YAML frontmatter, and entries without a `name` field.
|
|
72
|
+
*
|
|
73
|
+
* @returns {Array<{ name: string, description: string, color?: string }>}
|
|
74
|
+
*/
|
|
75
|
+
export function readCanonicalAgents() {
|
|
76
|
+
if (!existsSync(AGENTS_DIR)) return []
|
|
77
|
+
|
|
78
|
+
const files = readdirSync(AGENTS_DIR)
|
|
79
|
+
.filter((f) => f.endsWith('.agent.md') || f.endsWith('.md'))
|
|
80
|
+
.sort()
|
|
81
|
+
|
|
82
|
+
const agents = []
|
|
83
|
+
for (const file of files) {
|
|
84
|
+
const filePath = join(AGENTS_DIR, file)
|
|
85
|
+
let content
|
|
86
|
+
try {
|
|
87
|
+
content = readFileSync(filePath, 'utf8')
|
|
88
|
+
} catch {
|
|
89
|
+
continue // skip unreadable files
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const parsed = parseAgentFrontmatter(content)
|
|
93
|
+
if (parsed) {
|
|
94
|
+
agents.push(parsed)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return agents.sort((a, b) => a.name.localeCompare(b.name))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Generate a markdown table from an agents array.
|
|
103
|
+
*
|
|
104
|
+
* @param {Array<{ name: string, description: string }>} agents
|
|
105
|
+
* @returns {string} Markdown table string.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* | Agent | Role |
|
|
109
|
+
* |-------|------|
|
|
110
|
+
* | @zeus | Central orchestrator |
|
|
111
|
+
* | @hermes | Backend (FastAPI) |
|
|
112
|
+
*/
|
|
113
|
+
export function generateAgentsTable(agents) {
|
|
114
|
+
const rows = ['| Agent | Role |', '|-------|------|']
|
|
115
|
+
for (const agent of agents) {
|
|
116
|
+
const role = (agent.description || '').replace(/\n/g, ' ')
|
|
117
|
+
rows.push(`| @${agent.name} | ${role} |`)
|
|
118
|
+
}
|
|
119
|
+
return rows.join('\n')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Generate complete AGENTS.md content.
|
|
124
|
+
*
|
|
125
|
+
* @param {Array<{ name: string, description: string }>} agents
|
|
126
|
+
* Agent list from `readCanonicalAgents()`.
|
|
127
|
+
* @param {string} [platformName='']
|
|
128
|
+
* Optional platform name appended to the title (e.g., 'Cursor', 'Cline').
|
|
129
|
+
* @param {string} [extraSections='']
|
|
130
|
+
* Optional extra markdown content inserted between the agent table and
|
|
131
|
+
* the Commands section. Pass platform-specific notes, workflow references,
|
|
132
|
+
* setup instructions, etc.
|
|
133
|
+
* @returns {string} Complete AGENTS.md content.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* const md = generateAgentsMd(agents, 'Cline', '## Cline Setup\n...');
|
|
137
|
+
* writeFileSync('AGENTS.md', md);
|
|
138
|
+
*/
|
|
139
|
+
export function generateAgentsMd(agents, platformName = '', extraSections = '') {
|
|
140
|
+
const agentCount = agents.length
|
|
141
|
+
const title = platformName
|
|
142
|
+
? `# Pantheon Agent System — ${platformName}`
|
|
143
|
+
: '# Pantheon Agent System'
|
|
144
|
+
const agentTable = generateAgentsTable(agents)
|
|
145
|
+
|
|
146
|
+
const sections = [
|
|
147
|
+
title,
|
|
148
|
+
'',
|
|
149
|
+
`This project uses the Pantheon multi-agent framework with ${agentCount} specialized agents.`,
|
|
150
|
+
'',
|
|
151
|
+
'## Available Agents',
|
|
152
|
+
'',
|
|
153
|
+
agentTable,
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
if (extraSections) {
|
|
157
|
+
sections.push('', extraSections.trimEnd())
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
sections.push(
|
|
161
|
+
'',
|
|
162
|
+
'## Commands',
|
|
163
|
+
'',
|
|
164
|
+
'- Build: `npm run build`',
|
|
165
|
+
'- Test: `npm test`',
|
|
166
|
+
'- Lint: `npm run lint`',
|
|
167
|
+
'',
|
|
168
|
+
'## Conventions',
|
|
169
|
+
'',
|
|
170
|
+
'- TDD: Write failing test first, then implement',
|
|
171
|
+
'- Coverage minimum: 80%',
|
|
172
|
+
'- Async/await on all I/O',
|
|
173
|
+
'- Type hints on all functions',
|
|
174
|
+
'',
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
return sections.join('\n')
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* High-level convenience function that reads canonical agents, generates
|
|
182
|
+
* AGENTS.md content, and writes it to the target directory.
|
|
183
|
+
*
|
|
184
|
+
* @param {string} targetDir - Directory to write AGENTS.md into.
|
|
185
|
+
* @param {string} [platformName=''] - Optional platform name for the title.
|
|
186
|
+
* @param {string} [extraSections=''] - Optional extra sections to include.
|
|
187
|
+
* @returns {string} The generated content (also written to disk).
|
|
188
|
+
*/
|
|
189
|
+
export function generateAgentsMdForPlatform(targetDir, platformName = '', extraSections = '') {
|
|
190
|
+
const agents = readCanonicalAgents()
|
|
191
|
+
const content = generateAgentsMd(agents, platformName, extraSections)
|
|
192
|
+
const outputPath = join(targetDir, 'AGENTS.md')
|
|
193
|
+
writeFileSync(outputPath, content, 'utf8')
|
|
194
|
+
return content
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
// CLI entry point
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
202
|
+
const targetDir = process.argv[2] || process.cwd()
|
|
203
|
+
const platformName = process.argv[3] || ''
|
|
204
|
+
const extraSections = process.argv[4] || ''
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const content = generateAgentsMdForPlatform(targetDir, platformName, extraSections)
|
|
208
|
+
const agentCount = (content.match(/^\| @/gm) || []).length
|
|
209
|
+
console.log(`✅ Generated AGENTS.md in ${targetDir} (${agentCount} agents)`)
|
|
210
|
+
} catch (err) {
|
|
211
|
+
console.error(`❌ Failed to generate AGENTS.md: ${err.message}`)
|
|
212
|
+
process.exit(1)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* health-check.mjs — Post-installation validation checks
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node scripts/install/health-check.mjs --target ~/.config/opencode
|
|
8
|
+
* node scripts/install/health-check.mjs --target ~/.config/opencode --dry-run
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { spawnSync } from 'node:child_process'
|
|
12
|
+
import { existsSync } from 'node:fs'
|
|
13
|
+
import { join } from 'node:path'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Run health checks against a Pantheon installation target.
|
|
17
|
+
* Reports results as { passed, failed, warnings } arrays.
|
|
18
|
+
* NON-FATAL — all checks are reported, none throw.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} target - Installation directory
|
|
21
|
+
* @param {{ dryRun?: boolean }} [options]
|
|
22
|
+
* @returns {{ passed: Array<{check: string, detail: string}>, failed: Array<{check: string, detail: string}>, warnings: Array<{check: string, detail: string}> }}
|
|
23
|
+
*/
|
|
24
|
+
export function healthCheck(target, { dryRun = false } = {}) {
|
|
25
|
+
const results = { passed: [], failed: [], warnings: [] }
|
|
26
|
+
const python = findPython(target)
|
|
27
|
+
|
|
28
|
+
// Check 1: Critical runtime scripts exist
|
|
29
|
+
const scripts = ['mcp_persistence_server.py', '_pantheon_paths.py']
|
|
30
|
+
for (const script of scripts) {
|
|
31
|
+
const path = join(target, 'scripts', script)
|
|
32
|
+
if (existsSync(path)) {
|
|
33
|
+
results.passed.push({ check: `scripts/${script}`, detail: 'exists' })
|
|
34
|
+
} else {
|
|
35
|
+
results.failed.push({
|
|
36
|
+
check: `scripts/${script}`,
|
|
37
|
+
detail: 'NOT FOUND',
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (dryRun) return results
|
|
43
|
+
|
|
44
|
+
// Check 2: Syntax check on each MCP server script
|
|
45
|
+
const mcpScripts = [
|
|
46
|
+
'mcp_persistence_server.py',
|
|
47
|
+
'mcp_resources_server.py',
|
|
48
|
+
'code_mode_server.py',
|
|
49
|
+
'memory_mcp_server.py',
|
|
50
|
+
]
|
|
51
|
+
for (const script of mcpScripts) {
|
|
52
|
+
const path = join(target, 'scripts', script)
|
|
53
|
+
if (existsSync(path) && python) {
|
|
54
|
+
const result = spawnSync(python, ['-m', 'py_compile', path], {
|
|
55
|
+
stdio: 'pipe',
|
|
56
|
+
})
|
|
57
|
+
if (result.status === 0) {
|
|
58
|
+
results.passed.push({
|
|
59
|
+
check: `syntax:${script}`,
|
|
60
|
+
detail: 'valid',
|
|
61
|
+
})
|
|
62
|
+
} else {
|
|
63
|
+
results.failed.push({
|
|
64
|
+
check: `syntax:${script}`,
|
|
65
|
+
detail: result.stderr.toString().trim().split('\n').slice(-1)[0],
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check 3: _pantheon_paths import works
|
|
72
|
+
if (python) {
|
|
73
|
+
const importCmd = [
|
|
74
|
+
'-c',
|
|
75
|
+
"import sys; sys.path.insert(0, '" +
|
|
76
|
+
join(target, 'scripts') +
|
|
77
|
+
"'); " +
|
|
78
|
+
'from _pantheon_paths import pantheon_home; print(pantheon_home())',
|
|
79
|
+
]
|
|
80
|
+
const result = spawnSync(python, importCmd, { stdio: 'pipe' })
|
|
81
|
+
if (result.status === 0) {
|
|
82
|
+
results.passed.push({
|
|
83
|
+
check: '_pantheon_paths import',
|
|
84
|
+
detail: result.stdout.toString().trim(),
|
|
85
|
+
})
|
|
86
|
+
} else {
|
|
87
|
+
results.failed.push({
|
|
88
|
+
check: '_pantheon_paths import',
|
|
89
|
+
detail: result.stderr.toString().trim().split('\n').slice(-1)[0],
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Check 3.5: mcp SDK importable (required by ALL MCP servers)
|
|
95
|
+
if (python) {
|
|
96
|
+
const mcpResult = spawnSync(
|
|
97
|
+
python,
|
|
98
|
+
['-c', 'from mcp.server.fastmcp import FastMCP; print(FastMCP.__module__)'],
|
|
99
|
+
{ stdio: 'pipe' },
|
|
100
|
+
)
|
|
101
|
+
if (mcpResult.status === 0) {
|
|
102
|
+
results.passed.push({ check: 'mcp-sdk', detail: mcpResult.stdout.toString().trim() })
|
|
103
|
+
} else {
|
|
104
|
+
results.failed.push({ check: 'mcp-sdk', detail: 'NOT INSTALLED — all MCP servers will fail' })
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Check 4: chromadb importable
|
|
108
|
+
if (python) {
|
|
109
|
+
const result = spawnSync(
|
|
110
|
+
python,
|
|
111
|
+
['-c', 'from fastembed import TextEmbedding; print(TextEmbedding.__module__)'],
|
|
112
|
+
{ stdio: 'pipe' },
|
|
113
|
+
)
|
|
114
|
+
if (result.status === 0) {
|
|
115
|
+
results.passed.push({
|
|
116
|
+
check: 'chromadb',
|
|
117
|
+
detail: result.stdout.toString().trim(),
|
|
118
|
+
})
|
|
119
|
+
} else {
|
|
120
|
+
results.warnings.push({
|
|
121
|
+
check: 'chromadb',
|
|
122
|
+
detail: 'NOT INSTALLED — semantic search will fail',
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check 5: SQLite FTS5 available
|
|
128
|
+
if (python) {
|
|
129
|
+
const fts5Cmd = [
|
|
130
|
+
'-c',
|
|
131
|
+
'import sqlite3; ' +
|
|
132
|
+
"row = sqlite3.connect(':memory:').execute('PRAGMA compile_options').fetchall(); " +
|
|
133
|
+
"fts5 = any('ENABLE_FTS5' in str(r[0]) for r in row); " +
|
|
134
|
+
"print(f'SQLite {sqlite3.sqlite_version} FTS5={fts5}')",
|
|
135
|
+
]
|
|
136
|
+
const result = spawnSync(python, fts5Cmd, { stdio: 'pipe' })
|
|
137
|
+
if (result.status === 0) {
|
|
138
|
+
results.passed.push({
|
|
139
|
+
check: 'sqlite-fts5',
|
|
140
|
+
detail: result.stdout.toString().trim(),
|
|
141
|
+
})
|
|
142
|
+
} else {
|
|
143
|
+
results.failed.push({
|
|
144
|
+
check: 'sqlite-fts5',
|
|
145
|
+
detail: result.stderr.toString().trim().split('\n').slice(-1)[0],
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return results
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Find the target's venv python, falling back to system python3.
|
|
155
|
+
* @param {string} target
|
|
156
|
+
* @returns {string|null}
|
|
157
|
+
*/
|
|
158
|
+
function findPython(target) {
|
|
159
|
+
const candidates = [
|
|
160
|
+
join(target, '.venv', 'bin', 'python3'),
|
|
161
|
+
join(target, '.venv', 'bin', 'python'),
|
|
162
|
+
]
|
|
163
|
+
for (const c of candidates) {
|
|
164
|
+
if (existsSync(c)) return c
|
|
165
|
+
}
|
|
166
|
+
// Check if python3 is even available
|
|
167
|
+
const probe = spawnSync('which', ['python3'], { stdio: 'pipe' })
|
|
168
|
+
return probe.status === 0 ? 'python3' : null
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// CLI entry point
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
function main() {
|
|
175
|
+
const args = process.argv.slice(2)
|
|
176
|
+
const targetIdx = args.indexOf('--target')
|
|
177
|
+
const target = targetIdx !== -1 ? args[targetIdx + 1] : process.cwd()
|
|
178
|
+
const dryRun = args.includes('--dry-run')
|
|
179
|
+
|
|
180
|
+
const results = healthCheck(target, { dryRun })
|
|
181
|
+
|
|
182
|
+
console.log('\n 📊 Health Check:')
|
|
183
|
+
for (const p of results.passed) console.log(` ✅ ${p.check}: ${p.detail}`)
|
|
184
|
+
for (const w of results.warnings) console.log(` ⚠️ ${w.check}: ${w.detail}`)
|
|
185
|
+
for (const f of results.failed) console.log(` ❌ ${f.check}: ${f.detail}`)
|
|
186
|
+
|
|
187
|
+
console.log(
|
|
188
|
+
`\n Summary: ${results.passed.length} passed, ${results.warnings.length} warnings, ${results.failed.length} failed`,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if (results.failed.length > 0) process.exit(1)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) {
|
|
195
|
+
main()
|
|
196
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* migrate.mjs — Multi-version migration logic for Pantheon installations
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node scripts/install/migrate.mjs --target ~/.config/opencode
|
|
7
|
+
* node scripts/install/migrate.mjs --target ~/.config/opencode --dry-run
|
|
8
|
+
* node scripts/install/migrate.mjs --target ~/.config/opencode --force
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
12
|
+
import { dirname, join } from 'node:path'
|
|
13
|
+
import { fileURLToPath } from 'node:url'
|
|
14
|
+
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
16
|
+
const ROOT = join(__dirname, '..', '..')
|
|
17
|
+
|
|
18
|
+
// Package version from project root
|
|
19
|
+
const PANTHON_VERSION = readPackageVersion()
|
|
20
|
+
|
|
21
|
+
function readPackageVersion() {
|
|
22
|
+
try {
|
|
23
|
+
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'))
|
|
24
|
+
return pkg.version || '0.0.0'
|
|
25
|
+
} catch {
|
|
26
|
+
return '0.0.0'
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Read the installed version from the target's install state file.
|
|
32
|
+
* @param {string} target
|
|
33
|
+
* @returns {string|null} - version string or null for fresh install
|
|
34
|
+
*/
|
|
35
|
+
export function detectVersion(target) {
|
|
36
|
+
const stateFile = join(target, '.pantheon', 'install-state.json')
|
|
37
|
+
if (existsSync(stateFile)) {
|
|
38
|
+
try {
|
|
39
|
+
const state = JSON.parse(readFileSync(stateFile, 'utf8'))
|
|
40
|
+
return state.pantheon_version || state.version || 'unknown'
|
|
41
|
+
} catch {
|
|
42
|
+
/* corrupted state — treat as unknown */
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return null // fresh install
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Run version-specific migrations sequentially.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} target - Installation directory
|
|
52
|
+
* @param {string|null} currentVersion - Detected version (null = fresh install)
|
|
53
|
+
* @param {{ dryRun?: boolean }} [options]
|
|
54
|
+
* @returns {{ applied: number, messages: string[] }}
|
|
55
|
+
*/
|
|
56
|
+
export function runMigrations(target, currentVersion, { dryRun = false } = {}) {
|
|
57
|
+
const applied = []
|
|
58
|
+
const semver = parseSemver(currentVersion)
|
|
59
|
+
|
|
60
|
+
// ── Migration: v3.10.0 → v3.18.0 ──────────────────────────────────
|
|
61
|
+
// Fix dangling references, add Memory Protocol
|
|
62
|
+
if (semver && lt(semver, [3, 18, 0])) {
|
|
63
|
+
applied.push({
|
|
64
|
+
from: formatSemver(semver),
|
|
65
|
+
to: '3.18.0',
|
|
66
|
+
description: 'Fix dangling references, add Memory Protocol',
|
|
67
|
+
})
|
|
68
|
+
semver[0] = 3
|
|
69
|
+
semver[1] = 18
|
|
70
|
+
semver[2] = 0
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Migration: v3.18.0 → v3.19.0 ──────────────────────────────────
|
|
74
|
+
// Add runtime component, _pantheon_paths
|
|
75
|
+
if (semver && lt(semver, [3, 19, 0])) {
|
|
76
|
+
applied.push({
|
|
77
|
+
from: formatSemver(semver),
|
|
78
|
+
to: '3.19.0',
|
|
79
|
+
description: 'Add runtime component, _pantheon_paths',
|
|
80
|
+
})
|
|
81
|
+
semver[0] = 3
|
|
82
|
+
semver[1] = 19
|
|
83
|
+
semver[2] = 0
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── Migration: v3.19.0 → v3.19.1 ──────────────────────────────────
|
|
87
|
+
// Add persistence MCP, fix 157 dangling refs
|
|
88
|
+
if (semver && lt(semver, [3, 19, 1])) {
|
|
89
|
+
applied.push({
|
|
90
|
+
from: formatSemver(semver),
|
|
91
|
+
to: '3.19.1',
|
|
92
|
+
description: 'Add persistence MCP, fix dangling refs',
|
|
93
|
+
})
|
|
94
|
+
semver[0] = 3
|
|
95
|
+
semver[1] = 19
|
|
96
|
+
semver[2] = 1
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── Write updated state ───────────────────────────────────────────
|
|
100
|
+
if (applied.length > 0 && !dryRun) {
|
|
101
|
+
writeInstallState(target, formatSemver(semver))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
applied: applied.length,
|
|
106
|
+
messages: applied.map((m) => `${m.from} → ${m.to}: ${m.description}`),
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Write install state with current version to target.
|
|
112
|
+
*/
|
|
113
|
+
function writeInstallState(target, version) {
|
|
114
|
+
const stateDir = join(target, '.pantheon')
|
|
115
|
+
const stateFile = join(stateDir, 'install-state.json')
|
|
116
|
+
if (!existsSync(stateDir)) {
|
|
117
|
+
mkdirSync(stateDir, { recursive: true })
|
|
118
|
+
}
|
|
119
|
+
const state = {
|
|
120
|
+
pantheon_version: version,
|
|
121
|
+
updated_at: new Date().toISOString(),
|
|
122
|
+
}
|
|
123
|
+
writeFileSync(stateFile, `${JSON.stringify(state, null, 2)}\n`, 'utf8')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// Minimal semver helpers (no external deps)
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Parse "x.y.z" → [major, minor, patch] or null.
|
|
132
|
+
* @param {string|null} v
|
|
133
|
+
* @returns {number[]|null}
|
|
134
|
+
*/
|
|
135
|
+
function parseSemver(v) {
|
|
136
|
+
if (!v || typeof v !== 'string') return null
|
|
137
|
+
const parts = v.split('.').map(Number)
|
|
138
|
+
if (parts.length < 2 || parts.some(Number.isNaN)) return null
|
|
139
|
+
return [parts[0], parts[1], parts[2] ?? 0]
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Format [major, minor, patch] → "x.y.z".
|
|
144
|
+
* @param {number[]} v
|
|
145
|
+
* @returns {string}
|
|
146
|
+
*/
|
|
147
|
+
function formatSemver(v) {
|
|
148
|
+
return `${v[0]}.${v[1]}.${v[2]}`
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Compare two semver arrays: is a < b?
|
|
153
|
+
* @param {number[]} a
|
|
154
|
+
* @param {number[]} b
|
|
155
|
+
* @returns {boolean}
|
|
156
|
+
*/
|
|
157
|
+
function lt(a, b) {
|
|
158
|
+
for (let i = 0; i < 3; i++) {
|
|
159
|
+
if ((a[i] ?? 0) !== (b[i] ?? 0)) {
|
|
160
|
+
return (a[i] ?? 0) < (b[i] ?? 0)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return false
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// CLI entry point
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
function main() {
|
|
170
|
+
const args = process.argv.slice(2)
|
|
171
|
+
const targetIdx = args.indexOf('--target')
|
|
172
|
+
const target = targetIdx !== -1 ? args[targetIdx + 1] : process.cwd()
|
|
173
|
+
const dryRun = args.includes('--dry-run')
|
|
174
|
+
const force = args.includes('--force')
|
|
175
|
+
|
|
176
|
+
const currentVersion = detectVersion(target)
|
|
177
|
+
if (!currentVersion) {
|
|
178
|
+
// Fresh install — write state and exit
|
|
179
|
+
if (!dryRun) {
|
|
180
|
+
writeInstallState(target, PANTHON_VERSION)
|
|
181
|
+
console.log(` ✅ Fresh install — state written (${PANTHON_VERSION})`)
|
|
182
|
+
} else {
|
|
183
|
+
console.log(` ✅ Fresh install — would write state (${PANTHON_VERSION})`)
|
|
184
|
+
}
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.log(` Current version: ${currentVersion}`)
|
|
189
|
+
|
|
190
|
+
if (!force && currentVersion === PANTHON_VERSION) {
|
|
191
|
+
console.log(' ⏭️ Already at latest version — no migrations needed')
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const migration = runMigrations(target, currentVersion, { dryRun })
|
|
196
|
+
|
|
197
|
+
if (migration.applied > 0) {
|
|
198
|
+
console.log(` ✅ Applied ${migration.applied} migration(s):`)
|
|
199
|
+
for (const msg of migration.messages) {
|
|
200
|
+
console.log(` • ${msg}`)
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
console.log(' ⏭️ No applicable migrations')
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) {
|
|
208
|
+
main()
|
|
209
|
+
}
|