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.
Files changed (128) hide show
  1. package/AGENTS.md +37 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1013 -0
  4. package/bin/pantheon-init.mjs +183 -0
  5. package/commands/pantheon-audit.md +25 -0
  6. package/commands/pantheon-bg.md +10 -0
  7. package/commands/pantheon-consolidate.md +11 -0
  8. package/commands/pantheon-deepwork.md +128 -0
  9. package/commands/pantheon-doc.md +10 -0
  10. package/commands/pantheon-focus.md +9 -0
  11. package/commands/pantheon-forget.md +58 -0
  12. package/commands/pantheon-hash.md +11 -0
  13. package/commands/pantheon-optimize.md +79 -0
  14. package/commands/pantheon-remember.md +44 -0
  15. package/commands/pantheon-search.md +48 -0
  16. package/commands/pantheon-status.md +71 -0
  17. package/commands/pantheon-todo.md +11 -0
  18. package/commands/pantheon.md +49 -0
  19. package/docs/AGENT-MCP.md +194 -0
  20. package/docs/ARCHITECTURE.md +384 -0
  21. package/docs/BRANCH-PROTECTION.md +142 -0
  22. package/docs/INDEX.md +81 -0
  23. package/docs/INSTALLATION.md +217 -0
  24. package/docs/MCP.md +238 -0
  25. package/docs/MEMORY.md +471 -0
  26. package/docs/MIGRATION-MEMORY-BANK.md +139 -0
  27. package/docs/PLATFORMS.md +5 -0
  28. package/docs/QUICKSTART.md +49 -0
  29. package/docs/README.md +18 -0
  30. package/docs/RELEASING.md +256 -0
  31. package/docs/SETUP.md +5 -0
  32. package/docs/UPGRADING.md +41 -0
  33. package/docs/mcp-recommendations.md +439 -0
  34. package/docs/mcp-tools.md +156 -0
  35. package/docs/mcp-user-guide.md +204 -0
  36. package/docs/persistence-mcp.md +111 -0
  37. package/package.json +72 -0
  38. package/pantheon.schema.json +158 -0
  39. package/scripts/__init__.py +0 -0
  40. package/scripts/_pantheon_paths.py +68 -0
  41. package/scripts/check-version-consistency.sh +23 -0
  42. package/scripts/code_mode_server.py +202 -0
  43. package/scripts/doctor.mjs +763 -0
  44. package/scripts/generate-prompts.sh +222 -0
  45. package/scripts/generate-routing-docs.mjs +104 -0
  46. package/scripts/hash_verify.py +192 -0
  47. package/scripts/init-pantheon-mcp.sh +118 -0
  48. package/scripts/install/agents-md.mjs +214 -0
  49. package/scripts/install/health-check.mjs +196 -0
  50. package/scripts/install/migrate.mjs +209 -0
  51. package/scripts/install/opencode.mjs +645 -0
  52. package/scripts/install/shared.mjs +655 -0
  53. package/scripts/install/venv.mjs +116 -0
  54. package/scripts/install-mcp.mjs +885 -0
  55. package/scripts/install.mjs +26 -0
  56. package/scripts/manifest.mjs +622 -0
  57. package/scripts/mcp_persistence_server.py +459 -0
  58. package/scripts/mcp_resources_server.py +206 -0
  59. package/scripts/memory_cache.py +78 -0
  60. package/scripts/memory_mcp_server.py +605 -0
  61. package/scripts/paths.py +64 -0
  62. package/scripts/prune_context.py +72 -0
  63. package/scripts/release-bundle.mjs +109 -0
  64. package/scripts/scrub-secrets.py +282 -0
  65. package/scripts/scrub_secrets.py +281 -0
  66. package/scripts/test-context-compression.sh +166 -0
  67. package/scripts/themis_heuristic_scan.py +287 -0
  68. package/scripts/todo_enforcer.py +242 -0
  69. package/scripts/uninstall.mjs +1057 -0
  70. package/scripts/validate-routing.mjs +160 -0
  71. package/scripts/validate_agent_frontmatter.py +226 -0
  72. package/scripts/versioning.mjs +254 -0
  73. package/skills-lock.json +16 -0
  74. package/src/agents/aphrodite.md +162 -0
  75. package/src/agents/apollo.md +109 -0
  76. package/src/agents/athena.md +226 -0
  77. package/src/agents/demeter.md +146 -0
  78. package/src/agents/gaia.md +82 -0
  79. package/src/agents/hephaestus.md +105 -0
  80. package/src/agents/hermes.md +302 -0
  81. package/src/agents/iris.md +99 -0
  82. package/src/agents/mnemosyne.md +226 -0
  83. package/src/agents/nyx.md +87 -0
  84. package/src/agents/prometheus.md +199 -0
  85. package/src/agents/talos.md +93 -0
  86. package/src/agents/themis.md +187 -0
  87. package/src/agents/zeus.md +209 -0
  88. package/src/instructions/agent-return-format.instructions.md +26 -0
  89. package/src/instructions/backend-standards.instructions.md +45 -0
  90. package/src/instructions/documentation-standards.instructions.md +53 -0
  91. package/src/instructions/frontend-standards.instructions.md +46 -0
  92. package/src/instructions/memory-protocol.instructions.md +67 -0
  93. package/src/instructions/yagni.instructions.md +21 -0
  94. package/src/instructions/zeus-anti-stall.instructions.md +72 -0
  95. package/src/instructions/zeus-communication-rules.instructions.md +15 -0
  96. package/src/instructions/zeus-council-synthesis.instructions.md +105 -0
  97. package/src/instructions/zeus-timeout-retry.instructions.md +127 -0
  98. package/src/mcp/_pantheon_paths.py +67 -0
  99. package/src/mcp/code_mode_server.py +202 -0
  100. package/src/mcp/init-pantheon-mcp.sh +118 -0
  101. package/src/mcp/install-mcp.mjs +885 -0
  102. package/src/mcp/mcp_persistence_server.py +458 -0
  103. package/src/mcp/mcp_resources_server.py +205 -0
  104. package/src/mcp/memory_mcp_server.py +604 -0
  105. package/src/mcp/requirements-mcp-core.txt +5 -0
  106. package/src/mcp/requirements-mcp.txt +5 -0
  107. package/src/plugin.ts +19 -0
  108. package/src/plugins/tui/dist/tui.js +143 -0
  109. package/src/plugins/tui/dist/tui.tsx +144 -0
  110. package/src/plugins/tui/package.json +32 -0
  111. package/src/plugins/tui/src/index.tsx +144 -0
  112. package/src/plugins/tui/tsdown.config.ts +22 -0
  113. package/src/routing.yml +499 -0
  114. package/src/skills/README.md +230 -0
  115. package/src/skills/agent-coordination/SKILL.md +95 -0
  116. package/src/skills/artifact-management/SKILL.md +118 -0
  117. package/src/skills/auto-continue/SKILL.md +280 -0
  118. package/src/skills/code-review-checklist/SKILL.md +139 -0
  119. package/src/skills/context-compression/SKILL.md +861 -0
  120. package/src/skills/git-workflow-and-versioning/SKILL.md +32 -0
  121. package/src/skills/incremental-implementation/SKILL.md +27 -0
  122. package/src/skills/memory-bank/SKILL.md +165 -0
  123. package/src/skills/orchestration-workflow/SKILL.md +311 -0
  124. package/src/skills/security-hardening/SKILL.md +36 -0
  125. package/src/skills/session-goal/SKILL.md +138 -0
  126. package/src/skills/spec-driven-development/SKILL.md +23 -0
  127. package/src/skills/tdd-with-agents/SKILL.md +170 -0
  128. 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
+ }