@vzn/vx 0.0.637 → 0.0.638
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 +41 -14
- package/docs/cli.md +115 -73
- package/docs/execution.md +1 -1
- package/docs/features.md +6 -5
- package/docs/schema.md +24 -15
- package/package.json +5 -5
- package/schemas/plan.json +2 -2
- package/skills/vx/SKILL.md +13 -2
- package/src/bin.ts +13 -7
- package/src/cache/cache.ts +21 -2
- package/src/cache/chained-cache.ts +4 -0
- package/src/cache/index.ts +9 -1
- package/src/cache/inputs.ts +0 -24
- package/src/cache/layer.ts +7 -1
- package/src/cache/layered-cache.ts +4 -0
- package/src/cache/plugin-cache.ts +248 -0
- package/src/cache/policy.ts +9 -0
- package/src/cache/run-history.ts +68 -0
- package/src/cache/run-store.ts +77 -0
- package/src/cache/schema.ts +38 -9
- package/src/cache/scoped-cache.ts +15 -2
- package/src/cli/cache.ts +33 -16
- package/src/cli/completions.ts +9 -5
- package/src/cli/help.ts +16 -7
- package/src/cli/index.ts +25 -11
- package/src/cli/info.ts +6 -5
- package/src/cli/init.ts +14 -15
- package/src/cli/last.ts +14 -7
- package/src/cli/lock.ts +39 -27
- package/src/cli/plan-format.ts +5 -3
- package/src/cli/plugin-commands.ts +77 -6
- package/src/cli/plugin-templates.ts +1 -1
- package/src/cli/run.ts +14 -12
- package/src/cli/select.ts +18 -9
- package/src/cli/show.ts +66 -40
- package/src/cli/task-verb.ts +4 -4
- package/src/cli/upgrade.ts +20 -11
- package/src/cli/watch-filter.ts +7 -2
- package/src/cli/watch-judge.ts +6 -1
- package/src/cli/watch.ts +44 -20
- package/src/cli/why.ts +10 -1
- package/src/cli/workspace-config.ts +2 -2
- package/src/graph/scheduler.ts +8 -0
- package/src/graph/task-graph.ts +32 -4
- package/src/index.ts +11 -1
- package/src/orchestrator/affected-tasks.ts +8 -3
- package/src/orchestrator/doctor.ts +1 -1
- package/src/orchestrator/execute-task.ts +26 -43
- package/src/orchestrator/failure-mode.ts +19 -10
- package/src/orchestrator/framed-output.ts +1 -2
- package/src/orchestrator/history.ts +60 -4
- package/src/orchestrator/index.ts +2 -1
- package/src/orchestrator/logger.ts +9 -2
- package/src/orchestrator/metrics.ts +7 -3
- package/src/orchestrator/plugin-host.ts +14 -4
- package/src/orchestrator/plugin.ts +80 -8
- package/src/orchestrator/prepare.ts +51 -7
- package/src/orchestrator/projects.ts +3 -0
- package/src/orchestrator/remote-prefetch.ts +9 -5
- package/src/orchestrator/run-records.ts +1 -1
- package/src/orchestrator/run.ts +126 -27
- package/src/orchestrator/status-line.ts +1 -1
- package/src/orchestrator/summary.ts +69 -44
- package/src/orchestrator/telemetry.ts +1 -1
- package/src/settings.ts +12 -1
- package/src/{orchestrator → util}/colors.ts +5 -0
- package/src/util/config-frame.ts +31 -2
- package/src/util/errors.ts +32 -0
- package/src/util/index.ts +8 -1
- package/src/util/refusal-notice.ts +111 -0
- package/src/util/report.ts +31 -0
- package/src/util/ui.ts +186 -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
|
@@ -9,7 +9,8 @@ import { existsSync, readFileSync } from 'node:fs'
|
|
|
9
9
|
import { unlink } from 'node:fs/promises'
|
|
10
10
|
import path from 'node:path'
|
|
11
11
|
import { workspaceFileWritten } from '../handoff.js'
|
|
12
|
-
import { DOT_VX_GITIGNORE, executablePath, gitEnv, relPosix, UserError } from '../util/index.js'
|
|
12
|
+
import { DOT_VX_GITIGNORE, executablePath, gitEnv, relPosix, UserError, ui } from '../util/index.js'
|
|
13
|
+
import { VERSION } from '../version.js'
|
|
13
14
|
import { WORKSPACE_CONFIG_FILENAMES } from './project-loader.js'
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -136,6 +137,8 @@ export interface GeneratedProject {
|
|
|
136
137
|
importLines: string[]
|
|
137
138
|
/** The project's `tags` (an Nx project's); none written when empty. */
|
|
138
139
|
tags?: readonly string[]
|
|
140
|
+
/** Writes `name`: no manifest names the project, so its path would. */
|
|
141
|
+
writeName?: boolean
|
|
139
142
|
tasks: GeneratedTask[]
|
|
140
143
|
}
|
|
141
144
|
|
|
@@ -177,7 +180,7 @@ export interface ApplyMigrationArgs {
|
|
|
177
180
|
* migration with nothing to convert is an error. Default false.
|
|
178
181
|
*/
|
|
179
182
|
init?: boolean
|
|
180
|
-
/**
|
|
183
|
+
/** The report's `note` lines (e.g. "turbo.json found and not read"). */
|
|
181
184
|
notes?: readonly string[]
|
|
182
185
|
/**
|
|
183
186
|
* Scripts exist but no project can carry them (a member without a
|
|
@@ -205,11 +208,14 @@ export interface ApplyMigrationArgs {
|
|
|
205
208
|
/**
|
|
206
209
|
* Run after the files are written (not under `dry`): `vx lock`'s step, so
|
|
207
210
|
* the migrated workspace has its .vx/projects.lock.json and its graph rules checked
|
|
208
|
-
* (owner, 2026-10-14).
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
+
* (owner, 2026-10-14). On success `said` is its line, shown as the
|
|
212
|
+
* report's `lock` row; a refusal (a graph rule, a plugin not installed
|
|
213
|
+
* yet) prints above the report and the migration still succeeds: its
|
|
214
|
+
* files are written, and `vx lock` is the step to re-run.
|
|
211
215
|
*/
|
|
212
|
-
lock?: (root: string) => Promise<number>
|
|
216
|
+
lock?: (root: string) => Promise<{ code: number; said: string }>
|
|
217
|
+
/** Rows of what the tool did before the write, e.g. `['ran', 'bun add -d …']`. */
|
|
218
|
+
steps?: readonly (readonly [label: string, value: string])[]
|
|
213
219
|
/** What the written workspace file declares, e.g. `pnpm()` from `@vzn/vx-lockfile`. */
|
|
214
220
|
plugins?: readonly WorkspacePlugin[]
|
|
215
221
|
}
|
|
@@ -363,89 +369,20 @@ export async function applyMigration(args: ApplyMigrationArgs): Promise<number>
|
|
|
363
369
|
)
|
|
364
370
|
const ignoredNote = ignored.length === 0 ? [] : [ignoreAdvice(root, ignored, dry)]
|
|
365
371
|
const todos = new Map<string, string[]>()
|
|
366
|
-
let todoCount = 0
|
|
367
372
|
let clean = 0
|
|
368
|
-
let
|
|
373
|
+
let todo = 0
|
|
369
374
|
for (const p of projects) {
|
|
370
375
|
for (const t of p.tasks) {
|
|
371
376
|
if (t.todos.length === 0 && t.task !== null) clean++
|
|
372
|
-
if (t.
|
|
377
|
+
if (t.todos.length > 0) todo++
|
|
373
378
|
for (const reason of t.todos) {
|
|
374
|
-
todoCount++
|
|
375
379
|
const ids = todos.get(reason) ?? []
|
|
376
380
|
ids.push(`${p.name}#${t.name}`)
|
|
377
381
|
todos.set(reason, ids)
|
|
378
382
|
}
|
|
379
383
|
}
|
|
380
384
|
}
|
|
381
|
-
const report: string[] = []
|
|
382
|
-
const emptyReason = empty ? args.emptyReason : undefined
|
|
383
|
-
if (emptyReason !== undefined) {
|
|
384
|
-
report.push(
|
|
385
|
-
`${verb}: ${emptyReason}`,
|
|
386
|
-
hasWorkspaceFile
|
|
387
|
-
? `${workspaceName} already exists.`
|
|
388
|
-
: dry
|
|
389
|
-
? `would write ${workspaceName} (dry run, nothing written).`
|
|
390
|
-
: `wrote ${workspaceName}.`,
|
|
391
|
-
)
|
|
392
|
-
} else if (empty) {
|
|
393
|
-
report.push(
|
|
394
|
-
args.unmapped === true
|
|
395
|
-
? `${verb}: no package.json scripts became tasks.`
|
|
396
|
-
: `${verb}: no package.json scripts to turn into tasks.`,
|
|
397
|
-
...[...(args.notes ?? []), ...ignoredNote].map((n) => `note: ${n}`),
|
|
398
|
-
hasWorkspaceFile
|
|
399
|
-
? `${workspaceName} already exists.`
|
|
400
|
-
: dry
|
|
401
|
-
? `would write ${workspaceName} (dry run, nothing written).`
|
|
402
|
-
: `wrote ${workspaceName}.`,
|
|
403
|
-
`Declare tasks in a ${configName} beside a package.json — your own command, for example:`,
|
|
404
|
-
'',
|
|
405
|
-
...exampleConfig(format)
|
|
406
|
-
.trimEnd()
|
|
407
|
-
.split('\n')
|
|
408
|
-
.map((l) => ` ${l}`),
|
|
409
|
-
)
|
|
410
|
-
} else {
|
|
411
|
-
report.push(`${verb}: ${source} → ${[...new Set(names.values())].join(', ') || configName}`)
|
|
412
|
-
for (const n of [...(args.notes ?? []), ...ignoredNote, ...formatted]) report.push(`note: ${n}`)
|
|
413
|
-
for (const n of plan.headerNotes) report.push(`note: ${n}`)
|
|
414
|
-
report.push(
|
|
415
|
-
'',
|
|
416
|
-
`${clean} task${clean === 1 ? '' : 's'} migrated clean, ` +
|
|
417
|
-
`${todoCount} TODO${todoCount === 1 ? '' : 's'}${todoCount > 0 ? ':' : ''}`,
|
|
418
|
-
)
|
|
419
|
-
for (const [reason, ids] of todos) {
|
|
420
|
-
if (ids.length === 1) report.push(` ${ids[0]}: ${reason}`)
|
|
421
|
-
else {
|
|
422
|
-
const shown = ids.slice(0, TODO_IDS_SHOWN).join(', ')
|
|
423
|
-
const more = ids.length - TODO_IDS_SHOWN
|
|
424
|
-
report.push(` ${shown}${more > 0 ? ` and ${more} more` : ''}:`, ` ${reason}`)
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
report.push(...plan.notes)
|
|
428
|
-
if (files.length === 0) report.push(dry ? 'no files (dry run)' : 'no files written')
|
|
429
|
-
else report.push(dry ? 'files (dry run, nothing written):' : 'files written:')
|
|
430
|
-
report.push(...fileLines(files.map((f) => f.relPath)))
|
|
431
|
-
if (kept.length > 0) {
|
|
432
|
-
report.push('kept (each already has a vx config):')
|
|
433
|
-
for (const f of kept) report.push(` ${f}`)
|
|
434
|
-
}
|
|
435
|
-
if (replaced.length > 0) {
|
|
436
|
-
report.push(dry ? 'would replace (dry run):' : 'replaced:')
|
|
437
|
-
for (const f of replaced) report.push(` ${relPosix(root, f)}`)
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
385
|
const firstTask = nextTask(plan.projects.flatMap((p) => p.tasks))
|
|
441
|
-
// A run of tasks none of which caches is never a hit, and a first try
|
|
442
|
-
// that runs twice to see the cache work saw it run twice.
|
|
443
|
-
if (!cached && [...todos.keys()].some(isCacheTodo)) {
|
|
444
|
-
report.push(
|
|
445
|
-
'',
|
|
446
|
-
'no task caches yet: uncomment the cache line in each TODO task with its real globs, and a second run hits',
|
|
447
|
-
)
|
|
448
|
-
}
|
|
449
386
|
const installed = existsSync(path.join(root, 'node_modules', '@vzn', 'vx', 'package.json'))
|
|
450
387
|
// The line is the next thing to type, so it names what that run would
|
|
451
388
|
// refuse without: a git work tree (vx keys inputs on git's view) and,
|
|
@@ -460,6 +397,14 @@ export async function applyMigration(args: ApplyMigrationArgs): Promise<number>
|
|
|
460
397
|
...(!dry && args.lock === undefined ? [`${vx} lock`] : []),
|
|
461
398
|
]
|
|
462
399
|
const next = steps.length === 0 ? run : `${steps.join(', ')}, then ${run}`
|
|
400
|
+
const emptyReason = empty ? args.emptyReason : undefined
|
|
401
|
+
const notes = [
|
|
402
|
+
...(args.notes ?? []),
|
|
403
|
+
...ignoredNote,
|
|
404
|
+
...formatted,
|
|
405
|
+
...plan.headerNotes,
|
|
406
|
+
...plan.notes,
|
|
407
|
+
]
|
|
463
408
|
if (args.json === true) {
|
|
464
409
|
process.stdout.write(
|
|
465
410
|
`${JSON.stringify({
|
|
@@ -471,24 +416,75 @@ export async function applyMigration(args: ApplyMigrationArgs): Promise<number>
|
|
|
471
416
|
todos: [...todos].map(([reason, tasks]) => ({ reason, tasks })),
|
|
472
417
|
notes: [
|
|
473
418
|
...(emptyReason === undefined ? [] : [emptyReason]),
|
|
474
|
-
...(args.
|
|
475
|
-
...
|
|
476
|
-
...plan.headerNotes,
|
|
477
|
-
...plan.notes,
|
|
419
|
+
...(args.steps ?? []).map(([label, value]) => `${label}: ${value}`),
|
|
420
|
+
...notes,
|
|
478
421
|
],
|
|
479
422
|
next,
|
|
480
423
|
})}\n`,
|
|
481
424
|
)
|
|
482
425
|
return 0
|
|
483
426
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
427
|
+
|
|
428
|
+
// The lock's own words become a row; a refusal prints above the report.
|
|
429
|
+
const lock = dry || args.lock === undefined ? undefined : await args.lock(root)
|
|
430
|
+
if (lock !== undefined && lock.code !== 0) process.stderr.write(lock.said)
|
|
431
|
+
|
|
432
|
+
const rows: ui.FieldRow[] = [['from', source]]
|
|
433
|
+
if (dry) rows.push(['dry run', 'nothing written'])
|
|
434
|
+
for (const [label, value] of args.steps ?? []) rows.push([label, value])
|
|
435
|
+
if (empty) {
|
|
436
|
+
const why =
|
|
437
|
+
emptyReason ??
|
|
438
|
+
(args.unmapped === true
|
|
439
|
+
? 'no package.json script became a task'
|
|
440
|
+
: 'no package.json scripts to turn into tasks')
|
|
441
|
+
rows.push(['tasks', [ui.dim('none'), `: ${why}`]])
|
|
442
|
+
} else {
|
|
443
|
+
const parts = [
|
|
444
|
+
...(clean > 0 ? [ui.tone('ok', `${clean} clean`)] : []),
|
|
445
|
+
...(todo > 0 ? [ui.tone('warn', `${todo} todo`)] : []),
|
|
446
|
+
]
|
|
447
|
+
if (parts.length > 0)
|
|
448
|
+
rows.push(['tasks', parts.flatMap((p, i) => (i === 0 ? [p] : [ui.dim(' · '), p]))])
|
|
449
|
+
const todoLines: string[] = []
|
|
450
|
+
for (const [reason, ids] of todos) {
|
|
451
|
+
if (ids.length === 1) todoLines.push(`${ids[0]}: ${reason}`)
|
|
452
|
+
else {
|
|
453
|
+
const shown = ids.slice(0, TODO_IDS_SHOWN).join(', ')
|
|
454
|
+
const more = ids.length - TODO_IDS_SHOWN
|
|
455
|
+
todoLines.push(`${shown}${more > 0 ? ` and ${more} more` : ''}:`, ` ${reason}`)
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (todoLines.length > 0) rows.push(['todo', ...todoLines])
|
|
459
|
+
}
|
|
460
|
+
if (files.length > 0) rows.push(['files', ...fileLines(files.map((f) => f.relPath))])
|
|
461
|
+
const keptLines = [
|
|
462
|
+
...kept.map((f) => `${f} (has a vx config)`),
|
|
463
|
+
...(empty && hasWorkspaceFile ? [workspaceName] : []),
|
|
464
|
+
]
|
|
465
|
+
if (keptLines.length > 0) rows.push(['kept', ...keptLines])
|
|
466
|
+
if (replaced.length > 0) rows.push(['replaced', ...replaced.map((f) => relPosix(root, f))])
|
|
467
|
+
if (lock !== undefined) {
|
|
468
|
+
rows.push([
|
|
469
|
+
'lock',
|
|
470
|
+
lock.code === 0
|
|
471
|
+
? lock.said.trim().replace(/^✓ vx lock\s+/, '')
|
|
472
|
+
: ui.tone('error', 'not written'),
|
|
473
|
+
])
|
|
474
|
+
}
|
|
475
|
+
if (notes.length > 0) rows.push(['note', ...notes])
|
|
476
|
+
if (empty && emptyReason === undefined) {
|
|
477
|
+
rows.push([
|
|
478
|
+
'example',
|
|
479
|
+
`a ${configName} beside a package.json, with your own command:`,
|
|
480
|
+
'',
|
|
481
|
+
exampleConfig(format).trimEnd(),
|
|
482
|
+
])
|
|
488
483
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
484
|
+
const said =
|
|
485
|
+
lock === undefined || lock.code === 0 ? next : `fix the above, then vx lock and ${next}`
|
|
486
|
+
rows.push(null, ['next', said])
|
|
487
|
+
ui.print('out', ui.report(`${verb} ${VERSION}`, rows))
|
|
492
488
|
return 0
|
|
493
489
|
}
|
|
494
490
|
|
|
@@ -615,10 +611,9 @@ export function vxInvocation(userAgent: string | undefined, installed: boolean):
|
|
|
615
611
|
const FILES_SHOWN = 10
|
|
616
612
|
|
|
617
613
|
function fileLines(paths: readonly string[]): string[] {
|
|
618
|
-
if (paths.length <= FILES_SHOWN) return paths
|
|
614
|
+
if (paths.length <= FILES_SHOWN) return [...paths]
|
|
619
615
|
const root = paths.filter((p) => !p.includes('/') || p.startsWith('.vx/'))
|
|
620
|
-
|
|
621
|
-
return [...root.map((p) => ` ${p}`), ` and ${more} more in package directories`]
|
|
616
|
+
return [...root, `and ${paths.length - root.length} more in package directories`]
|
|
622
617
|
}
|
|
623
618
|
|
|
624
619
|
/** Ids named per shared TODO; the files carry each one (sveltejs/kit: 92 persistent tasks). */
|
|
@@ -691,6 +686,7 @@ function renderConfigFile(p: GeneratedProject, format: MigrationFormat): string
|
|
|
691
686
|
if (p.importLines.length > 0) lines.push(...p.importLines)
|
|
692
687
|
if (lines.length > 0) lines.push('')
|
|
693
688
|
lines.push('export default {')
|
|
689
|
+
if (p.writeName === true) lines.push(` name: ${quoteTsLiteral(p.name)},`)
|
|
694
690
|
if (p.tags !== undefined && p.tags.length > 0) lines.push(` tags: ${renderValue(p.tags, ' ')},`)
|
|
695
691
|
lines.push(p.tasks.length === 0 ? ' tasks: {},' : ' tasks: {')
|
|
696
692
|
for (const t of p.tasks) {
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
gitSpawnRefusal,
|
|
16
16
|
isExecutableMissing,
|
|
17
17
|
isOutOfFds,
|
|
18
|
+
refusal,
|
|
18
19
|
relPosix,
|
|
19
20
|
UserError,
|
|
20
21
|
} from '../util/index.js'
|
|
@@ -75,10 +76,10 @@ export async function findWorkspaceRoot(
|
|
|
75
76
|
start = path.resolve(start)
|
|
76
77
|
const file = await workspaceFile(start)
|
|
77
78
|
if (file === null) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
'VX_E_NO_WORKSPACE',
|
|
79
|
+
const headline = `no .vx/workspace.ts in ${start} or a parent${(await gitTop(start)) === null ? '' : ' up to the git root'}`
|
|
80
|
+
throw refusal(
|
|
81
|
+
{ headline, fix: 'vx init' },
|
|
82
|
+
{ code: 'VX_E_NO_WORKSPACE', message: `${headline} — run \`vx init\` to create one` },
|
|
82
83
|
)
|
|
83
84
|
}
|
|
84
85
|
const root = workspaceRootOf(file)
|