@vzn/vx 0.0.143 → 0.0.145
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 +5 -5
- package/src/cache/git-inputs.ts +24 -0
- package/src/cli/foreign-flags.ts +369 -0
- package/src/cli/index.ts +13 -3
- package/src/cli/run.ts +6 -1
- package/src/cli/task-verb.ts +38 -0
- package/src/orchestrator/execute-task.ts +17 -0
- package/src/orchestrator/run.ts +1 -0
- package/src/orchestrator/sandbox-request.ts +15 -3
- package/src/workspace/migrate-scripts.ts +21 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.145",
|
|
4
4
|
"description": "A fast, correct task runner and build cache for JavaScript monorepos.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
"node": ">=18"
|
|
18
18
|
},
|
|
19
19
|
"optionalDependencies": {
|
|
20
|
-
"@vzn/vx-linux-x64": "0.0.
|
|
21
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
22
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
23
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
20
|
+
"@vzn/vx-linux-x64": "0.0.145",
|
|
21
|
+
"@vzn/vx-linux-arm64": "0.0.145",
|
|
22
|
+
"@vzn/vx-darwin-x64": "0.0.145",
|
|
23
|
+
"@vzn/vx-darwin-arm64": "0.0.145"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/cache/git-inputs.ts
CHANGED
|
@@ -201,6 +201,30 @@ export class GitFilesCache extends Map<string, readonly string[]> {
|
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
/** Tracked files a task's clean removed this run, and whose clean (A-48). */
|
|
205
|
+
private trackedCleans: Array<{ path: string; by: string }> = []
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Note which of the files a task's clean just removed git tracks. Called
|
|
209
|
+
* before `markOutputsChanged`, which drops their OIDs. A committed output
|
|
210
|
+
* another task reads with no edge to its producer is gone for as long as
|
|
211
|
+
* the producer runs (vueuse's `metadata/index.json`, N's dogfood), and the
|
|
212
|
+
* reader failed naming only the missing file (A-48).
|
|
213
|
+
*/
|
|
214
|
+
noteClean(by: string, dir: string, rels: readonly string[]): void {
|
|
215
|
+
const oids = this.oids.get(dir)
|
|
216
|
+
if (oids === undefined) return
|
|
217
|
+
for (const rel of rels) {
|
|
218
|
+
const abs = path.resolve(dir, rel)
|
|
219
|
+
if (oids.has(abs)) this.trackedCleans.push({ path: abs, by })
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** The tracked files other tasks' cleans removed that are still missing. */
|
|
224
|
+
trackedCleansMissing(except: string): Array<{ path: string; by: string }> {
|
|
225
|
+
return this.trackedCleans.filter((c) => c.by !== except && !existsSync(c.path))
|
|
226
|
+
}
|
|
227
|
+
|
|
204
228
|
/** Trusted index OIDs for a project (abs path → oid), if any survive. */
|
|
205
229
|
oidsFor(projectDir: string): ReadonlyMap<string, string> | undefined {
|
|
206
230
|
return this.oids.get(projectDir)
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
// What a Turbo or Nx user's hands type, and what `vx run` does with it.
|
|
2
|
+
// Every flag here has one outcome: vx takes it as it is (`same`), takes it
|
|
3
|
+
// as another spelling (`alias`, rewritten before the parse), or refuses it
|
|
4
|
+
// with the vx way to say it (`refuse`). Nothing is dropped in silence: a
|
|
5
|
+
// flag vx does not know was already a refusal, but one that named no way
|
|
6
|
+
// on left a `turbo run --dry-run` user at a dead end. The table renders
|
|
7
|
+
// cli.md's parity section (`renderForeignFlags`), pinned by a test.
|
|
8
|
+
|
|
9
|
+
export interface ForeignFlag {
|
|
10
|
+
runner: 'turbo' | 'nx'
|
|
11
|
+
/** Every spelling, first the one shown. */
|
|
12
|
+
names: readonly string[]
|
|
13
|
+
/** It takes a value: `--flag <v>` or `--flag=<v>`. */
|
|
14
|
+
value: boolean
|
|
15
|
+
/** When only some values are foreign (`--continue=dependencies-successful`). */
|
|
16
|
+
when?: (value: string | undefined) => boolean
|
|
17
|
+
/** How the table shows a `when` row. */
|
|
18
|
+
shown?: string
|
|
19
|
+
outcome: 'same' | 'alias' | 'refuse'
|
|
20
|
+
/** The vx spelling, or what to use instead. */
|
|
21
|
+
vx: string
|
|
22
|
+
/** For an alias: the vx argv. For a refusal: none. */
|
|
23
|
+
to?: (value: string | undefined) => string[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const list = (v: string | undefined): string[] =>
|
|
27
|
+
(v ?? '')
|
|
28
|
+
.split(',')
|
|
29
|
+
.map((s) => s.trim())
|
|
30
|
+
.filter((s) => s !== '')
|
|
31
|
+
|
|
32
|
+
const GRAPH_IMAGE = /\.(svg|png|jpe?g|pdf|json|html|mermaid)$/i
|
|
33
|
+
|
|
34
|
+
export const FOREIGN_FLAGS: readonly ForeignFlag[] = [
|
|
35
|
+
{
|
|
36
|
+
runner: 'turbo',
|
|
37
|
+
names: ['--filter'],
|
|
38
|
+
value: true,
|
|
39
|
+
outcome: 'same',
|
|
40
|
+
vx: '`--filter`, the same grammar (`...[ref]`, `{dir}`, `^`, `!`)',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
runner: 'turbo',
|
|
44
|
+
names: ['--concurrency'],
|
|
45
|
+
value: true,
|
|
46
|
+
outcome: 'same',
|
|
47
|
+
vx: '`--concurrency <n|n%>`',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
runner: 'turbo',
|
|
51
|
+
names: ['--continue'],
|
|
52
|
+
value: true,
|
|
53
|
+
when: (v) => v === 'dependencies-successful',
|
|
54
|
+
shown: '`--continue=dependencies-successful`',
|
|
55
|
+
outcome: 'alias',
|
|
56
|
+
vx: '`--continue=deps-ok`',
|
|
57
|
+
to: () => ['--continue=deps-ok'],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
runner: 'turbo',
|
|
61
|
+
names: ['--continue'],
|
|
62
|
+
value: false,
|
|
63
|
+
outcome: 'same',
|
|
64
|
+
vx: '`--continue[=never|deps-ok|always]`',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
runner: 'turbo',
|
|
68
|
+
names: ['--dry-run'],
|
|
69
|
+
value: false,
|
|
70
|
+
outcome: 'alias',
|
|
71
|
+
vx: '`--dry[=text|json]`',
|
|
72
|
+
to: (v) => [v === undefined ? '--dry' : `--dry=${v}`],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
runner: 'turbo',
|
|
76
|
+
names: ['--graph'],
|
|
77
|
+
value: true,
|
|
78
|
+
when: (v) => v !== undefined && GRAPH_IMAGE.test(v),
|
|
79
|
+
shown: '`--graph=<file>.svg|png|json|html|…`',
|
|
80
|
+
outcome: 'refuse',
|
|
81
|
+
vx: 'vx writes Graphviz DOT only: `--graph=<file>.dot`, then `dot -Tsvg`',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
runner: 'turbo',
|
|
85
|
+
names: ['--graph'],
|
|
86
|
+
value: false,
|
|
87
|
+
outcome: 'same',
|
|
88
|
+
vx: '`--graph[=<file>.dot]`',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
runner: 'turbo',
|
|
92
|
+
names: ['--force'],
|
|
93
|
+
value: false,
|
|
94
|
+
outcome: 'same',
|
|
95
|
+
vx: '`--force`: skip cache reads, keep writes',
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
runner: 'turbo',
|
|
99
|
+
names: ['--affected'],
|
|
100
|
+
value: false,
|
|
101
|
+
outcome: 'same',
|
|
102
|
+
vx: '`--affected[=<base>]`',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
runner: 'turbo',
|
|
106
|
+
names: ['--summarize'],
|
|
107
|
+
value: false,
|
|
108
|
+
outcome: 'same',
|
|
109
|
+
vx: '`--summarize[=<path>]`',
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
runner: 'turbo',
|
|
113
|
+
names: ['--output-logs'],
|
|
114
|
+
value: true,
|
|
115
|
+
when: (v) => v === 'new-only',
|
|
116
|
+
shown: '`--output-logs=new-only`',
|
|
117
|
+
outcome: 'refuse',
|
|
118
|
+
vx: 'use `--output-logs=full` (a hit replays its log) or `errors-only`',
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
runner: 'turbo',
|
|
122
|
+
names: ['--output-logs'],
|
|
123
|
+
value: true,
|
|
124
|
+
outcome: 'same',
|
|
125
|
+
vx: '`--output-logs full|errors-only|hash-only|none`',
|
|
126
|
+
},
|
|
127
|
+
{ runner: 'turbo', names: ['--no-cache'], value: false, outcome: 'same', vx: '`--no-cache`' },
|
|
128
|
+
{
|
|
129
|
+
runner: 'turbo',
|
|
130
|
+
names: ['--cache'],
|
|
131
|
+
value: true,
|
|
132
|
+
outcome: 'same',
|
|
133
|
+
vx: '`--cache local:rw,remote:r`',
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
runner: 'turbo',
|
|
137
|
+
names: ['--cache-dir'],
|
|
138
|
+
value: true,
|
|
139
|
+
outcome: 'same',
|
|
140
|
+
vx: '`--cache-dir <path>`',
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
runner: 'turbo',
|
|
144
|
+
names: ['--profile'],
|
|
145
|
+
value: false,
|
|
146
|
+
outcome: 'same',
|
|
147
|
+
vx: '`--profile[=<path>]` (Chrome trace)',
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
runner: 'turbo',
|
|
151
|
+
names: ['--only'],
|
|
152
|
+
value: false,
|
|
153
|
+
outcome: 'alias',
|
|
154
|
+
vx: '`--exclude-dependencies`',
|
|
155
|
+
to: () => ['--exclude-dependencies'],
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
runner: 'nx',
|
|
159
|
+
names: ['--parallel'],
|
|
160
|
+
value: true,
|
|
161
|
+
when: (v) => v !== undefined && v !== 'true',
|
|
162
|
+
shown: '`--parallel <n>`',
|
|
163
|
+
outcome: 'alias',
|
|
164
|
+
vx: '`--concurrency <n>` (`--parallel=false` is 1)',
|
|
165
|
+
to: (v) => ['--concurrency', v === 'false' ? '1' : (v ?? '')],
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
runner: 'turbo',
|
|
169
|
+
names: ['--parallel'],
|
|
170
|
+
value: false,
|
|
171
|
+
when: (v) => v === undefined || v === 'true',
|
|
172
|
+
shown: '`--parallel`',
|
|
173
|
+
outcome: 'refuse',
|
|
174
|
+
vx: 'vx always honours `dependsOn`; `--concurrency <n>` sets how many run at once',
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
runner: 'turbo',
|
|
178
|
+
names: ['--scope'],
|
|
179
|
+
value: true,
|
|
180
|
+
outcome: 'refuse',
|
|
181
|
+
vx: 'use `--filter <pkg>`',
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
runner: 'turbo',
|
|
185
|
+
names: ['--since'],
|
|
186
|
+
value: true,
|
|
187
|
+
outcome: 'refuse',
|
|
188
|
+
vx: "use `--filter '[<ref>]'` or `--affected=<ref>`",
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
runner: 'turbo',
|
|
192
|
+
names: ['--remote-only'],
|
|
193
|
+
value: false,
|
|
194
|
+
outcome: 'refuse',
|
|
195
|
+
vx: 'use `--cache local:,remote:rw`',
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
runner: 'turbo',
|
|
199
|
+
names: ['--remote-cache-read-only'],
|
|
200
|
+
value: false,
|
|
201
|
+
outcome: 'refuse',
|
|
202
|
+
vx: 'use `--cache local:rw,remote:r`',
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
runner: 'turbo',
|
|
206
|
+
names: ['--token', '--team', '--api'],
|
|
207
|
+
value: true,
|
|
208
|
+
outcome: 'refuse',
|
|
209
|
+
vx: 'a remote cache is a plugin: `turboCache()` from @vzn/vx-migrate in vx.workspace.ts reads TURBO_TOKEN / TURBO_TEAM / TURBO_API',
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
runner: 'turbo',
|
|
213
|
+
names: ['--no-daemon', '--daemon'],
|
|
214
|
+
value: false,
|
|
215
|
+
outcome: 'refuse',
|
|
216
|
+
vx: 'vx has no daemon: drop it',
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
runner: 'turbo',
|
|
220
|
+
names: ['--ui', '--log-order', '--log-prefix'],
|
|
221
|
+
value: true,
|
|
222
|
+
outcome: 'refuse',
|
|
223
|
+
vx: 'vx frames each task’s output: `--output-logs <mode>` sets how much',
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
runner: 'nx',
|
|
227
|
+
names: ['-t', '--targets', '--target'],
|
|
228
|
+
value: true,
|
|
229
|
+
outcome: 'alias',
|
|
230
|
+
vx: 'the task names, positional: `vx run build test`',
|
|
231
|
+
to: (v) => list(v),
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
runner: 'nx',
|
|
235
|
+
names: ['-p', '--projects'],
|
|
236
|
+
value: true,
|
|
237
|
+
outcome: 'alias',
|
|
238
|
+
vx: '`--filter <pattern>`, one per project',
|
|
239
|
+
to: (v) => list(v).flatMap((p) => ['--filter', p]),
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
runner: 'nx',
|
|
243
|
+
names: ['--exclude'],
|
|
244
|
+
value: true,
|
|
245
|
+
outcome: 'alias',
|
|
246
|
+
vx: "`--filter '!<pattern>'`, one per project",
|
|
247
|
+
to: (v) => list(v).flatMap((p) => ['--filter', `!${p}`]),
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
runner: 'nx',
|
|
251
|
+
names: ['--base'],
|
|
252
|
+
value: true,
|
|
253
|
+
outcome: 'alias',
|
|
254
|
+
vx: '`--affected=<ref>`',
|
|
255
|
+
to: (v) => [`--affected=${v ?? ''}`],
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
runner: 'nx',
|
|
259
|
+
names: ['--head'],
|
|
260
|
+
value: true,
|
|
261
|
+
when: (v) => v === 'HEAD',
|
|
262
|
+
shown: '`--head HEAD`',
|
|
263
|
+
outcome: 'alias',
|
|
264
|
+
vx: 'nothing: vx compares `--affected=<base>` with the working tree',
|
|
265
|
+
to: () => [],
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
runner: 'nx',
|
|
269
|
+
names: ['--head'],
|
|
270
|
+
value: true,
|
|
271
|
+
outcome: 'refuse',
|
|
272
|
+
vx: 'vx compares `--affected=<base>` with the working tree: check out the head first',
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
runner: 'nx',
|
|
276
|
+
names: ['--skip-nx-cache'],
|
|
277
|
+
value: false,
|
|
278
|
+
outcome: 'alias',
|
|
279
|
+
vx: '`--force`',
|
|
280
|
+
to: () => ['--force'],
|
|
281
|
+
},
|
|
282
|
+
{ runner: 'nx', names: ['--all'], value: false, outcome: 'same', vx: '`--all`' },
|
|
283
|
+
{
|
|
284
|
+
runner: 'nx',
|
|
285
|
+
names: ['--nx-bail'],
|
|
286
|
+
value: false,
|
|
287
|
+
outcome: 'alias',
|
|
288
|
+
vx: '`--continue=never`',
|
|
289
|
+
to: () => ['--continue=never'],
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
runner: 'nx',
|
|
293
|
+
names: ['-c', '--configuration'],
|
|
294
|
+
value: true,
|
|
295
|
+
outcome: 'refuse',
|
|
296
|
+
vx: 'a configuration is its own task: `vx run <target>:<configuration>`',
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
runner: 'nx',
|
|
300
|
+
names: ['--output-style'],
|
|
301
|
+
value: true,
|
|
302
|
+
outcome: 'refuse',
|
|
303
|
+
vx: 'use `--output-logs <mode>`',
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
runner: 'nx',
|
|
307
|
+
names: ['--uncommitted', '--untracked'],
|
|
308
|
+
value: false,
|
|
309
|
+
outcome: 'refuse',
|
|
310
|
+
vx: 'use `--affected=HEAD` (the working tree against the last commit)',
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
runner: 'nx',
|
|
314
|
+
names: ['--no-cloud'],
|
|
315
|
+
value: false,
|
|
316
|
+
outcome: 'refuse',
|
|
317
|
+
vx: 'vx has no cloud: drop it',
|
|
318
|
+
},
|
|
319
|
+
]
|
|
320
|
+
|
|
321
|
+
/** Nx's verbs, as its users type them: `vx run` takes their flags. */
|
|
322
|
+
export const FOREIGN_VERBS: Readonly<Record<string, string>> = {
|
|
323
|
+
'run-many':
|
|
324
|
+
'`nx run-many` is `vx run <task> --all` here; -t, -p, --exclude and --parallel work as they are',
|
|
325
|
+
affected:
|
|
326
|
+
'`nx affected` is `vx run <task> --affected` here; -t, --base and --exclude work as they are',
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Rewrite each aliased foreign flag into its vx argv, or refuse the first
|
|
331
|
+
* refused one. A `same` flag and anything unknown pass through to the
|
|
332
|
+
* parser. Arguments after `--` are the task's and are never read.
|
|
333
|
+
*/
|
|
334
|
+
export function translateForeign(args: readonly string[]): string[] | { error: string } {
|
|
335
|
+
const out: string[] = []
|
|
336
|
+
const sep = args.indexOf('--')
|
|
337
|
+
const before = sep === -1 ? args : args.slice(0, sep)
|
|
338
|
+
for (let i = 0; i < before.length; i++) {
|
|
339
|
+
const a = before[i]!
|
|
340
|
+
const eq = a.indexOf('=')
|
|
341
|
+
const name = eq === -1 ? a : a.slice(0, eq)
|
|
342
|
+
const inline = eq === -1 ? undefined : a.slice(eq + 1)
|
|
343
|
+
const entries = FOREIGN_FLAGS.filter((f) => f.names.includes(name) || f.names.includes(a))
|
|
344
|
+
const next = before[i + 1]
|
|
345
|
+
const spaced = inline === undefined && next !== undefined && !next.startsWith('-')
|
|
346
|
+
const hit = entries.find((f) => {
|
|
347
|
+
const v = f.value ? (inline ?? (spaced ? next : undefined)) : inline
|
|
348
|
+
return f.when === undefined || f.when(v)
|
|
349
|
+
})
|
|
350
|
+
if (hit === undefined || hit.outcome === 'same') {
|
|
351
|
+
out.push(a)
|
|
352
|
+
continue
|
|
353
|
+
}
|
|
354
|
+
const v = hit.value ? (inline ?? (spaced ? next : undefined)) : inline
|
|
355
|
+
if (hit.value && inline === undefined && spaced) i++
|
|
356
|
+
if (hit.outcome === 'refuse') return { error: `${name} (${hit.runner}): ${hit.vx}` }
|
|
357
|
+
out.push(...hit.to!(v))
|
|
358
|
+
}
|
|
359
|
+
return sep === -1 ? out : [...out, ...args.slice(sep)]
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** cli.md's parity table, one row per entry. */
|
|
363
|
+
export function renderForeignFlags(): string {
|
|
364
|
+
const rows = FOREIGN_FLAGS.map((f) => {
|
|
365
|
+
const flag = f.shown ?? f.names.map((n) => `\`${n}${f.value ? ' <v>' : ''}\``).join(', ')
|
|
366
|
+
return `| ${f.runner} | ${flag.replaceAll('|', '\\|')} | ${f.outcome} | ${f.vx.replaceAll('|', '\\|')} |`
|
|
367
|
+
})
|
|
368
|
+
return ['| runner | flag | outcome | in vx |', '| --- | --- | --- | --- |', ...rows].join('\n')
|
|
369
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { VERSION } from '../version.js'
|
|
|
6
6
|
import { runCmd } from './run.js'
|
|
7
7
|
import { CORE_VERBS, printHelp } from './help.js'
|
|
8
8
|
import { pluginCommandHelp, resolvePluginCommand, pluginVerbs } from './plugin-commands.js'
|
|
9
|
+
import { FOREIGN_VERBS } from './foreign-flags.js'
|
|
10
|
+
import { taskVerbHint } from './task-verb.js'
|
|
9
11
|
import { isUserError, MOVED_VERBS, nearest, UserError } from '../util/index.js'
|
|
10
12
|
|
|
11
13
|
// Every verb but `run` is imported when invoked. `vx run` is the hot path
|
|
@@ -45,8 +47,9 @@ export async function run(argv: readonly string[]): Promise<number> {
|
|
|
45
47
|
// for the whole reference: it printed 145 lines and no hint.
|
|
46
48
|
const resolved = await resolvePluginCommand(verb)
|
|
47
49
|
const unknown = resolved === null || 'declaredVerbs' in resolved
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
const pointer = MOVED_VERBS[verb] ?? FOREIGN_VERBS[verb]
|
|
51
|
+
if (unknown && pointer !== undefined) {
|
|
52
|
+
process.stderr.write(`${pointer}\n`)
|
|
50
53
|
return 1
|
|
51
54
|
}
|
|
52
55
|
if (unknown) {
|
|
@@ -127,11 +130,18 @@ export async function run(argv: readonly string[]): Promise<number> {
|
|
|
127
130
|
// A verb core owned once (`migrate`, `prune`) and a package owns now:
|
|
128
131
|
// the pointer, but only after the plugins had their chance — a
|
|
129
132
|
// workspace that declares a plugin verb of that name keeps it.
|
|
130
|
-
const moved = MOVED_VERBS[command]
|
|
133
|
+
const moved = MOVED_VERBS[command] ?? FOREIGN_VERBS[command]
|
|
131
134
|
if (moved !== undefined) {
|
|
132
135
|
process.stderr.write(`${moved}${loadNote}\n`)
|
|
133
136
|
return 1
|
|
134
137
|
}
|
|
138
|
+
// A task typed where the verb goes (`turbo build`, `nx build app`),
|
|
139
|
+
// `turbo dev` included: a repo's own `dev` task beats the no-service note.
|
|
140
|
+
const task = loadNote === '' ? await taskVerbHint(command, rest, process.cwd()) : null
|
|
141
|
+
if (task !== null) {
|
|
142
|
+
process.stderr.write(`vx: ${task}\n`)
|
|
143
|
+
return 1
|
|
144
|
+
}
|
|
135
145
|
if (command === 'serve' || command === 'dev') {
|
|
136
146
|
// vx core is only a task runner — it has no service layer of its
|
|
137
147
|
// own. A dashboard, remote cache, distributed execution, etc. are
|
package/src/cli/run.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { appendFileSync, mkdirSync } from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
+
import { translateForeign } from './foreign-flags.js'
|
|
3
4
|
import { flagHint, seeHelp } from './help.js'
|
|
4
5
|
import { defaultAffectedBase, findWorkspaceRoot } from '../workspace/index.js'
|
|
5
6
|
import {
|
|
@@ -109,7 +110,7 @@ export function parseConcurrency(v: string, cpus = machineParallelism()): number
|
|
|
109
110
|
return n === null || n < 1 ? null : n
|
|
110
111
|
}
|
|
111
112
|
|
|
112
|
-
export function parseRunArgs(
|
|
113
|
+
export function parseRunArgs(rawArgs: readonly string[]): RunArgs {
|
|
113
114
|
const out: RunArgs = {
|
|
114
115
|
tasks: [],
|
|
115
116
|
filters: [],
|
|
@@ -133,6 +134,10 @@ export function parseRunArgs(args: readonly string[]): RunArgs {
|
|
|
133
134
|
reportFile: undefined,
|
|
134
135
|
}
|
|
135
136
|
|
|
137
|
+
// A Turbo or Nx spelling becomes vx's, or a refusal naming vx's.
|
|
138
|
+
const translated = translateForeign(rawArgs)
|
|
139
|
+
if ('error' in translated) return { ...out, error: translated.error }
|
|
140
|
+
const args = translated
|
|
136
141
|
const sepIdx = args.indexOf('--')
|
|
137
142
|
const before = sepIdx === -1 ? args : args.slice(0, sepIdx)
|
|
138
143
|
out.forwardArgs = sepIdx === -1 ? [] : args.slice(sepIdx + 1)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// `turbo build` runs the build; `nx build app` builds app. A Turbo or Nx
|
|
2
|
+
// user types the task where vx wants a verb, and `vx build` answered
|
|
3
|
+
// "unknown command" with no way on. The answer names the `vx run` that
|
|
4
|
+
// does it. It stays a refusal: a verb that runs whatever task shares its
|
|
5
|
+
// name would change meaning the day a plugin declares that verb.
|
|
6
|
+
|
|
7
|
+
import { findWorkspaceRoot, listProjects, loadWorkspace } from '../workspace/index.js'
|
|
8
|
+
import { findCwdProject } from './select.js'
|
|
9
|
+
import { loadCliProjects } from './workspace-config.js'
|
|
10
|
+
|
|
11
|
+
/** The `vx run` a task typed as a verb means, or null when no project declares it. */
|
|
12
|
+
export async function taskVerbHint(
|
|
13
|
+
command: string,
|
|
14
|
+
rest: readonly string[],
|
|
15
|
+
cwd: string,
|
|
16
|
+
): Promise<string | null> {
|
|
17
|
+
if (command.includes('#')) return `\`${command}\` is a task: vx run ${command}`
|
|
18
|
+
let projects: Awaited<ReturnType<typeof loadCliProjects>>
|
|
19
|
+
try {
|
|
20
|
+
const root = await findWorkspaceRoot(cwd)
|
|
21
|
+
projects = await loadCliProjects(root, await listProjects(await loadWorkspace(root)), 'all', {
|
|
22
|
+
noCreate: true,
|
|
23
|
+
})
|
|
24
|
+
} catch {
|
|
25
|
+
// No workspace, or a config that does not load: the plain unknown-command line stands.
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
if (![...projects.values()].some((p) => p.config.tasks?.[command] !== undefined)) return null
|
|
29
|
+
// Nx's `nx build app`: the word after the target is its project.
|
|
30
|
+
const project = rest[0] !== undefined && projects.has(rest[0]) ? rest[0] : undefined
|
|
31
|
+
const run =
|
|
32
|
+
project !== undefined
|
|
33
|
+
? `vx run ${command} --filter ${project}`
|
|
34
|
+
: (await findCwdProject(cwd)) === null
|
|
35
|
+
? `vx run ${command} --all`
|
|
36
|
+
: `vx run ${command}`
|
|
37
|
+
return `\`${command}\` is a task here, not a command: ${run}`
|
|
38
|
+
}
|
|
@@ -106,6 +106,8 @@ export interface ExecuteArgs {
|
|
|
106
106
|
* where a locally-placed consumer fetches its producers from. */
|
|
107
107
|
deferred?: DeferredOutputs
|
|
108
108
|
nestedProjectDirs: string[]
|
|
109
|
+
/** The run's cache directory: walled off from a sandboxed task's grants. */
|
|
110
|
+
cacheDir?: string
|
|
109
111
|
/** Anchor for hrtime spans across all tasks in this run. */
|
|
110
112
|
runStartHrTimeNs: bigint
|
|
111
113
|
/**
|
|
@@ -312,6 +314,7 @@ async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
312
314
|
args.workspaceRoot,
|
|
313
315
|
undefined,
|
|
314
316
|
args.nestedProjectDirs,
|
|
317
|
+
args.cacheDir,
|
|
315
318
|
)
|
|
316
319
|
placeholders = sb.placeholders
|
|
317
320
|
const wrapped = await wrapSandboxedCommand({
|
|
@@ -757,6 +760,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
757
760
|
const endClean = span('miss: clean outputs')
|
|
758
761
|
const cleanedRels = await cleanOutputs(cleanArgs)
|
|
759
762
|
endClean()
|
|
763
|
+
args.gitFilesCache?.noteClean(node.id, node.projectDir, cleanedRels)
|
|
760
764
|
args.gitFilesCache?.markOutputsChanged(node.projectDir, cleanedRels)
|
|
761
765
|
}
|
|
762
766
|
if (additive && willWrite && !deferralRequested && wsOutputs.length > 0) {
|
|
@@ -872,6 +876,18 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
872
876
|
signal: res.signal,
|
|
873
877
|
})
|
|
874
878
|
if (verdict !== undefined) log.taskStderr(node, `\n${verdict}\n`)
|
|
879
|
+
// A committed file another task's clean removed, still gone: vx
|
|
880
|
+
// cleans outputs before a run where Turbo does not, so a reader with
|
|
881
|
+
// no edge to the producer fails naming only the file (A-48).
|
|
882
|
+
if (code !== 0) {
|
|
883
|
+
for (const c of (args.gitFilesCache?.trackedCleansMissing(node.id) ?? []).slice(0, 3)) {
|
|
884
|
+
const rel = path.relative(args.workspaceRoot, c.path).split(path.sep).join('/')
|
|
885
|
+
log.taskStderr(
|
|
886
|
+
node,
|
|
887
|
+
`\n[vx] ${rel} is tracked by git, and ${c.by} removed it as an output before its run; it is still missing. A task that reads it needs dependsOn on ${c.by}.\n`,
|
|
888
|
+
)
|
|
889
|
+
}
|
|
890
|
+
}
|
|
875
891
|
}
|
|
876
892
|
return { result: res, exitCode: code }
|
|
877
893
|
}
|
|
@@ -1002,6 +1018,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1002
1018
|
args.workspaceRoot,
|
|
1003
1019
|
cfgCacheable ? args.keyedProjects(node) : undefined,
|
|
1004
1020
|
args.nestedProjectDirs,
|
|
1021
|
+
args.cacheDir,
|
|
1005
1022
|
)
|
|
1006
1023
|
placeholders = sb.placeholders
|
|
1007
1024
|
withheld = sb.withheld
|
package/src/orchestrator/run.ts
CHANGED
|
@@ -755,6 +755,7 @@ async function runOnBus(
|
|
|
755
755
|
...(placements.remoteOnlyNoop.has(node.id) ? { remoteOnlyNoop: true } : {}),
|
|
756
756
|
...(placements.remoteOnly.has(node.id) ? { remoteOnly: true } : {}),
|
|
757
757
|
nestedProjectDirs: nestedDirsByProject.get(node.projectName) ?? [],
|
|
758
|
+
cacheDir,
|
|
758
759
|
runStartHrTimeNs,
|
|
759
760
|
persistentRegistry,
|
|
760
761
|
liveChildren,
|
|
@@ -174,6 +174,12 @@ export async function sandboxRequestFor(
|
|
|
174
174
|
keyed: ReadonlySet<string> | undefined,
|
|
175
175
|
/** The projects nested in this one (their dirs): a wall its grants do not reach. */
|
|
176
176
|
nested: readonly string[] = [],
|
|
177
|
+
/**
|
|
178
|
+
* The run's cache directory. `.vx` is walled wherever it is; a
|
|
179
|
+
* `cacheDir` configured inside a project was not, and a task whose write
|
|
180
|
+
* grant covered it could plant artifacts and index rows (L-24).
|
|
181
|
+
*/
|
|
182
|
+
cacheDir?: string,
|
|
177
183
|
): Promise<SandboxRequest> {
|
|
178
184
|
const depDirs = [
|
|
179
185
|
path.join(node.projectDir, 'node_modules'),
|
|
@@ -192,9 +198,15 @@ export async function sandboxRequestFor(
|
|
|
192
198
|
// Pre-create what the task said it will write — after the grants are
|
|
193
199
|
// resolved, which refuses one that leaves the project through a link
|
|
194
200
|
// before anything is created along it (item 1003).
|
|
195
|
-
const walls = [
|
|
196
|
-
|
|
197
|
-
|
|
201
|
+
const walls = [
|
|
202
|
+
...nested,
|
|
203
|
+
path.join(workspaceRoot, '.git'),
|
|
204
|
+
path.join(workspaceRoot, '.vx'),
|
|
205
|
+
...(cacheDir !== undefined &&
|
|
206
|
+
toRealPath(cacheDir).startsWith(toRealPath(workspaceRoot) + path.sep)
|
|
207
|
+
? [cacheDir]
|
|
208
|
+
: []),
|
|
209
|
+
].map(toRealPath)
|
|
198
210
|
const config = wallOff(
|
|
199
211
|
resolveSandboxConfig(sandbox, node.projectDir, walls),
|
|
200
212
|
workspaceRoot,
|
|
@@ -243,7 +243,20 @@ function upstreamBuildOnWorker(tasks: GeneratedTask[]): void {
|
|
|
243
243
|
export function migrateScripts(metas: readonly ProjectMeta[]): MigrationPlan {
|
|
244
244
|
const projects: GeneratedProject[] = []
|
|
245
245
|
const hookMemo = new Map<string, boolean>()
|
|
246
|
+
// The workspace root among members: its scripts run the workspace
|
|
247
|
+
// (`npm run build --workspaces`, `pnpm -r build`), and a root task made
|
|
248
|
+
// of one ran every member's build again under `--all`. Since D-39 a
|
|
249
|
+
// hand-written root config makes the root a project, and `--force`
|
|
250
|
+
// replaced it so (D-45). A lone package is its repo's project and maps.
|
|
251
|
+
const root = metas.length > 1 ? workspaceRootOf(metas) : undefined
|
|
252
|
+
const notes: string[] = []
|
|
253
|
+
if (root !== undefined) {
|
|
254
|
+
notes.push(
|
|
255
|
+
`${root.name} (the workspace root) not mapped: its scripts run the workspace; declare its own tasks in its vx.config by hand`,
|
|
256
|
+
)
|
|
257
|
+
}
|
|
246
258
|
for (const meta of metas) {
|
|
259
|
+
if (meta === root) continue
|
|
247
260
|
const scripts = scriptsOf(meta)
|
|
248
261
|
const runsHooks = runsScriptHooks(meta.dir, hookMemo)
|
|
249
262
|
const runnable = Object.keys(scripts).filter(
|
|
@@ -350,6 +363,13 @@ export function migrateScripts(metas: readonly ProjectMeta[]): MigrationPlan {
|
|
|
350
363
|
],
|
|
351
364
|
projects,
|
|
352
365
|
extraFiles: [],
|
|
353
|
-
notes
|
|
366
|
+
notes,
|
|
354
367
|
}
|
|
355
368
|
}
|
|
369
|
+
|
|
370
|
+
/** The member whose directory holds every other member's, if one does. */
|
|
371
|
+
function workspaceRootOf(metas: readonly ProjectMeta[]): ProjectMeta | undefined {
|
|
372
|
+
const outer = metas.reduce((a, b) => (b.dir.length < a.dir.length ? b : a))
|
|
373
|
+
const prefix = outer.dir.endsWith(path.sep) ? outer.dir : outer.dir + path.sep
|
|
374
|
+
return metas.every((m) => m === outer || m.dir.startsWith(prefix)) ? outer : undefined
|
|
375
|
+
}
|