@vzn/vx 0.0.637 → 0.0.639
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/docs/caching.md +84 -28
- package/docs/cli.md +273 -408
- package/docs/execution.md +19 -44
- package/docs/features.md +13 -15
- package/docs/patterns.md +5 -7
- package/docs/schema.md +68 -61
- package/package.json +5 -5
- package/schemas/last.json +7 -4
- package/schemas/plan.json +2 -2
- package/skills/vx/SKILL.md +19 -6
- package/src/bin.ts +13 -7
- package/src/cache/cache.ts +60 -11
- package/src/cache/git-inputs.ts +6 -0
- package/src/cache/index.ts +10 -2
- package/src/cache/inputs.ts +0 -24
- package/src/cache/layer.ts +32 -12
- package/src/cache/layered-cache.ts +112 -315
- package/src/cache/plugin-cache.ts +240 -0
- package/src/cache/policy.ts +9 -0
- package/src/cache/remote-lane.ts +383 -0
- package/src/cache/run-history.ts +92 -8
- package/src/cache/run-store.ts +77 -0
- package/src/cache/schema.ts +44 -13
- package/src/cache/scoped-cache.ts +22 -2
- package/src/cli/cache.ts +33 -16
- package/src/cli/completions.ts +11 -7
- package/src/cli/foreign-flags.ts +10 -22
- package/src/cli/help.ts +22 -38
- package/src/cli/index.ts +25 -11
- package/src/cli/info.ts +8 -6
- package/src/cli/init.ts +14 -15
- package/src/cli/last.ts +20 -10
- package/src/cli/lock.ts +39 -27
- package/src/cli/plan-format.ts +10 -61
- package/src/cli/plugin-commands.ts +77 -6
- package/src/cli/plugin-templates.ts +1 -1
- package/src/cli/run.ts +31 -191
- package/src/cli/select.ts +18 -9
- package/src/cli/show.ts +72 -40
- package/src/cli/task-verb.ts +4 -4
- package/src/cli/upgrade.ts +132 -39
- package/src/cli/watch-cycle.ts +2 -1
- package/src/cli/watch-filter.ts +7 -2
- package/src/cli/watch-judge.ts +6 -1
- package/src/cli/watch.ts +73 -34
- package/src/cli/why.ts +14 -5
- package/src/cli/workspace-config.ts +2 -2
- package/src/config.ts +0 -6
- package/src/exec/executor.ts +1 -1
- package/src/exec/sandbox-runtime.ts +1 -1
- package/src/exec/sandbox-violations.ts +3 -1
- package/src/graph/index.ts +9 -1
- package/src/graph/priorities.ts +1 -1
- package/src/graph/scheduler.ts +12 -16
- package/src/graph/task-graph.ts +119 -21
- package/src/graph/task-status.ts +95 -0
- package/src/{orchestrator → history}/failure-mode.ts +20 -11
- package/src/{orchestrator → history}/history.ts +61 -5
- package/src/history/index.ts +48 -0
- package/src/{orchestrator → history}/metrics.ts +14 -23
- package/src/{orchestrator → history}/miss-reason.ts +1 -1
- package/src/index.ts +21 -29
- package/src/orchestrator/affected-tasks.ts +8 -3
- package/src/orchestrator/doctor.ts +2 -2
- package/src/orchestrator/execute-args.ts +80 -0
- package/src/orchestrator/execute-task.ts +47 -429
- package/src/orchestrator/fingerprint-watch.ts +2 -2
- package/src/orchestrator/index.ts +3 -74
- package/src/orchestrator/miss-save.ts +33 -3
- package/src/orchestrator/options.ts +1 -36
- package/src/orchestrator/output-host.ts +4 -3
- package/src/orchestrator/persistent-task.ts +321 -0
- package/src/orchestrator/persistent.ts +130 -3
- package/src/orchestrator/placement.ts +3 -3
- package/src/orchestrator/plan.ts +18 -5
- package/src/orchestrator/plugin-host.ts +77 -47
- package/src/orchestrator/plugin.ts +86 -14
- package/src/orchestrator/prepare.ts +74 -31
- package/src/orchestrator/projects.ts +3 -0
- package/src/orchestrator/remote-prefetch.ts +23 -6
- package/src/orchestrator/run-artifacts.ts +5 -177
- package/src/orchestrator/run-context.ts +55 -0
- package/src/orchestrator/run-failures.ts +15 -3
- package/src/orchestrator/run-records.ts +36 -4
- package/src/orchestrator/run-report.ts +81 -179
- package/src/orchestrator/run-settings.ts +1 -16
- package/src/orchestrator/run-stop.ts +153 -0
- package/src/orchestrator/run.ts +313 -555
- package/src/orchestrator/sandbox-fix.ts +0 -16
- package/src/orchestrator/signals.ts +14 -6
- package/src/orchestrator/stable-keys.ts +107 -3
- package/src/orchestrator/steps.ts +50 -0
- package/src/orchestrator/task-hash.ts +3 -16
- package/src/orchestrator/task-log-buffer.ts +1 -1
- package/src/orchestrator/task-process.ts +94 -0
- package/src/orchestrator/telemetry-host.ts +1 -1
- package/src/orchestrator/telemetry.ts +6 -83
- package/src/{orchestrator → report}/events.ts +41 -8
- package/src/{orchestrator → report}/forecast.ts +35 -2
- package/src/{orchestrator → report}/framed-output.ts +39 -34
- package/src/report/index.ts +36 -0
- package/src/{orchestrator → report}/logger.ts +166 -21
- package/src/{orchestrator → report}/status-line.ts +84 -7
- package/src/{orchestrator → report}/summary.ts +179 -63
- package/src/report/tally.ts +83 -0
- package/src/settings.ts +13 -23
- package/src/{orchestrator → util}/colors.ts +49 -4
- package/src/util/config-frame.ts +31 -2
- package/src/util/errors.ts +32 -0
- package/src/util/git-ignored.ts +143 -0
- package/src/util/index.ts +25 -3
- package/src/util/refusal-notice.ts +111 -0
- package/src/util/report.ts +39 -0
- package/src/util/settle.ts +14 -39
- package/src/util/tail.ts +1 -1
- package/src/util/ui.ts +197 -0
- package/src/workspace/affected.ts +20 -23
- package/src/workspace/dependency-rules.ts +12 -4
- package/src/workspace/migration.ts +87 -91
- package/src/workspace/workspace.ts +5 -4
- package/schemas/summary.json +0 -133
- package/src/cache/chained-cache.ts +0 -276
- package/src/orchestrator/tally.ts +0 -112
- /package/src/{orchestrator → report}/path-links.ts +0 -0
- /package/src/{orchestrator → report}/plain-output.ts +0 -0
- /package/src/{orchestrator → report}/terminal-signals.ts +0 -0
package/src/cli/select.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
maskedLine,
|
|
33
33
|
nearest,
|
|
34
34
|
UserError,
|
|
35
|
+
sayNotice,
|
|
35
36
|
} from '../util/index.js'
|
|
36
37
|
import {
|
|
37
38
|
claimedAffected,
|
|
@@ -434,8 +435,10 @@ export async function resolveFilters(
|
|
|
434
435
|
// Something matched, so the run proceeds; a pattern that matched nothing
|
|
435
436
|
// alongside it is still worth a line — it is probably a typo.
|
|
436
437
|
for (const f of unmatched)
|
|
437
|
-
|
|
438
|
-
|
|
438
|
+
sayNotice(
|
|
439
|
+
'warn',
|
|
440
|
+
'vx run',
|
|
441
|
+
`filter "${f}" matched no projects${didYouMean([f], projects, tags)}`,
|
|
439
442
|
)
|
|
440
443
|
// Each include is a union: what `--filter other` selected runs its tasks
|
|
441
444
|
// whether or not the `--affected` diff reaches them, and an exclude
|
|
@@ -511,14 +514,18 @@ export async function pickTask(
|
|
|
511
514
|
}
|
|
512
515
|
}
|
|
513
516
|
if (entries.length === 0 && only !== undefined) {
|
|
514
|
-
|
|
515
|
-
|
|
517
|
+
sayNotice(
|
|
518
|
+
'error',
|
|
519
|
+
'vx run',
|
|
520
|
+
`no tasks declared in the selected projects (${[...only].sort().join(', ')})`,
|
|
516
521
|
)
|
|
517
522
|
return null
|
|
518
523
|
}
|
|
519
524
|
if (entries.length === 0) {
|
|
520
|
-
|
|
521
|
-
'
|
|
525
|
+
sayNotice(
|
|
526
|
+
'error',
|
|
527
|
+
'vx run',
|
|
528
|
+
'no tasks declared in any project. Declare one under `tasks` in a vx.config, or run `vx init` to write them from package.json scripts, then `vx lock`',
|
|
522
529
|
)
|
|
523
530
|
return null
|
|
524
531
|
}
|
|
@@ -562,13 +569,15 @@ export async function pickTask(
|
|
|
562
569
|
if (!(err instanceof Error) || err.name !== 'AbortError') throw err
|
|
563
570
|
if (interrupted) return 'interrupted'
|
|
564
571
|
out.write('\n')
|
|
565
|
-
|
|
572
|
+
sayNotice('info', 'vx run', 'no task picked')
|
|
566
573
|
return null
|
|
567
574
|
}
|
|
568
575
|
const n = Number(answer)
|
|
569
576
|
if (!Number.isInteger(n) || n < 1 || n > entries.length) {
|
|
570
|
-
|
|
571
|
-
|
|
577
|
+
sayNotice(
|
|
578
|
+
'error',
|
|
579
|
+
'vx run',
|
|
580
|
+
`invalid selection: ${answer} (type a number from 1 to ${entries.length})`,
|
|
572
581
|
)
|
|
573
582
|
return null
|
|
574
583
|
}
|
package/src/cli/show.ts
CHANGED
|
@@ -11,7 +11,17 @@ import type { ProjectConfig, TaskConfig } from '../config.js'
|
|
|
11
11
|
import { declaredTask } from '../graph/index.js'
|
|
12
12
|
import { affectedTaskProjects, isDefaultBuild } from '../orchestrator/index.js'
|
|
13
13
|
import { flagHint, formatValue, refuse, seeHelp } from './help.js'
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
type ColorSupport,
|
|
16
|
+
detectColors,
|
|
17
|
+
listed,
|
|
18
|
+
nearMatches,
|
|
19
|
+
relPosix,
|
|
20
|
+
secretMask,
|
|
21
|
+
UserError,
|
|
22
|
+
sayNotice,
|
|
23
|
+
ui,
|
|
24
|
+
} from '../util/index.js'
|
|
15
25
|
import { affectedFilterFor, resolveFilters } from './select.js'
|
|
16
26
|
import {
|
|
17
27
|
discoverCliProjects,
|
|
@@ -149,7 +159,7 @@ export async function showCmd(args: readonly string[]): Promise<number> {
|
|
|
149
159
|
}
|
|
150
160
|
const r = await resolveFilters(process.cwd(), raw, {}, affected)
|
|
151
161
|
if ('error' in r) throw new UserError(`vx show: ${r.error}`, r.code ?? 'VX_E_USAGE')
|
|
152
|
-
if ('empty' in r)
|
|
162
|
+
if ('empty' in r) sayNotice('info', 'vx show', r.empty)
|
|
153
163
|
selected = new Set('empty' in r ? [] : r.names)
|
|
154
164
|
if ('names' in r && r.affected !== undefined)
|
|
155
165
|
byDiff = { changes: r.affected, outright: r.outright }
|
|
@@ -159,8 +169,14 @@ export async function showCmd(args: readonly string[]): Promise<number> {
|
|
|
159
169
|
const projects = await loadCliProjects(root, metas, scope, { closure: true })
|
|
160
170
|
|
|
161
171
|
if (parsed.target === undefined) {
|
|
172
|
+
if (metas.length === 0)
|
|
173
|
+
sayNotice(
|
|
174
|
+
'info',
|
|
175
|
+
'vx show',
|
|
176
|
+
'no projects: no directory holds a vx.config — run `vx init` to write them, then `vx lock`',
|
|
177
|
+
)
|
|
162
178
|
const shown = metas.filter((m) => inSelection(m.name))
|
|
163
|
-
process.stdout.write(renderList(root, shown, projects, parsed.format))
|
|
179
|
+
process.stdout.write(renderList(root, shown, projects, parsed.format, colorsOut()))
|
|
164
180
|
return 0
|
|
165
181
|
}
|
|
166
182
|
|
|
@@ -204,7 +220,7 @@ export async function showCmd(args: readonly string[]): Promise<number> {
|
|
|
204
220
|
)
|
|
205
221
|
shown = shown.filter((p) => kept.has(p.name))
|
|
206
222
|
}
|
|
207
|
-
process.stdout.write(renderTaskAcross(root, shown, projectName!, parsed.format))
|
|
223
|
+
process.stdout.write(renderTaskAcross(root, shown, projectName!, parsed.format, colorsOut()))
|
|
208
224
|
return 0
|
|
209
225
|
}
|
|
210
226
|
|
|
@@ -215,7 +231,7 @@ export async function showCmd(args: readonly string[]): Promise<number> {
|
|
|
215
231
|
|
|
216
232
|
if (taskName === undefined) {
|
|
217
233
|
process.stdout.write(
|
|
218
|
-
renderProject(meta.name, dir, config, meta.configPath !== null, parsed.format),
|
|
234
|
+
renderProject(meta.name, dir, config, meta.configPath !== null, parsed.format, colorsOut()),
|
|
219
235
|
)
|
|
220
236
|
return 0
|
|
221
237
|
}
|
|
@@ -227,7 +243,7 @@ export async function showCmd(args: readonly string[]): Promise<number> {
|
|
|
227
243
|
'VX_E_UNKNOWN_TASK',
|
|
228
244
|
)
|
|
229
245
|
}
|
|
230
|
-
process.stdout.write(renderTask(meta.name, dir, taskName, task, parsed.format))
|
|
246
|
+
process.stdout.write(renderTask(meta.name, dir, taskName, task, parsed.format, colorsOut()))
|
|
231
247
|
return 0
|
|
232
248
|
}
|
|
233
249
|
|
|
@@ -284,11 +300,20 @@ function shownConfig(config: ProjectConfig | null): ProjectConfig | null {
|
|
|
284
300
|
}
|
|
285
301
|
}
|
|
286
302
|
|
|
303
|
+
const colorsOut = (): ColorSupport => detectColors(process.stdout)
|
|
304
|
+
const PLAIN: ColorSupport = { enabled: false }
|
|
305
|
+
|
|
306
|
+
/** One block of several, with no blank line between them. */
|
|
307
|
+
const joined = (blocks: readonly ui.Block[]): ui.Block => ({
|
|
308
|
+
lines: (c) => blocks.flatMap((b) => b.lines(c)),
|
|
309
|
+
})
|
|
310
|
+
|
|
287
311
|
function renderList(
|
|
288
312
|
root: string,
|
|
289
313
|
metas: readonly ProjectMeta[],
|
|
290
314
|
projects: ReadonlyMap<string, ProjectEntry>,
|
|
291
315
|
format: 'pretty' | 'json',
|
|
316
|
+
colors: ColorSupport = PLAIN,
|
|
292
317
|
): string {
|
|
293
318
|
const rows = metas.map((meta) => {
|
|
294
319
|
const tasks = projects.get(meta.name)?.config.tasks ?? {}
|
|
@@ -309,9 +334,8 @@ function renderList(
|
|
|
309
334
|
)}\n`
|
|
310
335
|
}
|
|
311
336
|
if (rows.length === 0) return ''
|
|
312
|
-
const
|
|
313
|
-
const
|
|
314
|
-
const lines = rows.map((r) => {
|
|
337
|
+
const anyTags = rows.some((r) => r.tags.length > 0)
|
|
338
|
+
const cells = rows.map((r) => {
|
|
315
339
|
const n = r.tasks.length
|
|
316
340
|
const count = `${n} task${n === 1 ? '' : 's'}`
|
|
317
341
|
// A package with no config file has only tasks a plugin gave it, or the
|
|
@@ -319,14 +343,14 @@ function renderList(
|
|
|
319
343
|
const tasks = r.configured
|
|
320
344
|
? count
|
|
321
345
|
: n === 0
|
|
322
|
-
? '
|
|
346
|
+
? 'no vx config'
|
|
323
347
|
: r.onlyDefault
|
|
324
|
-
? `${count} (no vx config
|
|
325
|
-
: `${count} (no vx config
|
|
326
|
-
|
|
327
|
-
return `${r.name.padEnd(nameW)} ${r.dir.padEnd(dirW)} ${tasks}${tags}`
|
|
348
|
+
? `${count}, default build (no vx config)`
|
|
349
|
+
: `${count} from plugins (no vx config)`
|
|
350
|
+
return [ui.id(r.name), r.dir, tasks, ...(anyTags ? [r.tags.join(', ')] : [])]
|
|
328
351
|
})
|
|
329
|
-
|
|
352
|
+
const columns = ['project', 'dir', 'tasks', ...(anyTags ? ['tags'] : [])]
|
|
353
|
+
return ui.render([ui.table(columns, cells)], colors)
|
|
330
354
|
}
|
|
331
355
|
|
|
332
356
|
function renderProject(
|
|
@@ -335,6 +359,7 @@ function renderProject(
|
|
|
335
359
|
config: ProjectConfig | null,
|
|
336
360
|
hasConfigFile: boolean,
|
|
337
361
|
format: 'pretty' | 'json',
|
|
362
|
+
colors: ColorSupport = PLAIN,
|
|
338
363
|
): string {
|
|
339
364
|
if (format === 'json') {
|
|
340
365
|
// Round-trip so the printed object is exactly the JSON form of the
|
|
@@ -344,14 +369,17 @@ function renderProject(
|
|
|
344
369
|
}
|
|
345
370
|
const tags = config?.tags ?? []
|
|
346
371
|
const tasks = Object.entries(config?.tasks ?? {})
|
|
347
|
-
const head
|
|
372
|
+
const head: ui.FieldRow[] = [
|
|
373
|
+
['dir', dir],
|
|
374
|
+
...(tags.length > 0 ? [['tags', tags.join(', ')] as const] : []),
|
|
348
375
|
// A config-less package a dependant reaches has the default `build` (X-9).
|
|
349
|
-
hasConfigFile
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
376
|
+
...(hasConfigFile ? [] : [['config', 'none'] as const]),
|
|
377
|
+
...(tasks.length === 0 && hasConfigFile ? [['tasks', 'none declared'] as const] : []),
|
|
378
|
+
]
|
|
379
|
+
const blocks = tasks.map(([taskName, task]) =>
|
|
380
|
+
joined([ui.text(ui.id(`${name}#${taskName}`)), ui.fields(taskRows(task))]),
|
|
381
|
+
)
|
|
382
|
+
return ui.render([ui.report(name, head), ...blocks], colors)
|
|
355
383
|
}
|
|
356
384
|
|
|
357
385
|
function renderTask(
|
|
@@ -360,12 +388,13 @@ function renderTask(
|
|
|
360
388
|
taskName: string,
|
|
361
389
|
task: TaskConfig,
|
|
362
390
|
format: 'pretty' | 'json',
|
|
391
|
+
colors: ColorSupport = PLAIN,
|
|
363
392
|
): string {
|
|
364
393
|
if (format === 'json') {
|
|
365
394
|
const obj = { name, dir, task: taskName, config: shownTask(task) }
|
|
366
395
|
return `${JSON.stringify(obj)}\n`
|
|
367
396
|
}
|
|
368
|
-
return
|
|
397
|
+
return ui.render([taskReport(name, dir, taskName, task)], colors)
|
|
369
398
|
}
|
|
370
399
|
|
|
371
400
|
function renderTaskAcross(
|
|
@@ -373,6 +402,7 @@ function renderTaskAcross(
|
|
|
373
402
|
declaring: readonly ProjectEntry[],
|
|
374
403
|
taskName: string,
|
|
375
404
|
format: 'pretty' | 'json',
|
|
405
|
+
colors: ColorSupport,
|
|
376
406
|
): string {
|
|
377
407
|
if (format === 'json') {
|
|
378
408
|
const list = declaring.map((p) => ({
|
|
@@ -383,18 +413,28 @@ function renderTaskAcross(
|
|
|
383
413
|
}))
|
|
384
414
|
return `${JSON.stringify(list)}\n`
|
|
385
415
|
}
|
|
386
|
-
return
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
416
|
+
if (declaring.length === 0) return ''
|
|
417
|
+
return ui.render(
|
|
418
|
+
[
|
|
419
|
+
joined(
|
|
420
|
+
declaring.map((p) =>
|
|
421
|
+
taskReport(p.name, relPosix(root, p.dir) || '.', taskName, p.config.tasks![taskName]!),
|
|
422
|
+
),
|
|
423
|
+
),
|
|
424
|
+
],
|
|
425
|
+
colors,
|
|
426
|
+
)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** `─ app#build ───`, the project's dir, then every field the run reads. */
|
|
430
|
+
function taskReport(name: string, dir: string, taskName: string, task: TaskConfig): ui.Block {
|
|
431
|
+
return ui.report(`${name}#${taskName}`, [['dir', dir], ...taskRows(task)])
|
|
392
432
|
}
|
|
393
433
|
|
|
394
434
|
/** Every field the run reads, in the order the schema declares them. */
|
|
395
|
-
function
|
|
435
|
+
function taskRows(raw: TaskConfig): ui.FieldRow[] {
|
|
396
436
|
const task = shownTask(raw)
|
|
397
|
-
const rows: [
|
|
437
|
+
const rows: ui.FieldRow[] = []
|
|
398
438
|
const list = (xs: readonly string[] | undefined): string | undefined =>
|
|
399
439
|
xs === undefined ? undefined : xs.join(', ')
|
|
400
440
|
const add = (label: string, value: string | number | boolean | undefined): void => {
|
|
@@ -437,13 +477,5 @@ function taskBlock(taskName: string, raw: TaskConfig): string {
|
|
|
437
477
|
add('outputs.files', list(cache.outputs.files))
|
|
438
478
|
add('outputs.workspaceFiles', list(cache.outputs.workspaceFiles))
|
|
439
479
|
}
|
|
440
|
-
|
|
441
|
-
// A multi-line value (a generated `vx_script` wrapper, a heredoc)
|
|
442
|
-
// continues under its first line, not at column 0 where it reads as
|
|
443
|
-
// the next task's header.
|
|
444
|
-
const indent = `\n${' '.repeat(labelW + 4)}`
|
|
445
|
-
const body = rows.map(
|
|
446
|
-
([label, value]) => ` ${`${label}:`.padEnd(labelW + 1)} ${value.replaceAll('\n', indent)}`,
|
|
447
|
-
)
|
|
448
|
-
return `${taskName}\n${body.join('\n')}\n`
|
|
480
|
+
return rows
|
|
449
481
|
}
|
package/src/cli/task-verb.ts
CHANGED
|
@@ -21,7 +21,7 @@ export async function taskVerbHint(
|
|
|
21
21
|
cwd: string,
|
|
22
22
|
typo = false,
|
|
23
23
|
): Promise<string | null> {
|
|
24
|
-
if (command.includes('#')) return `\`${command}\` is a task
|
|
24
|
+
if (command.includes('#')) return `\`${command}\` is a task (did you mean vx run ${command}?)`
|
|
25
25
|
const projects = await workspaceProjects(cwd)
|
|
26
26
|
if (projects === null) return null
|
|
27
27
|
const tasks = new Set<string>()
|
|
@@ -40,8 +40,8 @@ export async function taskVerbHint(
|
|
|
40
40
|
? `vx run ${task} --all`
|
|
41
41
|
: `vx run ${task}`
|
|
42
42
|
return task === command
|
|
43
|
-
? `\`${command}\` is a task here, not a command
|
|
44
|
-
: `unknown command: ${command}
|
|
43
|
+
? `\`${command}\` is a task here, not a command (did you mean ${run}?)`
|
|
44
|
+
: `unknown command: ${command}, a typo of the task \`${task}\` (did you mean ${run}?)`
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/**
|
|
@@ -54,7 +54,7 @@ export async function nxTargetHint(tasks: readonly string[], cwd: string): Promi
|
|
|
54
54
|
if (projects === null) return null
|
|
55
55
|
const specs = tasks.map((t) => nxProjectTarget(t, projects))
|
|
56
56
|
if (specs.some((s) => s === undefined)) return null
|
|
57
|
-
return `\`${tasks.join(' ')}\` is Nx's project:target
|
|
57
|
+
return `\`${tasks.join(' ')}\` is Nx's project:target (did you mean vx run ${specs.join(' ')}?)`
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/**
|
package/src/cli/upgrade.ts
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
// package manager (npm, pnpm, bun, yarn; global or a project's
|
|
11
11
|
// dependency) is upgraded by that package manager, never by a rename over
|
|
12
12
|
// a file it owns. vx never installs Node or a package manager.
|
|
13
|
+
// Handed a call by a global vx, the project's vx moves that global too,
|
|
14
|
+
// to the same version (`upgradeGlobal`).
|
|
13
15
|
// Named `upgrade` (not `update`) per CLI convention: bun upgrade,
|
|
14
16
|
// deno upgrade — "update" is what package managers do to indexes.
|
|
15
17
|
|
|
@@ -18,7 +20,7 @@ import { createPublicKey, verify } from 'node:crypto'
|
|
|
18
20
|
import { access, chmod, chown, link, mkdtemp, rename, rm, stat } from 'node:fs/promises'
|
|
19
21
|
import path from 'node:path'
|
|
20
22
|
import { flagHint, seeHelp } from './help.js'
|
|
21
|
-
import { UserError } from '../util/index.js'
|
|
23
|
+
import { UserError, sayNotice, ui } from '../util/index.js'
|
|
22
24
|
import { VERSION } from '../version.js'
|
|
23
25
|
|
|
24
26
|
const REGISTRY = 'https://registry.npmjs.org'
|
|
@@ -290,17 +292,19 @@ export function binaryFromTarball(tgz: Uint8Array): Uint8Array {
|
|
|
290
292
|
/**
|
|
291
293
|
* The binary of `pkg@spec`, verified: the registry document's signature,
|
|
292
294
|
* then the tarball's integrity, then the extraction, each refusing before
|
|
293
|
-
* the next step runs. Null when the release is
|
|
294
|
-
* `announce` hears the version before the download
|
|
295
|
+
* the next step runs. Null when the release is `current`, the version of
|
|
296
|
+
* the vx being replaced; `announce` hears the version before the download
|
|
297
|
+
* starts.
|
|
295
298
|
*/
|
|
296
299
|
export async function releaseBinary(
|
|
297
300
|
pkg: string,
|
|
298
301
|
spec: string,
|
|
299
302
|
announce: (version: string) => void,
|
|
300
303
|
key: SigningKey = NPM_SIGNING_KEY,
|
|
304
|
+
current = VERSION,
|
|
301
305
|
): Promise<Uint8Array | null> {
|
|
302
306
|
const release = verifiedRelease(await fetchManifest(pkg, spec), pkg, key)
|
|
303
|
-
if (release.version ===
|
|
307
|
+
if (release.version === current) return null
|
|
304
308
|
announce(release.version)
|
|
305
309
|
return binaryFromTarball(await downloadVerified(release))
|
|
306
310
|
}
|
|
@@ -518,22 +522,33 @@ async function targetVersion(tag: string | undefined): Promise<string> {
|
|
|
518
522
|
return v
|
|
519
523
|
}
|
|
520
524
|
|
|
521
|
-
/**
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
525
|
+
/**
|
|
526
|
+
* Run the package manager that owns a vx at `current` to bring it to
|
|
527
|
+
* `version`; vx never installs one. `step` names the install when a run
|
|
528
|
+
* moves two (`project `, `global `).
|
|
529
|
+
*/
|
|
530
|
+
async function upgradeManaged(
|
|
531
|
+
install: ManagedInstall,
|
|
532
|
+
version: string,
|
|
533
|
+
current = VERSION,
|
|
534
|
+
step = '',
|
|
535
|
+
): Promise<void> {
|
|
536
|
+
if (version === current) {
|
|
537
|
+
ui.print('out', ui.status('info', 'vx upgrade', `${step}already at ${current}`))
|
|
538
|
+
return
|
|
527
539
|
}
|
|
528
540
|
const argv = upgradeArgv(install, version)
|
|
529
|
-
|
|
541
|
+
// PATH as it is now: `Bun.which` alone reads the one the process started with.
|
|
542
|
+
const bin = Bun.which(argv[0]!, { PATH: process.env['PATH'] ?? '' })
|
|
530
543
|
if (bin === null) {
|
|
531
544
|
throw new UserError(
|
|
532
545
|
`vx upgrade: this vx was installed by ${argv[0]}, which is not on PATH — vx does not install package managers; run it yourself: ${argv.join(' ')} (in ${install.cwd})`,
|
|
533
546
|
)
|
|
534
547
|
}
|
|
535
|
-
|
|
536
|
-
|
|
548
|
+
ui.print(
|
|
549
|
+
'out',
|
|
550
|
+
ui.status('info', 'vx upgrade', `${step}${current} → ${version}`),
|
|
551
|
+
ui.text([ui.cmd(argv.join(' ')), ui.dim(` in ${install.cwd}`)]),
|
|
537
552
|
)
|
|
538
553
|
const proc = Bun.spawnSync({
|
|
539
554
|
cmd: [bin, ...argv.slice(1)],
|
|
@@ -545,14 +560,99 @@ async function upgradeManaged(install: ManagedInstall, tag: string | undefined):
|
|
|
545
560
|
`vx upgrade: ${argv.join(' ')} failed (exit ${proc.exitCode}) — vx changed nothing itself`,
|
|
546
561
|
)
|
|
547
562
|
}
|
|
548
|
-
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Replace the binary at `dest`, at `current`, with the verified release
|
|
567
|
+
* `spec` names (`latest` or a version). `step` as in `upgradeManaged`.
|
|
568
|
+
*/
|
|
569
|
+
async function upgradeBinary(
|
|
570
|
+
dest: string,
|
|
571
|
+
spec: string,
|
|
572
|
+
current = VERSION,
|
|
573
|
+
step = '',
|
|
574
|
+
key: SigningKey = NPM_SIGNING_KEY,
|
|
575
|
+
): Promise<void> {
|
|
576
|
+
const pkg = platformPackage()
|
|
577
|
+
let installed: string | undefined
|
|
578
|
+
const replaced = await replaceBinary(
|
|
579
|
+
dest,
|
|
580
|
+
() =>
|
|
581
|
+
releaseBinary(
|
|
582
|
+
pkg,
|
|
583
|
+
spec,
|
|
584
|
+
(version) =>
|
|
585
|
+
ui.print(
|
|
586
|
+
'out',
|
|
587
|
+
ui.status('info', 'vx upgrade', `${step}${current} → ${version}`),
|
|
588
|
+
ui.text(ui.dim(dest)),
|
|
589
|
+
),
|
|
590
|
+
key,
|
|
591
|
+
current,
|
|
592
|
+
),
|
|
593
|
+
// The replaced binary's own version: the new build speaks for itself
|
|
594
|
+
// rather than this process guessing, and one that cannot is rolled back.
|
|
595
|
+
(bin) => {
|
|
596
|
+
installed = answeredVersion(bin)
|
|
597
|
+
return installed !== undefined
|
|
598
|
+
},
|
|
599
|
+
)
|
|
600
|
+
ui.print(
|
|
601
|
+
'out',
|
|
602
|
+
replaced
|
|
603
|
+
? ui.status('ok', 'vx upgrade', `${step}installed ${installed}`)
|
|
604
|
+
: ui.status('info', 'vx upgrade', `${step}already at ${current}`),
|
|
605
|
+
)
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/** The version `bin --version` answers, without the hand-off note a handed-off run adds. */
|
|
609
|
+
function answeredVersion(bin: string): string | undefined {
|
|
610
|
+
return startedVersion(bin)?.split(' ')[1]
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Bring `from`, the global vx that handed this run to the project's own
|
|
615
|
+
* (`VX_HANDED_OFF`), to `version`, the way it was installed: a binary
|
|
616
|
+
* installed by hand is replaced, a package manager's global install is
|
|
617
|
+
* updated by that manager. A refusal names it.
|
|
618
|
+
*/
|
|
619
|
+
export async function upgradeGlobal(
|
|
620
|
+
from: string,
|
|
621
|
+
version: string,
|
|
622
|
+
key: SigningKey = NPM_SIGNING_KEY,
|
|
623
|
+
): Promise<void> {
|
|
624
|
+
await named(`global vx ${from}`, async () => {
|
|
625
|
+
const current = answeredVersion(from)
|
|
626
|
+
if (current === undefined) {
|
|
627
|
+
throw new UserError('vx upgrade: it did not answer `--version`, so it is left as it is')
|
|
628
|
+
}
|
|
629
|
+
if (current === version) {
|
|
630
|
+
ui.print('out', ui.status('info', 'vx upgrade', `global already at ${current}`))
|
|
631
|
+
return
|
|
632
|
+
}
|
|
633
|
+
const install = managedInstall(from, path.dirname(from))
|
|
634
|
+
if (install === null) await upgradeBinary(from, version, current, 'global ', key)
|
|
635
|
+
else await upgradeManaged(install, version, current, 'global ')
|
|
636
|
+
})
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/** `run`, its refusal naming `what` after the verb: a run that moves two installs says which failed. */
|
|
640
|
+
async function named(what: string, run: () => Promise<void>): Promise<void> {
|
|
641
|
+
try {
|
|
642
|
+
await run()
|
|
643
|
+
} catch (err) {
|
|
644
|
+
if (!(err instanceof UserError)) throw err
|
|
645
|
+
throw new UserError(err.message.replace(/^vx upgrade: /, `vx upgrade: ${what}: `), err.code)
|
|
646
|
+
}
|
|
549
647
|
}
|
|
550
648
|
|
|
551
649
|
export async function upgradeCmd(args: readonly string[]): Promise<number> {
|
|
552
650
|
const unknown = args.find((a) => a.startsWith('-'))
|
|
553
651
|
if (unknown !== undefined) {
|
|
554
|
-
|
|
555
|
-
|
|
652
|
+
sayNotice(
|
|
653
|
+
'error',
|
|
654
|
+
'vx upgrade',
|
|
655
|
+
`unknown flag: ${unknown}${flagHint('upgrade', unknown)}${seeHelp('upgrade')}`,
|
|
556
656
|
)
|
|
557
657
|
return 1
|
|
558
658
|
}
|
|
@@ -560,7 +660,7 @@ export async function upgradeCmd(args: readonly string[]): Promise<number> {
|
|
|
560
660
|
// the first one went in (`vx upgrade v0.0.22 v0.0.23`).
|
|
561
661
|
const [tag, extra] = args
|
|
562
662
|
if (extra !== undefined) {
|
|
563
|
-
|
|
663
|
+
sayNotice('error', 'vx upgrade', `unexpected argument: ${extra}${seeHelp('upgrade')}`)
|
|
564
664
|
return 1
|
|
565
665
|
}
|
|
566
666
|
if (!isCompiledBinary()) {
|
|
@@ -571,28 +671,21 @@ export async function upgradeCmd(args: readonly string[]): Promise<number> {
|
|
|
571
671
|
}
|
|
572
672
|
const dest = process.execPath
|
|
573
673
|
const install = managedInstall(dest, process.cwd())
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
},
|
|
590
|
-
)
|
|
591
|
-
process.stdout.write(
|
|
592
|
-
replaced
|
|
593
|
-
? `vx upgrade: installed ${installed}\n`
|
|
594
|
-
: `vx upgrade: already at ${VERSION} — nothing to do\n`,
|
|
595
|
-
)
|
|
674
|
+
// A global vx handed this run to the project's own: both move, to one
|
|
675
|
+
// version, the project first (owner, 2026-10-15).
|
|
676
|
+
const from = process.env['VX_HANDED_OFF']
|
|
677
|
+
if (from !== undefined) {
|
|
678
|
+
const version = await targetVersion(tag)
|
|
679
|
+
await named('project vx', () =>
|
|
680
|
+
install === null
|
|
681
|
+
? upgradeBinary(dest, version, VERSION, 'project ')
|
|
682
|
+
: upgradeManaged(install, version, VERSION, 'project '),
|
|
683
|
+
)
|
|
684
|
+
await upgradeGlobal(from, version)
|
|
685
|
+
return 0
|
|
686
|
+
}
|
|
687
|
+
if (install !== null) await upgradeManaged(install, await targetVersion(tag))
|
|
688
|
+
else await upgradeBinary(dest, versionSpec(tag))
|
|
596
689
|
return 0
|
|
597
690
|
}
|
|
598
691
|
|
package/src/cli/watch-cycle.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// as Ctrl-C would and the loop starts the next one; a cycle running any
|
|
6
6
|
// other work is never stopped.
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { type RunOptions } from '../orchestrator/index.js'
|
|
9
|
+
import { createEventBus } from '../report/index.js'
|
|
9
10
|
|
|
10
11
|
export interface WatchCycle {
|
|
11
12
|
/** The options for this cycle's run: its own bus and a stop of its own beside watch's. */
|
package/src/cli/watch-filter.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { asTrees } from '../cache/index.js'
|
|
|
17
17
|
import { PROJECT_CONFIG_FILENAMES, WORKSPACE_CONFIG_FILENAMES } from '../workspace/index.js'
|
|
18
18
|
import { isIgnoredDir } from './watch-fs.js'
|
|
19
19
|
|
|
20
|
-
const IGNORED_SUFFIXES = ['
|
|
20
|
+
const IGNORED_SUFFIXES = ['~']
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* True for a path the ignore filter drops. Every project dir is watched
|
|
@@ -32,8 +32,13 @@ export function isIgnoredWatchPath(rel: string): boolean {
|
|
|
32
32
|
const segments = rel.split(path.sep)
|
|
33
33
|
for (let i = 1; i <= segments.length; i++) if (isIgnoredDir(segments.slice(0, i))) return true
|
|
34
34
|
if (IGNORED_SUFFIXES.some((suf) => rel.endsWith(suf))) return true
|
|
35
|
+
const name = segments[segments.length - 1]!
|
|
36
|
+
// A restore's own temporary file, never an input (`ALWAYS_IGNORE`):
|
|
37
|
+
// renamed away before the judgement, it read as a deletion and ran one
|
|
38
|
+
// more cycle where an uncached task makes ignored paths count.
|
|
39
|
+
if (name.startsWith('.vx-tmp-')) return true
|
|
35
40
|
// `vx lock`'s review copy: no input, so a re-lock is no cycle.
|
|
36
|
-
return
|
|
41
|
+
return name === 'vx.lock.json'
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
/**
|
package/src/cli/watch-judge.ts
CHANGED
|
@@ -161,7 +161,12 @@ export class ChangeJudge {
|
|
|
161
161
|
spell !== undefined && this.ctx.held() && this.ctx.trackedAtArm?.has(spell(abs)) !== true
|
|
162
162
|
if (this.lastCycle === undefined && !open) return false
|
|
163
163
|
try {
|
|
164
|
-
|
|
164
|
+
// The later of the two clocks: a cache hit restores an output with
|
|
165
|
+
// the mtime it was saved with, and read by mtime alone the restore
|
|
166
|
+
// was the user's edit and ran one more cycle (solid, M 2026-10-15).
|
|
167
|
+
// The restore's write moves the ctime.
|
|
168
|
+
const st = fs.statSync(abs)
|
|
169
|
+
const m = Math.max(st.mtimeMs, st.ctimeMs)
|
|
165
170
|
const start = this.lastCycle?.start ?? this.ctx.armedAt
|
|
166
171
|
return m >= start && (open || m <= this.lastCycle!.end)
|
|
167
172
|
} catch {
|