pantheon-opencode 1.6.0-beta.1 → 1.6.0-beta.11
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.global.md +206 -0
- package/AGENTS.md +10 -891
- package/CHANGELOG.md +350 -0
- package/README.md +561 -52
- package/README.pt-BR.md +285 -20
- package/bin/pantheon-init.mjs +70 -4
- package/commands/pantheon-audit.md +7 -1
- package/commands/pantheon-consolidate.md +1 -1
- package/commands/pantheon-deepwork.md +14 -3
- package/commands/pantheon-focus.md +17 -0
- package/commands/pantheon-vision.md +61 -12
- package/package.json +24 -17
- package/plugin.json +2 -2
- package/pyproject.toml +8 -6
- package/scripts/build-agents-md.mjs +428 -56
- package/scripts/changelog-merge.mjs +161 -0
- package/scripts/{code_mode_server.py → code_mode.py} +3 -3
- package/scripts/cost.mjs +244 -41
- package/scripts/doctor.mjs +151 -1
- package/scripts/install/agents-md.mjs +4 -4
- package/scripts/install/backup.mjs +475 -0
- package/scripts/install/cli-ui.mjs +6 -1
- package/scripts/install/config-migration.mjs +317 -4
- package/scripts/install/exit-codes.mjs +26 -0
- package/scripts/install/health-check.mjs +21 -19
- package/scripts/install/interactive.mjs +16 -2
- package/scripts/install/migrate.mjs +41 -22
- package/scripts/install/opencode-version.mjs +246 -4
- package/scripts/install/opencode.mjs +1175 -215
- package/scripts/install/plan.mjs +287 -0
- package/scripts/install/plugin.mjs +62 -14
- package/scripts/install/shared.mjs +190 -48
- package/scripts/install/strings.mjs +20 -0
- package/scripts/install/venv.mjs +13 -3
- package/scripts/install/vision-prompt.mjs +171 -0
- package/scripts/install-mcp.mjs +28 -9
- package/scripts/{mcp_persistence_server.py → mcp_persistence.py} +52 -17
- package/{src/mcp/mcp_resources_server.py → scripts/mcp_resources.py} +2 -2
- package/scripts/{memory_mcp_server.py → memory_mcp.py} +2 -2
- package/scripts/probe-context-rehydrate.mjs +8 -2
- package/scripts/probe-pantheon-cost.mjs +71 -15
- package/scripts/prune.mjs +9 -3
- package/scripts/restore.mjs +222 -0
- package/scripts/run-node-tests.mjs +189 -0
- package/scripts/sync-artifacts.mjs +26 -13
- package/scripts/sync-tui.mjs +24 -3
- package/scripts/test-opencode-v2-sandbox.sh +180 -26
- package/scripts/validate-routing.mjs +3 -3
- package/scripts/validate-sandbox-install.mjs +138 -0
- package/scripts/versioning.mjs +13 -1
- package/scripts/vision-status.mjs +682 -0
- package/skills-lock.json +8 -8
- package/src/agents/aphrodite.md +20 -37
- package/src/agents/apollo.md +7 -3
- package/src/agents/athena.md +63 -35
- package/src/agents/demeter.md +8 -28
- package/src/agents/gaia.md +1 -1
- package/src/agents/hephaestus.md +10 -21
- package/src/agents/hermes.md +15 -38
- package/src/agents/iris.md +7 -1
- package/src/agents/mnemosyne.md +18 -54
- package/src/agents/nyx.md +1 -1
- package/src/agents/prometheus.md +16 -20
- package/src/agents/talos.md +10 -2
- package/src/agents/themis.md +5 -5
- package/src/agents/zeus.md +99 -172
- package/src/instructions/agent-return-format.instructions.md +34 -17
- package/src/instructions/backend-standards.instructions.md +2 -0
- package/src/instructions/frontend-standards.instructions.md +3 -1
- package/src/instructions/memory-protocol.instructions.md +12 -110
- package/src/instructions/test-economy.instructions.md +21 -0
- package/src/instructions/zeus-anti-stall.instructions.md +20 -67
- package/src/instructions/zeus-communication-rules.instructions.md +0 -1
- package/src/instructions/zeus-council-synthesis.instructions.md +31 -260
- package/src/instructions/zeus-memory-operations.instructions.md +30 -0
- package/src/instructions/zeus-timeout-retry.instructions.md +61 -89
- package/src/mcp/{code_mode_server.py → code_mode.py} +3 -3
- package/src/mcp/eval_store.py +2 -2
- package/src/mcp/{mcp_persistence_server.py → mcp_persistence.py} +52 -17
- package/{scripts/mcp_resources_server.py → src/mcp/mcp_resources.py} +2 -2
- package/src/mcp/{memory_mcp_server.py → memory_mcp.py} +2 -2
- package/src/mcp/{pantheon_vision_server.py → pantheon_vision.py} +286 -42
- package/src/mcp/requirements-mcp.txt +6 -4
- package/src/mcp/requirements-vision.txt +2 -2
- package/src/pantheon/cost-command.ts +403 -102
- package/src/pantheon/delegation-enforce.ts +10 -1
- package/src/pantheon/model-agents.ts +19 -0
- package/src/pantheon/model-command.ts +218 -56
- package/src/pantheon/presets.d.mts +0 -8
- package/src/pantheon/presets.mjs +5 -32
- package/src/pantheon/v2-bounded-read.ts +640 -0
- package/src/pantheon/v2-delegation-enforce.ts +214 -0
- package/src/pantheon/v2-execute-after.ts +186 -0
- package/src/pantheon/v2-model-overrides.ts +275 -0
- package/src/pantheon/v2-tools.ts +418 -0
- package/src/pantheon/v2-unsupported.d.mts +11 -0
- package/src/pantheon/v2-unsupported.mjs +90 -0
- package/src/pantheon/v2-vision.ts +622 -0
- package/src/pantheon/vision-local.ts +531 -0
- package/src/pantheon/vision.ts +1160 -115
- package/src/plugin-v2.ts +1158 -219
- package/src/plugin.ts +12 -9
- package/src/plugins/tui/dist/server.d.ts +1 -2
- package/src/plugins/tui/dist/server.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui-v2.d.ts +154 -0
- package/src/plugins/tui/dist/tui-v2.d.ts.map +1 -0
- package/src/plugins/tui/dist/tui-v2.js +659 -0
- package/src/plugins/tui/dist/tui-v2.js.map +1 -0
- package/src/plugins/tui/dist/tui.d.ts +85 -82
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +72 -59
- package/src/plugins/tui/index-v2.ts +6 -0
- package/src/plugins/tui/package-lock.json +5516 -1353
- package/src/plugins/tui/package.json +22 -12
- package/src/plugins/tui/src/index-v2.tsx +734 -0
- package/src/plugins/tui/src/index.tsx +106 -85
- package/src/plugins/tui/src/opencode-v2.d.ts +13 -0
- package/src/plugins/tui/tsconfig.build.json +5 -0
- package/src/plugins/tui/tsconfig.typecheck.json +5 -0
- package/src/plugins/tui/tsdown.config.ts +22 -2
- package/src/plugins/tui/tui-v2.ts +2 -0
- package/src/plugins/tui/tui.ts +3 -0
- package/src/routing.yml +11 -21
- package/src/skills/README.md +3 -3
- package/src/skills/agent-coordination/SKILL.md +7 -2
- package/src/skills/artifact-management/SKILL.md +12 -5
- package/src/skills/auto-continue/SKILL.md +52 -35
- package/src/skills/code-review-checklist/SKILL.md +5 -5
- package/src/skills/context-compression/SKILL.md +26 -879
- package/src/skills/session-goal/SKILL.md +14 -5
- package/src/skills/spec-driven-development/SKILL.md +3 -1
- package/src/skills/tdd-with-agents/SKILL.md +18 -10
- package/opencode.json +0 -58
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* plan.mjs — Per-file change planning for the Pantheon installer.
|
|
3
|
+
*
|
|
4
|
+
* Beta 11, Setup phase 1: `pantheon-opencode init ... --dry-run` must print the
|
|
5
|
+
* REAL per-file change set (create / update / remove / unchanged) that the SAME
|
|
6
|
+
* code path would produce, and write nothing.
|
|
7
|
+
*
|
|
8
|
+
* The plan is built by a change recorder threaded through the writer helpers as
|
|
9
|
+
* an OPTIONAL argument whose default is a module-level no-op singleton, so any
|
|
10
|
+
* caller (or writer) that is not instrumented keeps byte-identical behavior.
|
|
11
|
+
*
|
|
12
|
+
* NET-OUTCOME semantics: several writers can touch the same path in one pass
|
|
13
|
+
* (the TUI `unregister`→`register` toggle; the code-mode `syncDir` payload and
|
|
14
|
+
* its generated `manifest.json`). The LAST writer decides the net disk result,
|
|
15
|
+
* so a later no-write (skipped/unchanged) leaves the path UNCHANGED even when an
|
|
16
|
+
* earlier writer computed a would-be update. A no-write never promotes a path to
|
|
17
|
+
* a scored change.
|
|
18
|
+
*
|
|
19
|
+
* "Scored" vs "unscored": writers that increment the install summary
|
|
20
|
+
* (`stats.created`) alongside a genuine change are SCORED. Artifacts that are
|
|
21
|
+
* not part of that summary (the synthetic `.venv` entry, the state-only
|
|
22
|
+
* `.pantheon/install-state.json` write, the backup-set manifest, and the TUI
|
|
23
|
+
* stale-ref cleanup) are recorded UNSCORED so they still appear in the printed
|
|
24
|
+
* plan but never affect the consistency guard.
|
|
25
|
+
*
|
|
26
|
+
* Security: the recorder, the summary and the formatter deal in paths, actions
|
|
27
|
+
* and byte sizes ONLY. File contents and hashes stay internal — hashes are kept
|
|
28
|
+
* so a later phase can detect drift between a preview and an apply, and are
|
|
29
|
+
* never printed.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { createHash } from 'node:crypto'
|
|
33
|
+
import { lstatSync, readFileSync } from 'node:fs'
|
|
34
|
+
import { isAbsolute, relative } from 'node:path'
|
|
35
|
+
|
|
36
|
+
/** The action vocabulary shared by the recorder, the summary and the formatter. */
|
|
37
|
+
export const ACTIONS = Object.freeze({
|
|
38
|
+
CREATE: 'create',
|
|
39
|
+
UPDATE: 'update',
|
|
40
|
+
REMOVE: 'remove',
|
|
41
|
+
UNCHANGED: 'unchanged',
|
|
42
|
+
CONFLICT: 'conflict',
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const ACTION_VALUES = new Set(Object.values(ACTIONS))
|
|
46
|
+
|
|
47
|
+
/** Whether an action represents a real change to the target (not a no-write). */
|
|
48
|
+
function isChange(action) {
|
|
49
|
+
return action === ACTIONS.CREATE || action === ACTIONS.UPDATE || action === ACTIONS.REMOVE
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Display order for the formatted plan.
|
|
53
|
+
const DISPLAY_ORDER = Object.freeze([
|
|
54
|
+
ACTIONS.CREATE,
|
|
55
|
+
ACTIONS.UPDATE,
|
|
56
|
+
ACTIONS.REMOVE,
|
|
57
|
+
ACTIONS.CONFLICT,
|
|
58
|
+
ACTIONS.UNCHANGED,
|
|
59
|
+
])
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The module-level no-op recorder. Instrumented writers whose caller did not
|
|
63
|
+
* pass a recorder fall back to this, so their behavior is byte-identical to an
|
|
64
|
+
* uninstrumented run (no allocation, no side effects).
|
|
65
|
+
*/
|
|
66
|
+
export function noopRecorder() {}
|
|
67
|
+
|
|
68
|
+
/** The default recorder instance — the no-op singleton. */
|
|
69
|
+
export const NOOP_RECORDER = noopRecorder
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Whether a recorder will actually collect anything. Writers in hot loops use
|
|
73
|
+
* this to skip `Buffer.byteLength(content)` on the common (uninstrumented) path.
|
|
74
|
+
*
|
|
75
|
+
* @param {unknown} recorder
|
|
76
|
+
* @returns {boolean}
|
|
77
|
+
*/
|
|
78
|
+
export function isRecording(recorder) {
|
|
79
|
+
return typeof recorder === 'function' && recorder !== NOOP_RECORDER
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Normalize an arbitrary change into the `{ path, action, bytes, scored }` shape. */
|
|
83
|
+
function normalizeChange(change) {
|
|
84
|
+
const action = change && ACTION_VALUES.has(change.action) ? change.action : ACTIONS.UNCHANGED
|
|
85
|
+
return {
|
|
86
|
+
path: String(change?.path ?? ''),
|
|
87
|
+
action,
|
|
88
|
+
bytes: Number.isFinite(change?.bytes) ? change.bytes : 0,
|
|
89
|
+
// Scored by default: most writers feed the install summary. Synthetic and
|
|
90
|
+
// state-only entries opt out explicitly with `scored: false`.
|
|
91
|
+
scored: !(change && change.scored === false),
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Create a recorder that collects changes in memory. `recorder` is the function
|
|
97
|
+
* to thread into the writer helpers; `entries` holds the raw recorded changes.
|
|
98
|
+
*
|
|
99
|
+
* @returns {{ recorder: (change: object) => void, entries: object[] }}
|
|
100
|
+
*/
|
|
101
|
+
export function createRecorder() {
|
|
102
|
+
const entries = []
|
|
103
|
+
const recorder = (change) => {
|
|
104
|
+
entries.push(normalizeChange(change))
|
|
105
|
+
}
|
|
106
|
+
return { recorder, entries }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Record a single change. Writers call this instead of touching fs bookkeeping.
|
|
111
|
+
* With no explicit recorder the module-level no-op singleton is used.
|
|
112
|
+
*
|
|
113
|
+
* @param {{ path: string, action: string, bytes?: number, scored?: boolean }} change
|
|
114
|
+
* @param {(change: object) => void} [recorder]
|
|
115
|
+
*/
|
|
116
|
+
export function recordChange(change, recorder = NOOP_RECORDER) {
|
|
117
|
+
if (!isRecording(recorder)) return
|
|
118
|
+
recorder(normalizeChange(change))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Sort helper: display order first, then path for determinism. */
|
|
122
|
+
function compareFiles(a, b) {
|
|
123
|
+
const order = DISPLAY_ORDER.indexOf(a.action) - DISPLAY_ORDER.indexOf(b.action)
|
|
124
|
+
if (order !== 0) return order
|
|
125
|
+
return a.path.localeCompare(b.path)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function zeroTotals() {
|
|
129
|
+
return { create: 0, update: 0, remove: 0, unchanged: 0, conflict: 0 }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Tally a change into one of the five action buckets. */
|
|
133
|
+
function tally(totals, action) {
|
|
134
|
+
if (Object.hasOwn(totals, action)) totals[action] += 1
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Collapse recorded changes into the final change set: one entry per path,
|
|
139
|
+
* reflecting the NET disk outcome, plus totals.
|
|
140
|
+
*
|
|
141
|
+
* The last record for a path decides the action, so a later no-write wins over
|
|
142
|
+
* an earlier would-be write. A path is scored only if at least one of its
|
|
143
|
+
* CHANGE records was scored AND the net action is still a change — a no-write
|
|
144
|
+
* can never turn a path into a scored change.
|
|
145
|
+
*
|
|
146
|
+
* @param {object[]} entries
|
|
147
|
+
* @returns {{
|
|
148
|
+
* files: object[],
|
|
149
|
+
* totals: { create: number, update: number, remove: number, unchanged: number, conflict: number },
|
|
150
|
+
* scored: { create: number, update: number, remove: number, unchanged: number, conflict: number },
|
|
151
|
+
* }}
|
|
152
|
+
*/
|
|
153
|
+
export function summarizeChanges(entries = []) {
|
|
154
|
+
const byPath = new Map()
|
|
155
|
+
for (const entry of entries ?? []) {
|
|
156
|
+
const change = normalizeChange(entry)
|
|
157
|
+
let record = byPath.get(change.path)
|
|
158
|
+
if (!record) {
|
|
159
|
+
record = { path: change.path, action: change.action, bytes: change.bytes, scored: false }
|
|
160
|
+
byPath.set(change.path, record)
|
|
161
|
+
}
|
|
162
|
+
// The LAST writer decides the net on-disk outcome. A later no-write
|
|
163
|
+
// (skipped/unchanged) leaves the path unchanged even if an earlier writer
|
|
164
|
+
// computed a would-be update (e.g. unregister→register toggling).
|
|
165
|
+
record.action = change.action
|
|
166
|
+
record.bytes = change.bytes
|
|
167
|
+
// Only a genuine change can score a path; a no-write never does.
|
|
168
|
+
if (isChange(change.action) && change.scored) record.scored = true
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const files = [...byPath.values()].sort(compareFiles)
|
|
172
|
+
for (const file of files) {
|
|
173
|
+
if (!isChange(file.action)) file.scored = false
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const totals = zeroTotals()
|
|
177
|
+
const scored = zeroTotals()
|
|
178
|
+
for (const file of files) {
|
|
179
|
+
tally(totals, file.action)
|
|
180
|
+
if (file.scored) tally(scored, file.action)
|
|
181
|
+
}
|
|
182
|
+
return { files, totals, scored }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Human-readable byte size. The caller never sees raw bytes or hashes. */
|
|
186
|
+
function formatBytes(bytes) {
|
|
187
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return '0 B'
|
|
188
|
+
if (bytes < 1024) return `${bytes} B`
|
|
189
|
+
const kb = bytes / 1024
|
|
190
|
+
if (kb < 1024) return `${kb.toFixed(1)} KB`
|
|
191
|
+
return `${(kb / 1024).toFixed(1)} MB`
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Path shown in the plan: relative to the target when it lives inside it. */
|
|
195
|
+
export function displayPath(target, filePath) {
|
|
196
|
+
if (!target) return filePath
|
|
197
|
+
const rel = relative(target, filePath)
|
|
198
|
+
if (rel === '') return '.'
|
|
199
|
+
return rel.startsWith('..') || isAbsolute(rel) ? filePath : rel
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Render the secret-safe plan: paths + action + byte size ONLY. No contents,
|
|
204
|
+
* no hashes.
|
|
205
|
+
*
|
|
206
|
+
* @param {string} target - install target (used to relativize paths)
|
|
207
|
+
* @param {{ files: object[], totals: object }} plan
|
|
208
|
+
* @returns {string}
|
|
209
|
+
*/
|
|
210
|
+
export function formatChangePlan(target, plan) {
|
|
211
|
+
const { files, totals } = plan
|
|
212
|
+
const lines = []
|
|
213
|
+
lines.push(`Change plan — ${target}`)
|
|
214
|
+
for (const file of files) {
|
|
215
|
+
const isWrite = file.action === ACTIONS.CREATE || file.action === ACTIONS.UPDATE
|
|
216
|
+
const size = isWrite ? ` (${formatBytes(file.bytes)})` : ''
|
|
217
|
+
lines.push(` ${file.action.padEnd(9)} ${displayPath(target, file.path)}${size}`)
|
|
218
|
+
}
|
|
219
|
+
lines.push(` ${'─'.repeat(28)}`)
|
|
220
|
+
lines.push(
|
|
221
|
+
` ${totals.create} create · ${totals.update} update · ${totals.remove} remove · ` +
|
|
222
|
+
`${totals.unchanged} unchanged · ${totals.conflict} conflict`,
|
|
223
|
+
)
|
|
224
|
+
lines.push(' No files were written.')
|
|
225
|
+
return lines.join('\n')
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Snapshot a path for later drift comparison. Hashes stay internal.
|
|
230
|
+
*
|
|
231
|
+
* @param {string} filePath
|
|
232
|
+
* @returns {{ exists: boolean, sha256: string|null, size: number, mode: number|null }}
|
|
233
|
+
*/
|
|
234
|
+
export function snapshot(filePath) {
|
|
235
|
+
let stat
|
|
236
|
+
try {
|
|
237
|
+
stat = lstatSync(filePath)
|
|
238
|
+
} catch {
|
|
239
|
+
return { exists: false, sha256: null, size: 0, mode: null }
|
|
240
|
+
}
|
|
241
|
+
if (!stat.isFile()) {
|
|
242
|
+
return { exists: true, sha256: null, size: stat.size, mode: stat.mode }
|
|
243
|
+
}
|
|
244
|
+
let buffer
|
|
245
|
+
try {
|
|
246
|
+
buffer = readFileSync(filePath)
|
|
247
|
+
} catch {
|
|
248
|
+
return { exists: true, sha256: null, size: stat.size, mode: stat.mode }
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
exists: true,
|
|
252
|
+
sha256: createHash('sha256').update(buffer).digest('hex'),
|
|
253
|
+
size: buffer.length,
|
|
254
|
+
mode: stat.mode,
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Compare the preview's SCORED change count against this call's install
|
|
260
|
+
* summary delta. Returns a descriptor on mismatch, or `null` when they agree.
|
|
261
|
+
*
|
|
262
|
+
* Only the CHANGED count is gated (`create + update + remove` vs
|
|
263
|
+
* `stats.created`). `stats.skipped` deliberately mixes genuine file-unchanged
|
|
264
|
+
* outcomes with logical no-op skips that never touch a file (e.g. the V2 TUI
|
|
265
|
+
* registration skip), so it cannot be reconciled against `plan.scored.unchanged`
|
|
266
|
+
* without making the guard cry wolf. The meaningful invariant is: every change
|
|
267
|
+
* the summary counted must have been recorded — a preview that silently missed
|
|
268
|
+
* a writer is exactly a `stats.created` with no recorded change.
|
|
269
|
+
*
|
|
270
|
+
* Both sides must be scoped to a SINGLE `installOpenCode` call.
|
|
271
|
+
*
|
|
272
|
+
* @param {{ scored: object }} plan
|
|
273
|
+
* @param {{ created: number, skipped: number }} stats - per-call delta
|
|
274
|
+
* @returns {{ changed: number, unchanged: number, recordedChanged: number, recordedUnchanged: number }|null}
|
|
275
|
+
*/
|
|
276
|
+
export function planStatsMismatch(plan, stats) {
|
|
277
|
+
const changed = plan.scored.create + plan.scored.update + plan.scored.remove
|
|
278
|
+
if (changed !== stats.created) {
|
|
279
|
+
return {
|
|
280
|
+
changed,
|
|
281
|
+
unchanged: plan.scored.unchanged,
|
|
282
|
+
recordedChanged: stats.created,
|
|
283
|
+
recordedUnchanged: stats.skipped,
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return null
|
|
287
|
+
}
|
|
@@ -8,24 +8,29 @@ import { isAbsolute, join, resolve } from 'node:path'
|
|
|
8
8
|
import { ROOT, writeIfChanged } from './shared.mjs'
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Copy a plugin's runtime payload (the bundled dist/*,
|
|
12
|
-
* package-lock.json) from source to destination.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* Copy a plugin's runtime payload (the root TUI loader, bundled dist/*,
|
|
12
|
+
* package.json and package-lock.json) from source to destination. The raw TSX
|
|
13
|
+
* implementation is never shipped to the destination; tui.ts only re-exports
|
|
14
|
+
* the host-specific bundle so the local plugin stays hermetic.
|
|
15
15
|
* @param {string} srcDir - Source plugin directory (e.g. ROOT/src/plugins/tui)
|
|
16
16
|
* @param {string} dstDir - Destination plugin directory
|
|
17
17
|
* @param {object} [options]
|
|
18
18
|
* @param {boolean} [options.dryRun=false] - If true, don't write files
|
|
19
|
+
* @param {'v1'|'v2'} [options.version='v1'] - Host plugin generation
|
|
20
|
+
* @param {(change: object) => void} [options.recorder] - optional change recorder for preview
|
|
19
21
|
* @returns {{ created: number, skipped: number }}
|
|
20
22
|
*/
|
|
21
|
-
export function copyPluginFiles(
|
|
23
|
+
export function copyPluginFiles(
|
|
24
|
+
srcDir,
|
|
25
|
+
dstDir,
|
|
26
|
+
{ dryRun = false, version = 'v1', recorder = null } = {},
|
|
27
|
+
) {
|
|
22
28
|
const result = { created: 0, skipped: 0 }
|
|
23
29
|
|
|
24
30
|
if (!dryRun) mkdirSync(dstDir, { recursive: true })
|
|
25
31
|
if (!dryRun) mkdirSync(join(dstDir, 'dist'), { recursive: true })
|
|
26
32
|
|
|
27
33
|
// Drop legacy raw copies from previous installs (root index.tsx + dist/tui.tsx).
|
|
28
|
-
// The loader consumes only the bundle, so stale raws must not linger.
|
|
29
34
|
if (!dryRun) {
|
|
30
35
|
rmSync(join(dstDir, 'index.tsx'), { force: true })
|
|
31
36
|
rmSync(join(dstDir, 'dist', 'tui.tsx'), { force: true })
|
|
@@ -36,7 +41,7 @@ export function copyPluginFiles(srcDir, dstDir, { dryRun = false } = {}) {
|
|
|
36
41
|
if (existsSync(distSrc)) {
|
|
37
42
|
for (const f of readdirSync(distSrc)) {
|
|
38
43
|
const c = readFileSync(join(distSrc, f), 'utf8')
|
|
39
|
-
const status = writeIfChanged(join(dstDir, 'dist', f), c, dryRun)
|
|
44
|
+
const status = writeIfChanged(join(dstDir, 'dist', f), c, dryRun, { recorder })
|
|
40
45
|
if (status === 'created') result.created++
|
|
41
46
|
else result.skipped++
|
|
42
47
|
}
|
|
@@ -45,15 +50,48 @@ export function copyPluginFiles(srcDir, dstDir, { dryRun = false } = {}) {
|
|
|
45
50
|
// Copy package.json
|
|
46
51
|
const pkgSrc = join(srcDir, 'package.json')
|
|
47
52
|
if (existsSync(pkgSrc)) {
|
|
53
|
+
const packageJson = JSON.parse(readFileSync(pkgSrc, 'utf8'))
|
|
54
|
+
if (version === 'v2' && packageJson.exports?.['./tui-v2']) {
|
|
55
|
+
packageJson.exports['./tui'] = packageJson.exports['./tui-v2']
|
|
56
|
+
}
|
|
48
57
|
const pkgStatus = writeIfChanged(
|
|
49
58
|
join(dstDir, 'package.json'),
|
|
50
|
-
|
|
59
|
+
`${JSON.stringify(packageJson, null, 2)}\n`,
|
|
51
60
|
dryRun,
|
|
61
|
+
{ recorder },
|
|
52
62
|
)
|
|
53
63
|
if (pkgStatus === 'created') result.created++
|
|
54
64
|
else result.skipped++
|
|
55
65
|
}
|
|
56
66
|
|
|
67
|
+
// Each host generation receives its own root loader and compiled bundle.
|
|
68
|
+
const tuiEntrySrc = join(srcDir, version === 'v2' ? 'tui-v2.ts' : 'tui.ts')
|
|
69
|
+
if (existsSync(tuiEntrySrc)) {
|
|
70
|
+
const entryStatus = writeIfChanged(
|
|
71
|
+
join(dstDir, 'tui.ts'),
|
|
72
|
+
readFileSync(tuiEntrySrc, 'utf8'),
|
|
73
|
+
dryRun,
|
|
74
|
+
{ recorder },
|
|
75
|
+
)
|
|
76
|
+
if (entryStatus === 'created') result.created++
|
|
77
|
+
else result.skipped++
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const v2ServerEntry = join(srcDir, 'index-v2.ts')
|
|
81
|
+
const v2ServerDestination = join(dstDir, 'index.ts')
|
|
82
|
+
if (version === 'v2' && existsSync(v2ServerEntry)) {
|
|
83
|
+
const entryStatus = writeIfChanged(
|
|
84
|
+
v2ServerDestination,
|
|
85
|
+
readFileSync(v2ServerEntry, 'utf8'),
|
|
86
|
+
dryRun,
|
|
87
|
+
{ recorder },
|
|
88
|
+
)
|
|
89
|
+
if (entryStatus === 'created') result.created++
|
|
90
|
+
else result.skipped++
|
|
91
|
+
} else if (!dryRun) {
|
|
92
|
+
rmSync(v2ServerDestination, { force: true })
|
|
93
|
+
}
|
|
94
|
+
|
|
57
95
|
// Copy the lockfile so dependency installation is reproducible. A missing
|
|
58
96
|
// lockfile is intentionally not synthesized by the installer.
|
|
59
97
|
const lockSrc = join(srcDir, 'package-lock.json')
|
|
@@ -62,6 +100,7 @@ export function copyPluginFiles(srcDir, dstDir, { dryRun = false } = {}) {
|
|
|
62
100
|
join(dstDir, 'package-lock.json'),
|
|
63
101
|
readFileSync(lockSrc, 'utf8'),
|
|
64
102
|
dryRun,
|
|
103
|
+
{ recorder },
|
|
65
104
|
)
|
|
66
105
|
if (lockStatus === 'created') result.created++
|
|
67
106
|
else result.skipped++
|
|
@@ -77,16 +116,21 @@ export function copyPluginFiles(srcDir, dstDir, { dryRun = false } = {}) {
|
|
|
77
116
|
* @param {object} [options]
|
|
78
117
|
* @param {boolean} [options.dryRun=false] - If true, don't write files
|
|
79
118
|
* @param {boolean} [options.clean=false] - If true, remove existing before install
|
|
119
|
+
* @param {(change: object) => void} [options.recorder] - optional change recorder for preview
|
|
80
120
|
* @returns {{ created: number, skipped: number, errors: number }}
|
|
81
121
|
*/
|
|
82
|
-
export function installPlugin(
|
|
122
|
+
export function installPlugin(
|
|
123
|
+
srcDir,
|
|
124
|
+
dstDir,
|
|
125
|
+
{ dryRun = false, clean = false, version = 'v1', recorder = null } = {},
|
|
126
|
+
) {
|
|
83
127
|
const result = { created: 0, skipped: 0, errors: 0 }
|
|
84
128
|
|
|
85
129
|
if (clean && existsSync(dstDir) && !dryRun) {
|
|
86
130
|
rmSync(dstDir, { recursive: true, force: true })
|
|
87
131
|
}
|
|
88
132
|
|
|
89
|
-
const copyResult = copyPluginFiles(srcDir, dstDir, { dryRun })
|
|
133
|
+
const copyResult = copyPluginFiles(srcDir, dstDir, { dryRun, version, recorder })
|
|
90
134
|
result.created += copyResult.created
|
|
91
135
|
result.skipped += copyResult.skipped
|
|
92
136
|
|
|
@@ -116,9 +160,10 @@ export function installPluginDependencies(pluginDir, { dryRun = false } = {}) {
|
|
|
116
160
|
* @param {string} pluginId - Plugin identifier string
|
|
117
161
|
* @param {object} [options]
|
|
118
162
|
* @param {boolean} [options.dryRun=false]
|
|
163
|
+
* @param {(change: object) => void} [options.recorder] - optional change recorder for preview
|
|
119
164
|
* @returns {'created'|'skipped'}
|
|
120
165
|
*/
|
|
121
|
-
export function registerPlugin(tuiJsonPath, pluginId, { dryRun = false } = {}) {
|
|
166
|
+
export function registerPlugin(tuiJsonPath, pluginId, { dryRun = false, recorder = null } = {}) {
|
|
122
167
|
let tuiConfig = { plugin: [] }
|
|
123
168
|
if (existsSync(tuiJsonPath)) {
|
|
124
169
|
try {
|
|
@@ -134,7 +179,7 @@ export function registerPlugin(tuiJsonPath, pluginId, { dryRun = false } = {}) {
|
|
|
134
179
|
tuiConfig.plugin.push(pluginId)
|
|
135
180
|
}
|
|
136
181
|
const tuiContent = `${JSON.stringify(tuiConfig, null, 2)}\n`
|
|
137
|
-
return writeIfChanged(tuiJsonPath, tuiContent, dryRun)
|
|
182
|
+
return writeIfChanged(tuiJsonPath, tuiContent, dryRun, { recorder })
|
|
138
183
|
}
|
|
139
184
|
|
|
140
185
|
/**
|
|
@@ -222,12 +267,14 @@ export function staleTuiRefs(pluginArray, knownPantheonPaths = []) {
|
|
|
222
267
|
* @param {string} pluginId - Plugin to unregister
|
|
223
268
|
* @param {object} [options]
|
|
224
269
|
* @param {boolean} [options.dryRun=false]
|
|
270
|
+
* @param {string[]} [options.knownPantheonPaths=[]]
|
|
271
|
+
* @param {(change: object) => void} [options.recorder] - optional change recorder for preview
|
|
225
272
|
* @returns {'created'|'skipped'}
|
|
226
273
|
*/
|
|
227
274
|
export function unregisterPlugin(
|
|
228
275
|
tuiJsonPath,
|
|
229
276
|
pluginId,
|
|
230
|
-
{ dryRun = false, knownPantheonPaths = [] } = {},
|
|
277
|
+
{ dryRun = false, knownPantheonPaths = [], recorder = null } = {},
|
|
231
278
|
) {
|
|
232
279
|
if (!existsSync(tuiJsonPath)) {
|
|
233
280
|
// Nothing to clean — and never create an empty tui.json in a location
|
|
@@ -258,5 +305,6 @@ export function unregisterPlugin(
|
|
|
258
305
|
return !isPantheonTuiRef(ref, knownPantheonPaths)
|
|
259
306
|
})
|
|
260
307
|
const tuiContent = `${JSON.stringify(tuiConfig, null, 2)}\n`
|
|
261
|
-
|
|
308
|
+
// Stale-ref cleanup is not part of the install summary, so record it unscored.
|
|
309
|
+
return writeIfChanged(tuiJsonPath, tuiContent, dryRun, { recorder, scored: false })
|
|
262
310
|
}
|