pantheon-opencode 1.4.2 → 1.5.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.pantheon/code-mode/.gitkeep +0 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-1.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-10.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-11.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-2.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-3.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-4.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-5.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-6.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-7.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-8.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-9.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/heartbeat.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/session.json +11 -0
- package/.pantheon/code-mode/README.md +93 -0
- package/.pantheon/code-mode/checkpoint-session.py +7 -0
- package/.pantheon/code-mode/checkpoint-session.sh +11 -0
- package/.pantheon/code-mode/checkpoint_session.py +481 -0
- package/.pantheon/code-mode/compress-inline.py +450 -0
- package/.pantheon/code-mode/eval-llm-judge.py +255 -0
- package/.pantheon/code-mode/eval-monte-carlo.py +368 -0
- package/.pantheon/code-mode/eval-run.py +197 -0
- package/.pantheon/code-mode/eval-static.py +321 -0
- package/.pantheon/code-mode/example-sync.sh +5 -0
- package/.pantheon/code-mode/session-end-save.py +186 -0
- package/.pantheon/code-mode/session-end-save.sh +18 -0
- package/.pantheon/code-mode/test-allowed.sh +5 -0
- package/.pantheon/code-mode/test-auto-continue-checkpoint.sh +179 -0
- package/.pantheon/code-mode/test-env.sh +7 -0
- package/.pantheon/code-mode/test-prlimit.sh +9 -0
- package/.pantheon/code-mode/test-timeout.sh +6 -0
- package/README.md +158 -1189
- package/README.pt-BR.md +222 -0
- package/bin/pantheon-init.mjs +30 -11
- package/opencode.json +1 -0
- package/package.json +14 -3
- package/plugin.json +15 -0
- package/scripts/_pantheon_paths.py +48 -7
- package/scripts/ci-validate-yaml.py +7 -2
- package/scripts/code_mode_server.py +33 -2
- package/scripts/doctor.mjs +135 -127
- package/scripts/generate-preset-docs.mjs +127 -66
- package/scripts/generate-prompts.sh +2 -2
- package/scripts/install/cli-ui.mjs +8 -1
- package/scripts/install/config-migration.mjs +401 -0
- package/scripts/install/health-check.mjs +3 -3
- package/scripts/install/model-picker.mjs +72 -23
- package/scripts/install/opencode-version.mjs +47 -0
- package/scripts/install/opencode.mjs +261 -88
- package/scripts/install/plugin.mjs +112 -42
- package/scripts/install/shared.mjs +13 -158
- package/scripts/install-mcp.mjs +44 -232
- package/scripts/mcp_persistence_server.py +344 -15
- package/scripts/mcp_resources_server.py +144 -40
- package/scripts/memory_mcp_server.py +2 -2
- package/scripts/recover-zenodo-deposition.mjs +146 -0
- package/scripts/sync-tui.mjs +32 -7
- package/scripts/test-opencode-v1-v2-sandbox.sh +591 -0
- package/scripts/uninstall.mjs +298 -954
- package/scripts/validate-zenodo-release.mjs +154 -0
- package/scripts/zenodo-artifact.mjs +21 -0
- package/scripts/zenodo-release-state.mjs +53 -0
- package/src/mcp/_pantheon_paths.py +48 -7
- package/src/mcp/code_mode_server.py +33 -2
- package/src/mcp/eval_store.py +1 -1
- package/src/mcp/mcp_persistence_server.py +344 -15
- package/src/mcp/mcp_resources_server.py +144 -40
- package/src/mcp/pantheon_vision_server.py +1 -1
- package/src/mcp/toon_codec.py +491 -0
- package/src/pantheon/background-job-board.ts +67 -1
- package/src/pantheon/cost-command.ts +6 -2
- package/src/pantheon/delegate-manager.ts +384 -0
- package/src/pantheon/delegate-task-adapter.ts +444 -0
- package/src/pantheon/delegation-notify.ts +35 -20
- package/src/pantheon/model-command.ts +86 -32
- package/src/pantheon/token-opt.ts +555 -0
- package/src/pantheon/toon-codec.ts +481 -0
- package/src/pantheon/tui-version-check.ts +1 -3
- package/src/pantheon/v2-bridge.ts +129 -0
- package/src/pantheon/v2-events.ts +131 -0
- package/src/pantheon/v2-hooks.ts +287 -0
- package/src/pantheon/v2-tool-definitions.ts +331 -0
- package/src/plugin-v2/index.ts +21 -0
- package/src/plugin-v2.ts +836 -0
- package/src/plugin.ts +57 -10
- package/src/plugins/tui/dist/server.d.ts +1 -1
- package/src/plugins/tui/dist/server.d.ts.map +1 -1
- package/src/plugins/tui/dist/server.js +1 -1
- package/src/plugins/tui/dist/server.js.map +1 -1
- package/src/plugins/tui/dist/tui.d.ts +10 -7
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +11 -8
- package/src/plugins/tui/dist/tui.js.map +1 -1
- package/src/plugins/tui/dist/tui.tsx +22 -16
- package/src/plugins/tui/package-lock.json +51 -51
- package/src/plugins/tui/package.json +7 -7
- package/src/plugins/tui/src/index.tsx +22 -16
- package/src/skills/README.md +0 -2
package/scripts/uninstall.mjs
CHANGED
|
@@ -1,32 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* The reverse of the installer.
|
|
8
|
-
*
|
|
9
|
-
* Usage:
|
|
10
|
-
* node scripts/uninstall.mjs auto-detect, cwd
|
|
11
|
-
* node scripts/uninstall.mjs --target /path/to/project auto-detect, target
|
|
12
|
-
* node scripts/uninstall.mjs --platforms opencode,claude specific platforms
|
|
13
|
-
* node scripts/uninstall.mjs --target /path --platforms all all platforms
|
|
14
|
-
* node scripts/uninstall.mjs --dry-run preview without deleting
|
|
15
|
-
* node scripts/uninstall.mjs --force skip confirmation
|
|
16
|
-
* node scripts/uninstall.mjs --help show this help
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { existsSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
20
|
-
import { join } from 'node:path'
|
|
3
|
+
/** Remove only files and registrations owned by Pantheon for OpenCode. */
|
|
4
|
+
import { lstatSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
5
|
+
import { homedir } from 'node:os'
|
|
6
|
+
import { basename, join, relative, resolve } from 'node:path'
|
|
21
7
|
import { createInterface } from 'node:readline'
|
|
22
8
|
import { isatty } from 'node:tty'
|
|
23
|
-
import { detectPlatforms, resolveTarget } from './install/shared.mjs'
|
|
24
9
|
|
|
25
|
-
|
|
26
|
-
// Constants
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
|
|
29
|
-
const PANTHEON_AGENT_NAMES = [
|
|
10
|
+
const AGENT_NAMES = new Set([
|
|
30
11
|
'zeus',
|
|
31
12
|
'athena',
|
|
32
13
|
'apollo',
|
|
@@ -36,1020 +17,383 @@ const PANTHEON_AGENT_NAMES = [
|
|
|
36
17
|
'themis',
|
|
37
18
|
'prometheus',
|
|
38
19
|
'hephaestus',
|
|
39
|
-
|
|
40
20
|
'nyx',
|
|
41
21
|
'gaia',
|
|
42
22
|
'iris',
|
|
43
23
|
'mnemosyne',
|
|
44
24
|
'talos',
|
|
45
|
-
]
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
claude: 'Claude Code',
|
|
50
|
-
cursor: 'Cursor',
|
|
51
|
-
windsurf: 'Windsurf',
|
|
52
|
-
copilot: 'VS Code / Copilot',
|
|
53
|
-
continue: 'Continue.dev',
|
|
54
|
-
cline: 'Cline',
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const ALL_PLATFORMS = Object.keys(PLATFORM_LABELS)
|
|
58
|
-
|
|
59
|
-
// ---------------------------------------------------------------------------
|
|
60
|
-
// Summary tracker
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
|
|
63
|
-
const stats = {
|
|
64
|
-
opencode: { removed: 0, skipped: 0, errors: 0 },
|
|
65
|
-
claude: { removed: 0, skipped: 0, errors: 0 },
|
|
66
|
-
cursor: { removed: 0, skipped: 0, errors: 0 },
|
|
67
|
-
windsurf: { removed: 0, skipped: 0, errors: 0 },
|
|
68
|
-
copilot: { removed: 0, skipped: 0, errors: 0 },
|
|
69
|
-
continue: { removed: 0, skipped: 0, errors: 0 },
|
|
70
|
-
cline: { removed: 0, skipped: 0, errors: 0 },
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
// Help
|
|
75
|
-
// ---------------------------------------------------------------------------
|
|
25
|
+
])
|
|
26
|
+
const OWNERSHIP_MARKER =
|
|
27
|
+
/(?:pantheon-opencode|pantheon[- ]agent system|managed[- ]by[: ]+pantheon|pantheon-(?:resources|memory|persistence|code-mode|tui|hooks))/i
|
|
28
|
+
const stats = { removed: 0, skipped: 0, errors: 0 }
|
|
76
29
|
|
|
77
30
|
function showHelp() {
|
|
78
31
|
console.log(`
|
|
79
|
-
uninstall.mjs — Multi-platform Pantheon agent uninstaller
|
|
80
32
|
|
|
81
33
|
Usage:
|
|
82
|
-
node scripts/uninstall.mjs
|
|
83
|
-
node scripts/uninstall.mjs --target /path/to/project auto-detect, target
|
|
84
|
-
node scripts/uninstall.mjs --platforms opencode,claude specific platforms, cwd
|
|
85
|
-
node scripts/uninstall.mjs --target /path --platforms all all platforms
|
|
86
|
-
node scripts/uninstall.mjs --dry-run preview without deleting
|
|
87
|
-
node scripts/uninstall.mjs --force skip confirmation prompt
|
|
88
|
-
node scripts/uninstall.mjs --help show this help
|
|
89
|
-
|
|
90
|
-
Flags:
|
|
91
|
-
--target /path Project to uninstall from (default: cwd)
|
|
92
|
-
--platforms list Comma-separated platforms (default: auto-detect or all)
|
|
93
|
-
--dry-run Preview what would be removed without deleting
|
|
94
|
-
--force Skip confirmation prompt
|
|
95
|
-
--help Show this help
|
|
34
|
+
node scripts/uninstall.mjs [--project|--global] [--target /path] [--dry-run] [--force]
|
|
96
35
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
claude → .claude/agents/ + CLAUDE.md (if Pantheon content)
|
|
100
|
-
cursor → .cursor/rules/*.mdc (only Pantheon rules)
|
|
101
|
-
windsurf → .windsurf/rules/ + .windsurf/workflows/ (if empty)
|
|
102
|
-
copilot → .github/agents/ + .vscode/settings.json Pantheon keys
|
|
103
|
-
continue → .continue/rules/ + check .continue/config.yaml
|
|
104
|
-
cline → .clinerules/ (only agent rules, not commands/skills)
|
|
105
|
-
all → uninstall every platform
|
|
106
|
-
|
|
107
|
-
When --platforms is omitted, auto-detects which platforms are configured.
|
|
108
|
-
If none detected, all platforms are uninstalled.
|
|
36
|
+
Target may be a project directory (containing .opencode), or a flat global
|
|
37
|
+
OpenCode directory (~/.config/opencode or ~/.opencode). Symlinks are rejected.
|
|
109
38
|
`)
|
|
110
39
|
}
|
|
111
40
|
|
|
112
|
-
// ---------------------------------------------------------------------------
|
|
113
|
-
// CLI args
|
|
114
|
-
// ---------------------------------------------------------------------------
|
|
115
|
-
|
|
116
41
|
function parseArgs(argv) {
|
|
117
|
-
const args = {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
default:
|
|
143
|
-
console.warn(`⚠️ Unknown option: ${argv[i]}`)
|
|
144
|
-
break
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (!args.target) {
|
|
149
|
-
args.target = process.cwd()
|
|
150
|
-
}
|
|
151
|
-
args.target = resolveTarget(args.target)
|
|
152
|
-
|
|
42
|
+
const args = { target: process.cwd(), dryRun: false, force: false, help: false, scope: 'project' }
|
|
43
|
+
let explicitTarget = false
|
|
44
|
+
for (let index = 2; index < argv.length; index += 1) {
|
|
45
|
+
const option = argv[index]
|
|
46
|
+
if (option === '--target') {
|
|
47
|
+
args.target = argv[++index]
|
|
48
|
+
explicitTarget = true
|
|
49
|
+
} else if (option === '--project') {
|
|
50
|
+
if (args.scope === 'global') throw new Error('--project and --global are mutually exclusive')
|
|
51
|
+
args.scope = 'project'
|
|
52
|
+
} else if (option === '--global') {
|
|
53
|
+
if (args.scope === 'project' && argv.slice(2, index).includes('--project'))
|
|
54
|
+
throw new Error('--project and --global are mutually exclusive')
|
|
55
|
+
args.scope = 'global'
|
|
56
|
+
} else if (option === '--dry-run') args.dryRun = true
|
|
57
|
+
else if (option === '--force') args.force = true
|
|
58
|
+
else if (option === '--help') args.help = true
|
|
59
|
+
else if (option === '--platforms')
|
|
60
|
+
throw new Error('--platforms was removed; this uninstaller supports OpenCode only')
|
|
61
|
+
else throw new Error(`Unknown option: ${option}`)
|
|
62
|
+
}
|
|
63
|
+
if (args.scope === 'global' && !explicitTarget) {
|
|
64
|
+
args.target = resolveGlobalTarget()
|
|
65
|
+
}
|
|
66
|
+
args.target = resolve(args.target)
|
|
153
67
|
return args
|
|
154
68
|
}
|
|
155
69
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Resolve the isolated global OpenCode directory without consulting cwd.
|
|
72
|
+
*
|
|
73
|
+
* PANTHEON_HOME is intentionally a directory override (not a parent), which
|
|
74
|
+
* makes sandboxed installs safe and keeps uninstall symmetric with the MCP
|
|
75
|
+
* path resolver. XDG remains the normal user-wide fallback.
|
|
76
|
+
*
|
|
77
|
+
* @param {NodeJS.ProcessEnv} [env] environment to inspect
|
|
78
|
+
* @param {string} [home] home directory fallback
|
|
79
|
+
* @returns {string} absolute global configuration directory
|
|
80
|
+
*/
|
|
81
|
+
function resolveGlobalTarget(env = process.env, home = homedir()) {
|
|
82
|
+
const pantheonHome = env.PANTHEON_HOME?.trim()
|
|
83
|
+
if (pantheonHome) return resolve(pantheonHome)
|
|
84
|
+
const xdg = env.XDG_CONFIG_HOME?.trim() || join(home, '.config')
|
|
85
|
+
return resolve(join(xdg, 'opencode'))
|
|
86
|
+
}
|
|
159
87
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
88
|
+
function lstat(path) {
|
|
89
|
+
try {
|
|
90
|
+
return lstatSync(path)
|
|
91
|
+
} catch {
|
|
92
|
+
return null
|
|
164
93
|
}
|
|
165
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
166
|
-
return new Promise((resolve) => {
|
|
167
|
-
rl.question(message, (answer) => {
|
|
168
|
-
rl.close()
|
|
169
|
-
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes')
|
|
170
|
-
})
|
|
171
|
-
})
|
|
172
94
|
}
|
|
173
95
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
96
|
+
function isSymlink(path) {
|
|
97
|
+
return lstat(path)?.isSymbolicLink() === true
|
|
98
|
+
}
|
|
177
99
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
return PANTHEON_AGENT_NAMES.includes(basename)
|
|
100
|
+
function isInside(path, root) {
|
|
101
|
+
const child = relative(resolve(root), resolve(path))
|
|
102
|
+
return child === '' || (!child.startsWith('..') && !child.includes(`${pathSeparator()}..`))
|
|
182
103
|
}
|
|
183
104
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
// Skill directories are dynamically fetched from ROOT/skills/
|
|
187
|
-
// We use a dynamic check via readdirSync when needed, but for safety
|
|
188
|
-
// we check against a known set of patterns.
|
|
189
|
-
return /^[a-z-]+$/.test(dirname) && dirname.length > 1
|
|
105
|
+
function pathSeparator() {
|
|
106
|
+
return '/'
|
|
190
107
|
}
|
|
191
108
|
|
|
192
|
-
|
|
193
|
-
|
|
109
|
+
function isPantheonContent(path) {
|
|
110
|
+
const info = lstat(path)
|
|
111
|
+
if (!info?.isFile() || info.isSymbolicLink()) return false
|
|
194
112
|
try {
|
|
195
|
-
|
|
196
|
-
const content = readFileSync(filePath, 'utf8')
|
|
197
|
-
return (
|
|
198
|
-
content.includes('Pantheon Agent System') ||
|
|
199
|
-
content.includes('# Pantheon Agent System') ||
|
|
200
|
-
content.includes('Pantheon multi-agent framework')
|
|
201
|
-
)
|
|
113
|
+
return OWNERSHIP_MARKER.test(readFileSync(path, 'utf8'))
|
|
202
114
|
} catch {
|
|
203
115
|
return false
|
|
204
116
|
}
|
|
205
117
|
}
|
|
206
118
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (existsSync(agentsDir)) {
|
|
220
|
-
if (!dryRun) {
|
|
221
|
-
try {
|
|
222
|
-
rmSync(agentsDir, { recursive: true, force: true })
|
|
223
|
-
s.removed++
|
|
224
|
-
} catch (err) {
|
|
225
|
-
console.error(` ⚠️ Failed to remove ${agentsDir}: ${err.message}`)
|
|
226
|
-
s.errors++
|
|
227
|
-
}
|
|
228
|
-
} else {
|
|
229
|
-
const count = readdirSync(agentsDir).length
|
|
230
|
-
s.removed++
|
|
231
|
-
console.log(` ~ Would remove .opencode/agents/ (${count} files)`)
|
|
232
|
-
}
|
|
233
|
-
} else {
|
|
234
|
-
s.skipped++
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// 2. Remove .opencode/skills/ (entire directory — Pantheon-created)
|
|
238
|
-
const skillsDir = join(target, '.opencode', 'skills')
|
|
239
|
-
if (existsSync(skillsDir)) {
|
|
240
|
-
if (!dryRun) {
|
|
241
|
-
try {
|
|
242
|
-
rmSync(skillsDir, { recursive: true, force: true })
|
|
243
|
-
s.removed++
|
|
244
|
-
} catch (err) {
|
|
245
|
-
console.error(` ⚠️ Failed to remove ${skillsDir}: ${err.message}`)
|
|
246
|
-
s.errors++
|
|
247
|
-
}
|
|
248
|
-
} else {
|
|
249
|
-
s.removed++
|
|
250
|
-
console.log(' ~ Would remove .opencode/skills/')
|
|
251
|
-
}
|
|
252
|
-
} else {
|
|
253
|
-
s.skipped++
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// 3. Remove .opencode/commands/ (entire directory — Pantheon-created)
|
|
257
|
-
const cmdsDir = join(target, '.opencode', 'commands')
|
|
258
|
-
if (existsSync(cmdsDir)) {
|
|
259
|
-
if (!dryRun) {
|
|
260
|
-
try {
|
|
261
|
-
rmSync(cmdsDir, { recursive: true, force: true })
|
|
262
|
-
s.removed++
|
|
263
|
-
} catch (err) {
|
|
264
|
-
console.error(` ⚠️ Failed to remove ${cmdsDir}: ${err.message}`)
|
|
265
|
-
s.errors++
|
|
266
|
-
}
|
|
267
|
-
} else {
|
|
268
|
-
s.removed++
|
|
269
|
-
console.log(' ~ Would remove .opencode/commands/')
|
|
270
|
-
}
|
|
271
|
-
} else {
|
|
272
|
-
s.skipped++
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// 4. Remove .opencode/plugins/ (Pantheon TUI plugin)
|
|
276
|
-
const pluginsDir = join(target, '.opencode', 'plugins')
|
|
277
|
-
if (existsSync(pluginsDir)) {
|
|
278
|
-
let isPluginEmpty = true
|
|
279
|
-
try {
|
|
280
|
-
isPluginEmpty = readdirSync(pluginsDir).length === 0
|
|
281
|
-
} catch {
|
|
282
|
-
/* ignore */
|
|
283
|
-
}
|
|
284
|
-
if (!dryRun) {
|
|
285
|
-
try {
|
|
286
|
-
rmSync(pluginsDir, { recursive: true, force: true })
|
|
287
|
-
s.removed++
|
|
288
|
-
} catch (err) {
|
|
289
|
-
console.error(` ⚠️ Failed to remove ${pluginsDir}: ${err.message}`)
|
|
290
|
-
s.errors++
|
|
291
|
-
}
|
|
292
|
-
} else if (!isPluginEmpty) {
|
|
293
|
-
s.removed++
|
|
294
|
-
console.log(' ~ Would remove .opencode/plugins/ (Pantheon TUI plugin)')
|
|
295
|
-
} else {
|
|
296
|
-
s.skipped++
|
|
297
|
-
}
|
|
298
|
-
} else {
|
|
299
|
-
s.skipped++
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// 5. Remove .opencode/package.json and .opencode/tsconfig.json (Pantheon-created)
|
|
303
|
-
for (const f of ['package.json', 'tsconfig.json']) {
|
|
304
|
-
const fp = join(target, '.opencode', f)
|
|
305
|
-
if (existsSync(fp)) {
|
|
306
|
-
if (!dryRun) {
|
|
307
|
-
try {
|
|
308
|
-
rmSync(fp, { force: true })
|
|
309
|
-
s.removed++
|
|
310
|
-
} catch (err) {
|
|
311
|
-
console.error(` ⚠️ Failed to remove ${fp}: ${err.message}`)
|
|
312
|
-
s.errors++
|
|
313
|
-
}
|
|
314
|
-
} else {
|
|
315
|
-
s.removed++
|
|
316
|
-
console.log(` ~ Would remove .opencode/${f}`)
|
|
317
|
-
}
|
|
318
|
-
} else {
|
|
319
|
-
s.skipped++
|
|
320
|
-
}
|
|
119
|
+
function isPantheonRouting(path) {
|
|
120
|
+
const info = lstat(path)
|
|
121
|
+
if (!info?.isFile() || info.isSymbolicLink()) return false
|
|
122
|
+
try {
|
|
123
|
+
// routing.yml is also a legitimate user configuration. Unlike generated
|
|
124
|
+
// agents, its contents can mention Pantheon without being Pantheon-owned,
|
|
125
|
+
// so only an explicit ownership marker permits removal.
|
|
126
|
+
return /(?:^|\n)\s*(?:#|<!--)\s*(?:managed-by:\s*)?pantheon-opencode\b/i.test(
|
|
127
|
+
readFileSync(path, 'utf8'),
|
|
128
|
+
)
|
|
129
|
+
} catch {
|
|
130
|
+
return false
|
|
321
131
|
}
|
|
132
|
+
}
|
|
322
133
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
try {
|
|
328
|
-
isEmpty = readdirSync(opencodeDir).length === 0
|
|
329
|
-
} catch {
|
|
330
|
-
/* ignore */
|
|
331
|
-
}
|
|
332
|
-
if (isEmpty) {
|
|
333
|
-
if (!dryRun) {
|
|
334
|
-
try {
|
|
335
|
-
rmSync(opencodeDir, { recursive: true, force: true })
|
|
336
|
-
s.removed++
|
|
337
|
-
} catch (_err) {
|
|
338
|
-
/* ignore */
|
|
339
|
-
}
|
|
340
|
-
} else {
|
|
341
|
-
s.removed++
|
|
342
|
-
console.log(' ~ Would remove empty .opencode/ directory')
|
|
343
|
-
}
|
|
344
|
-
}
|
|
134
|
+
function removePath(path, dryRun, root, label = path) {
|
|
135
|
+
if (!isInside(path, root) || isSymlink(path)) {
|
|
136
|
+
stats.skipped += 1
|
|
137
|
+
return false
|
|
345
138
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
try {
|
|
351
|
-
const raw = readFileSync(configPath, 'utf8')
|
|
352
|
-
const config = JSON.parse(raw)
|
|
353
|
-
let changed = false
|
|
354
|
-
|
|
355
|
-
// Remove Pantheon-managed agent entries
|
|
356
|
-
if (config.agent && typeof config.agent === 'object') {
|
|
357
|
-
for (const [name, cfg] of Object.entries(config.agent)) {
|
|
358
|
-
const source = cfg?.source || ''
|
|
359
|
-
if (
|
|
360
|
-
source.startsWith('.opencode/agents/') ||
|
|
361
|
-
source.startsWith('agents/') ||
|
|
362
|
-
(PANTHEON_AGENT_NAMES.includes(name) && source === '')
|
|
363
|
-
) {
|
|
364
|
-
delete config.agent[name]
|
|
365
|
-
changed = true
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
if (Object.keys(config.agent).length === 0) {
|
|
369
|
-
delete config.agent
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Remove Pantheon-specific instructions
|
|
374
|
-
if (Array.isArray(config.instructions)) {
|
|
375
|
-
const filtered = config.instructions.filter(
|
|
376
|
-
(i) => i !== 'AGENTS.md' && i !== 'instructions/*.instructions.md',
|
|
377
|
-
)
|
|
378
|
-
if (filtered.length !== config.instructions.length) {
|
|
379
|
-
changed = true
|
|
380
|
-
}
|
|
381
|
-
if (filtered.length === 0) {
|
|
382
|
-
delete config.instructions
|
|
383
|
-
} else {
|
|
384
|
-
config.instructions = filtered
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// Remove Pantheon plugin entry (stub — reserved for future plugin cleanup)
|
|
389
|
-
if (Array.isArray(config.plugin) && config.plugin.length === 0) {
|
|
390
|
-
delete config.plugin
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// Remove default_agent if it points to zeus
|
|
394
|
-
if (config.default_agent === 'zeus') {
|
|
395
|
-
delete config.default_agent
|
|
396
|
-
changed = true
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
if (changed) {
|
|
400
|
-
if (!dryRun) {
|
|
401
|
-
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8')
|
|
402
|
-
s.removed++
|
|
403
|
-
} else {
|
|
404
|
-
s.removed++
|
|
405
|
-
console.log(' ~ Would restore opencode.json (remove Pantheon agent config entries)')
|
|
406
|
-
}
|
|
407
|
-
} else {
|
|
408
|
-
s.skipped++
|
|
409
|
-
}
|
|
410
|
-
} catch (err) {
|
|
411
|
-
console.error(` ⚠️ Failed to process opencode.json: ${err.message}`)
|
|
412
|
-
s.errors++
|
|
413
|
-
}
|
|
414
|
-
} else {
|
|
415
|
-
s.skipped++
|
|
139
|
+
const info = lstat(path)
|
|
140
|
+
if (!info) {
|
|
141
|
+
stats.skipped += 1
|
|
142
|
+
return false
|
|
416
143
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
const instrDir = join(target, 'instructions')
|
|
420
|
-
if (existsSync(instrDir)) {
|
|
421
|
-
// Only remove if it was Pantheon-installed (heuristic: contains .instructions.md files)
|
|
144
|
+
if (dryRun) console.log(` ~ Would remove ${label}`)
|
|
145
|
+
else {
|
|
422
146
|
try {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
s.removed++
|
|
429
|
-
} else {
|
|
430
|
-
s.removed++
|
|
431
|
-
console.log(' ~ Would remove instructions/ (Pantheon-installed)')
|
|
432
|
-
}
|
|
433
|
-
} else {
|
|
434
|
-
s.skipped++
|
|
435
|
-
}
|
|
436
|
-
} catch {
|
|
437
|
-
s.skipped++
|
|
147
|
+
rmSync(path, { recursive: info.isDirectory(), force: false })
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.error(` ⚠️ Failed to remove ${path}: ${error.message}`)
|
|
150
|
+
stats.errors += 1
|
|
151
|
+
return false
|
|
438
152
|
}
|
|
439
|
-
} else {
|
|
440
|
-
s.skipped++
|
|
441
153
|
}
|
|
154
|
+
stats.removed += 1
|
|
155
|
+
return true
|
|
156
|
+
}
|
|
442
157
|
|
|
443
|
-
|
|
444
|
-
const
|
|
445
|
-
if (
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
const hasPromptMds = entries.some((e) => e.endsWith('.prompt.md'))
|
|
449
|
-
if (hasPromptMds) {
|
|
450
|
-
if (!dryRun) {
|
|
451
|
-
rmSync(promptsDir, { recursive: true, force: true })
|
|
452
|
-
s.removed++
|
|
453
|
-
} else {
|
|
454
|
-
s.removed++
|
|
455
|
-
console.log(' ~ Would remove prompts/ (Pantheon-installed)')
|
|
456
|
-
}
|
|
457
|
-
} else {
|
|
458
|
-
s.skipped++
|
|
459
|
-
}
|
|
460
|
-
} catch {
|
|
461
|
-
s.skipped++
|
|
462
|
-
}
|
|
463
|
-
} else {
|
|
464
|
-
s.skipped++
|
|
158
|
+
function removeMatchingEntries(directory, predicate, dryRun, root) {
|
|
159
|
+
const info = lstat(directory)
|
|
160
|
+
if (!info || info.isSymbolicLink() || !info.isDirectory()) {
|
|
161
|
+
stats.skipped += 1
|
|
162
|
+
return
|
|
465
163
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
rmSync(agentsMd, { force: true })
|
|
473
|
-
s.removed++
|
|
474
|
-
} catch (err) {
|
|
475
|
-
console.error(` ⚠️ Failed to remove AGENTS.md: ${err.message}`)
|
|
476
|
-
s.errors++
|
|
477
|
-
}
|
|
478
|
-
} else {
|
|
479
|
-
s.removed++
|
|
480
|
-
console.log(' ~ Would remove AGENTS.md (Pantheon content)')
|
|
164
|
+
for (const entry of readdirSync(directory)) {
|
|
165
|
+
const path = join(directory, entry)
|
|
166
|
+
const child = lstat(path)
|
|
167
|
+
if (!child || child.isSymbolicLink()) {
|
|
168
|
+
stats.skipped += 1
|
|
169
|
+
continue
|
|
481
170
|
}
|
|
482
|
-
|
|
483
|
-
|
|
171
|
+
if (predicate(path, entry)) removePath(path, dryRun, root, path)
|
|
172
|
+
else if (child.isDirectory()) removeMatchingEntries(path, predicate, dryRun, root)
|
|
484
173
|
}
|
|
174
|
+
const remaining = lstat(directory)
|
|
175
|
+
if (remaining?.isDirectory() && readdirSync(directory).length === 0)
|
|
176
|
+
removePath(directory, dryRun, root, directory)
|
|
485
177
|
}
|
|
486
178
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
const agentsDir = join(target, '.claude', 'agents')
|
|
495
|
-
if (existsSync(agentsDir)) {
|
|
496
|
-
if (!dryRun) {
|
|
497
|
-
try {
|
|
498
|
-
rmSync(agentsDir, { recursive: true, force: true })
|
|
499
|
-
s.removed++
|
|
500
|
-
} catch (err) {
|
|
501
|
-
console.error(` ⚠️ Failed to remove ${agentsDir}: ${err.message}`)
|
|
502
|
-
s.errors++
|
|
503
|
-
}
|
|
504
|
-
} else {
|
|
505
|
-
const count = existsSync(agentsDir) ? readdirSync(agentsDir).length : 0
|
|
506
|
-
s.removed++
|
|
507
|
-
console.log(` ~ Would remove .claude/agents/ (${count} files)`)
|
|
508
|
-
}
|
|
509
|
-
} else {
|
|
510
|
-
s.skipped++
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
// 2. Remove .claude/settings.json Pantheon section (or entire file if Pantheon-only)
|
|
514
|
-
const settingsPath = join(target, '.claude', 'settings.json')
|
|
515
|
-
if (existsSync(settingsPath)) {
|
|
516
|
-
try {
|
|
517
|
-
const raw = readFileSync(settingsPath, 'utf8')
|
|
518
|
-
const settings = JSON.parse(raw)
|
|
519
|
-
// The installer creates settings with only permissions.allow
|
|
520
|
-
// If the file only has permissions, it's Pantheon-created
|
|
521
|
-
if (settings.permissions && Object.keys(settings).length === 1 && !dryRun) {
|
|
522
|
-
rmSync(settingsPath, { force: true })
|
|
523
|
-
s.removed++
|
|
524
|
-
} else if (settings.permissions && !dryRun) {
|
|
525
|
-
// User had other settings — just remove the permissions key
|
|
526
|
-
delete settings.permissions
|
|
527
|
-
writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, 'utf8')
|
|
528
|
-
s.removed++
|
|
529
|
-
} else if (settings.permissions) {
|
|
530
|
-
s.removed++
|
|
531
|
-
console.log(' ~ Would remove Pantheon permissions from .claude/settings.json')
|
|
532
|
-
} else {
|
|
533
|
-
s.skipped++
|
|
534
|
-
}
|
|
535
|
-
} catch {
|
|
536
|
-
s.skipped++
|
|
537
|
-
}
|
|
538
|
-
} else {
|
|
539
|
-
s.skipped++
|
|
179
|
+
function readJson(path) {
|
|
180
|
+
const info = lstat(path)
|
|
181
|
+
if (!info || info.isSymbolicLink() || !info.isFile()) return null
|
|
182
|
+
try {
|
|
183
|
+
return JSON.parse(readFileSync(path, 'utf8'))
|
|
184
|
+
} catch {
|
|
185
|
+
return null
|
|
540
186
|
}
|
|
187
|
+
}
|
|
541
188
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
if (!dryRun) {
|
|
546
|
-
try {
|
|
547
|
-
rmSync(claudeMdPath, { force: true })
|
|
548
|
-
s.removed++
|
|
549
|
-
} catch (err) {
|
|
550
|
-
console.error(` ⚠️ Failed to remove CLAUDE.md: ${err.message}`)
|
|
551
|
-
s.errors++
|
|
552
|
-
}
|
|
553
|
-
} else {
|
|
554
|
-
s.removed++
|
|
555
|
-
console.log(' ~ Would remove CLAUDE.md (Pantheon content)')
|
|
556
|
-
}
|
|
557
|
-
} else {
|
|
558
|
-
s.skipped++
|
|
559
|
-
}
|
|
189
|
+
function ownedReference(value) {
|
|
190
|
+
return typeof value === 'string' && /pantheon(?:[-/]|$)/i.test(value)
|
|
191
|
+
}
|
|
560
192
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
if (entries.length === 0) {
|
|
567
|
-
if (!dryRun) {
|
|
568
|
-
rmSync(claudeDir, { recursive: true, force: true })
|
|
569
|
-
}
|
|
570
|
-
// No need to count this — it's a cleanup step
|
|
571
|
-
}
|
|
572
|
-
} catch {
|
|
573
|
-
/* ignore */
|
|
574
|
-
}
|
|
193
|
+
function ownedPluginEntry(value) {
|
|
194
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
195
|
+
return ['managed-by', 'managedBy', 'owner'].some(
|
|
196
|
+
(key) => typeof value[key] === 'string' && /^pantheon(?:-|$)/i.test(value[key]),
|
|
197
|
+
)
|
|
575
198
|
}
|
|
576
|
-
|
|
199
|
+
if (typeof value !== 'string') return false
|
|
200
|
+
const normalized = value.replaceAll('\\', '/')
|
|
201
|
+
return (
|
|
202
|
+
/(?:^|\/)src\/plugin\.ts$/i.test(normalized) ||
|
|
203
|
+
/(?:^|\/)src\/plugins\/pantheon-hooks\.ts$/i.test(normalized) ||
|
|
204
|
+
/^(?:plugins\/)?pantheon-(?:hooks|plugin|tui)$/i.test(normalized) ||
|
|
205
|
+
/^pantheon-opencode$/i.test(normalized)
|
|
206
|
+
)
|
|
577
207
|
}
|
|
578
208
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
209
|
+
function ownedMcpEntry(name, value) {
|
|
210
|
+
if (typeof name === 'string' && /^pantheon(?:-|$)/i.test(name)) return true
|
|
211
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false
|
|
212
|
+
return ['managed-by', 'managedBy', 'owner'].some(
|
|
213
|
+
(key) => typeof value[key] === 'string' && /^pantheon(?:-|$)/i.test(value[key]),
|
|
214
|
+
)
|
|
215
|
+
}
|
|
584
216
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
217
|
+
function cleanOpenCodeConfig(target, dryRun, ownedRoot = target) {
|
|
218
|
+
const configPath = join(target, 'opencode.json')
|
|
219
|
+
const config = readJson(configPath)
|
|
220
|
+
if (!config) {
|
|
221
|
+
stats.skipped += 1
|
|
588
222
|
return
|
|
589
223
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
const
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
rmSync(fp, { force: true })
|
|
605
|
-
s.removed++
|
|
606
|
-
} catch (err) {
|
|
607
|
-
console.error(` ⚠️ Failed to remove ${fp}: ${err.message}`)
|
|
608
|
-
s.errors++
|
|
609
|
-
}
|
|
610
|
-
} else {
|
|
611
|
-
s.removed++
|
|
612
|
-
console.log(` ~ Would remove .cursor/rules/${f}`)
|
|
224
|
+
let changed = false
|
|
225
|
+
const removedAgents = new Set()
|
|
226
|
+
if (config.agent && typeof config.agent === 'object' && !Array.isArray(config.agent)) {
|
|
227
|
+
for (const [name, value] of Object.entries(config.agent)) {
|
|
228
|
+
const source = value && typeof value === 'object' ? value.source : undefined
|
|
229
|
+
const sourcePath = typeof source === 'string' ? resolve(target, source) : ''
|
|
230
|
+
if (
|
|
231
|
+
typeof source === 'string' &&
|
|
232
|
+
isInside(sourcePath, ownedRoot) &&
|
|
233
|
+
isPantheonContent(sourcePath)
|
|
234
|
+
) {
|
|
235
|
+
delete config.agent[name]
|
|
236
|
+
removedAgents.add(name)
|
|
237
|
+
changed = true
|
|
613
238
|
}
|
|
614
239
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
try {
|
|
619
|
-
rmSync(rulesDir, { recursive: true, force: true })
|
|
620
|
-
} catch {
|
|
621
|
-
/* ignore */
|
|
622
|
-
}
|
|
240
|
+
if (!Object.keys(config.agent).length) {
|
|
241
|
+
delete config.agent
|
|
242
|
+
changed = true
|
|
623
243
|
}
|
|
624
|
-
} catch (err) {
|
|
625
|
-
console.error(` ⚠️ Failed to read ${rulesDir}: ${err.message}`)
|
|
626
|
-
s.errors++
|
|
627
244
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
s.removed++
|
|
645
|
-
} catch (err) {
|
|
646
|
-
console.error(` ⚠️ Failed to remove ${rulesDir}: ${err.message}`)
|
|
647
|
-
s.errors++
|
|
648
|
-
}
|
|
649
|
-
} else {
|
|
650
|
-
const count = readdirSync(rulesDir).length
|
|
651
|
-
s.removed++
|
|
652
|
-
console.log(` ~ Would remove .windsurf/rules/ (${count} files)`)
|
|
653
|
-
}
|
|
654
|
-
} else {
|
|
655
|
-
s.skipped++
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
// 2. Remove workflows that match Pantheon workflow names
|
|
659
|
-
const wfDir = join(target, '.windsurf', 'workflows')
|
|
660
|
-
if (existsSync(wfDir)) {
|
|
661
|
-
try {
|
|
662
|
-
const entries = readdirSync(wfDir)
|
|
663
|
-
const pantheonWorkflows = entries.filter(
|
|
664
|
-
(f) =>
|
|
665
|
-
f.endsWith('.md') &&
|
|
666
|
-
[
|
|
667
|
-
'orchestrate.md',
|
|
668
|
-
'code-review.md',
|
|
669
|
-
'audit.md',
|
|
670
|
-
'cancel.md',
|
|
671
|
-
'deepwork.md',
|
|
672
|
-
'focus.md',
|
|
673
|
-
'forge.md',
|
|
674
|
-
'metamorphosis.md',
|
|
675
|
-
'mirrordeps.md',
|
|
676
|
-
'optimize.md',
|
|
677
|
-
'pantheon-status.md',
|
|
678
|
-
'pantheon.md',
|
|
679
|
-
'ping.md',
|
|
680
|
-
'praxis.md',
|
|
681
|
-
'reflect.md',
|
|
682
|
-
'sketch.md',
|
|
683
|
-
'stop-continuation.md',
|
|
684
|
-
'subtask.md',
|
|
685
|
-
].includes(f),
|
|
245
|
+
if (Array.isArray(config.instructions)) {
|
|
246
|
+
const instructions = config.instructions.filter((entry) => {
|
|
247
|
+
if (typeof entry !== 'string') return true
|
|
248
|
+
const path = resolve(target, entry)
|
|
249
|
+
return !(isInside(path, ownedRoot) && isPantheonContent(path))
|
|
250
|
+
})
|
|
251
|
+
changed ||= instructions.length !== config.instructions.length
|
|
252
|
+
if (instructions.length) config.instructions = instructions
|
|
253
|
+
else delete config.instructions
|
|
254
|
+
}
|
|
255
|
+
for (const key of ['mcp', 'plugin', 'plugins']) {
|
|
256
|
+
if (Array.isArray(config[key])) {
|
|
257
|
+
const kept = config[key].filter((entry) =>
|
|
258
|
+
key === 'mcp'
|
|
259
|
+
? !ownedMcpEntry(typeof entry === 'string' ? entry : '', entry)
|
|
260
|
+
: !ownedPluginEntry(entry),
|
|
686
261
|
)
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
}
|
|
698
|
-
} else {
|
|
699
|
-
s.removed++
|
|
700
|
-
console.log(` ~ Would remove .windsurf/workflows/${f}`)
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
// Remove workflows/ directory if empty
|
|
705
|
-
if (!dryRun && existsSync(wfDir) && readdirSync(wfDir).length === 0) {
|
|
706
|
-
try {
|
|
707
|
-
rmSync(wfDir, { recursive: true, force: true })
|
|
708
|
-
} catch {
|
|
709
|
-
/* ignore */
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
} catch (err) {
|
|
713
|
-
console.error(` ⚠️ Failed to read ${wfDir}: ${err.message}`)
|
|
714
|
-
s.errors++
|
|
715
|
-
}
|
|
716
|
-
} else {
|
|
717
|
-
s.skipped++
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
// Note: AGENTS.md is handled by opencode uninstaller
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
/**
|
|
724
|
-
* VS Code / Copilot — remove .github/agents/, restore .vscode/settings.json
|
|
725
|
-
*/
|
|
726
|
-
function uninstallCopilot(target, dryRun) {
|
|
727
|
-
const s = stats.copilot
|
|
728
|
-
|
|
729
|
-
// 1. Remove .github/agents/ (entire directory — Pantheon-created)
|
|
730
|
-
const agentsDir = join(target, '.github', 'agents')
|
|
731
|
-
if (existsSync(agentsDir)) {
|
|
732
|
-
if (!dryRun) {
|
|
733
|
-
try {
|
|
734
|
-
rmSync(agentsDir, { recursive: true, force: true })
|
|
735
|
-
s.removed++
|
|
736
|
-
} catch (err) {
|
|
737
|
-
console.error(` ⚠️ Failed to remove ${agentsDir}: ${err.message}`)
|
|
738
|
-
s.errors++
|
|
739
|
-
}
|
|
740
|
-
} else {
|
|
741
|
-
const count = existsSync(agentsDir) ? readdirSync(agentsDir).length : 0
|
|
742
|
-
s.removed++
|
|
743
|
-
console.log(` ~ Would remove .github/agents/ (${count} files)`)
|
|
744
|
-
}
|
|
745
|
-
} else {
|
|
746
|
-
s.skipped++
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
// 2. Restore .vscode/settings.json — remove Pantheon-added keys
|
|
750
|
-
const settingsPath = join(target, '.vscode', 'settings.json')
|
|
751
|
-
if (existsSync(settingsPath)) {
|
|
752
|
-
try {
|
|
753
|
-
const raw = readFileSync(settingsPath, 'utf8')
|
|
754
|
-
const settings = JSON.parse(raw)
|
|
755
|
-
let changed = false
|
|
756
|
-
|
|
757
|
-
// Remove the Pantheon-specific settings that the installer added
|
|
758
|
-
const pantheonKeys = ['chat.subagents.allowInvocationsFromSubagents', 'chat.plugins.enabled']
|
|
759
|
-
|
|
760
|
-
for (const key of pantheonKeys) {
|
|
761
|
-
if (key in settings) {
|
|
762
|
-
delete settings[key]
|
|
262
|
+
changed ||= kept.length !== config[key].length
|
|
263
|
+
config[key] = kept
|
|
264
|
+
} else if (config[key] && typeof config[key] === 'object') {
|
|
265
|
+
for (const [name, value] of Object.entries(config[key])) {
|
|
266
|
+
if (
|
|
267
|
+
key === 'mcp'
|
|
268
|
+
? ownedMcpEntry(name, value)
|
|
269
|
+
: ownedPluginEntry(name) || ownedPluginEntry(value)
|
|
270
|
+
) {
|
|
271
|
+
delete config[key][name]
|
|
763
272
|
changed = true
|
|
764
273
|
}
|
|
765
274
|
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, 'utf8')
|
|
770
|
-
s.removed++
|
|
771
|
-
} else {
|
|
772
|
-
s.removed++
|
|
773
|
-
console.log(' ~ Would restore .vscode/settings.json (remove Pantheon keys)')
|
|
774
|
-
}
|
|
775
|
-
} else {
|
|
776
|
-
s.skipped++
|
|
275
|
+
if (!Object.keys(config[key]).length) {
|
|
276
|
+
delete config[key]
|
|
277
|
+
changed = true
|
|
777
278
|
}
|
|
778
|
-
} catch (err) {
|
|
779
|
-
console.error(` ⚠️ Failed to process .vscode/settings.json: ${err.message}`)
|
|
780
|
-
s.errors++
|
|
781
279
|
}
|
|
782
|
-
} else {
|
|
783
|
-
s.skipped++
|
|
784
280
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
/**
|
|
790
|
-
* Continue.dev — remove .continue/rules/, check .continue/config.yaml
|
|
791
|
-
*/
|
|
792
|
-
function uninstallContinue(target, dryRun) {
|
|
793
|
-
const s = stats.continue
|
|
794
|
-
|
|
795
|
-
// 1. Remove .continue/rules/ (entire directory — Pantheon-created)
|
|
796
|
-
const rulesDir = join(target, '.continue', 'rules')
|
|
797
|
-
if (existsSync(rulesDir)) {
|
|
798
|
-
if (!dryRun) {
|
|
799
|
-
try {
|
|
800
|
-
rmSync(rulesDir, { recursive: true, force: true })
|
|
801
|
-
s.removed++
|
|
802
|
-
} catch (err) {
|
|
803
|
-
console.error(` ⚠️ Failed to remove ${rulesDir}: ${err.message}`)
|
|
804
|
-
s.errors++
|
|
805
|
-
}
|
|
806
|
-
} else {
|
|
807
|
-
const count = readdirSync(rulesDir).length
|
|
808
|
-
s.removed++
|
|
809
|
-
console.log(` ~ Would remove .continue/rules/ (${count} files)`)
|
|
810
|
-
}
|
|
811
|
-
} else {
|
|
812
|
-
s.skipped++
|
|
281
|
+
if (removedAgents.has(config.default_agent)) {
|
|
282
|
+
delete config.default_agent
|
|
283
|
+
changed = true
|
|
813
284
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
if (existsSync(configPath)) {
|
|
818
|
-
try {
|
|
819
|
-
const content = readFileSync(configPath, 'utf8')
|
|
820
|
-
if (
|
|
821
|
-
content.includes('Pantheon') ||
|
|
822
|
-
content.includes('pantheon') ||
|
|
823
|
-
/rules\/\w+\.md/.test(content)
|
|
824
|
-
) {
|
|
825
|
-
if (!dryRun) {
|
|
826
|
-
// Note: We don't auto-edit yaml (too risky with formatting),
|
|
827
|
-
// but we inform the user
|
|
828
|
-
console.log(
|
|
829
|
-
' ℹ️ .continue/config.yaml references Pantheon rules — manual cleanup may be needed',
|
|
830
|
-
)
|
|
831
|
-
} else {
|
|
832
|
-
console.log(' ~ Would check .continue/config.yaml for Pantheon references')
|
|
833
|
-
}
|
|
834
|
-
s.removed++
|
|
835
|
-
} else {
|
|
836
|
-
s.skipped++
|
|
837
|
-
}
|
|
838
|
-
} catch {
|
|
839
|
-
s.skipped++
|
|
840
|
-
}
|
|
841
|
-
} else {
|
|
842
|
-
s.skipped++
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
// 3. Remove .continue/.opencode/ if present (from sync)
|
|
846
|
-
const opencodeDir = join(target, '.continue', '.opencode')
|
|
847
|
-
if (existsSync(opencodeDir)) {
|
|
848
|
-
if (!dryRun) {
|
|
849
|
-
try {
|
|
850
|
-
rmSync(opencodeDir, { recursive: true, force: true })
|
|
851
|
-
s.removed++
|
|
852
|
-
} catch (_err) {
|
|
853
|
-
/* ignore */
|
|
854
|
-
}
|
|
855
|
-
} else {
|
|
856
|
-
s.removed++
|
|
857
|
-
console.log(' ~ Would remove .continue/.opencode/ shared config')
|
|
858
|
-
}
|
|
285
|
+
if (!changed) {
|
|
286
|
+
stats.skipped += 1
|
|
287
|
+
return
|
|
859
288
|
}
|
|
289
|
+
if (dryRun) console.log(' ~ Would update opencode.json (remove Pantheon-owned entries)')
|
|
290
|
+
else writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8')
|
|
291
|
+
stats.removed += 1
|
|
860
292
|
}
|
|
861
293
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
294
|
+
function configDirFor(target) {
|
|
295
|
+
const name = basename(target)
|
|
296
|
+
const looksFlat = lstat(join(target, 'agents'))?.isDirectory() === true
|
|
297
|
+
return name === '.opencode' || name === 'opencode' || looksFlat
|
|
298
|
+
? target
|
|
299
|
+
: join(target, '.opencode')
|
|
300
|
+
}
|
|
867
301
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
302
|
+
function uninstallOpenCode(target, dryRun) {
|
|
303
|
+
const root = resolve(target)
|
|
304
|
+
if (isSymlink(root)) throw new Error(`Target must not be a symlink: ${root}`)
|
|
305
|
+
const configDir = configDirFor(root)
|
|
306
|
+
const configInfo = lstat(configDir)
|
|
307
|
+
if (configInfo?.isSymbolicLink()) {
|
|
308
|
+
stats.skipped += 1
|
|
871
309
|
return
|
|
872
310
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
}
|
|
895
|
-
} else {
|
|
896
|
-
s.removed++
|
|
897
|
-
console.log(` ~ Would remove .clinerules/${f}`)
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
// Remove .clinerules/ directory if empty (no shared commands/skills remain)
|
|
902
|
-
if (!dryRun && readdirSync(clineDir).length === 0) {
|
|
903
|
-
try {
|
|
904
|
-
rmSync(clineDir, { recursive: true, force: true })
|
|
905
|
-
} catch {
|
|
906
|
-
/* ignore */
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
} catch (err) {
|
|
910
|
-
console.error(` ⚠️ Failed to read ${clineDir}: ${err.message}`)
|
|
911
|
-
s.errors++
|
|
912
|
-
}
|
|
311
|
+
const ownedRoot = root
|
|
312
|
+
cleanOpenCodeConfig(root, dryRun, ownedRoot)
|
|
313
|
+
removeMatchingEntries(
|
|
314
|
+
join(configDir, 'agents'),
|
|
315
|
+
(_path, name) => AGENT_NAMES.has(name.replace(/\.md$/, '')) && isPantheonContent(_path),
|
|
316
|
+
dryRun,
|
|
317
|
+
ownedRoot,
|
|
318
|
+
)
|
|
319
|
+
for (const directory of ['skills', 'commands', 'plugins'])
|
|
320
|
+
removeMatchingEntries(join(configDir, directory), isPantheonContent, dryRun, ownedRoot)
|
|
321
|
+
for (const file of ['package.json', 'tsconfig.json', 'routing.yml']) {
|
|
322
|
+
const path = join(configDir, file)
|
|
323
|
+
if (file === 'routing.yml' ? isPantheonRouting(path) : isPantheonContent(path))
|
|
324
|
+
removePath(path, dryRun, ownedRoot, path)
|
|
325
|
+
}
|
|
326
|
+
const tui = join(configDir, 'tui.json')
|
|
327
|
+
cleanTuiConfig(tui, dryRun, ownedRoot)
|
|
328
|
+
const agentsMd = join(root, 'AGENTS.md')
|
|
329
|
+
if (isPantheonContent(agentsMd)) removePath(agentsMd, dryRun, ownedRoot, agentsMd)
|
|
330
|
+
if (configInfo?.isDirectory() && readdirSync(configDir).length === 0)
|
|
331
|
+
removePath(configDir, dryRun, ownedRoot, configDir)
|
|
913
332
|
}
|
|
914
333
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
console.log('')
|
|
921
|
-
console.log('='.repeat(60))
|
|
922
|
-
console.log('🗑️ Uninstall Summary')
|
|
923
|
-
console.log(` Target: ${target}`)
|
|
924
|
-
console.log('='.repeat(60))
|
|
925
|
-
|
|
926
|
-
let totalRemoved = 0
|
|
927
|
-
let totalSkipped = 0
|
|
928
|
-
let totalErrors = 0
|
|
929
|
-
|
|
930
|
-
for (const platform of platforms) {
|
|
931
|
-
const label = PLATFORM_LABELS[platform] ?? platform
|
|
932
|
-
const pStats = stats[platform]
|
|
933
|
-
totalRemoved += pStats.removed
|
|
934
|
-
totalSkipped += pStats.skipped
|
|
935
|
-
totalErrors += pStats.errors
|
|
936
|
-
|
|
937
|
-
const status = pStats.errors > 0 ? '⚠️' : '✅'
|
|
938
|
-
console.log(
|
|
939
|
-
` ${status} ${label}: ${pStats.removed} removed, ${pStats.skipped} skipped${pStats.errors > 0 ? `, ${pStats.errors} errors` : ''}`,
|
|
940
|
-
)
|
|
334
|
+
function cleanTuiConfig(path, dryRun, root) {
|
|
335
|
+
const config = readJson(path)
|
|
336
|
+
if (!config || !Array.isArray(config.plugin)) {
|
|
337
|
+
stats.skipped += 1
|
|
338
|
+
return
|
|
941
339
|
}
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
console.log(
|
|
945
|
-
` Total: ${totalRemoved} items removed, ${totalSkipped} items skipped, ${totalErrors} errors`,
|
|
340
|
+
const plugin = config.plugin.filter(
|
|
341
|
+
(entry) => !(ownedReference(entry) && isInside(resolve(dirname(path), entry), root)),
|
|
946
342
|
)
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
if (totalRemoved === 0 && totalSkipped > 0) {
|
|
953
|
-
console.log(' ℹ️ No Pantheon files found for the selected platforms.')
|
|
343
|
+
if (plugin.length === config.plugin.length) {
|
|
344
|
+
stats.skipped += 1
|
|
345
|
+
return
|
|
954
346
|
}
|
|
347
|
+
config.plugin = plugin
|
|
348
|
+
if (dryRun) console.log(' ~ Would update tui.json (remove Pantheon-owned plugin references)')
|
|
349
|
+
else writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, 'utf8')
|
|
350
|
+
stats.removed += 1
|
|
351
|
+
}
|
|
955
352
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
console.log(' 📖 To verify, check the target directory for remaining Pantheon files.')
|
|
959
|
-
console.log(` 📚 Reinstall: npx pantheon-opencode init --project (in ${target})`)
|
|
960
|
-
}
|
|
961
|
-
console.log('')
|
|
353
|
+
function dirname(path) {
|
|
354
|
+
return path.slice(0, path.lastIndexOf('/'))
|
|
962
355
|
}
|
|
963
356
|
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
357
|
+
async function confirmPrompt(message) {
|
|
358
|
+
if (!isatty(process.stdin.fd)) return true
|
|
359
|
+
const readline = createInterface({ input: process.stdin, output: process.stdout })
|
|
360
|
+
return new Promise((answer) =>
|
|
361
|
+
readline.question(message, (value) => {
|
|
362
|
+
readline.close()
|
|
363
|
+
answer(['y', 'yes'].includes(value.trim().toLowerCase()))
|
|
364
|
+
}),
|
|
365
|
+
)
|
|
366
|
+
}
|
|
967
367
|
|
|
968
368
|
async function main() {
|
|
969
369
|
const args = parseArgs(process.argv)
|
|
370
|
+
if (args.help) return showHelp()
|
|
371
|
+
const info = lstat(args.target)
|
|
372
|
+
if (!info?.isDirectory() || info.isSymbolicLink())
|
|
373
|
+
throw new Error(`Target directory does not exist or is a symlink: ${args.target}`)
|
|
374
|
+
if (
|
|
375
|
+
!args.dryRun &&
|
|
376
|
+
!args.force &&
|
|
377
|
+
!(await confirmPrompt(`⚠️ Remove Pantheon OpenCode artifacts from ${args.target}? [y/N] `))
|
|
378
|
+
)
|
|
379
|
+
return console.log('❌ Uninstall cancelled.')
|
|
380
|
+
console.log(`${args.dryRun ? '🔍 Dry-run' : '🗑️ Uninstall'}: OpenCode/Pantheon only`)
|
|
381
|
+
uninstallOpenCode(args.target, args.dryRun)
|
|
382
|
+
console.log(`✅ ${stats.removed} removed, ${stats.skipped} skipped, ${stats.errors} errors`)
|
|
383
|
+
if (stats.errors) process.exitCode = 1
|
|
384
|
+
}
|
|
970
385
|
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
if (!existsSync(target)) {
|
|
979
|
-
console.error(`❌ Target directory does not exist: ${target}`)
|
|
980
|
-
process.exit(1)
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
let platforms = args.platforms
|
|
984
|
-
|
|
985
|
-
if (platforms?.includes('all')) {
|
|
986
|
-
platforms = [...ALL_PLATFORMS]
|
|
987
|
-
} else if (!platforms) {
|
|
988
|
-
const detected = detectPlatforms(target)
|
|
989
|
-
if (detected.length === 0) {
|
|
990
|
-
console.log(`🔍 No Pantheon platform config detected in ${target}`)
|
|
991
|
-
console.log(' Uninstalling all platforms.\n')
|
|
992
|
-
platforms = [...ALL_PLATFORMS]
|
|
993
|
-
} else {
|
|
994
|
-
console.log(`🔍 Detected platforms in ${target}: ${detected.join(', ')}\n`)
|
|
995
|
-
platforms = detected
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
console.log(args.dryRun ? '🔍 Dry-run mode — no files will be deleted\n' : '')
|
|
1000
|
-
console.log(`📦 Pantheon Uninstaller`)
|
|
1001
|
-
console.log(` Target: ${target}`)
|
|
1002
|
-
console.log(` Platforms: ${platforms.join(', ')}\n`)
|
|
1003
|
-
|
|
1004
|
-
// Confirmation (skip in dry-run mode, unless --force)
|
|
1005
|
-
if (!args.dryRun && !args.force) {
|
|
1006
|
-
const ok = await confirmPrompt(
|
|
1007
|
-
`⚠️ This will remove Pantheon agent files from ${target}\n Proceed? [y/N] `,
|
|
1008
|
-
)
|
|
1009
|
-
if (!ok) {
|
|
1010
|
-
console.log('❌ Uninstall cancelled.')
|
|
1011
|
-
process.exit(0)
|
|
1012
|
-
}
|
|
1013
|
-
console.log('')
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
for (const platform of platforms) {
|
|
1017
|
-
const label = PLATFORM_LABELS[platform] ?? platform
|
|
1018
|
-
console.log(`🗑️ ${label}`)
|
|
1019
|
-
|
|
1020
|
-
switch (platform) {
|
|
1021
|
-
case 'opencode':
|
|
1022
|
-
uninstallOpenCode(target, args.dryRun)
|
|
1023
|
-
break
|
|
1024
|
-
case 'claude':
|
|
1025
|
-
uninstallClaude(target, args.dryRun)
|
|
1026
|
-
break
|
|
1027
|
-
case 'cursor':
|
|
1028
|
-
uninstallCursor(target, args.dryRun)
|
|
1029
|
-
break
|
|
1030
|
-
case 'windsurf':
|
|
1031
|
-
uninstallWindsurf(target, args.dryRun)
|
|
1032
|
-
break
|
|
1033
|
-
case 'copilot':
|
|
1034
|
-
uninstallCopilot(target, args.dryRun)
|
|
1035
|
-
break
|
|
1036
|
-
case 'continue':
|
|
1037
|
-
uninstallContinue(target, args.dryRun)
|
|
1038
|
-
break
|
|
1039
|
-
case 'cline':
|
|
1040
|
-
uninstallCline(target, args.dryRun)
|
|
1041
|
-
break
|
|
1042
|
-
default:
|
|
1043
|
-
console.warn(` ⚠️ Unknown platform: ${platform} — skipping`)
|
|
1044
|
-
break
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
printUninstallSummary(target, platforms)
|
|
1049
|
-
process.exit(0)
|
|
386
|
+
export {
|
|
387
|
+
cleanOpenCodeConfig,
|
|
388
|
+
isPantheonContent,
|
|
389
|
+
main,
|
|
390
|
+
parseArgs,
|
|
391
|
+
resolveGlobalTarget,
|
|
392
|
+
uninstallOpenCode,
|
|
1050
393
|
}
|
|
1051
394
|
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
395
|
+
if (process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname))
|
|
396
|
+
main().catch((error) => {
|
|
397
|
+
console.error(`❌ Uninstall failed: ${error.message}`)
|
|
398
|
+
process.exit(1)
|
|
399
|
+
})
|