@vzn/vx 0.0.423 → 0.0.425
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.425",
|
|
4
4
|
"description": "The fastest task runner and build cache for JavaScript monorepos.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
"node": ">=18"
|
|
22
22
|
},
|
|
23
23
|
"optionalDependencies": {
|
|
24
|
-
"@vzn/vx-linux-x64": "0.0.
|
|
25
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
26
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
27
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
24
|
+
"@vzn/vx-linux-x64": "0.0.425",
|
|
25
|
+
"@vzn/vx-linux-arm64": "0.0.425",
|
|
26
|
+
"@vzn/vx-darwin-x64": "0.0.425",
|
|
27
|
+
"@vzn/vx-darwin-arm64": "0.0.425"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/cli/watch.ts
CHANGED
|
@@ -205,6 +205,11 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
205
205
|
const swept = await sweepConfigs(allProjects, workspaceRoot, load, opts.tasks)
|
|
206
206
|
const watched = await watchedProjects(workspaceRoot, allProjects, scope, load, swept.staged)
|
|
207
207
|
const ws = await loadCliWorkspace(workspaceRoot)
|
|
208
|
+
const claimsOf = (plugins: Parameters<typeof fingerprintClaims>[0]): Set<string> =>
|
|
209
|
+
new Set([
|
|
210
|
+
...fingerprintClaims(plugins).keys(),
|
|
211
|
+
...(opts.frozen === true ? [LOCKFILE_NAME] : []),
|
|
212
|
+
])
|
|
208
213
|
return await runWatchLoop({
|
|
209
214
|
opts,
|
|
210
215
|
held,
|
|
@@ -232,6 +237,10 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
232
237
|
const now = await watchedProjects(workspaceRoot, all, inScope(all), load, sweep.staged)
|
|
233
238
|
return {
|
|
234
239
|
projects: now,
|
|
240
|
+
// A plugin the workspace config gained since claims its file from
|
|
241
|
+
// the cycle that loaded it; read once, its edits started nothing
|
|
242
|
+
// until a restart.
|
|
243
|
+
claimedRootFiles: claimsOf((await loadCliWorkspace(workspaceRoot)).plugins),
|
|
235
244
|
memberBases: memberBaseDirs(workspace),
|
|
236
245
|
workspaceWide: sweep.workspaceWide,
|
|
237
246
|
workspaceInputs: sweep.workspaceInputs,
|
|
@@ -246,10 +255,7 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
246
255
|
// Under --frozen every cycle's configs are the lock's, so a re-lock is
|
|
247
256
|
// the one edit that changes what a cycle runs; unheard, the loop ran
|
|
248
257
|
// the old lock until a restart (item 971).
|
|
249
|
-
claimedRootFiles:
|
|
250
|
-
...fingerprintClaims(ws.plugins).keys(),
|
|
251
|
-
...(opts.frozen === true ? [LOCKFILE_NAME] : []),
|
|
252
|
-
]),
|
|
258
|
+
claimedRootFiles: claimsOf(ws.plugins),
|
|
253
259
|
// The RESOLVED cache dir, not the `.vx` literal — see `makeWatchIgnore`.
|
|
254
260
|
cacheDir: opts.cacheDir ?? ws.cacheDir,
|
|
255
261
|
})
|
|
@@ -296,6 +302,7 @@ interface WatchLoopArgs {
|
|
|
296
302
|
|
|
297
303
|
interface Rediscovered {
|
|
298
304
|
projects: readonly ProjectMeta[]
|
|
305
|
+
claimedRootFiles: ReadonlySet<string>
|
|
299
306
|
memberBases: readonly string[]
|
|
300
307
|
workspaceWide: boolean
|
|
301
308
|
workspaceInputs: readonly string[]
|
|
@@ -319,6 +326,7 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
|
|
|
319
326
|
let outputs = args.outputs
|
|
320
327
|
let inputs = args.inputs
|
|
321
328
|
let uncached = args.uncached
|
|
329
|
+
let claimedRootFiles = args.claimedRootFiles
|
|
322
330
|
let configImportFiles = args.configImports
|
|
323
331
|
let wsConfigImportFiles = args.workspaceConfigImports
|
|
324
332
|
// A dev server stays up while the loop idles; the cycle that replaces it
|
|
@@ -398,12 +406,7 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
|
|
|
398
406
|
// project would otherwise trigger every save during `bun install` —
|
|
399
407
|
// and vx's own cache writes would trigger a cycle that writes again.
|
|
400
408
|
let isIgnoredPath = makeWatchIgnore(cacheDir, outputs, inputs)
|
|
401
|
-
let matters = makeRootEventFilter(
|
|
402
|
-
workspaceRoot,
|
|
403
|
-
projectDirs,
|
|
404
|
-
workspaceInputs,
|
|
405
|
-
args.claimedRootFiles,
|
|
406
|
-
)
|
|
409
|
+
let matters = makeRootEventFilter(workspaceRoot, projectDirs, workspaceInputs, claimedRootFiles)
|
|
407
410
|
/** Since the last cycle, a member came or went, or a file that shapes the watched set changed (`shapesWatchedSet`). */
|
|
408
411
|
let reread = false
|
|
409
412
|
|
|
@@ -466,7 +469,7 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
|
|
|
466
469
|
isWorkspaceFingerprintFile(filename) ||
|
|
467
470
|
isWorkspaceConfigFile(filename) ||
|
|
468
471
|
filename === 'package.json' ||
|
|
469
|
-
|
|
472
|
+
claimedRootFiles.has(filename)
|
|
470
473
|
) {
|
|
471
474
|
if (shapesWatchedSet(filename) || filename === LOCKFILE_NAME) reread = true
|
|
472
475
|
trigger(`root ${filename}`, path.join(workspaceRoot, filename))
|
|
@@ -608,18 +611,14 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
|
|
|
608
611
|
outputs = next.outputs
|
|
609
612
|
inputs = next.inputs
|
|
610
613
|
uncached = next.uncached
|
|
614
|
+
claimedRootFiles = next.claimedRootFiles
|
|
611
615
|
configImportFiles = next.configImports
|
|
612
616
|
wsConfigImportFiles = next.workspaceConfigImports
|
|
613
617
|
packageDirs = next.packageDirs
|
|
614
618
|
memberBases = next.memberBases
|
|
615
619
|
armBases()
|
|
616
620
|
isIgnoredPath = makeWatchIgnore(cacheDir, outputs, inputs)
|
|
617
|
-
matters = makeRootEventFilter(
|
|
618
|
-
workspaceRoot,
|
|
619
|
-
projectDirs,
|
|
620
|
-
workspaceInputs,
|
|
621
|
-
args.claimedRootFiles,
|
|
622
|
-
)
|
|
621
|
+
matters = makeRootEventFilter(workspaceRoot, projectDirs, workspaceInputs, claimedRootFiles)
|
|
623
622
|
if (next.workspaceWide !== workspaceWide) {
|
|
624
623
|
// A task started or stopped declaring `workspaceFiles`: the other
|
|
625
624
|
// arm's shape. Until item 891 the choice was made once, at start, and
|
|
@@ -160,6 +160,12 @@ export interface ExecuteArgs {
|
|
|
160
160
|
* Absent → probe lazily, exactly as today (unstable / unclassified).
|
|
161
161
|
*/
|
|
162
162
|
preProbed?: { hash: string; hit: CacheEntry | null }
|
|
163
|
+
/**
|
|
164
|
+
* An uncached task's key as the up-front pass derived it, present only
|
|
165
|
+
* when no upstream can change it (`deriveStableKeys`' `uncachedKeys`):
|
|
166
|
+
* used verbatim instead of deriving the same key again.
|
|
167
|
+
*/
|
|
168
|
+
upfrontKey?: string
|
|
163
169
|
/**
|
|
164
170
|
* Start the sandbox runtime, on the first task that executes inside one
|
|
165
171
|
* (run.ts, `prepareSandbox`). Absent when no task in the run declares a
|
|
@@ -274,7 +280,8 @@ async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
274
280
|
const hash =
|
|
275
281
|
args.noDependants === true
|
|
276
282
|
? undefined
|
|
277
|
-
:
|
|
283
|
+
: (args.upfrontKey ??
|
|
284
|
+
(await computeTaskHash({
|
|
278
285
|
node,
|
|
279
286
|
upstream: args.upstream,
|
|
280
287
|
workspaceRoot: args.workspaceRoot,
|
|
@@ -284,7 +291,7 @@ async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
284
291
|
nestedProjectDirs: args.nestedProjectDirs,
|
|
285
292
|
...(args.gitFilesCache !== undefined ? { gitFilesCache: args.gitFilesCache } : {}),
|
|
286
293
|
...(args.hashCache !== undefined ? { hashCache: args.hashCache } : {}),
|
|
287
|
-
})
|
|
294
|
+
})))
|
|
288
295
|
|
|
289
296
|
// The args after `--` reach a server as they reach any task. A readyWhen
|
|
290
297
|
// server once got none — "so the matcher sees the unmodified output",
|
|
@@ -658,19 +665,19 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
658
665
|
// authoritative stable key) and skip the probe below.
|
|
659
666
|
const preProbed = args.preProbed
|
|
660
667
|
const hash =
|
|
661
|
-
preProbed
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
668
|
+
preProbed?.hash ??
|
|
669
|
+
args.upfrontKey ??
|
|
670
|
+
(await computeTaskHash({
|
|
671
|
+
node,
|
|
672
|
+
upstream,
|
|
673
|
+
workspaceRoot: args.workspaceRoot,
|
|
674
|
+
workspaceFingerprint: args.workspaceFingerprint,
|
|
675
|
+
cache,
|
|
676
|
+
forwardArgs: args.forwardArgs,
|
|
677
|
+
nestedProjectDirs: args.nestedProjectDirs,
|
|
678
|
+
...(args.gitFilesCache !== undefined ? { gitFilesCache: args.gitFilesCache } : {}),
|
|
679
|
+
...(args.hashCache !== undefined ? { hashCache: args.hashCache } : {}),
|
|
680
|
+
}))
|
|
674
681
|
|
|
675
682
|
// The local no-op half of `exec.remote: 'only'`: no remote executor took
|
|
676
683
|
// the task, so it succeeds without running. The hash was still computed —
|
|
@@ -84,10 +84,14 @@ export interface ShortCircuit {
|
|
|
84
84
|
* with a non-null hit, minus the workspace-outputs exclusion).
|
|
85
85
|
*/
|
|
86
86
|
restoreTier: Set<string>
|
|
87
|
+
/**
|
|
88
|
+
* Uncached task id → its up-front key, for the tasks whose key no
|
|
89
|
+
* upstream can change: execute-task reuses it instead of deriving it
|
|
90
|
+
* again (`deriveStableKeys`' `uncachedKeys`).
|
|
91
|
+
*/
|
|
92
|
+
uncachedKeys: Map<string, string>
|
|
87
93
|
}
|
|
88
94
|
|
|
89
|
-
const EMPTY: ShortCircuit = { preProbed: new Map(), restoreTier: new Set() }
|
|
90
|
-
|
|
91
95
|
/**
|
|
92
96
|
* Classify the graph's stable tasks: derive keys + probe local ONCE,
|
|
93
97
|
* returning the `preProbed` map (probe reuse) and the `restoreTier` set
|
|
@@ -99,11 +103,12 @@ const EMPTY: ShortCircuit = { preProbed: new Map(), restoreTier: new Set() }
|
|
|
99
103
|
*/
|
|
100
104
|
export async function startLocalShortCircuit(args: ShortCircuitArgs): Promise<ShortCircuit> {
|
|
101
105
|
let stableKeys
|
|
106
|
+
const uncachedKeys = new Map<string, string>()
|
|
102
107
|
const endKeys = span('stable keys')
|
|
103
108
|
try {
|
|
104
|
-
stableKeys = await deriveStableKeys(args)
|
|
109
|
+
stableKeys = await deriveStableKeys({ ...args, uncachedKeys })
|
|
105
110
|
} catch {
|
|
106
|
-
return
|
|
111
|
+
return { preProbed: new Map(), restoreTier: new Set(), uncachedKeys: new Map() }
|
|
107
112
|
} finally {
|
|
108
113
|
endKeys()
|
|
109
114
|
}
|
|
@@ -111,12 +116,17 @@ export async function startLocalShortCircuit(args: ShortCircuitArgs): Promise<Sh
|
|
|
111
116
|
// `cacheEnabled && !unstable` (item 640 deleted a second filter here and
|
|
112
117
|
// nothing reddened).
|
|
113
118
|
const candidates = stableKeys
|
|
114
|
-
if (candidates.length === 0) return EMPTY
|
|
115
|
-
|
|
116
|
-
const keptOut = restoreTierExclusions(args.nodes, args.workspaceRoot)
|
|
117
|
-
|
|
118
119
|
const preProbed = new Map<string, ProbedEntry>()
|
|
119
120
|
const restoreTier = new Set<string>()
|
|
121
|
+
if (candidates.length === 0 && uncachedKeys.size === 0) {
|
|
122
|
+
return { preProbed, restoreTier, uncachedKeys }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const keptOut = restoreTierExclusions(args.nodes, args.workspaceRoot)
|
|
126
|
+
// A root-anchored output may land in a project no edge leads from, so an
|
|
127
|
+
// uncached key there is derived again once the writer may have run.
|
|
128
|
+
for (const id of keptOut) uncachedKeys.delete(id)
|
|
129
|
+
if (candidates.length === 0) return { preProbed, restoreTier, uncachedKeys }
|
|
120
130
|
|
|
121
131
|
// Bounded pool over the stable candidates: probe local ONCE each. A
|
|
122
132
|
// confirmed hit becomes restore-tier (unless workspace outputs disable
|
|
@@ -135,7 +145,7 @@ export async function startLocalShortCircuit(args: ShortCircuitArgs): Promise<Sh
|
|
|
135
145
|
preProbed.set(node.id, { hash, hit })
|
|
136
146
|
if (hit !== null && !keptOut.has(node.id)) restoreTier.add(node.id)
|
|
137
147
|
}
|
|
138
|
-
return { preProbed, restoreTier }
|
|
148
|
+
return { preProbed, restoreTier, uncachedKeys }
|
|
139
149
|
} catch {
|
|
140
150
|
// Fall through to the per-hash pool, which isolates a failing probe
|
|
141
151
|
// to its own task.
|
|
@@ -159,7 +169,7 @@ export async function startLocalShortCircuit(args: ShortCircuitArgs): Promise<Sh
|
|
|
159
169
|
}
|
|
160
170
|
await Promise.all(Array.from({ length: workers }, () => pump()))
|
|
161
171
|
|
|
162
|
-
return { preProbed, restoreTier }
|
|
172
|
+
return { preProbed, restoreTier, uncachedKeys }
|
|
163
173
|
}
|
|
164
174
|
|
|
165
175
|
/**
|
package/src/orchestrator/run.ts
CHANGED
|
@@ -103,7 +103,11 @@ import type { RunOptions, RunSummary } from './options.js'
|
|
|
103
103
|
|
|
104
104
|
// Per run, never shared: a `vx watch` process runs many, and a shared map
|
|
105
105
|
// is one `preProbed.set` away from leaking a hit across cycles.
|
|
106
|
-
const emptyShortCircuit = (): ShortCircuit => ({
|
|
106
|
+
const emptyShortCircuit = (): ShortCircuit => ({
|
|
107
|
+
preProbed: new Map(),
|
|
108
|
+
restoreTier: new Set(),
|
|
109
|
+
uncachedKeys: new Map(),
|
|
110
|
+
})
|
|
107
111
|
|
|
108
112
|
/**
|
|
109
113
|
* Parse the `VX_TASK_TIMEOUT` env var (ms) — the "global" run-level task
|
|
@@ -891,6 +895,7 @@ async function runOnBus(
|
|
|
891
895
|
|
|
892
896
|
const buildExecuteArgs = (node: TaskNode, upstream: TaskOutcome[], reuseProbe = true) => {
|
|
893
897
|
const probe = reuseProbe ? shortCircuit.preProbed.get(node.id) : undefined
|
|
898
|
+
const upfrontKey = shortCircuit.uncachedKeys.get(node.id)
|
|
894
899
|
if (
|
|
895
900
|
options.continueMode === 'always' &&
|
|
896
901
|
node.deps.some((d) => deadServerBehind(nodes, serverDied, d) !== undefined)
|
|
@@ -921,6 +926,7 @@ async function runOnBus(
|
|
|
921
926
|
gitFilesCache,
|
|
922
927
|
hashCache,
|
|
923
928
|
...(probe !== undefined ? { preProbed: probe } : {}),
|
|
929
|
+
...(upfrontKey !== undefined ? { upfrontKey } : {}),
|
|
924
930
|
...(tainted ? { taintedUpstream: true } : {}),
|
|
925
931
|
...(dependedOn.has(node.id) ? {} : { noDependants: true as const }),
|
|
926
932
|
fingerprintWatch,
|
|
@@ -27,6 +27,11 @@ export interface DeriveStableKeysArgs {
|
|
|
27
27
|
nestedDirsByProject: Map<string, string[]>
|
|
28
28
|
gitFilesCache: GitFilesCache
|
|
29
29
|
hashCache: HashCache
|
|
30
|
+
/**
|
|
31
|
+
* Filled with each uncached task's key when no upstream can write where
|
|
32
|
+
* that key reads, so execute-task need not derive it a second time.
|
|
33
|
+
*/
|
|
34
|
+
uncachedKeys?: Map<string, string>
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
export interface StableKey {
|
|
@@ -237,6 +242,18 @@ export async function deriveStableKeys(args: DeriveStableKeysArgs): Promise<Stab
|
|
|
237
242
|
|
|
238
243
|
const cacheEnabled = node.config.cache !== undefined
|
|
239
244
|
if (cacheEnabled && !unstable) stableKeys.push({ hash, node })
|
|
245
|
+
// An uncached key reads its whole project, so `dependsOnSiblingOutputs`
|
|
246
|
+
// (which answers false for it) is not the gate: any upstream that may
|
|
247
|
+
// write into this project, or anywhere, leaves the key preliminary.
|
|
248
|
+
else if (
|
|
249
|
+
!cacheEnabled &&
|
|
250
|
+
!unstable &&
|
|
251
|
+
!outputProjects.has(node.projectName) &&
|
|
252
|
+
unfolded?.has(node.projectName) !== true &&
|
|
253
|
+
!wsOutputUpstream &&
|
|
254
|
+
!wsUnfoldedHere
|
|
255
|
+
)
|
|
256
|
+
args.uncachedKeys?.set(id, hash)
|
|
240
257
|
}
|
|
241
258
|
return stableKeys
|
|
242
259
|
}
|
|
@@ -256,6 +256,18 @@ export async function applyMigration(args: ApplyMigrationArgs): Promise<number>
|
|
|
256
256
|
const configName = `vx.config.${format}`
|
|
257
257
|
const workspaceName = `vx.workspace.${format}`
|
|
258
258
|
const empty = plan.projects.length === 0
|
|
259
|
+
// `vx init` never overwrites a member's config: a half-adopted workspace
|
|
260
|
+
// keeps each one it has, by name, and gets the rest (M-52). It refused
|
|
261
|
+
// the whole workspace, and `--force` would have replaced the hand-written
|
|
262
|
+
// file; `--force` still replaces, as asked.
|
|
263
|
+
const kept: string[] = []
|
|
264
|
+
const projects = plan.projects.filter((p) => {
|
|
265
|
+
if (!init || force || p.tasks.length === 0) return true
|
|
266
|
+
const existing = metas.find((m) => m.dir === p.dir)?.configPath
|
|
267
|
+
if (!existing) return true
|
|
268
|
+
kept.push(relPosix(root, existing))
|
|
269
|
+
return false
|
|
270
|
+
})
|
|
259
271
|
if (empty && !init) {
|
|
260
272
|
throw new UserError(
|
|
261
273
|
`nothing to migrate: no ${source === 'package.json scripts' ? 'package.json scripts in any workspace member' : 'tasks in ' + source}`,
|
|
@@ -263,7 +275,7 @@ export async function applyMigration(args: ApplyMigrationArgs): Promise<number>
|
|
|
263
275
|
}
|
|
264
276
|
|
|
265
277
|
const files: { relPath: string; abs: string; contents: string }[] = []
|
|
266
|
-
for (const p of
|
|
278
|
+
for (const p of projects) {
|
|
267
279
|
if (p.tasks.length === 0) continue
|
|
268
280
|
const abs = path.join(p.dir, configName)
|
|
269
281
|
files.push({
|
|
@@ -291,7 +303,7 @@ export async function applyMigration(args: ApplyMigrationArgs): Promise<number>
|
|
|
291
303
|
const conflicts = new Set<string>()
|
|
292
304
|
// A discovered project with ANY existing vx config (.ts/.mjs/.js) — refuse
|
|
293
305
|
// so we never shadow a hand-written config with a fresh .ts.
|
|
294
|
-
for (const p of
|
|
306
|
+
for (const p of projects) {
|
|
295
307
|
if (p.tasks.length === 0) continue
|
|
296
308
|
const meta = metas.find((m) => m.dir === p.dir)
|
|
297
309
|
if (meta?.configPath) conflicts.add(relPosix(root, meta.configPath))
|
|
@@ -316,7 +328,7 @@ export async function applyMigration(args: ApplyMigrationArgs): Promise<number>
|
|
|
316
328
|
// order, and the report said "written" for a file the run never loaded
|
|
317
329
|
// (item 1033).
|
|
318
330
|
const replaced: string[] = []
|
|
319
|
-
for (const p of
|
|
331
|
+
for (const p of projects) {
|
|
320
332
|
if (p.tasks.length === 0) continue
|
|
321
333
|
const existing = metas.find((m) => m.dir === p.dir)?.configPath
|
|
322
334
|
if (existing && existing !== path.join(p.dir, configName)) replaced.push(existing)
|
|
@@ -337,7 +349,7 @@ export async function applyMigration(args: ApplyMigrationArgs): Promise<number>
|
|
|
337
349
|
let todoCount = 0
|
|
338
350
|
let clean = 0
|
|
339
351
|
let cached = false
|
|
340
|
-
for (const p of
|
|
352
|
+
for (const p of projects) {
|
|
341
353
|
for (const t of p.tasks) {
|
|
342
354
|
if (t.todos.length === 0 && t.task !== null) clean++
|
|
343
355
|
if (t.task !== null && 'cache' in t.task) cached = true
|
|
@@ -408,8 +420,13 @@ export async function applyMigration(args: ApplyMigrationArgs): Promise<number>
|
|
|
408
420
|
}
|
|
409
421
|
}
|
|
410
422
|
report.push(...plan.notes)
|
|
411
|
-
report.push(dry ? 'files (dry run
|
|
423
|
+
if (files.length === 0) report.push(dry ? 'no files (dry run)' : 'no files written')
|
|
424
|
+
else report.push(dry ? 'files (dry run, nothing written):' : 'files written:')
|
|
412
425
|
for (const f of files) report.push(` ${f.relPath}`)
|
|
426
|
+
if (kept.length > 0) {
|
|
427
|
+
report.push('kept (each already has a vx config):')
|
|
428
|
+
for (const f of kept) report.push(` ${f}`)
|
|
429
|
+
}
|
|
413
430
|
if (replaced.length > 0) {
|
|
414
431
|
report.push(dry ? 'would replace (dry run):' : 'replaced:')
|
|
415
432
|
for (const f of replaced) report.push(` ${relPosix(root, f)}`)
|