@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.
Files changed (76) hide show
  1. package/docs/caching.md +41 -14
  2. package/docs/cli.md +115 -73
  3. package/docs/execution.md +1 -1
  4. package/docs/features.md +6 -5
  5. package/docs/schema.md +24 -15
  6. package/package.json +5 -5
  7. package/schemas/plan.json +2 -2
  8. package/skills/vx/SKILL.md +13 -2
  9. package/src/bin.ts +13 -7
  10. package/src/cache/cache.ts +21 -2
  11. package/src/cache/chained-cache.ts +4 -0
  12. package/src/cache/index.ts +9 -1
  13. package/src/cache/inputs.ts +0 -24
  14. package/src/cache/layer.ts +7 -1
  15. package/src/cache/layered-cache.ts +4 -0
  16. package/src/cache/plugin-cache.ts +248 -0
  17. package/src/cache/policy.ts +9 -0
  18. package/src/cache/run-history.ts +68 -0
  19. package/src/cache/run-store.ts +77 -0
  20. package/src/cache/schema.ts +38 -9
  21. package/src/cache/scoped-cache.ts +15 -2
  22. package/src/cli/cache.ts +33 -16
  23. package/src/cli/completions.ts +9 -5
  24. package/src/cli/help.ts +16 -7
  25. package/src/cli/index.ts +25 -11
  26. package/src/cli/info.ts +6 -5
  27. package/src/cli/init.ts +14 -15
  28. package/src/cli/last.ts +14 -7
  29. package/src/cli/lock.ts +39 -27
  30. package/src/cli/plan-format.ts +5 -3
  31. package/src/cli/plugin-commands.ts +77 -6
  32. package/src/cli/plugin-templates.ts +1 -1
  33. package/src/cli/run.ts +14 -12
  34. package/src/cli/select.ts +18 -9
  35. package/src/cli/show.ts +66 -40
  36. package/src/cli/task-verb.ts +4 -4
  37. package/src/cli/upgrade.ts +20 -11
  38. package/src/cli/watch-filter.ts +7 -2
  39. package/src/cli/watch-judge.ts +6 -1
  40. package/src/cli/watch.ts +44 -20
  41. package/src/cli/why.ts +10 -1
  42. package/src/cli/workspace-config.ts +2 -2
  43. package/src/graph/scheduler.ts +8 -0
  44. package/src/graph/task-graph.ts +32 -4
  45. package/src/index.ts +11 -1
  46. package/src/orchestrator/affected-tasks.ts +8 -3
  47. package/src/orchestrator/doctor.ts +1 -1
  48. package/src/orchestrator/execute-task.ts +26 -43
  49. package/src/orchestrator/failure-mode.ts +19 -10
  50. package/src/orchestrator/framed-output.ts +1 -2
  51. package/src/orchestrator/history.ts +60 -4
  52. package/src/orchestrator/index.ts +2 -1
  53. package/src/orchestrator/logger.ts +9 -2
  54. package/src/orchestrator/metrics.ts +7 -3
  55. package/src/orchestrator/plugin-host.ts +14 -4
  56. package/src/orchestrator/plugin.ts +80 -8
  57. package/src/orchestrator/prepare.ts +51 -7
  58. package/src/orchestrator/projects.ts +3 -0
  59. package/src/orchestrator/remote-prefetch.ts +9 -5
  60. package/src/orchestrator/run-records.ts +1 -1
  61. package/src/orchestrator/run.ts +126 -27
  62. package/src/orchestrator/status-line.ts +1 -1
  63. package/src/orchestrator/summary.ts +69 -44
  64. package/src/orchestrator/telemetry.ts +1 -1
  65. package/src/settings.ts +12 -1
  66. package/src/{orchestrator → util}/colors.ts +5 -0
  67. package/src/util/config-frame.ts +31 -2
  68. package/src/util/errors.ts +32 -0
  69. package/src/util/index.ts +8 -1
  70. package/src/util/refusal-notice.ts +111 -0
  71. package/src/util/report.ts +31 -0
  72. package/src/util/ui.ts +186 -0
  73. package/src/workspace/affected.ts +20 -23
  74. package/src/workspace/dependency-rules.ts +12 -4
  75. package/src/workspace/migration.ts +87 -91
  76. 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
- /** Report lines printed under the source line (e.g. "turbo.json found and not read"). */
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). Prints its own line. A refusal (a graph rule, a
209
- * plugin not installed yet) is printed and the migration still succeeds:
210
- * its files are written, and `vx lock` is the step to re-run.
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 cached = false
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.task !== null && 'cache' in t.task) cached = true
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.notes ?? []),
475
- ...ignoredNote,
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
- if (dry || args.lock === undefined) {
485
- report.push('', `next: ${next}`)
486
- process.stdout.write(`${report.join('\n')}\n`)
487
- return 0
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
- process.stdout.write(`${report.join('\n')}\n\n`)
490
- const locked = (await args.lock(root)) === 0
491
- process.stdout.write(`\nnext: ${locked ? next : `fix the above and run vx lock, then ${next}`}\n`)
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.map((p) => ` ${p}`)
614
+ if (paths.length <= FILES_SHOWN) return [...paths]
619
615
  const root = paths.filter((p) => !p.includes('/') || p.startsWith('.vx/'))
620
- const more = paths.length - root.length
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
- throw new UserError(
79
- `no .vx/workspace.ts in ${start} or a parent${(await gitTop(start)) === null ? '' : ' up to the git root'} — ` +
80
- 'run `vx init` to create one',
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)