@vzn/vx 0.0.651 → 0.0.653
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 +3 -4
- package/launcher.sh +2 -1
- package/package.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/show.json +15 -20
- package/skills/vx/SKILL.md +3 -3
- package/src/bin.ts +37 -3
- package/src/cache/archive.ts +197 -32
- package/src/cache/cache.ts +882 -230
- package/src/cache/checkpointer.ts +102 -0
- package/src/cache/file-hashes.ts +2 -1
- package/src/cache/fold-memo.ts +24 -4
- package/src/cache/fs-clock.ts +26 -0
- package/src/cache/git-inputs.ts +188 -22
- package/src/cache/history-stream.ts +151 -0
- package/src/cache/history-writer.ts +76 -0
- package/src/cache/index.ts +12 -1
- package/src/cache/inputs.ts +616 -135
- package/src/cache/key-fold.ts +1 -1
- package/src/cache/layer.ts +29 -16
- package/src/cache/layered-cache.ts +28 -7
- package/src/cache/lockfile-memo.ts +2 -1
- package/src/cache/output-index.ts +106 -44
- package/src/cache/plugin-cache.ts +2 -2
- package/src/cache/policy.ts +6 -2
- package/src/cache/remote-lane.ts +3 -1
- package/src/cache/run-history.ts +94 -25
- package/src/cache/schema.ts +46 -10
- package/src/cli/cache.ts +149 -36
- package/src/cli/completions.ts +2 -2
- package/src/cli/core-alias.ts +9 -2
- package/src/cli/docs.ts +4 -2
- package/src/cli/help.ts +27 -4
- package/src/cli/index.ts +62 -7
- package/src/cli/info.ts +5 -4
- package/src/cli/init.ts +55 -20
- package/src/cli/last.ts +218 -43
- package/src/cli/lock.ts +187 -24
- package/src/cli/plan-format.ts +24 -21
- package/src/cli/plugin-commands.ts +6 -1
- package/src/cli/plugin-templates.ts +36 -45
- package/src/cli/run.ts +68 -34
- package/src/cli/select.ts +160 -64
- package/src/cli/show.ts +70 -28
- package/src/cli/task-verb.ts +12 -7
- package/src/cli/upgrade.ts +210 -76
- package/src/cli/watch-filter.ts +88 -15
- package/src/cli/watch-fs.ts +34 -47
- package/src/cli/watch-judge.ts +73 -9
- package/src/cli/watch-set.ts +10 -8
- package/src/cli/watch.ts +190 -33
- package/src/cli/why.ts +107 -33
- package/src/cli/workspace-config.ts +11 -1
- package/src/config.ts +56 -30
- package/src/exec/env.ts +3 -1
- package/src/exec/executor.ts +116 -16
- package/src/exec/index.ts +1 -0
- package/src/exec/kill-tree.ts +7 -7
- package/src/exec/runner.ts +270 -108
- package/src/exec/sandbox-binds.ts +109 -18
- package/src/exec/sandbox-paths.ts +3 -1
- package/src/exec/sandbox-runtime.ts +446 -64
- package/src/exec/sandbox-violations.ts +250 -46
- package/src/exec/spawn-off.ts +10 -5
- package/src/exec/task-proxy.ts +93 -0
- package/src/exec/warm-shell.ts +29 -7
- package/src/graph/definition.ts +16 -3
- package/src/graph/folds.ts +89 -16
- package/src/graph/index.ts +8 -0
- package/src/graph/priorities.ts +8 -5
- package/src/graph/scheduler.ts +176 -39
- package/src/graph/stand-ins.ts +36 -0
- package/src/graph/task-graph.ts +347 -160
- package/src/graph/task-status.ts +5 -1
- package/src/handoff.ts +45 -10
- package/src/history/failure-mode.ts +39 -6
- package/src/history/history.ts +103 -30
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +193 -73
- package/src/index.ts +6 -3
- package/src/orchestrator/admission.ts +14 -10
- package/src/orchestrator/affected-tasks.ts +35 -13
- package/src/orchestrator/command-windows.ts +8 -5
- package/src/orchestrator/deferred-outputs.ts +42 -13
- package/src/orchestrator/doctor.ts +21 -9
- package/src/orchestrator/download-policy.ts +8 -10
- package/src/orchestrator/excluded-keys.ts +15 -3
- package/src/orchestrator/execute-args.ts +9 -2
- package/src/orchestrator/execute-task.ts +612 -138
- package/src/orchestrator/graph-memo.ts +26 -4
- package/src/orchestrator/graph-stage.ts +27 -4
- package/src/orchestrator/held-servers.ts +36 -14
- package/src/orchestrator/hit-restore.ts +141 -93
- package/src/orchestrator/index.ts +4 -1
- package/src/orchestrator/keyed-projects.ts +53 -0
- package/src/orchestrator/local-floor.ts +21 -3
- package/src/orchestrator/local-shortcircuit.ts +2 -2
- package/src/orchestrator/lockfile-claim.ts +30 -4
- package/src/orchestrator/miss-save.ts +60 -61
- package/src/orchestrator/options.ts +13 -0
- package/src/orchestrator/output-host.ts +16 -4
- package/src/orchestrator/output-log.ts +1 -54
- package/src/orchestrator/persistent-task.ts +32 -16
- package/src/orchestrator/persistent.ts +152 -15
- package/src/orchestrator/placement.ts +82 -16
- package/src/orchestrator/plan.ts +39 -17
- package/src/orchestrator/plugin-host.ts +238 -110
- package/src/orchestrator/plugin.ts +59 -27
- package/src/orchestrator/prepare.ts +194 -38
- package/src/orchestrator/projects.ts +89 -10
- package/src/orchestrator/read-only.ts +115 -0
- package/src/orchestrator/remote-prefetch.ts +1 -1
- package/src/orchestrator/run-context.ts +6 -2
- package/src/orchestrator/run-failures.ts +101 -23
- package/src/orchestrator/run-id.ts +13 -2
- package/src/orchestrator/run-keys.ts +87 -28
- package/src/orchestrator/run-live.ts +16 -13
- package/src/orchestrator/run-lock.ts +27 -4
- package/src/orchestrator/run-records.ts +95 -74
- package/src/orchestrator/run-report.ts +3 -2
- package/src/orchestrator/run-stop.ts +17 -9
- package/src/orchestrator/run.ts +531 -122
- package/src/orchestrator/sandbox-fix.ts +40 -12
- package/src/orchestrator/sandbox-request.ts +314 -44
- package/src/orchestrator/save-lane.ts +24 -13
- package/src/orchestrator/shell-verdict.ts +29 -1
- package/src/orchestrator/signals.ts +103 -8
- package/src/orchestrator/stable-keys.ts +34 -27
- package/src/orchestrator/task-artifacts.ts +16 -3
- package/src/orchestrator/task-hash.ts +191 -34
- package/src/orchestrator/telemetry-host.ts +25 -6
- package/src/orchestrator/telemetry.ts +85 -13
- package/src/orchestrator/upstream.ts +3 -1
- package/src/report/events.ts +23 -6
- package/src/report/forecast.ts +103 -37
- package/src/report/framed-output.ts +179 -162
- package/src/report/index.ts +2 -1
- package/src/report/logger.ts +108 -31
- package/src/report/plain-output.ts +15 -9
- package/src/report/status-line.ts +28 -55
- package/src/report/summary.ts +61 -18
- package/src/settings.ts +50 -14
- package/src/ui/components.ts +36 -42
- package/src/ui/index.ts +3 -1
- package/src/ui/live-region.ts +67 -8
- package/src/ui/terminal.ts +12 -0
- package/src/ui/theme.ts +16 -1
- package/src/util/config-frame.ts +142 -6
- package/src/util/edit-distance.ts +17 -1
- package/src/util/errors.ts +23 -7
- package/src/util/foreign-cache-dir.ts +60 -0
- package/src/util/git-ignored.ts +59 -5
- package/src/util/index.ts +15 -1
- package/src/util/paths.ts +51 -7
- package/src/util/plain-text.ts +19 -0
- package/src/util/secret-mask.ts +57 -1
- package/src/util/settle.ts +18 -1
- package/src/util/size.ts +12 -2
- package/src/util/tail.ts +4 -1
- package/src/util/write-file.ts +86 -9
- package/src/workspace/affected.ts +225 -27
- package/src/workspace/bracket-literal.ts +2 -2
- package/src/workspace/config-bytecode.ts +23 -4
- package/src/workspace/config-eval.ts +90 -16
- package/src/workspace/config-imports.ts +96 -41
- package/src/workspace/config-schema.ts +192 -86
- package/src/workspace/dependencies-lock.ts +7 -14
- package/src/workspace/filter.ts +18 -2
- package/src/workspace/index.ts +3 -1
- package/src/workspace/json-data.ts +34 -8
- package/src/workspace/lockfile.ts +86 -37
- package/src/workspace/migration.ts +58 -31
- package/src/workspace/module-state.ts +62 -0
- package/src/workspace/nested-dirs.ts +8 -0
- package/src/workspace/project-loader.ts +276 -32
- package/src/workspace/repo-id.ts +29 -6
- package/src/workspace/workspace.ts +190 -30
package/src/cli/core-alias.ts
CHANGED
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
* and two module states — two `definePlugin` memos, two of everything
|
|
10
10
|
* keyed by identity rather than by registry symbol.
|
|
11
11
|
*
|
|
12
|
-
* The alias is a Bun virtual module for the exact
|
|
12
|
+
* The alias is a Bun virtual module for the exact specifiers `@vzn/vx`
|
|
13
|
+
* and `@vzn/vx/config` (the second for `import { defineProject }` in a
|
|
14
|
+
* config: with a node_modules and no `@vzn/vx` in it, the import failed
|
|
15
|
+
* though the page says vx runs a config without it),
|
|
13
16
|
* served from the host's own façade, loaded lazily on the first import
|
|
14
17
|
* so a verb that never loads a plugin never loads the façade. The
|
|
15
18
|
* source form (`bun src/bin.ts`) resolves the specifier to the same file
|
|
@@ -18,11 +21,15 @@
|
|
|
18
21
|
* (workspace/config-bytecode.ts) is CommonJS, and `require` refuses a
|
|
19
22
|
* module whose loader is async.
|
|
20
23
|
*/
|
|
21
|
-
export function registerCoreAlias(
|
|
24
|
+
export function registerCoreAlias(
|
|
25
|
+
load: () => Record<string, unknown>,
|
|
26
|
+
loadConfig: () => Record<string, unknown>,
|
|
27
|
+
): void {
|
|
22
28
|
Bun.plugin({
|
|
23
29
|
name: 'vx-core-alias',
|
|
24
30
|
setup(build) {
|
|
25
31
|
build.module('@vzn/vx', () => ({ exports: load(), loader: 'object' }))
|
|
32
|
+
build.module('@vzn/vx/config', () => ({ exports: loadConfig(), loader: 'object' }))
|
|
26
33
|
},
|
|
27
34
|
})
|
|
28
35
|
}
|
package/src/cli/docs.ts
CHANGED
|
@@ -31,10 +31,12 @@ export function parseDocsArgs(args: readonly string[]): DocsArgs {
|
|
|
31
31
|
out.json = fv === 'json'
|
|
32
32
|
} else if (a === '--limit' || a.startsWith('--limit=')) {
|
|
33
33
|
const v = a === '--limit' ? args[++i] : a.slice('--limit='.length)
|
|
34
|
-
|
|
34
|
+
// The space form took the next flag as the count and refused it as one.
|
|
35
|
+
const flag = a === '--limit' && v !== undefined && /^-\D/.test(v) ? ` (got flag ${v})` : ''
|
|
36
|
+
if (v === undefined || v === '' || flag !== '') {
|
|
35
37
|
return {
|
|
36
38
|
...out,
|
|
37
|
-
error: `--limit requires a value: a whole number above 0${seeHelp('docs')}`,
|
|
39
|
+
error: `--limit requires a value: a whole number above 0${flag}${seeHelp('docs')}`,
|
|
38
40
|
}
|
|
39
41
|
}
|
|
40
42
|
const n = parseDecimalInt(v)
|
package/src/cli/help.ts
CHANGED
|
@@ -90,12 +90,33 @@ export function verbHelpText(verb: string): string {
|
|
|
90
90
|
if (own.length > 0) kept.push(['Usage:', ...own].join('\n'))
|
|
91
91
|
continue
|
|
92
92
|
}
|
|
93
|
-
if (lines.some(names)) kept.push(
|
|
93
|
+
if (lines.some(names)) kept.push(own(lines, names, quoted))
|
|
94
94
|
}
|
|
95
95
|
if (kept.length === 0) return helpText()
|
|
96
96
|
return [title, ...kept, 'Full reference: vx help', ''].join('\n\n')
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* A section's entries for one verb, when the section holds several verbs'
|
|
101
|
+
* entries (Introspection): an entry is a two-space line and the deeper
|
|
102
|
+
* lines under it; a shared one (`info, why and last:`) stays if it names
|
|
103
|
+
* the verb. A one-verb or `(for <verb>)` section stays whole.
|
|
104
|
+
*/
|
|
105
|
+
function own(lines: string[], names: (line: string) => boolean, quoted: string): string {
|
|
106
|
+
const entries: string[][] = []
|
|
107
|
+
for (const line of lines.slice(1)) {
|
|
108
|
+
if (/^ {2}\S/.test(line) || entries.length === 0) entries.push([line])
|
|
109
|
+
else entries.at(-1)!.push(line)
|
|
110
|
+
}
|
|
111
|
+
const verbs = new Set(entries.map((e) => /^ {2}vx ([\w-]+)/.exec(e[0]!)?.[1]).filter(Boolean))
|
|
112
|
+
if (verbs.size < 2) return lines.join('\n')
|
|
113
|
+
const word = new RegExp(`\\b${quoted}\\b`)
|
|
114
|
+
const mine = entries.filter(
|
|
115
|
+
(e) => names(e[0]!) || (!e[0]!.startsWith(' vx ') && word.test(e[0]!)),
|
|
116
|
+
)
|
|
117
|
+
return [lines[0]!, ...mine.flat()].join('\n')
|
|
118
|
+
}
|
|
119
|
+
|
|
99
120
|
export function helpText(pluginCommands: readonly string[] = []): string {
|
|
100
121
|
return [
|
|
101
122
|
'vx — task orchestration you own',
|
|
@@ -141,7 +162,7 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
141
162
|
' --no-cache No cache reads or writes; outputs are left',
|
|
142
163
|
' alone.',
|
|
143
164
|
' --force Skip cache reads but still write: every task',
|
|
144
|
-
' runs
|
|
165
|
+
' runs; a key already stored is kept.',
|
|
145
166
|
' --cache-policy <spec> Per-layer read/write: a comma list of',
|
|
146
167
|
' <layer>:<flags> (layer local or remote, flags',
|
|
147
168
|
' from rw). A named layer is set to exactly its',
|
|
@@ -204,9 +225,9 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
204
225
|
'Cache management:',
|
|
205
226
|
' vx cache clean --older-than 30d Evict entries last accessed > 30 days ago.',
|
|
206
227
|
' vx cache clean --max-size 1G Keep total cache under 1 GB (LRU eviction).',
|
|
207
|
-
" vx cache clean Clear .vx/cache and this
|
|
228
|
+
" vx cache clean Clear .vx/cache and this workspace's store",
|
|
208
229
|
' ~/.vx/<id>/cache.',
|
|
209
|
-
" --global Clear every
|
|
230
|
+
" --global Clear every workspace's store under ~/.vx.",
|
|
210
231
|
' --dry Say what the policy would reap; delete nothing.',
|
|
211
232
|
' --format <fmt> pretty (default) | json.',
|
|
212
233
|
' --cache-dir <path> The cache a run with the same flag uses (why,',
|
|
@@ -366,6 +387,8 @@ export function flagHint(verb: string, arg: string): string {
|
|
|
366
387
|
// `--json` is gh's and Nx's spelling; a verb with `--format` reads it
|
|
367
388
|
// there, and no edit budget reaches `--format json` from `--json`.
|
|
368
389
|
if (name === '--json' && flags.includes('--format')) return ' (did you mean --format json?)'
|
|
390
|
+
// pnpm's and Turbo's short `--filter`.
|
|
391
|
+
if (name === '-F') return flags.includes('--filter') ? ' (did you mean --filter?)' : ''
|
|
369
392
|
// `vx run` prints no JSON of its own, and edit distance reached `--force`
|
|
370
393
|
// from `--format`: a run with its cache reads turned off.
|
|
371
394
|
if ((name === '--format' || name === '--json') && flags.includes('--dry'))
|
package/src/cli/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { checkSettingsEnv } from '../settings.js'
|
|
|
6
6
|
import { CORE_VERBS, flagHint, printHelp, refuse, refusedWord, seeHelp } from './help.js'
|
|
7
7
|
import {
|
|
8
8
|
errorDocument,
|
|
9
|
+
hasControl,
|
|
9
10
|
hideEnvFromTasks,
|
|
10
11
|
isFsRefusal,
|
|
11
12
|
isOutOfFds,
|
|
@@ -30,6 +31,7 @@ const plugins = () => import('./plugin-commands.js')
|
|
|
30
31
|
export async function run(argv: readonly string[]): Promise<number> {
|
|
31
32
|
refuseLossyArgs(argv)
|
|
32
33
|
const [command, ...rest] = argv
|
|
34
|
+
if (command !== undefined) refuseControlArgs(command, rest)
|
|
33
35
|
await hideEnvFromTasks()
|
|
34
36
|
|
|
35
37
|
// `vx <verb> --help` is the universal reflex, and every verb answered
|
|
@@ -41,12 +43,21 @@ export async function run(argv: readonly string[]): Promise<number> {
|
|
|
41
43
|
await printHelp(await (await plugins()).pluginCommandHelp(), command)
|
|
42
44
|
return 0
|
|
43
45
|
}
|
|
46
|
+
if (command !== undefined && forwardsNothing(command, rest)) {
|
|
47
|
+
return refuse(
|
|
48
|
+
command,
|
|
49
|
+
rest,
|
|
50
|
+
'forwards nothing after -- (only vx run and vx watch pass arguments on to a task)',
|
|
51
|
+
'VX_E_USAGE',
|
|
52
|
+
)
|
|
53
|
+
}
|
|
44
54
|
// Each setting's variable once, before any verb: a renamed one is named,
|
|
45
55
|
// and a deep reader trusts what it reads. Not for help, as `vx run
|
|
46
56
|
// --help` above is not: help is where a setting's values are read. Nor
|
|
47
|
-
// for `vx info`, which reports a refused one (doctor.ts).
|
|
57
|
+
// for `vx info`, which reports a refused one (doctor.ts). Nor for one
|
|
58
|
+
// whose flag is given: the flag beats it.
|
|
48
59
|
if (command !== 'help' && command !== '--help' && command !== '-h' && command !== 'info')
|
|
49
|
-
checkSettingsEnv()
|
|
60
|
+
checkSettingsEnv(process.env, rest)
|
|
50
61
|
|
|
51
62
|
try {
|
|
52
63
|
return await dispatch(command, rest)
|
|
@@ -69,6 +80,43 @@ function refuseLossyArgs(argv: readonly string[]): void {
|
|
|
69
80
|
)
|
|
70
81
|
}
|
|
71
82
|
|
|
83
|
+
/**
|
|
84
|
+
* No core verb's argument holds a control character, and a refusal echoing
|
|
85
|
+
* one raw broke its headline at a newline. A forwarded one (after `--`) is
|
|
86
|
+
* the task's, a `--sql` query may span lines, and a plugin verb owns its own.
|
|
87
|
+
*/
|
|
88
|
+
function refuseControlArgs(command: string, rest: readonly string[]): void {
|
|
89
|
+
if (!(CORE_VERBS as readonly string[]).includes(command)) return
|
|
90
|
+
for (let i = 0; i < rest.length; i++) {
|
|
91
|
+
const a = rest[i]!
|
|
92
|
+
if (a === '--') return
|
|
93
|
+
if (a === '--sql') {
|
|
94
|
+
i++
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
97
|
+
if (a.startsWith('--sql=') || !hasControl(a)) continue
|
|
98
|
+
throw new UserError(
|
|
99
|
+
`argument ${i + 2} (${JSON.stringify(a)}) holds a control character, which no \`vx ${command}\` argument takes`,
|
|
100
|
+
'VX_E_USAGE',
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A `--` given to a core verb that hands no task its words: each parser
|
|
107
|
+
* refused it as `unknown flag: --`, true of no flag. A `--sql` query is
|
|
108
|
+
* the one value that may be `--`.
|
|
109
|
+
*/
|
|
110
|
+
function forwardsNothing(command: string, rest: readonly string[]): boolean {
|
|
111
|
+
if (command === 'run' || command === 'watch') return false
|
|
112
|
+
if (!(CORE_VERBS as readonly string[]).includes(command)) return false
|
|
113
|
+
for (let i = 0; i < rest.length; i++) {
|
|
114
|
+
if (rest[i] === '--sql') i++
|
|
115
|
+
else if (rest[i] === '--') return true
|
|
116
|
+
}
|
|
117
|
+
return false
|
|
118
|
+
}
|
|
119
|
+
|
|
72
120
|
/**
|
|
73
121
|
* A crash from a hand-broken lock entry is that entry's fault: runs validate
|
|
74
122
|
* no entry, so it surfaced as a raw TypeError (2026-10-14). The lock is
|
|
@@ -124,7 +172,9 @@ async function dispatch(command: string | undefined, rest: string[]): Promise<nu
|
|
|
124
172
|
// A name that is no verb here is the typo `vx rnu` is, not a request
|
|
125
173
|
// for the whole reference: it printed 145 lines and no hint.
|
|
126
174
|
const resolved = await (await plugins()).resolvePluginCommand(verb)
|
|
127
|
-
|
|
175
|
+
// A workspace that failed to load cannot say the verb exists: the
|
|
176
|
+
// whole reference and exit 0 hid the broken file (`vx okk` says so).
|
|
177
|
+
const unknown = resolved === null || !('plugin' in resolved)
|
|
128
178
|
if (unknown && verb.startsWith('-')) {
|
|
129
179
|
return refuse(
|
|
130
180
|
'help',
|
|
@@ -134,15 +184,20 @@ async function dispatch(command: string | undefined, rest: string[]): Promise<nu
|
|
|
134
184
|
)
|
|
135
185
|
}
|
|
136
186
|
if (unknown) {
|
|
137
|
-
const declared =
|
|
187
|
+
const declared =
|
|
188
|
+
resolved !== null && 'declaredVerbs' in resolved ? resolved.declaredVerbs : []
|
|
189
|
+
const loadNote =
|
|
190
|
+
resolved !== null && 'loadError' in resolved
|
|
191
|
+
? ` (plugin verbs could not be looked up: .vx/workspace failed to load: ${resolved.loadError})`
|
|
192
|
+
: ''
|
|
138
193
|
return refuse(
|
|
139
194
|
'help',
|
|
140
195
|
rest,
|
|
141
|
-
`unknown command: ${verb}${didYouMeanVerb(verb, declared)} (see \`vx help\`)`,
|
|
196
|
+
`unknown command: ${verb}${didYouMeanVerb(verb, declared)}${loadNote} (see \`vx help\`)`,
|
|
142
197
|
'VX_E_UNKNOWN_COMMAND',
|
|
143
198
|
)
|
|
144
199
|
}
|
|
145
|
-
|
|
200
|
+
return dispatch(verb, ['--help'])
|
|
146
201
|
}
|
|
147
202
|
await printHelp(await (await plugins()).pluginCommandHelp(), core ? verb : undefined)
|
|
148
203
|
return 0
|
|
@@ -215,7 +270,7 @@ async function dispatch(command: string | undefined, rest: string[]): Promise<nu
|
|
|
215
270
|
// that returned 256 (a count of failures, say) exited 0.
|
|
216
271
|
if (!Number.isInteger(code) || code < 0 || code > 255) {
|
|
217
272
|
throw new UserError(
|
|
218
|
-
`plugin '${resolved.plugin.name}': command '${command}' resolved ${JSON.stringify(code)} instead of an exit code (an integer 0–255)`,
|
|
273
|
+
`plugin '${resolved.plugin.name}': command '${command}' resolved ${typeof code === 'bigint' ? `${String(code)}n` : typeof code === 'number' || code === undefined ? String(code) : JSON.stringify(code)} instead of an exit code (an integer 0–255)`,
|
|
219
274
|
)
|
|
220
275
|
}
|
|
221
276
|
return code
|
package/src/cli/info.ts
CHANGED
|
@@ -97,11 +97,11 @@ function infoReport(f: InfoFacts): ui.Block {
|
|
|
97
97
|
'cache store',
|
|
98
98
|
f.cacheStore === null
|
|
99
99
|
? 'none: the cache dir holds the entries'
|
|
100
|
-
: `${tildePath(f.cacheStore)} (${formatBytes(f.cacheStoreBytes ?? 0)}, shared by this
|
|
100
|
+
: `${tildePath(f.cacheStore)} (${formatBytes(f.cacheStoreBytes ?? 0)}, shared by this workspace's checkouts)`,
|
|
101
101
|
],
|
|
102
102
|
['cache versions', `keys ${f.cacheVersion} · index schema ${f.schemaVersion}`],
|
|
103
103
|
['cache entries', describeEntries(f)],
|
|
104
|
-
// Only the settings a variable
|
|
104
|
+
// Only the settings a variable, the workspace or a plugin changed.
|
|
105
105
|
...(Object.values(f.settings).some((s) => s.source !== 'default')
|
|
106
106
|
? ([['settings', describeSettings(f.settings)]] as [string, string][])
|
|
107
107
|
: []),
|
|
@@ -141,14 +141,15 @@ function describeEntries(f: InfoFacts): string {
|
|
|
141
141
|
|
|
142
142
|
/**
|
|
143
143
|
* The settings not at their default, with where each came from:
|
|
144
|
-
* `run.timeout 30m (VX_RUN_TIMEOUT), cache.policy local:rw (.vx/workspace)
|
|
144
|
+
* `run.timeout 30m (VX_RUN_TIMEOUT), cache.policy local:rw (.vx/workspace)`,
|
|
145
|
+
* `cache.scope pr-2 (plugin @org/ci)`.
|
|
145
146
|
*/
|
|
146
147
|
function describeSettings(settings: InfoFacts['settings']): string {
|
|
147
148
|
return Object.entries(settings)
|
|
148
149
|
.filter(([, s]) => s.source !== 'default')
|
|
149
150
|
.map(
|
|
150
151
|
([path, s]) =>
|
|
151
|
-
`${path} ${String(s.value)} (${s.source === 'env' ? envName(path) : '.vx/workspace'}${s.refused === undefined ? '' : `, refused: ${s.refused}`})`,
|
|
152
|
+
`${path} ${String(s.value)} (${s.source === 'env' ? envName(path) : s.source === 'plugin' ? `plugin ${s.plugin}` : '.vx/workspace'}${s.refused === undefined ? '' : `, refused: ${s.refused}`})`,
|
|
152
153
|
)
|
|
153
154
|
.join(', ')
|
|
154
155
|
}
|
package/src/cli/init.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
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, readFileSync } from 'node:fs'
|
|
9
|
+
import { existsSync, lstatSync, readFileSync, statSync } 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'
|
|
@@ -132,11 +132,17 @@ export async function initCmd(args: readonly string[]): Promise<number> {
|
|
|
132
132
|
dirs.push(d)
|
|
133
133
|
if (d === top || path.dirname(d) === d) break
|
|
134
134
|
}
|
|
135
|
-
|
|
135
|
+
const pkgRoot = packageRoot(dirs, top !== null)
|
|
136
|
+
// Outside git `dirs` reach `/`: a stray turbo.json or nx.json above the
|
|
137
|
+
// project took init over, as a stray package.json did (2026-10-28).
|
|
138
|
+
const own = top !== null ? dirs : dirs.slice(0, dirs.indexOf(pkgRoot ?? cwd) + 1)
|
|
139
|
+
if (own.some((d) => TOOL_CONFIGS.some((f) => existsSync(path.join(d, f))))) {
|
|
136
140
|
return migrate(dirs, parsed)
|
|
137
141
|
}
|
|
138
|
-
|
|
139
|
-
|
|
142
|
+
// Asked of git itself: a kept workspace file is `top` too, and the
|
|
143
|
+
// `git init` hint went missing where the next run would refuse.
|
|
144
|
+
const noGit = marked === undefined ? top === null : (await gitTop(cwd)) === null
|
|
145
|
+
return starter(marked ?? pkgRoot ?? cwd, pkgRoot, noGit, parsed)
|
|
140
146
|
}
|
|
141
147
|
|
|
142
148
|
/**
|
|
@@ -314,6 +320,20 @@ function ownerNamed(dir: string): boolean {
|
|
|
314
320
|
}
|
|
315
321
|
}
|
|
316
322
|
|
|
323
|
+
/**
|
|
324
|
+
* Whether anything stands at `file`, a dangling symlink included: writing
|
|
325
|
+
* through one created its target outside the workspace.
|
|
326
|
+
*/
|
|
327
|
+
function occupied(file: string): boolean {
|
|
328
|
+
try {
|
|
329
|
+
lstatSync(file)
|
|
330
|
+
return true
|
|
331
|
+
} catch {
|
|
332
|
+
// quiet: nothing there (a parent that is a file is refused at the write)
|
|
333
|
+
return false
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
317
337
|
/** Write `plugins/<seam>.ts` and its test under `root`: the workspace root, else the cwd. */
|
|
318
338
|
async function scaffoldPlugin(seam: string, root: string, args: InitArgs): Promise<number> {
|
|
319
339
|
const template = PLUGIN_TEMPLATES[seam]!
|
|
@@ -329,13 +349,14 @@ async function scaffoldPlugin(seam: string, root: string, args: InitArgs): Promi
|
|
|
329
349
|
] as const
|
|
330
350
|
if (!args.force) {
|
|
331
351
|
for (const [rel] of files) {
|
|
332
|
-
if (
|
|
352
|
+
if (occupied(path.join(root, rel))) {
|
|
333
353
|
sayNotice('error', 'vx init', `${rel} exists. --force overwrites it`)
|
|
334
354
|
return 1
|
|
335
355
|
}
|
|
336
356
|
}
|
|
337
357
|
}
|
|
338
358
|
if (!args.dry) {
|
|
359
|
+
for (const [rel] of files) refuseSymlink(path.join(root, rel), root)
|
|
339
360
|
await mkdir(path.join(root, 'plugins'), { recursive: true }).catch(
|
|
340
361
|
(err: NodeJS.ErrnoException) => {
|
|
341
362
|
if (err.code !== 'EEXIST' && err.code !== 'ENOTDIR') throw err
|
|
@@ -344,7 +365,7 @@ async function scaffoldPlugin(seam: string, root: string, args: InitArgs): Promi
|
|
|
344
365
|
)
|
|
345
366
|
},
|
|
346
367
|
)
|
|
347
|
-
for (const [rel, text] of files) await
|
|
368
|
+
for (const [rel, text] of files) await writeNoFollow(path.join(root, rel), text, root)
|
|
348
369
|
}
|
|
349
370
|
const factory = /^export function (\w+)/m.exec(template.plugin)![1]!
|
|
350
371
|
const workspaceFile =
|
|
@@ -423,7 +444,7 @@ async function starter(
|
|
|
423
444
|
): Promise<number> {
|
|
424
445
|
if (args.mode !== undefined) {
|
|
425
446
|
throw new UserError(
|
|
426
|
-
`vx init: --${args.mode} answers a Turbo or Nx
|
|
447
|
+
`vx init: --${args.mode} answers a Turbo or Nx workspace's question, and there is no turbo.json or nx.json here`,
|
|
427
448
|
'VX_E_USAGE',
|
|
428
449
|
)
|
|
429
450
|
}
|
|
@@ -450,11 +471,20 @@ async function starter(
|
|
|
450
471
|
],
|
|
451
472
|
[PROJECT_CONFIG_FILENAMES, config, starterProject(ext)],
|
|
452
473
|
] as const) {
|
|
453
|
-
const existing = names.
|
|
454
|
-
|
|
474
|
+
const existing = names.filter((n) => existsSync(path.join(root, n)))
|
|
475
|
+
// Kept, a directory read as the workspace's marker, and every verb after
|
|
476
|
+
// then found none and sent the user back here.
|
|
477
|
+
const dir = existing.find((n) => statSync(path.join(root, n)).isDirectory())
|
|
478
|
+
if (dir !== undefined) {
|
|
479
|
+
throw new UserError(
|
|
480
|
+
`vx init: ${dir} is a directory, where the ${name === config ? 'project config' : 'workspace file'} goes; move it and re-run`,
|
|
481
|
+
)
|
|
482
|
+
}
|
|
483
|
+
if (existing.length > 0 && !args.force) kept.push(existing[0]!)
|
|
455
484
|
else {
|
|
456
485
|
files.push({ path: name, contents })
|
|
457
|
-
|
|
486
|
+
// Every other name goes too: a second config beside the one written.
|
|
487
|
+
replaced.push(...existing.filter((n) => n !== name))
|
|
458
488
|
}
|
|
459
489
|
}
|
|
460
490
|
// vx's own housekeeping, as a run's first cache open and `vx lock` write them.
|
|
@@ -465,7 +495,10 @@ async function starter(
|
|
|
465
495
|
if (!existsSync(path.join(root, name))) files.push({ path: name, contents })
|
|
466
496
|
}
|
|
467
497
|
const declare = `declare a task in ${kept.find((f) => f.startsWith('vx.config.')) ?? config}, then vx lock && vx run <task>`
|
|
468
|
-
|
|
498
|
+
// Run in the cwd, a git init below the root it chose hides the workspace from every verb.
|
|
499
|
+
const gitInit =
|
|
500
|
+
root === process.cwd() ? 'git init' : `git init ${path.relative(process.cwd(), root)}`
|
|
501
|
+
const next = `${noGit ? `${gitInit}, ` : ''}${hasScripts(pkgRoot) ? `bunx @vzn/vx-migrate${args.mjs ? ' --mjs' : ''} to make package.json scripts tasks, or ${declare}` : declare}`
|
|
469
502
|
if (args.json === true) {
|
|
470
503
|
ui.data(
|
|
471
504
|
'out',
|
|
@@ -473,7 +506,7 @@ async function starter(
|
|
|
473
506
|
)
|
|
474
507
|
return 0
|
|
475
508
|
}
|
|
476
|
-
const rows: ui.FieldRow[] = [['from', 'no turbo.json or nx.json
|
|
509
|
+
const rows: ui.FieldRow[] = [['from', 'no turbo.json or nx.json']]
|
|
477
510
|
// The files are root-relative: a root above the cwd is named, or they read as written here.
|
|
478
511
|
if (root !== process.cwd()) rows.push(['root', root])
|
|
479
512
|
if (args.dry) rows.push(['dry run', 'nothing written'])
|
|
@@ -486,17 +519,19 @@ async function starter(
|
|
|
486
519
|
ui.print('out', ui.report(`vx init ${VERSION}`, [...rows, null, ['next', next]]))
|
|
487
520
|
return 0
|
|
488
521
|
}
|
|
489
|
-
for (const f of files) refuseSymlink(path.join(root, f.path))
|
|
522
|
+
for (const f of files) refuseSymlink(path.join(root, f.path), root)
|
|
490
523
|
const ran = await install(pkgRoot, false, plugins)
|
|
491
524
|
if (ran.length > 0) rows.push(['installed', ...ran])
|
|
492
525
|
for (const f of files) {
|
|
493
|
-
await writeNoFollow(path.join(root, f.path), f.contents).catch(
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
526
|
+
await writeNoFollow(path.join(root, f.path), f.contents, root).catch(
|
|
527
|
+
(err: NodeJS.ErrnoException) => {
|
|
528
|
+
// writeNoFollow's mkdir of a parent that is a file says EEXIST.
|
|
529
|
+
if (err.code !== 'ENOTDIR' && err.code !== 'EEXIST') throw err
|
|
530
|
+
throw new UserError(
|
|
531
|
+
`vx init: cannot write ${f.path}: ${path.dirname(f.path)} is a file, not a directory; move it and re-run`,
|
|
532
|
+
)
|
|
533
|
+
},
|
|
534
|
+
)
|
|
500
535
|
}
|
|
501
536
|
for (const f of replaced) await unlink(path.join(root, f))
|
|
502
537
|
// The lock that follows loads the workspace file just written.
|