@vzn/vx 0.0.646 → 0.0.649
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/README.md +29 -22
- package/launcher.cjs +9 -2
- package/launcher.sh +2 -2
- package/package.json +5 -5
- package/schemas/cache.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/info.json +16 -2
- package/schemas/last.json +112 -5
- package/schemas/plan.json +28 -2
- package/schemas/show.json +10 -1
- package/skills/vx/SKILL.md +18 -2
- package/src/bin.ts +53 -12
- package/src/cache/archive.ts +324 -23
- package/src/cache/cache.ts +268 -53
- package/src/cache/git-inputs.ts +219 -87
- package/src/cache/index.ts +3 -2
- package/src/cache/inputs.ts +460 -78
- package/src/cache/key-fold.ts +15 -6
- package/src/cache/layer.ts +49 -9
- package/src/cache/layered-cache.ts +123 -58
- package/src/cache/linked-inputs.ts +1 -1
- package/src/cache/output-index.ts +19 -3
- package/src/cache/plugin-cache.ts +49 -23
- package/src/cache/remote-hits.ts +72 -0
- package/src/cache/remote-lane.ts +90 -14
- package/src/cache/run-history.ts +70 -23
- package/src/cache/run-store.ts +11 -10
- package/src/cache/schema.ts +25 -6
- package/src/cache/tar-stream.ts +5 -0
- package/src/cache/umask.ts +1 -2
- package/src/cli/cache.ts +58 -27
- package/src/cli/completions.ts +12 -5
- package/src/cli/docs.ts +42 -16
- package/src/cli/help.ts +58 -19
- package/src/cli/index.ts +79 -33
- package/src/cli/info.ts +12 -6
- package/src/cli/init.ts +186 -31
- package/src/cli/last.ts +439 -45
- package/src/cli/lock.ts +197 -52
- package/src/cli/plan-format.ts +91 -49
- package/src/cli/plugin-commands.ts +70 -14
- package/src/cli/plugin-templates.ts +13 -13
- package/src/cli/registry.ts +209 -0
- package/src/cli/run.ts +109 -34
- package/src/cli/select.ts +143 -46
- package/src/cli/show.ts +59 -16
- package/src/cli/task-verb.ts +16 -6
- package/src/cli/upgrade.ts +151 -49
- package/src/cli/watch-cycle.ts +10 -5
- package/src/cli/watch-filter.ts +71 -3
- package/src/cli/watch-fs.ts +22 -6
- package/src/cli/watch-judge.ts +48 -6
- package/src/cli/watch.ts +163 -42
- package/src/cli/why.ts +211 -73
- package/src/cli/workspace-config.ts +54 -15
- package/src/config.ts +13 -1
- package/src/exec/env.ts +7 -2
- package/src/exec/executor.ts +154 -10
- package/src/exec/index.ts +6 -2
- package/src/exec/kill-tree.ts +15 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/proc-sample.ts +3 -1
- package/src/exec/runner.ts +253 -54
- package/src/exec/sandbox-binds.ts +7 -3
- package/src/exec/sandbox-paths.ts +1 -1
- package/src/exec/sandbox-runtime.ts +211 -24
- package/src/exec/sandbox-violations.ts +52 -20
- package/src/graph/critical-path.ts +79 -0
- package/src/graph/default-build.ts +19 -0
- package/src/graph/definition.ts +38 -0
- package/src/graph/folds.ts +38 -5
- package/src/graph/index.ts +4 -1
- package/src/graph/priorities.ts +74 -8
- package/src/graph/scheduler.ts +270 -60
- package/src/graph/task-graph.ts +228 -35
- package/src/handoff.ts +97 -9
- package/src/history/failure-mode.ts +6 -3
- package/src/history/history.ts +167 -61
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +90 -30
- package/src/index.ts +30 -13
- package/src/orchestrator/admission.ts +10 -6
- package/src/orchestrator/affected-tasks.ts +11 -6
- package/src/orchestrator/deferred-outputs.ts +2 -0
- package/src/orchestrator/doctor.ts +134 -42
- package/src/orchestrator/execute-args.ts +4 -1
- package/src/orchestrator/execute-task.ts +130 -38
- package/src/orchestrator/fingerprint-watch.ts +1 -1
- package/src/orchestrator/graph-memo.ts +140 -0
- package/src/orchestrator/graph-stage.ts +18 -1
- package/src/orchestrator/held-servers.ts +1 -1
- package/src/orchestrator/hit-restore.ts +217 -119
- package/src/orchestrator/index.ts +4 -3
- package/src/orchestrator/local-floor.ts +119 -25
- package/src/orchestrator/local-shortcircuit.ts +28 -4
- package/src/orchestrator/lockfile-claim.ts +96 -16
- package/src/orchestrator/miss-save.ts +42 -8
- package/src/orchestrator/options.ts +8 -6
- package/src/orchestrator/output-host.ts +43 -14
- package/src/orchestrator/output-log.ts +90 -10
- package/src/orchestrator/persistent-task.ts +84 -29
- package/src/orchestrator/persistent.ts +16 -4
- package/src/orchestrator/placement.ts +185 -17
- package/src/orchestrator/plan.ts +64 -7
- package/src/orchestrator/plugin-host.ts +214 -42
- package/src/orchestrator/plugin.ts +113 -37
- package/src/orchestrator/prepare.ts +168 -30
- package/src/orchestrator/projects.ts +65 -40
- package/src/orchestrator/remote-prefetch.ts +119 -25
- package/src/orchestrator/run-artifacts.ts +95 -32
- package/src/orchestrator/run-context.ts +39 -41
- package/src/orchestrator/run-failures.ts +65 -23
- package/src/orchestrator/run-id.ts +6 -3
- package/src/orchestrator/run-keys.ts +149 -13
- package/src/orchestrator/run-live.ts +294 -0
- package/src/orchestrator/run-lock.ts +13 -7
- package/src/orchestrator/run-records.ts +35 -29
- package/src/orchestrator/run-report.ts +3 -3
- package/src/orchestrator/run-steps.ts +58 -22
- package/src/orchestrator/run-stop.ts +22 -5
- package/src/orchestrator/run.ts +331 -88
- package/src/orchestrator/sandbox-fix.ts +7 -5
- package/src/orchestrator/sandbox-request.ts +70 -20
- package/src/orchestrator/shell-verdict.ts +5 -5
- package/src/orchestrator/signals.ts +26 -1
- package/src/orchestrator/stable-keys.ts +92 -8
- package/src/orchestrator/steps.ts +2 -2
- package/src/orchestrator/task-artifacts.ts +90 -26
- package/src/orchestrator/task-hash.ts +78 -13
- package/src/orchestrator/task-log-buffer.ts +72 -36
- package/src/orchestrator/task-process.ts +7 -1
- package/src/orchestrator/telemetry-host.ts +22 -2
- package/src/orchestrator/telemetry.ts +53 -21
- package/src/orchestrator/upstream.ts +15 -0
- package/src/report/events.ts +32 -13
- package/src/report/forecast.ts +31 -9
- package/src/report/framed-output.ts +141 -50
- package/src/report/index.ts +1 -0
- package/src/report/logger.ts +170 -179
- package/src/report/path-links.ts +12 -4
- package/src/report/plain-output.ts +13 -5
- package/src/report/status-line.ts +81 -53
- package/src/report/summary.ts +114 -32
- package/src/report/terminal-signals.ts +13 -2
- package/src/settings.ts +7 -6
- package/src/util/bun-version.ts +4 -1
- package/src/util/colors.ts +4 -2
- package/src/util/config-frame.ts +41 -4
- package/src/{graph → util}/dependency-spec.ts +22 -13
- package/src/util/edit-distance.ts +13 -1
- package/src/util/env-flag.ts +9 -0
- package/src/util/errors.ts +59 -3
- package/src/util/essential-env.ts +57 -0
- package/src/util/git-ignored.ts +1 -1
- package/src/util/hash.ts +35 -0
- package/src/util/hide-env.ts +45 -5
- package/src/util/index.ts +21 -5
- package/src/util/num.ts +8 -0
- package/src/util/paths.ts +91 -21
- package/src/util/report.ts +6 -1
- package/src/util/secret-mask.ts +62 -1
- package/src/util/secrets.ts +7 -2
- package/src/util/size.ts +11 -3
- package/src/util/tail.ts +6 -1
- package/src/util/timing.ts +13 -1
- package/src/util/ui.ts +61 -3
- package/src/util/which.ts +1 -1
- package/src/util/write-file.ts +67 -0
- package/src/workspace/affected.ts +710 -107
- package/src/workspace/bracket-literal.ts +84 -0
- package/src/workspace/canonical-dir.ts +37 -0
- package/src/workspace/config-bytecode.ts +17 -11
- package/src/workspace/config-eval.ts +29 -8
- package/src/workspace/config-imports.ts +199 -58
- package/src/workspace/config-schema.ts +242 -39
- package/src/workspace/filter.ts +37 -8
- package/src/workspace/index.ts +11 -1
- package/src/workspace/json-data.ts +3 -4
- package/src/workspace/lockfile.ts +57 -8
- package/src/workspace/migration.ts +69 -18
- package/src/workspace/package-graph.ts +5 -4
- package/src/workspace/project-loader.ts +226 -37
- package/src/workspace/repo-id.ts +1 -1
- package/src/workspace/workspace.ts +176 -33
package/src/cli/index.ts
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
import { VERSION } from '../version.js'
|
|
5
5
|
import { checkSettingsEnv } from '../settings.js'
|
|
6
|
-
import { CORE_VERBS, flagHint, printHelp, refusedWord, seeHelp } from './help.js'
|
|
6
|
+
import { CORE_VERBS, flagHint, printHelp, refuse, refusedWord, seeHelp } from './help.js'
|
|
7
7
|
import {
|
|
8
|
+
detectColors,
|
|
8
9
|
errorDocument,
|
|
9
10
|
hideEnvFromTasks,
|
|
10
11
|
sayNotice,
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
maskedLine,
|
|
15
16
|
nearest,
|
|
16
17
|
refusalNotice,
|
|
18
|
+
thrownMessage,
|
|
17
19
|
ui,
|
|
18
20
|
UserError,
|
|
19
21
|
wantsJson,
|
|
@@ -28,6 +30,7 @@ import {
|
|
|
28
30
|
const plugins = () => import('./plugin-commands.js')
|
|
29
31
|
|
|
30
32
|
export async function run(argv: readonly string[]): Promise<number> {
|
|
33
|
+
refuseLossyArgs(argv)
|
|
31
34
|
const [command, ...rest] = argv
|
|
32
35
|
await hideEnvFromTasks()
|
|
33
36
|
|
|
@@ -41,8 +44,11 @@ export async function run(argv: readonly string[]): Promise<number> {
|
|
|
41
44
|
return 0
|
|
42
45
|
}
|
|
43
46
|
// Each setting's variable once, before any verb: a renamed one is named,
|
|
44
|
-
// and a deep reader trusts what it reads.
|
|
45
|
-
|
|
47
|
+
// and a deep reader trusts what it reads. Not for help, as `vx run
|
|
48
|
+
// --help` above is not: help is where a setting's values are read. Nor
|
|
49
|
+
// for `vx info`, which reports a refused one (doctor.ts).
|
|
50
|
+
if (command !== 'help' && command !== '--help' && command !== '-h' && command !== 'info')
|
|
51
|
+
checkSettingsEnv()
|
|
46
52
|
|
|
47
53
|
try {
|
|
48
54
|
return await dispatch(command, rest)
|
|
@@ -51,9 +57,23 @@ export async function run(argv: readonly string[]): Promise<number> {
|
|
|
51
57
|
}
|
|
52
58
|
}
|
|
53
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Bun decodes argv as lossy UTF-8 and exposes no raw bytes on macOS, so
|
|
62
|
+
* `-- $'a\xfe'` and `-- $'a\xff'` both became `a` U+FFFD: the task got
|
|
63
|
+
* bytes nobody typed and the two runs shared a key (a stale hit). A real
|
|
64
|
+
* U+FFFD cannot be told from a replaced byte, so any arg holding one is refused.
|
|
65
|
+
*/
|
|
66
|
+
function refuseLossyArgs(argv: readonly string[]): void {
|
|
67
|
+
const at = argv.findIndex((a) => a.includes('�'))
|
|
68
|
+
if (at === -1) return
|
|
69
|
+
throw new UserError(
|
|
70
|
+
`argument ${at + 1} (${JSON.stringify(argv[at])}) is not valid UTF-8, or holds U+FFFD; vx cannot pass it on byte for byte`,
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
54
74
|
/**
|
|
55
75
|
* A crash from a hand-broken lock entry is that entry's fault: runs validate
|
|
56
|
-
* no entry, so it surfaced as a raw TypeError (
|
|
76
|
+
* no entry, so it surfaced as a raw TypeError (2026-10-14). The lock is
|
|
57
77
|
* read only on the crash path, so a normal run pays nothing.
|
|
58
78
|
*/
|
|
59
79
|
async function brokenLockNamed(err: unknown): Promise<unknown> {
|
|
@@ -61,7 +81,8 @@ async function brokenLockNamed(err: unknown): Promise<unknown> {
|
|
|
61
81
|
try {
|
|
62
82
|
const { findWorkspaceRoot, LOCK_INVALID, lockEntryBroken } =
|
|
63
83
|
await import('../workspace/index.js')
|
|
64
|
-
if (lockEntryBroken(await findWorkspaceRoot(process.cwd())))
|
|
84
|
+
if (lockEntryBroken(await findWorkspaceRoot(process.cwd())))
|
|
85
|
+
return new UserError(LOCK_INVALID, 'VX_E_LOCK')
|
|
65
86
|
} catch {}
|
|
66
87
|
return err
|
|
67
88
|
}
|
|
@@ -85,13 +106,21 @@ async function dispatch(command: string | undefined, rest: string[]): Promise<nu
|
|
|
85
106
|
}
|
|
86
107
|
case '--help':
|
|
87
108
|
case '-h':
|
|
109
|
+
if (rest.length > 0) return dispatch('help', rest)
|
|
88
110
|
await printHelp(await (await plugins()).pluginCommandHelp())
|
|
89
111
|
return 0
|
|
90
112
|
case 'help': {
|
|
91
|
-
// `vx help run` is the same question as `vx run --help
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
113
|
+
// `vx help run` is the same question as `vx run --help`, and so is
|
|
114
|
+
// `vx help <plugin verb>`: a plugin verb owns its own help.
|
|
115
|
+
const [verb, extra] = rest
|
|
116
|
+
if (extra !== undefined) {
|
|
117
|
+
return refuse(
|
|
118
|
+
'help',
|
|
119
|
+
rest,
|
|
120
|
+
`${refusedWord(extra)}: ${extra}${flagHint('help', extra)} (see \`vx help\`)`,
|
|
121
|
+
'VX_E_USAGE',
|
|
122
|
+
)
|
|
123
|
+
}
|
|
95
124
|
const core = verb !== undefined && (CORE_VERBS as readonly string[]).includes(verb)
|
|
96
125
|
if (verb !== undefined && !core) {
|
|
97
126
|
// A name that is no verb here is the typo `vx rnu` is, not a request
|
|
@@ -99,22 +128,23 @@ async function dispatch(command: string | undefined, rest: string[]): Promise<nu
|
|
|
99
128
|
const resolved = await (await plugins()).resolvePluginCommand(verb)
|
|
100
129
|
const unknown = resolved === null || 'declaredVerbs' in resolved
|
|
101
130
|
if (unknown && verb.startsWith('-')) {
|
|
102
|
-
|
|
103
|
-
'
|
|
104
|
-
|
|
131
|
+
return refuse(
|
|
132
|
+
'help',
|
|
133
|
+
rest,
|
|
105
134
|
`unknown flag: ${verb}${flagHint('help', verb)} (see \`vx help\`)`,
|
|
135
|
+
'VX_E_USAGE',
|
|
106
136
|
)
|
|
107
|
-
return 1
|
|
108
137
|
}
|
|
109
138
|
if (unknown) {
|
|
110
139
|
const declared = resolved === null ? [] : resolved.declaredVerbs
|
|
111
|
-
|
|
112
|
-
'
|
|
113
|
-
|
|
140
|
+
return refuse(
|
|
141
|
+
'help',
|
|
142
|
+
rest,
|
|
114
143
|
`unknown command: ${verb}${didYouMeanVerb(verb, declared)} (see \`vx help\`)`,
|
|
144
|
+
'VX_E_UNKNOWN_COMMAND',
|
|
115
145
|
)
|
|
116
|
-
return 1
|
|
117
146
|
}
|
|
147
|
+
if (resolved !== null && !('loadError' in resolved)) return dispatch(verb, ['--help'])
|
|
118
148
|
}
|
|
119
149
|
await printHelp(await (await plugins()).pluginCommandHelp(), core ? verb : undefined)
|
|
120
150
|
return 0
|
|
@@ -125,14 +155,17 @@ async function dispatch(command: string | undefined, rest: string[]): Promise<nu
|
|
|
125
155
|
// takes no more of is refused, as every other verb refuses one.
|
|
126
156
|
const extra = rest[0]
|
|
127
157
|
if (extra !== undefined) {
|
|
128
|
-
|
|
129
|
-
'
|
|
130
|
-
|
|
158
|
+
return refuse(
|
|
159
|
+
'version',
|
|
160
|
+
rest,
|
|
131
161
|
`${refusedWord(extra)}: ${extra}${flagHint('version', extra)}${seeHelp('version')}`,
|
|
162
|
+
'VX_E_USAGE',
|
|
132
163
|
)
|
|
133
|
-
return 1
|
|
134
164
|
}
|
|
135
|
-
|
|
165
|
+
// Painted, and the same words piped: a script reads `vx <version>`.
|
|
166
|
+
process.stdout.write(
|
|
167
|
+
`${ui.inline([ui.cmd('vx'), ' ', ui.tone('info', VERSION)], detectColors(process.stdout))}\n`,
|
|
168
|
+
)
|
|
136
169
|
return 0
|
|
137
170
|
}
|
|
138
171
|
case 'run':
|
|
@@ -174,7 +207,7 @@ async function dispatch(command: string | undefined, rest: string[]): Promise<nu
|
|
|
174
207
|
// A verb's own refusal is its one line; anything else is the
|
|
175
208
|
// plugin's crash, named as every other stage names one.
|
|
176
209
|
if (isUserError(err)) throw err
|
|
177
|
-
const message =
|
|
210
|
+
const message = thrownMessage(err)
|
|
178
211
|
throw new UserError(
|
|
179
212
|
`plugin '${resolved.plugin.name}' failed in command '${command}': ${message}`,
|
|
180
213
|
)
|
|
@@ -201,10 +234,12 @@ async function dispatch(command: string | undefined, rest: string[]): Promise<nu
|
|
|
201
234
|
text: string,
|
|
202
235
|
code = 'VX_E_UNKNOWN_COMMAND',
|
|
203
236
|
tone: 'error' | 'info' = 'error',
|
|
237
|
+
json = text.split('\n')[0]!,
|
|
204
238
|
): number => {
|
|
205
239
|
sayNotice(tone, 'vx', text.replace(/^vx: /, '').trimEnd())
|
|
206
|
-
|
|
207
|
-
|
|
240
|
+
// `vx --format json` puts the flag where the verb goes.
|
|
241
|
+
if (wantsJson([command, ...rest]))
|
|
242
|
+
process.stdout.write(errorDocument(code, maskedLine(json)))
|
|
208
243
|
return 1
|
|
209
244
|
}
|
|
210
245
|
const loadNote =
|
|
@@ -232,12 +267,13 @@ async function dispatch(command: string | undefined, rest: string[]): Promise<nu
|
|
|
232
267
|
// provided by PLUGINS (declared in .vx/workspace.ts), never by core.
|
|
233
268
|
// We keep this neutral hint for the common muscle-memory verbs, but
|
|
234
269
|
// core names no specific plugin package: any package can provide
|
|
235
|
-
// these.
|
|
270
|
+
// these. The notice prints a line per fact, so each line is a
|
|
271
|
+
// whole sentence.
|
|
236
272
|
return unknown(
|
|
237
|
-
`vx:
|
|
238
|
-
` vx core runs tasks in-process
|
|
239
|
-
` distributed execution come from plugins
|
|
240
|
-
` guide: https://vznjs.github.io/vx/
|
|
273
|
+
`vx: \`${command}\` is not a vx core command.\n` +
|
|
274
|
+
` vx core runs tasks in-process.\n` +
|
|
275
|
+
` A dashboard, remote cache and distributed execution come from plugins, not core.\n` +
|
|
276
|
+
` See the plugin guide: https://vznjs.github.io/vx/docs/plugins/\n`,
|
|
241
277
|
)
|
|
242
278
|
}
|
|
243
279
|
// One line, as a verb's own unknown flag or subcommand gets; the full
|
|
@@ -259,9 +295,19 @@ async function dispatch(command: string | undefined, rest: string[]): Promise<nu
|
|
|
259
295
|
'VX_E_USAGE',
|
|
260
296
|
)
|
|
261
297
|
}
|
|
298
|
+
// A workspace that does not load is the refusal to fix first: the
|
|
299
|
+
// verb may be its own plugin's.
|
|
300
|
+
if (resolved !== null && 'loadError' in resolved) {
|
|
301
|
+
return unknown(
|
|
302
|
+
`vx: unknown command: ${command}${guess}${loadNote} (see \`vx help\`)\n`,
|
|
303
|
+
'VX_E_CONFIG',
|
|
304
|
+
'error',
|
|
305
|
+
`vx: unknown command: ${command}; .vx/workspace failed to load: ${resolved.loadError}`,
|
|
306
|
+
)
|
|
307
|
+
}
|
|
262
308
|
return unknown(
|
|
263
|
-
`vx: unknown command: ${command}${guess}
|
|
264
|
-
(guess === ''
|
|
309
|
+
`vx: unknown command: ${command}${guess} (see \`vx help\`)\n` +
|
|
310
|
+
(guess === '' ? verbSourceNote(resolved, declaredVerbs) : ''),
|
|
265
311
|
)
|
|
266
312
|
}
|
|
267
313
|
}
|
|
@@ -298,6 +344,6 @@ function verbSourceNote(resolved: unknown, declaredVerbs: readonly string[]): st
|
|
|
298
344
|
if (declaredVerbs.length > 0) {
|
|
299
345
|
return ` This workspace's plugins declare: ${[...declaredVerbs].sort().join(', ')}.\n`
|
|
300
346
|
}
|
|
301
|
-
const here = resolved === null ? 'there is no workspace here' : '
|
|
347
|
+
const here = resolved === null ? 'there is no workspace here' : 'no plugin here adds one'
|
|
302
348
|
return ` A plugin declared in .vx/workspace.ts can add verbs; ${here}.\n`
|
|
303
349
|
}
|
package/src/cli/info.ts
CHANGED
|
@@ -113,20 +113,24 @@ export function renderInfo(f: InfoFacts, colors: ColorSupport = { enabled: false
|
|
|
113
113
|
// the hits are a share of.
|
|
114
114
|
[
|
|
115
115
|
'task runs (24h)',
|
|
116
|
-
`${f.runs24h} (${f.hits24h} cache
|
|
116
|
+
`${f.runs24h} (${f.hits24h} cache hit${f.hits24h === 1 ? '' : 's'}` +
|
|
117
117
|
(f.hits24h > 0
|
|
118
118
|
? `: ${f.hits24h - f.restored24h} up-to-date, ${f.restored24h} restored)`
|
|
119
119
|
: ')'),
|
|
120
120
|
],
|
|
121
121
|
['flaky tasks', describeFlakyTasks(f.flakyTasks)],
|
|
122
122
|
['sandbox', describeSandbox(f.sandbox)],
|
|
123
|
-
[
|
|
123
|
+
[
|
|
124
|
+
'projects lock',
|
|
125
|
+
typeof f.lockError === 'string' ? `refused: ${f.lockError}` : f.lockfile ? 'yes' : 'no',
|
|
126
|
+
],
|
|
124
127
|
]
|
|
125
128
|
return ui.report('vx info', rows).lines(colors).join('\n')
|
|
126
129
|
}
|
|
127
130
|
|
|
128
|
-
/** `812 (1.2 GB)
|
|
131
|
+
/** `812 (1.2 GB)`, or why the index cannot be read and its fix. */
|
|
129
132
|
function describeEntries(f: InfoFacts): string {
|
|
133
|
+
if (f.cacheIndexError !== null) return `unreadable: ${f.cacheIndexError}`
|
|
130
134
|
return `${f.cacheEntries} (${formatBytes(f.cacheBytes)})`
|
|
131
135
|
}
|
|
132
136
|
|
|
@@ -139,7 +143,7 @@ function describeSettings(settings: InfoFacts['settings']): string {
|
|
|
139
143
|
.filter(([, s]) => s.source !== 'default')
|
|
140
144
|
.map(
|
|
141
145
|
([path, s]) =>
|
|
142
|
-
`${path} ${String(s.value)} (${s.source === 'env' ? envName(path) : '.vx/workspace'})`,
|
|
146
|
+
`${path} ${String(s.value)} (${s.source === 'env' ? envName(path) : '.vx/workspace'}${s.refused === undefined ? '' : `, refused: ${s.refused}`})`,
|
|
143
147
|
)
|
|
144
148
|
.join(', ')
|
|
145
149
|
}
|
|
@@ -169,9 +173,11 @@ function describeSandbox(s: InfoFacts['sandbox']): string {
|
|
|
169
173
|
/** `4 — the CPU count`, `2 — cgroup CPU quota 2 of 8 cores`, `8 — .vx/workspace.ts (4 cores)`. */
|
|
170
174
|
export function describeWorkers(w: InfoFacts['workers']): string {
|
|
171
175
|
const quota = w.cpuQuota === null ? '' : `, cgroup CPU quota ${trimCores(w.cpuQuota)}`
|
|
172
|
-
|
|
176
|
+
const cores = `${w.cores} core${w.cores === 1 ? '' : 's'}`
|
|
177
|
+
if (w.source === 'workspace') return `${w.count} — .vx/workspace.ts (${cores}${quota})`
|
|
178
|
+
if (w.source === 'env') return `${w.count} — ${envName('run.concurrency')} (${cores}${quota})`
|
|
173
179
|
if (w.source === 'cgroup')
|
|
174
|
-
return `${w.count} — cgroup CPU quota ${trimCores(w.cpuQuota ?? w.count)} of ${
|
|
180
|
+
return `${w.count} — cgroup CPU quota ${trimCores(w.cpuQuota ?? w.count)} of ${cores}`
|
|
175
181
|
return `${w.count} — the CPU count${quota}`
|
|
176
182
|
}
|
|
177
183
|
|
package/src/cli/init.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// `vx init [--dry [--format json]] [--force] [--mjs] [--native|--keep]` — a workspace from nowhere.
|
|
2
2
|
// It installs vx globally and, where a package.json is, as its devDependency
|
|
3
|
-
// (
|
|
3
|
+
// (2026-10-20). A Turbo or Nx repo is `@vzn/vx-migrate`'s, which maps it
|
|
4
4
|
// and installs what it writes (`migrate`); any other gets the workspace file
|
|
5
5
|
// and an empty root project, then the lock (`starter`).
|
|
6
|
-
// `vx init --plugin <
|
|
6
|
+
// `vx init --plugin <hook>` writes a runnable plugin for one hook and its
|
|
7
7
|
// test instead (plugin-templates.ts, the examples the gate runs).
|
|
8
8
|
|
|
9
|
-
import { existsSync } from 'node:fs'
|
|
9
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
10
10
|
import { mkdir, unlink } from 'node:fs/promises'
|
|
11
11
|
import path from 'node:path'
|
|
12
12
|
import { gitTop, workspaceFileWritten } from '../handoff.js'
|
|
@@ -22,10 +22,23 @@ import {
|
|
|
22
22
|
type ManagedInstall,
|
|
23
23
|
} from './upgrade.js'
|
|
24
24
|
import { VERSION } from '../version.js'
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
detectColors,
|
|
27
|
+
DOT_VX_GITATTRIBUTES,
|
|
28
|
+
DOT_VX_GITIGNORE,
|
|
29
|
+
paint,
|
|
30
|
+
refuseSymlink,
|
|
31
|
+
sayNotice,
|
|
32
|
+
SUCCESS,
|
|
33
|
+
ui,
|
|
34
|
+
UserError,
|
|
35
|
+
writeNoFollow,
|
|
36
|
+
} from '../util/index.js'
|
|
26
37
|
import {
|
|
27
38
|
findWorkspaceRoot,
|
|
28
39
|
PROJECT_CONFIG_FILENAMES,
|
|
40
|
+
renderWorkspace,
|
|
41
|
+
starterProject,
|
|
29
42
|
WORKSPACE_CONFIG_FILENAMES,
|
|
30
43
|
} from '../workspace/index.js'
|
|
31
44
|
|
|
@@ -34,7 +47,7 @@ interface InitArgs {
|
|
|
34
47
|
force: boolean
|
|
35
48
|
/** `vx.config.mjs` instead of `.ts` — see `ApplyMigrationArgs.format`. */
|
|
36
49
|
mjs: boolean
|
|
37
|
-
/** `--plugin <
|
|
50
|
+
/** `--plugin <hook>`: scaffold that hook's plugin instead of the workspace. */
|
|
38
51
|
plugin?: string
|
|
39
52
|
/** `--native` / `--keep` in a Turbo or Nx repo; unset, vx-migrate asks a terminal. */
|
|
40
53
|
mode?: 'native' | 'keep'
|
|
@@ -53,7 +66,16 @@ export function parseInitArgs(args: readonly string[]): InitArgs {
|
|
|
53
66
|
if (seam === undefined || !Object.hasOwn(PLUGIN_TEMPLATES, seam)) {
|
|
54
67
|
return {
|
|
55
68
|
...out,
|
|
56
|
-
error:
|
|
69
|
+
error:
|
|
70
|
+
seam === undefined || seam === ''
|
|
71
|
+
? `--plugin requires a value: one of ${seams}${seeHelp('init')}`
|
|
72
|
+
: `--plugin must be one of ${seams} (got ${seam})${seeHelp('init')}`,
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (out.plugin !== undefined && out.plugin !== seam) {
|
|
76
|
+
return {
|
|
77
|
+
...out,
|
|
78
|
+
error: `--plugin takes one hook; run vx init --plugin once per hook${seeHelp('init')}`,
|
|
57
79
|
}
|
|
58
80
|
}
|
|
59
81
|
out.plugin = seam
|
|
@@ -116,16 +138,47 @@ export async function initCmd(args: readonly string[]): Promise<number> {
|
|
|
116
138
|
if (dirs.some((d) => TOOL_CONFIGS.some((f) => existsSync(path.join(d, f))))) {
|
|
117
139
|
return migrate(dirs, parsed)
|
|
118
140
|
}
|
|
119
|
-
|
|
120
|
-
const pkgRoot = dirs.findLast((d) => existsSync(path.join(d, 'package.json'))) ?? null
|
|
141
|
+
const pkgRoot = packageRoot(dirs, top !== null)
|
|
121
142
|
return starter(marked ?? pkgRoot ?? cwd, pkgRoot, top === null, parsed)
|
|
122
143
|
}
|
|
123
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Inside git, the topmost package.json: a monorepo's root, wherever below
|
|
147
|
+
* it init ran. Outside git `dirs` reach `/`, and the topmost took a stray
|
|
148
|
+
* package.json in the home directory (2026-10-28); there it is the nearest
|
|
149
|
+
* one, or the outermost above it that declares a workspace.
|
|
150
|
+
*/
|
|
151
|
+
function packageRoot(dirs: readonly string[], inGit: boolean): string | null {
|
|
152
|
+
const has = (d: string, f: string): boolean => existsSync(path.join(d, f))
|
|
153
|
+
if (inGit) return dirs.findLast((d) => has(d, 'package.json')) ?? null
|
|
154
|
+
const at = dirs.findIndex((d) => has(d, 'package.json'))
|
|
155
|
+
if (at === -1) return null
|
|
156
|
+
const outer = dirs
|
|
157
|
+
.slice(at + 1)
|
|
158
|
+
.findLast(
|
|
159
|
+
(d) => has(d, 'package.json') && (has(d, 'pnpm-workspace.yaml') || declaresWorkspaces(d)),
|
|
160
|
+
)
|
|
161
|
+
return outer ?? dirs[at]!
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function declaresWorkspaces(dir: string): boolean {
|
|
165
|
+
try {
|
|
166
|
+
const pj = JSON.parse(
|
|
167
|
+
readFileSync(path.join(dir, 'package.json'), 'utf8').replace(/^\uFEFF/, ''),
|
|
168
|
+
) as {
|
|
169
|
+
workspaces?: unknown
|
|
170
|
+
}
|
|
171
|
+
return pj.workspaces !== undefined
|
|
172
|
+
} catch {
|
|
173
|
+
return false
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
124
177
|
/** What makes a repo vx-migrate's to map: a Turbo or Nx config. */
|
|
125
178
|
const TOOL_CONFIGS = ['turbo.json', 'turbo.jsonc', 'nx.json']
|
|
126
179
|
|
|
127
180
|
/**
|
|
128
|
-
* The installs `vx init` runs (
|
|
181
|
+
* The installs `vx init` runs (2026-10-20): vx at `version` globally,
|
|
129
182
|
* with the manager that started init, so a bare `vx` is on PATH, unless
|
|
130
183
|
* the global vx already answers `version`; and, where `pkgRoot` has a
|
|
131
184
|
* package.json, as its devDependency with the repo's own manager.
|
|
@@ -140,7 +193,9 @@ export function initInstalls(
|
|
|
140
193
|
): { argv: string[]; cwd: string | null }[] {
|
|
141
194
|
const out: { argv: string[]; cwd: string | null }[] = []
|
|
142
195
|
if (globalAt !== version) {
|
|
143
|
-
|
|
196
|
+
// Yarn 2+ has no `yarn global`: Bun, which vx needs anyway, as with no runner.
|
|
197
|
+
const ua = /^(npm|pnpm|yarn|bun)\/(\d+)/.exec(userAgent ?? '')
|
|
198
|
+
const runner = ua === null || (ua[1] === 'yarn' && Number(ua[2]) >= 2) ? 'bun' : ua[1]!
|
|
144
199
|
const manager = runner as ManagedInstall['manager']
|
|
145
200
|
out.push({ argv: upgradeArgv({ manager, scope: 'global', cwd: '' }, version), cwd: null })
|
|
146
201
|
}
|
|
@@ -169,6 +224,21 @@ function globalVersion(): string | undefined {
|
|
|
169
224
|
return undefined
|
|
170
225
|
}
|
|
171
226
|
|
|
227
|
+
/**
|
|
228
|
+
* The package a manager's output says the registry answered 404 for, as
|
|
229
|
+
* `name@version`: a release's platform tarball stays unserved for minutes
|
|
230
|
+
* after publish (v0.0.646, 2026-10-23). bun: `GET <url> - 404`; npm:
|
|
231
|
+
* `404 Not Found - GET <url>`; pnpm: `GET <url>: Not Found - 404`.
|
|
232
|
+
*/
|
|
233
|
+
export function notServed(output: string): string | undefined {
|
|
234
|
+
for (const line of output.split('\n')) {
|
|
235
|
+
if (!line.includes('404')) continue
|
|
236
|
+
const m = /https?:\/\/\S+?\/((?:@[^/\s]+\/)?[^/\s]+)\/-\/[^/\s]+?-(\d[^/\s]*?)\.tgz/.exec(line)
|
|
237
|
+
if (m !== null) return `${decodeURIComponent(m[1]!)}@${m[2]}`
|
|
238
|
+
}
|
|
239
|
+
return undefined
|
|
240
|
+
}
|
|
241
|
+
|
|
172
242
|
/**
|
|
173
243
|
* Run `initInstalls`, in order. A source checkout (0.0.0, which npm never
|
|
174
244
|
* had) installs nothing: the release would replace the code under test.
|
|
@@ -200,22 +270,62 @@ async function install(
|
|
|
200
270
|
'out',
|
|
201
271
|
ui.text([ui.cmd(s.argv.join(' ')), ui.dim(s.cwd === null ? '' : ` in ${s.cwd}`)]),
|
|
202
272
|
)
|
|
203
|
-
const proc = Bun.
|
|
273
|
+
const proc = Bun.spawn({
|
|
204
274
|
cmd: [bin, ...s.argv.slice(1)],
|
|
205
275
|
cwd: s.cwd ?? process.cwd(),
|
|
206
|
-
stdio: ['inherit', 'inherit', '
|
|
276
|
+
stdio: ['inherit', 'inherit', 'pipe'],
|
|
207
277
|
})
|
|
208
|
-
|
|
209
|
-
|
|
278
|
+
// Shown as it comes, and kept: a registry 404 is the cause to name.
|
|
279
|
+
let said = ''
|
|
280
|
+
const decoder = new TextDecoder()
|
|
281
|
+
for await (const chunk of proc.stderr) {
|
|
282
|
+
process.stderr.write(chunk)
|
|
283
|
+
said += decoder.decode(chunk, { stream: true })
|
|
284
|
+
}
|
|
285
|
+
const code = await proc.exited
|
|
286
|
+
if (code !== 0) {
|
|
287
|
+
const missing = notServed(said)
|
|
288
|
+
throw new UserError(
|
|
289
|
+
missing === undefined
|
|
290
|
+
? `vx init: ${s.argv.join(' ')} failed (exit ${code})`
|
|
291
|
+
: `vx init: the npm registry does not serve ${missing} yet (404), as a release can take minutes to reach it; run bunx @vzn/vx init again`,
|
|
292
|
+
)
|
|
210
293
|
}
|
|
211
294
|
}
|
|
212
295
|
return steps.map((s) => s.argv.join(' '))
|
|
213
296
|
}
|
|
214
297
|
|
|
298
|
+
const PLUGINS_MANIFEST = `${JSON.stringify({ name: 'workspace-plugins', private: true }, null, 2)}\n`
|
|
299
|
+
|
|
300
|
+
/** Whether the nearest package.json above `dir` has a name, as `definePlugin` needs. */
|
|
301
|
+
function ownerNamed(dir: string): boolean {
|
|
302
|
+
for (let d = dir; ; d = path.dirname(d)) {
|
|
303
|
+
let text: string
|
|
304
|
+
try {
|
|
305
|
+
text = readFileSync(path.join(d, 'package.json'), 'utf8').replace(/^/, '')
|
|
306
|
+
} catch {
|
|
307
|
+
if (path.dirname(d) === d) return false
|
|
308
|
+
continue
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
const name = (JSON.parse(text) as { name?: unknown } | null)?.name
|
|
312
|
+
return typeof name === 'string' && name !== ''
|
|
313
|
+
} catch {
|
|
314
|
+
return false
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
215
319
|
/** Write `plugins/<seam>.ts` and its test under `root`: the workspace root, else the cwd. */
|
|
216
320
|
async function scaffoldPlugin(seam: string, root: string, args: InitArgs): Promise<number> {
|
|
217
321
|
const template = PLUGIN_TEMPLATES[seam]!
|
|
322
|
+
// `definePlugin` names the plugin by the nearest package.json, and a
|
|
323
|
+
// monorepo root often has no name: the plugin refused to load.
|
|
324
|
+
const manifest: [string, string][] = ownerNamed(path.join(root, 'plugins'))
|
|
325
|
+
? []
|
|
326
|
+
: [[path.join('plugins', 'package.json'), PLUGINS_MANIFEST]]
|
|
218
327
|
const files = [
|
|
328
|
+
...manifest,
|
|
219
329
|
[path.join('plugins', `${seam}.ts`), template.plugin],
|
|
220
330
|
[path.join('plugins', `${seam}.test.ts`), template.test],
|
|
221
331
|
] as const
|
|
@@ -228,16 +338,26 @@ async function scaffoldPlugin(seam: string, root: string, args: InitArgs): Promi
|
|
|
228
338
|
}
|
|
229
339
|
}
|
|
230
340
|
if (!args.dry) {
|
|
231
|
-
await mkdir(path.join(root, 'plugins'), { recursive: true })
|
|
341
|
+
await mkdir(path.join(root, 'plugins'), { recursive: true }).catch(
|
|
342
|
+
(err: NodeJS.ErrnoException) => {
|
|
343
|
+
if (err.code !== 'EEXIST' && err.code !== 'ENOTDIR') throw err
|
|
344
|
+
throw new UserError(
|
|
345
|
+
`vx init: cannot write ${files[0][0]}: plugins is a file, not a directory; move it and re-run`,
|
|
346
|
+
)
|
|
347
|
+
},
|
|
348
|
+
)
|
|
232
349
|
for (const [rel, text] of files) await Bun.write(path.join(root, rel), text)
|
|
233
350
|
}
|
|
234
351
|
const factory = /^export function (\w+)/m.exec(template.plugin)![1]!
|
|
235
352
|
const workspaceFile =
|
|
236
353
|
WORKSPACE_CONFIG_FILENAMES.find((n) => existsSync(path.join(root, n))) ?? '.vx/workspace.ts'
|
|
354
|
+
const c = detectColors(process.stdout)
|
|
355
|
+
const dim = (t: string): string => paint('', t, c, { dim: true })
|
|
356
|
+
const bold = (t: string): string => paint('', t, c, { bold: true })
|
|
237
357
|
process.stdout.write(
|
|
238
|
-
`${args.dry ? 'would write' : 'wrote'} ${files.map(([rel]) => rel).join(' and ')}\n` +
|
|
239
|
-
|
|
240
|
-
|
|
358
|
+
`${paint(SUCCESS, args.dry ? 'would write' : 'wrote', c, { bold: true })} ${files.map(([rel]) => bold(rel)).join(' and ')}\n` +
|
|
359
|
+
`${dim('Declare it in')} ${workspaceFile}${dim(':')} ${bold(`import { ${factory} } from '../plugins/${seam}.ts'`)}${dim(', then')} ${bold(`plugins: [${factory}(…)]`)}\n` +
|
|
360
|
+
`${dim('Test it:')} ${bold(`bun test plugins/${seam}.test.ts`)} ${dim('(needs @vzn/vx installed)')}\n`,
|
|
241
361
|
)
|
|
242
362
|
return 0
|
|
243
363
|
}
|
|
@@ -257,13 +377,30 @@ function lockfilePlugin(pkgRoot: string | null): 'pnpm' | 'bun' | 'yarn' | 'npm'
|
|
|
257
377
|
return null
|
|
258
378
|
}
|
|
259
379
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
380
|
+
/**
|
|
381
|
+
* Whether `pkgRoot` has package.json scripts to map: its own, or a
|
|
382
|
+
* workspace's members (`workspaces`, `pnpm-workspace.yaml`). Core maps none
|
|
383
|
+
* (2026-10-20), so `next` names the tool that does.
|
|
384
|
+
*/
|
|
385
|
+
function hasScripts(pkgRoot: string | null): boolean {
|
|
386
|
+
if (pkgRoot === null) return false
|
|
387
|
+
if (existsSync(path.join(pkgRoot, 'pnpm-workspace.yaml'))) return true
|
|
388
|
+
try {
|
|
389
|
+
// A byte-order mark is stripped, as npm strips it.
|
|
390
|
+
const pj = JSON.parse(
|
|
391
|
+
readFileSync(path.join(pkgRoot, 'package.json'), 'utf8').replace(/^/, ''),
|
|
392
|
+
) as {
|
|
393
|
+
scripts?: unknown
|
|
394
|
+
workspaces?: unknown
|
|
395
|
+
}
|
|
396
|
+
return (
|
|
397
|
+
pj.workspaces !== undefined ||
|
|
398
|
+
(typeof pj.scripts === 'object' && pj.scripts !== null && Object.keys(pj.scripts).length > 0)
|
|
399
|
+
)
|
|
400
|
+
} catch {
|
|
401
|
+
return false
|
|
402
|
+
}
|
|
264
403
|
}
|
|
265
|
-
const PROJECT =
|
|
266
|
-
"import { defineProject } from '@vzn/vx'\n\nexport default defineProject({ tasks: {} })\n"
|
|
267
404
|
|
|
268
405
|
/**
|
|
269
406
|
* No Turbo or Nx config: the installs, then `.vx/workspace.ts` declaring
|
|
@@ -280,6 +417,7 @@ async function starter(
|
|
|
280
417
|
if (args.mode !== undefined) {
|
|
281
418
|
throw new UserError(
|
|
282
419
|
`vx init: --${args.mode} answers a Turbo or Nx repo's question, and there is no turbo.json or nx.json here`,
|
|
420
|
+
'VX_E_USAGE',
|
|
283
421
|
)
|
|
284
422
|
}
|
|
285
423
|
// A source checkout installs nothing, so it declares the plugin only where one is.
|
|
@@ -298,8 +436,12 @@ async function starter(
|
|
|
298
436
|
const kept: string[] = []
|
|
299
437
|
const replaced: string[] = []
|
|
300
438
|
for (const [names, name, contents] of [
|
|
301
|
-
[
|
|
302
|
-
|
|
439
|
+
[
|
|
440
|
+
WORKSPACE_CONFIG_FILENAMES,
|
|
441
|
+
`.vx/workspace.${ext}`,
|
|
442
|
+
renderWorkspace(ext, plugin === null ? [] : [{ pkg: '@vzn/vx-lockfile', factory: plugin }]),
|
|
443
|
+
],
|
|
444
|
+
[PROJECT_CONFIG_FILENAMES, config, starterProject(ext)],
|
|
303
445
|
] as const) {
|
|
304
446
|
const existing = names.find((n) => existsSync(path.join(root, n)))
|
|
305
447
|
if (existing !== undefined && !args.force) kept.push(existing)
|
|
@@ -315,7 +457,8 @@ async function starter(
|
|
|
315
457
|
] as const) {
|
|
316
458
|
if (!existsSync(path.join(root, name))) files.push({ path: name, contents })
|
|
317
459
|
}
|
|
318
|
-
const
|
|
460
|
+
const declare = `declare a task in ${kept.find((f) => f.startsWith('vx.config.')) ?? config}, then vx lock && vx run <task>`
|
|
461
|
+
const next = `${noGit ? 'git init, ' : ''}${hasScripts(pkgRoot) ? `bunx @vzn/vx-migrate${args.mjs ? ' --mjs' : ''} to make package.json scripts tasks, or ${declare}` : declare}`
|
|
319
462
|
if (args.json === true) {
|
|
320
463
|
process.stdout.write(
|
|
321
464
|
`${JSON.stringify({ dry: true, source: 'no turbo.json or nx.json', files, kept, replaced, todos: [], notes: [], next, installs })}\n`,
|
|
@@ -323,19 +466,30 @@ async function starter(
|
|
|
323
466
|
return 0
|
|
324
467
|
}
|
|
325
468
|
const rows: ui.FieldRow[] = [['from', 'no turbo.json or nx.json: one project, the root']]
|
|
469
|
+
// The files are root-relative: a root above the cwd is named, or they read as written here.
|
|
470
|
+
if (root !== process.cwd()) rows.push(['root', root])
|
|
326
471
|
if (args.dry) rows.push(['dry run', 'nothing written'])
|
|
327
472
|
if (installs.length > 0) rows.push(['install', ...installs])
|
|
328
473
|
if (files.length > 0) rows.push(['files', ...files.map((f) => f.path)])
|
|
329
474
|
if (kept.length > 0) rows.push(['kept', ...kept])
|
|
330
475
|
if (replaced.length > 0) rows.push(['replaced', ...replaced])
|
|
331
476
|
if (args.dry) {
|
|
332
|
-
for (const f of files)
|
|
477
|
+
for (const f of files) ui.print('out', ui.excerpt(f.path, f.contents))
|
|
333
478
|
ui.print('out', ui.report(`vx init ${VERSION}`, [...rows, null, ['next', next]]))
|
|
334
479
|
return 0
|
|
335
480
|
}
|
|
481
|
+
for (const f of files) refuseSymlink(path.join(root, f.path))
|
|
336
482
|
const ran = await install(pkgRoot, false, plugins)
|
|
337
483
|
if (ran.length > 0) rows.push(['installed', ...ran])
|
|
338
|
-
for (const f of files)
|
|
484
|
+
for (const f of files) {
|
|
485
|
+
await writeNoFollow(path.join(root, f.path), f.contents).catch((err: NodeJS.ErrnoException) => {
|
|
486
|
+
// writeNoFollow's mkdir of a parent that is a file says EEXIST.
|
|
487
|
+
if (err.code !== 'ENOTDIR' && err.code !== 'EEXIST') throw err
|
|
488
|
+
throw new UserError(
|
|
489
|
+
`vx init: cannot write ${f.path}: ${path.dirname(f.path)} is a file, not a directory; move it and re-run`,
|
|
490
|
+
)
|
|
491
|
+
})
|
|
492
|
+
}
|
|
339
493
|
for (const f of replaced) await unlink(path.join(root, f))
|
|
340
494
|
// The lock that follows loads the workspace file just written.
|
|
341
495
|
workspaceFileWritten()
|
|
@@ -345,14 +499,15 @@ async function starter(
|
|
|
345
499
|
lock.code === 0 ? next : `fix the above, then vx lock and ${next}`,
|
|
346
500
|
])
|
|
347
501
|
ui.print('out', ui.report(`vx init ${VERSION}`, rows))
|
|
348
|
-
|
|
502
|
+
// A run reads only the lock: init without one is not done, as vx-migrate exits.
|
|
503
|
+
return lock.code === 0 ? 0 : 1
|
|
349
504
|
}
|
|
350
505
|
|
|
351
506
|
/**
|
|
352
507
|
* A Turbo or Nx repo is `@vzn/vx-migrate`'s, after the global install, at the latest
|
|
353
508
|
* release: it installs @vzn/vx and every @vzn/vx-* plugin the root lists
|
|
354
509
|
* at its own version, so init always lands on latest with all of them
|
|
355
|
-
* together, as `vx upgrade` moves them (
|
|
510
|
+
* together, as `vx upgrade` moves them (2026-10-16: an installed 0.0.637
|
|
356
511
|
* pinned core back from 0.0.639). It finds the root itself, asks a
|
|
357
512
|
* terminal native or keep in a Turbo or Nx repo, and installs what it writes.
|
|
358
513
|
*/
|