pantheon-opencode 1.6.0-beta.11 → 1.6.0-beta.12
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 +3 -3
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +22 -0
- package/README.md +69 -30
- package/README.pt-BR.md +5 -4
- package/bin/pantheon-init.mjs +279 -101
- package/package.json +4 -3
- package/plugin.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/build-agents-md.mjs +77 -13
- package/scripts/check-plugin-v2-coverage.mjs +7 -7
- package/scripts/doctor.mjs +242 -19
- package/scripts/install/backup.mjs +191 -18
- package/scripts/install/cli-ui.mjs +73 -0
- package/scripts/install/file-inventory.mjs +555 -0
- package/scripts/install/migrate.mjs +45 -38
- package/scripts/install/model-picker.mjs +63 -22
- package/scripts/install/opencode-version.mjs +9 -1
- package/scripts/install/opencode.mjs +265 -79
- package/scripts/install/plan-artifact.mjs +517 -0
- package/scripts/install/plan.mjs +165 -3
- package/scripts/install/plugin.mjs +118 -14
- package/scripts/install/shared.mjs +186 -65
- package/scripts/install/state.mjs +63 -8
- package/scripts/install/venv.mjs +15 -6
- package/scripts/install/verify.mjs +128 -0
- package/scripts/install/vision-prompt.mjs +2 -0
- package/scripts/install-mcp.mjs +1 -1
- package/scripts/manifest-inventory.mjs +49 -3
- package/scripts/node-engine-policy.mjs +74 -0
- package/scripts/package-evidence.mjs +6 -0
- package/scripts/postinstall.mjs +3 -4
- package/scripts/reconcile.mjs +325 -0
- package/scripts/uninstall.mjs +258 -34
- package/scripts/update.mjs +164 -0
- package/scripts/validate-node-engine-policy.mjs +13 -0
- package/scripts/version-check.mjs +34 -1
- package/scripts/versioning.mjs +9 -1
- package/skills-lock.json +3 -3
- package/src/agents/mnemosyne.md +7 -2
- package/src/instructions/memory-protocol.instructions.md +2 -2
- package/src/instructions/zeus-memory-operations.instructions.md +4 -3
- package/src/pantheon/v2-unsupported.mjs +2 -3
- package/src/plugins/tui/dist/tui-v2.js +2 -2
- package/src/plugins/tui/dist/tui.js +1 -1
- package/src/plugins/tui/package-lock.json +3 -3
- package/src/plugins/tui/package.json +2 -2
- package/src/skills/artifact-management/SKILL.md +5 -1
- package/src/skills/memory-bank/SKILL.md +5 -1
- package/src/skills/worktrees/SKILL.md +30 -11
package/scripts/install/plan.mjs
CHANGED
|
@@ -30,8 +30,14 @@
|
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
32
|
import { createHash } from 'node:crypto'
|
|
33
|
-
import { lstatSync, readFileSync } from 'node:fs'
|
|
34
|
-
import { isAbsolute, relative } from 'node:path'
|
|
33
|
+
import { lstatSync, readdirSync, readFileSync, rmdirSync, unlinkSync } from 'node:fs'
|
|
34
|
+
import { isAbsolute, relative, resolve, sep } from 'node:path'
|
|
35
|
+
import {
|
|
36
|
+
assertNoSymlinkAncestors,
|
|
37
|
+
assertPathWithin,
|
|
38
|
+
assertSafeRemoval,
|
|
39
|
+
verifyPathRemoved,
|
|
40
|
+
} from './verify.mjs'
|
|
35
41
|
|
|
36
42
|
/** The action vocabulary shared by the recorder, the summary and the formatter. */
|
|
37
43
|
export const ACTIONS = Object.freeze({
|
|
@@ -86,6 +92,8 @@ function normalizeChange(change) {
|
|
|
86
92
|
path: String(change?.path ?? ''),
|
|
87
93
|
action,
|
|
88
94
|
bytes: Number.isFinite(change?.bytes) ? change.bytes : 0,
|
|
95
|
+
outputFingerprint:
|
|
96
|
+
typeof change?.outputFingerprint === 'string' ? change.outputFingerprint : null,
|
|
89
97
|
// Scored by default: most writers feed the install summary. Synthetic and
|
|
90
98
|
// state-only entries opt out explicitly with `scored: false`.
|
|
91
99
|
scored: !(change && change.scored === false),
|
|
@@ -118,6 +126,11 @@ export function recordChange(change, recorder = NOOP_RECORDER) {
|
|
|
118
126
|
recorder(normalizeChange(change))
|
|
119
127
|
}
|
|
120
128
|
|
|
129
|
+
/** Return the non-displayed SHA-256 fingerprint used by reviewed plan artifacts. */
|
|
130
|
+
export function fingerprintOutput(content) {
|
|
131
|
+
return createHash('sha256').update(content).digest('hex')
|
|
132
|
+
}
|
|
133
|
+
|
|
121
134
|
/** Sort helper: display order first, then path for determinism. */
|
|
122
135
|
function compareFiles(a, b) {
|
|
123
136
|
const order = DISPLAY_ORDER.indexOf(a.action) - DISPLAY_ORDER.indexOf(b.action)
|
|
@@ -156,7 +169,13 @@ export function summarizeChanges(entries = []) {
|
|
|
156
169
|
const change = normalizeChange(entry)
|
|
157
170
|
let record = byPath.get(change.path)
|
|
158
171
|
if (!record) {
|
|
159
|
-
record = {
|
|
172
|
+
record = {
|
|
173
|
+
path: change.path,
|
|
174
|
+
action: change.action,
|
|
175
|
+
bytes: change.bytes,
|
|
176
|
+
outputFingerprint: change.outputFingerprint,
|
|
177
|
+
scored: false,
|
|
178
|
+
}
|
|
160
179
|
byPath.set(change.path, record)
|
|
161
180
|
}
|
|
162
181
|
// The LAST writer decides the net on-disk outcome. A later no-write
|
|
@@ -164,6 +183,7 @@ export function summarizeChanges(entries = []) {
|
|
|
164
183
|
// computed a would-be update (e.g. unregister→register toggling).
|
|
165
184
|
record.action = change.action
|
|
166
185
|
record.bytes = change.bytes
|
|
186
|
+
record.outputFingerprint = change.outputFingerprint
|
|
167
187
|
// Only a genuine change can score a path; a no-write never does.
|
|
168
188
|
if (isChange(change.action) && change.scored) record.scored = true
|
|
169
189
|
}
|
|
@@ -238,6 +258,28 @@ export function snapshot(filePath) {
|
|
|
238
258
|
} catch {
|
|
239
259
|
return { exists: false, sha256: null, size: 0, mode: null }
|
|
240
260
|
}
|
|
261
|
+
if (stat.isDirectory()) {
|
|
262
|
+
// Directory snapshots include the direct child inventory. Descendant
|
|
263
|
+
// directories/files are snapshotted separately by collectRemovalTree().
|
|
264
|
+
const entries = readdirSync(filePath, { withFileTypes: true })
|
|
265
|
+
.map((entry) => [
|
|
266
|
+
entry.name,
|
|
267
|
+
entry.isDirectory()
|
|
268
|
+
? 'directory'
|
|
269
|
+
: entry.isFile()
|
|
270
|
+
? 'file'
|
|
271
|
+
: entry.isSymbolicLink()
|
|
272
|
+
? 'symlink'
|
|
273
|
+
: 'other',
|
|
274
|
+
])
|
|
275
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
276
|
+
return {
|
|
277
|
+
exists: true,
|
|
278
|
+
sha256: createHash('sha256').update(JSON.stringify(entries)).digest('hex'),
|
|
279
|
+
size: stat.size,
|
|
280
|
+
mode: stat.mode,
|
|
281
|
+
}
|
|
282
|
+
}
|
|
241
283
|
if (!stat.isFile()) {
|
|
242
284
|
return { exists: true, sha256: null, size: stat.size, mode: stat.mode }
|
|
243
285
|
}
|
|
@@ -255,6 +297,126 @@ export function snapshot(filePath) {
|
|
|
255
297
|
}
|
|
256
298
|
}
|
|
257
299
|
|
|
300
|
+
/**
|
|
301
|
+
* Enumerate a path and its complete subtree as removal operations with
|
|
302
|
+
* pre-apply snapshots. Symlinks are treated as leaf entries and never followed.
|
|
303
|
+
*
|
|
304
|
+
* @param {string} rootPath
|
|
305
|
+
* @returns {{path: string, snapshot: object}[]}
|
|
306
|
+
*/
|
|
307
|
+
export function collectRemovalTree(rootPath) {
|
|
308
|
+
const entries = []
|
|
309
|
+
const visit = (filePath, allowMissing = false) => {
|
|
310
|
+
let stat
|
|
311
|
+
try {
|
|
312
|
+
stat = lstatSync(filePath)
|
|
313
|
+
} catch (error) {
|
|
314
|
+
if (allowMissing && error?.code === 'ENOENT') return
|
|
315
|
+
throw error
|
|
316
|
+
}
|
|
317
|
+
entries.push({ path: filePath, snapshot: snapshot(filePath) })
|
|
318
|
+
if (stat.isDirectory()) {
|
|
319
|
+
for (const name of readdirSync(filePath).sort()) visit(`${filePath}${sep}${name}`)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
visit(rootPath, true)
|
|
323
|
+
return entries
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Record every path that a recursive removal will delete. */
|
|
327
|
+
export function recordRemovalTree(rootPath, recorder, { scored = false } = {}) {
|
|
328
|
+
const entries = collectRemovalTree(rootPath)
|
|
329
|
+
for (const entry of entries) {
|
|
330
|
+
recordChange({ path: entry.path, action: ACTIONS.REMOVE, bytes: 0, scored }, recorder)
|
|
331
|
+
}
|
|
332
|
+
return entries
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Fail closed unless the exact reviewed subtree and its snapshots still match.
|
|
337
|
+
*
|
|
338
|
+
* @param {string} rootPath
|
|
339
|
+
* @param {Set<string>|null} reviewedRemovals
|
|
340
|
+
* @param {Map<string, object>|null} reviewedSnapshots
|
|
341
|
+
*/
|
|
342
|
+
export function validateRemovalTree(rootPath, reviewedRemovals, reviewedSnapshots) {
|
|
343
|
+
if (!reviewedRemovals || !reviewedSnapshots) {
|
|
344
|
+
throw new Error(`Refusing destructive removal without a reviewed snapshot: ${rootPath}`)
|
|
345
|
+
}
|
|
346
|
+
const root = resolve(rootPath)
|
|
347
|
+
const isWithinRoot = (filePath) => {
|
|
348
|
+
const rel = relative(root, resolve(filePath))
|
|
349
|
+
return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel))
|
|
350
|
+
}
|
|
351
|
+
const expected = new Set(
|
|
352
|
+
[...reviewedRemovals].filter(isWithinRoot).map((filePath) => resolve(filePath)),
|
|
353
|
+
)
|
|
354
|
+
const current = collectRemovalTree(rootPath)
|
|
355
|
+
const actual = new Set(current.map((entry) => resolve(entry.path)))
|
|
356
|
+
if (actual.size !== expected.size || [...actual].some((filePath) => !expected.has(filePath))) {
|
|
357
|
+
throw new Error(`Removal set changed after review: ${rootPath}`)
|
|
358
|
+
}
|
|
359
|
+
for (const entry of current) {
|
|
360
|
+
const filePath = resolve(entry.path)
|
|
361
|
+
const before = reviewedSnapshots.get(filePath)
|
|
362
|
+
const now = entry.snapshot
|
|
363
|
+
if (
|
|
364
|
+
!before ||
|
|
365
|
+
before.exists !== now.exists ||
|
|
366
|
+
before.sha256 !== now.sha256 ||
|
|
367
|
+
before.size !== now.size ||
|
|
368
|
+
before.mode !== now.mode
|
|
369
|
+
) {
|
|
370
|
+
throw new Error(`Removal target drifted after review: ${entry.path}`)
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return true
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Remove only the reviewed entries, without recursive deletion that could
|
|
378
|
+
* consume a path added after preflight. Directory removal uses rmdir, which
|
|
379
|
+
* fails closed if any unreviewed entry appears during cleanup.
|
|
380
|
+
*/
|
|
381
|
+
export function removeReviewedTree(rootPath, reviewedRemovals, reviewedSnapshots) {
|
|
382
|
+
validateRemovalTree(rootPath, reviewedRemovals, reviewedSnapshots)
|
|
383
|
+
const entries = collectRemovalTree(rootPath)
|
|
384
|
+
for (const entry of entries.reverse()) {
|
|
385
|
+
const info = lstatSync(entry.path)
|
|
386
|
+
if (info.isDirectory()) {
|
|
387
|
+
assertSafeRemoval(rootPath, entry.path)
|
|
388
|
+
try {
|
|
389
|
+
rmdirSync(entry.path)
|
|
390
|
+
} catch (error) {
|
|
391
|
+
if (error?.code === 'ENOTEMPTY' || error?.code === 'EEXIST') {
|
|
392
|
+
throw new Error(`Removal set changed after review: ${entry.path}`, { cause: error })
|
|
393
|
+
}
|
|
394
|
+
throw error
|
|
395
|
+
}
|
|
396
|
+
} else {
|
|
397
|
+
// Unlinking a reviewed symlink leaf is safe and preserves historical
|
|
398
|
+
// cleanup behavior; reject linked ancestors so the parent is never
|
|
399
|
+
// redirected outside the reviewed root.
|
|
400
|
+
assertPathWithin(rootPath, entry.path)
|
|
401
|
+
assertNoSymlinkAncestors(entry.path, { includeLeaf: false })
|
|
402
|
+
const before = reviewedSnapshots?.get(resolve(entry.path))
|
|
403
|
+
const now = snapshot(entry.path)
|
|
404
|
+
if (
|
|
405
|
+
!before ||
|
|
406
|
+
before.exists !== now.exists ||
|
|
407
|
+
before.sha256 !== now.sha256 ||
|
|
408
|
+
before.size !== now.size ||
|
|
409
|
+
before.mode !== now.mode
|
|
410
|
+
) {
|
|
411
|
+
throw new Error(`Removal target drifted after review: ${entry.path}`)
|
|
412
|
+
}
|
|
413
|
+
unlinkSync(entry.path)
|
|
414
|
+
}
|
|
415
|
+
verifyPathRemoved(entry.path)
|
|
416
|
+
}
|
|
417
|
+
return true
|
|
418
|
+
}
|
|
419
|
+
|
|
258
420
|
/**
|
|
259
421
|
* Compare the preview's SCORED change count against this call's install
|
|
260
422
|
* summary delta. Returns a descriptor on mismatch, or `null` when they agree.
|
|
@@ -4,8 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
import { execSync } from 'node:child_process'
|
|
6
6
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:fs'
|
|
7
|
-
import { isAbsolute, join, resolve } from 'node:path'
|
|
7
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path'
|
|
8
|
+
import { ACTIONS, recordChange, recordRemovalTree, removeReviewedTree, snapshot } from './plan.mjs'
|
|
8
9
|
import { ROOT, writeIfChanged } from './shared.mjs'
|
|
10
|
+
import {
|
|
11
|
+
assertNoSymlinkAncestors,
|
|
12
|
+
assertPathWithin,
|
|
13
|
+
assertSafeRemoval,
|
|
14
|
+
verifyPathRemoved,
|
|
15
|
+
} from './verify.mjs'
|
|
9
16
|
|
|
10
17
|
/**
|
|
11
18
|
* Copy a plugin's runtime payload (the root TUI loader, bundled dist/*,
|
|
@@ -18,22 +25,37 @@ import { ROOT, writeIfChanged } from './shared.mjs'
|
|
|
18
25
|
* @param {boolean} [options.dryRun=false] - If true, don't write files
|
|
19
26
|
* @param {'v1'|'v2'} [options.version='v1'] - Host plugin generation
|
|
20
27
|
* @param {(change: object) => void} [options.recorder] - optional change recorder for preview
|
|
28
|
+
* @param {Set<string>|null} [options.reviewedRemovals] - paths approved by the current plan
|
|
29
|
+
* @param {Map<string, object>|null} [options.reviewedSnapshots] - pre-apply path snapshots
|
|
21
30
|
* @returns {{ created: number, skipped: number }}
|
|
22
31
|
*/
|
|
23
32
|
export function copyPluginFiles(
|
|
24
33
|
srcDir,
|
|
25
34
|
dstDir,
|
|
26
|
-
{
|
|
35
|
+
{
|
|
36
|
+
dryRun = false,
|
|
37
|
+
version = 'v1',
|
|
38
|
+
recorder = null,
|
|
39
|
+
reviewedRemovals = null,
|
|
40
|
+
reviewedSnapshots = null,
|
|
41
|
+
} = {},
|
|
27
42
|
) {
|
|
28
43
|
const result = { created: 0, skipped: 0 }
|
|
44
|
+
const distDir = join(dstDir, 'dist')
|
|
29
45
|
|
|
30
|
-
if (!dryRun)
|
|
31
|
-
|
|
46
|
+
if (!dryRun) {
|
|
47
|
+
assertNoSymlinkAncestors(dstDir)
|
|
48
|
+
mkdirSync(dstDir, { recursive: true })
|
|
49
|
+
assertNoSymlinkAncestors(dstDir)
|
|
50
|
+
assertPathWithin(dstDir, distDir)
|
|
51
|
+
assertNoSymlinkAncestors(distDir)
|
|
52
|
+
mkdirSync(distDir, { recursive: true })
|
|
53
|
+
assertNoSymlinkAncestors(distDir)
|
|
54
|
+
}
|
|
32
55
|
|
|
33
56
|
// Drop legacy raw copies from previous installs (root index.tsx + dist/tui.tsx).
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
rmSync(join(dstDir, 'dist', 'tui.tsx'), { force: true })
|
|
57
|
+
for (const legacyPath of [join(dstDir, 'index.tsx'), join(distDir, 'tui.tsx')]) {
|
|
58
|
+
removeLegacyFile(legacyPath, dstDir, { dryRun, recorder, reviewedRemovals, reviewedSnapshots })
|
|
37
59
|
}
|
|
38
60
|
|
|
39
61
|
// Copy dist/* files
|
|
@@ -41,7 +63,9 @@ export function copyPluginFiles(
|
|
|
41
63
|
if (existsSync(distSrc)) {
|
|
42
64
|
for (const f of readdirSync(distSrc)) {
|
|
43
65
|
const c = readFileSync(join(distSrc, f), 'utf8')
|
|
44
|
-
const
|
|
66
|
+
const destination = join(distDir, f)
|
|
67
|
+
assertPathWithin(dstDir, destination)
|
|
68
|
+
const status = writeIfChanged(destination, c, dryRun, { recorder })
|
|
45
69
|
if (status === 'created') result.created++
|
|
46
70
|
else result.skipped++
|
|
47
71
|
}
|
|
@@ -88,8 +112,13 @@ export function copyPluginFiles(
|
|
|
88
112
|
)
|
|
89
113
|
if (entryStatus === 'created') result.created++
|
|
90
114
|
else result.skipped++
|
|
91
|
-
} else
|
|
92
|
-
|
|
115
|
+
} else {
|
|
116
|
+
removeLegacyFile(v2ServerDestination, dstDir, {
|
|
117
|
+
dryRun,
|
|
118
|
+
recorder,
|
|
119
|
+
reviewedRemovals,
|
|
120
|
+
reviewedSnapshots,
|
|
121
|
+
})
|
|
93
122
|
}
|
|
94
123
|
|
|
95
124
|
// Copy the lockfile so dependency installation is reproducible. A missing
|
|
@@ -109,6 +138,49 @@ export function copyPluginFiles(
|
|
|
109
138
|
return result
|
|
110
139
|
}
|
|
111
140
|
|
|
141
|
+
function removeLegacyFile(
|
|
142
|
+
filePath,
|
|
143
|
+
rootDir,
|
|
144
|
+
{ dryRun, recorder, reviewedRemovals, reviewedSnapshots },
|
|
145
|
+
) {
|
|
146
|
+
assertSafeRemoval(rootDir, filePath)
|
|
147
|
+
const present = existsSync(filePath)
|
|
148
|
+
if (dryRun) {
|
|
149
|
+
recordChange(
|
|
150
|
+
{
|
|
151
|
+
path: filePath,
|
|
152
|
+
action: present ? ACTIONS.REMOVE : ACTIONS.UNCHANGED,
|
|
153
|
+
bytes: 0,
|
|
154
|
+
scored: false,
|
|
155
|
+
},
|
|
156
|
+
recorder,
|
|
157
|
+
)
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
if (!present) return
|
|
161
|
+
|
|
162
|
+
const normalizedPath = resolve(filePath)
|
|
163
|
+
if (reviewedRemovals && !reviewedRemovals.has(normalizedPath)) {
|
|
164
|
+
throw new Error(`Refusing unreviewed plugin removal: ${filePath}`)
|
|
165
|
+
}
|
|
166
|
+
const expected = reviewedSnapshots?.get(normalizedPath)
|
|
167
|
+
if (reviewedSnapshots && (!expected || !sameSnapshot(snapshot(filePath), expected))) {
|
|
168
|
+
throw new Error(`Plugin removal target drifted after review: ${filePath}`)
|
|
169
|
+
}
|
|
170
|
+
rmSync(filePath, { force: true })
|
|
171
|
+
verifyPathRemoved(filePath)
|
|
172
|
+
recordChange({ path: filePath, action: ACTIONS.REMOVE, bytes: 0, scored: false }, recorder)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function sameSnapshot(left, right) {
|
|
176
|
+
return (
|
|
177
|
+
left.exists === right.exists &&
|
|
178
|
+
left.sha256 === right.sha256 &&
|
|
179
|
+
left.size === right.size &&
|
|
180
|
+
left.mode === right.mode
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
112
184
|
/**
|
|
113
185
|
* Install a plugin from source to destination.
|
|
114
186
|
* @param {string} srcDir - Source plugin directory (e.g. ROOT/src/plugins/tui)
|
|
@@ -122,17 +194,49 @@ export function copyPluginFiles(
|
|
|
122
194
|
export function installPlugin(
|
|
123
195
|
srcDir,
|
|
124
196
|
dstDir,
|
|
125
|
-
{
|
|
197
|
+
{
|
|
198
|
+
dryRun = false,
|
|
199
|
+
clean = false,
|
|
200
|
+
version = 'v1',
|
|
201
|
+
recorder = null,
|
|
202
|
+
reviewedRemovals = null,
|
|
203
|
+
reviewedSnapshots = null,
|
|
204
|
+
} = {},
|
|
126
205
|
) {
|
|
127
206
|
const result = { created: 0, skipped: 0, errors: 0 }
|
|
207
|
+
const cleanTargetExisted = clean && existsSync(dstDir)
|
|
128
208
|
|
|
129
|
-
|
|
130
|
-
|
|
209
|
+
assertNoSymlinkAncestors(dstDir)
|
|
210
|
+
if (cleanTargetExisted) {
|
|
211
|
+
assertSafeRemoval(dirname(dstDir), dstDir)
|
|
212
|
+
if (dryRun) {
|
|
213
|
+
recordRemovalTree(dstDir, recorder)
|
|
214
|
+
} else {
|
|
215
|
+
removeReviewedTree(dstDir, reviewedRemovals, reviewedSnapshots)
|
|
216
|
+
}
|
|
131
217
|
}
|
|
132
218
|
|
|
133
|
-
const copyResult = copyPluginFiles(srcDir, dstDir, {
|
|
219
|
+
const copyResult = copyPluginFiles(srcDir, dstDir, {
|
|
220
|
+
dryRun,
|
|
221
|
+
version,
|
|
222
|
+
recorder,
|
|
223
|
+
reviewedRemovals,
|
|
224
|
+
reviewedSnapshots,
|
|
225
|
+
})
|
|
134
226
|
result.created += copyResult.created
|
|
135
227
|
result.skipped += copyResult.skipped
|
|
228
|
+
if (cleanTargetExisted) {
|
|
229
|
+
// The clean pass removes these directories before rebuilding their files;
|
|
230
|
+
// keep the net plan truthful while the separate removal review still lists
|
|
231
|
+
// every path actually cleared.
|
|
232
|
+
recordChange({ path: dstDir, action: ACTIONS.UNCHANGED, bytes: 0, scored: false }, recorder)
|
|
233
|
+
if (existsSync(join(srcDir, 'dist'))) {
|
|
234
|
+
recordChange(
|
|
235
|
+
{ path: join(dstDir, 'dist'), action: ACTIONS.UNCHANGED, bytes: 0, scored: false },
|
|
236
|
+
recorder,
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
136
240
|
|
|
137
241
|
// Install plugin dependencies (@opentui/core, @opentui/solid, solid-js).
|
|
138
242
|
// Lockfile installation is the only accepted dependency path.
|