@vzn/vx 0.0.650 → 0.0.651

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 (115) hide show
  1. package/README.md +5 -20
  2. package/package.json +5 -5
  3. package/schemas/last.json +15 -0
  4. package/src/bin.ts +9 -31
  5. package/src/cache/cache.ts +202 -25
  6. package/src/cache/file-hashes.ts +3 -0
  7. package/src/cache/git-inputs.ts +188 -39
  8. package/src/cache/index.ts +1 -0
  9. package/src/cache/inputs.ts +3 -0
  10. package/src/cache/layer.ts +11 -0
  11. package/src/cache/layered-cache.ts +4 -0
  12. package/src/cache/linked-inputs.ts +3 -0
  13. package/src/cache/lockfile-memo.ts +3 -2
  14. package/src/cache/output-index.ts +5 -2
  15. package/src/cache/remote-lane.ts +2 -1
  16. package/src/cache/run-history.ts +11 -1
  17. package/src/cache/schema.ts +11 -1
  18. package/src/cli/cache.ts +26 -16
  19. package/src/cli/completions.ts +4 -2
  20. package/src/cli/docs.ts +10 -11
  21. package/src/cli/help.ts +12 -23
  22. package/src/cli/index.ts +4 -9
  23. package/src/cli/info.ts +15 -7
  24. package/src/cli/init.ts +23 -15
  25. package/src/cli/last.ts +213 -203
  26. package/src/cli/lock.ts +55 -5
  27. package/src/cli/plan-format.ts +100 -118
  28. package/src/cli/plugin-commands.ts +8 -2
  29. package/src/cli/plugin-templates.ts +2 -2
  30. package/src/cli/run.ts +46 -27
  31. package/src/cli/select.ts +43 -5
  32. package/src/cli/show.ts +38 -42
  33. package/src/cli/task-verb.ts +22 -0
  34. package/src/cli/upgrade.ts +3 -1
  35. package/src/cli/watch-fs.ts +6 -1
  36. package/src/cli/watch-judge.ts +2 -1
  37. package/src/cli/watch.ts +83 -44
  38. package/src/cli/why.ts +123 -89
  39. package/src/cli/workspace-config.ts +2 -1
  40. package/src/exec/executor.ts +2 -1
  41. package/src/exec/kill-tree.ts +58 -5
  42. package/src/exec/local-executor.ts +1 -1
  43. package/src/exec/pipes.ts +152 -0
  44. package/src/exec/runner.ts +199 -182
  45. package/src/exec/sandbox-binds.ts +1 -1
  46. package/src/exec/sandbox-runtime.ts +11 -2
  47. package/src/exec/spawn-off.ts +154 -0
  48. package/src/exec/warm-shell.ts +146 -0
  49. package/src/graph/critical-path.ts +40 -28
  50. package/src/graph/priorities.ts +98 -44
  51. package/src/graph/scheduler.ts +14 -10
  52. package/src/graph/task-graph.ts +27 -1
  53. package/src/handoff.ts +10 -7
  54. package/src/history/history.ts +5 -0
  55. package/src/history/metrics.ts +9 -1
  56. package/src/index.ts +3 -1
  57. package/src/orchestrator/doctor.ts +1 -1
  58. package/src/orchestrator/execute-task.ts +16 -2
  59. package/src/orchestrator/graph-memo.ts +50 -1
  60. package/src/orchestrator/graph-stage.ts +6 -0
  61. package/src/orchestrator/index.ts +3 -0
  62. package/src/orchestrator/live-usage.ts +16 -11
  63. package/src/orchestrator/locked-digests.ts +63 -0
  64. package/src/orchestrator/lockfile-claim.ts +108 -29
  65. package/src/orchestrator/miss-save.ts +31 -3
  66. package/src/orchestrator/persistent-task.ts +3 -1
  67. package/src/orchestrator/plan.ts +5 -1
  68. package/src/orchestrator/plugin-host.ts +61 -0
  69. package/src/orchestrator/plugin.ts +47 -2
  70. package/src/orchestrator/prepare.ts +16 -1
  71. package/src/orchestrator/projects.ts +4 -1
  72. package/src/orchestrator/run-context.ts +29 -10
  73. package/src/orchestrator/run-failures.ts +1 -1
  74. package/src/orchestrator/run-records.ts +10 -0
  75. package/src/orchestrator/run.ts +53 -22
  76. package/src/orchestrator/sandbox-request.ts +2 -2
  77. package/src/orchestrator/signals.ts +8 -5
  78. package/src/orchestrator/task-hash.ts +17 -3
  79. package/src/report/events.ts +3 -11
  80. package/src/report/forecast.ts +10 -32
  81. package/src/report/framed-output.ts +272 -173
  82. package/src/report/index.ts +8 -3
  83. package/src/report/logger.ts +73 -38
  84. package/src/report/plain-output.ts +1 -1
  85. package/src/report/status-line.ts +49 -357
  86. package/src/report/summary.ts +100 -131
  87. package/src/ui/ansi.ts +48 -0
  88. package/src/ui/components.ts +506 -0
  89. package/src/ui/index.ts +79 -0
  90. package/src/ui/kit.ts +37 -0
  91. package/src/ui/live-region.ts +363 -0
  92. package/src/{util/refusal-notice.ts → ui/refusal.ts} +7 -5
  93. package/src/ui/terminal.ts +51 -0
  94. package/src/ui/theme.ts +224 -0
  95. package/src/ui/version-line.ts +28 -0
  96. package/src/util/config-frame.ts +65 -10
  97. package/src/util/edit-distance.ts +6 -1
  98. package/src/util/index.ts +2 -15
  99. package/src/util/paths.ts +20 -2
  100. package/src/util/secret-mask.ts +39 -3
  101. package/src/util/settle.ts +31 -0
  102. package/src/util/timing.ts +4 -16
  103. package/src/workspace/affected.ts +89 -30
  104. package/src/workspace/config-schema.ts +4 -2
  105. package/src/workspace/dependencies-lock.ts +191 -0
  106. package/src/workspace/filter.ts +28 -0
  107. package/src/workspace/index.ts +9 -0
  108. package/src/workspace/lockfile.ts +159 -6
  109. package/src/workspace/migration.ts +4 -3
  110. package/src/workspace/workspace.ts +3 -0
  111. package/src/util/colors.ts +0 -133
  112. package/src/util/report.ts +0 -44
  113. package/src/util/ui.ts +0 -258
  114. /package/src/{report/path-links.ts → ui/links.ts} +0 -0
  115. /package/src/{report → ui}/terminal-signals.ts +0 -0
@@ -6,7 +6,9 @@
6
6
  // Tab is the wrong price).
7
7
 
8
8
  import { acceptedFlags, CORE_VERBS, flagHint, refuse, seeHelp } from './help.js'
9
- import { nearest, sayNotice } from '../util/index.js'
9
+ import { nearest } from '../util/index.js'
10
+ import * as ui from '../ui/index.js'
11
+ import { sayNotice } from '../ui/index.js'
10
12
  import { setting } from '../settings.js'
11
13
 
12
14
  function enumValues(path: string): readonly string[] {
@@ -167,6 +169,6 @@ export async function completionsCmd(
167
169
  const verbs = [...CORE_VERBS.filter((v) => v !== 'help' && v !== 'version'), 'help', 'version']
168
170
  // No overlap to skip: the workspace load refuses a plugin verb named like a core one.
169
171
  verbs.push(...pluginVerbs)
170
- process.stdout.write(completionScript(shell as CompletionShell, verbs))
172
+ ui.data('out', completionScript(shell as CompletionShell, verbs))
171
173
  return 0
172
174
  }
package/src/cli/docs.ts CHANGED
@@ -5,7 +5,8 @@
5
5
  // whole, best first.
6
6
 
7
7
  import { VERSION } from '../version.js'
8
- import { detectColors, paint, parseDecimalInt, ui } from '../util/index.js'
8
+ import { parseDecimalInt } from '../util/index.js'
9
+ import * as ui from '../ui/index.js'
9
10
  import { flagHint, formatValue, refuse, refusedWord, seeHelp } from './help.js'
10
11
 
11
12
  const SITE = 'https://vznjs.github.io/vx/'
@@ -238,7 +239,7 @@ export async function docsCmd(
238
239
  const hits = searchDocs(pages, parsed.terms, parsed.limit)
239
240
  const query = parsed.terms.join(' ')
240
241
  if (parsed.json) {
241
- process.stdout.write(`${JSON.stringify({ query, hits })}\n`)
242
+ ui.data('out', `${JSON.stringify({ query, hits })}\n`)
242
243
  return 0
243
244
  }
244
245
  if (hits.length === 0) {
@@ -249,15 +250,13 @@ export async function docsCmd(
249
250
  )
250
251
  return 0
251
252
  }
252
- const c = detectColors(process.stdout)
253
- process.stdout.write(
254
- hits
255
- .map((h) =>
256
- c.enabled
257
- ? `${ui.render([ui.excerpt(`${h.page}.md${h.heading === '' ? '' : ` § ${h.heading}`}`, h.text)], c)}${paint('', h.url, c, { dim: true })}\n`
258
- : `── ${h.page}.md${h.heading === '' ? '' : ` § ${h.heading}`} · ${h.url}\n\n${h.text}\n`,
259
- )
260
- .join('\n'),
253
+ // Each hit an excerpt of the page, its link under it: off colour the
254
+ // bare markdown, as a pipe copies it.
255
+ ui.print(
256
+ 'out',
257
+ ...hits.map((h) =>
258
+ ui.excerpt(`${h.page}.md${h.heading === '' ? '' : ` § ${h.heading}`} · ${h.url}`, h.text),
259
+ ),
261
260
  )
262
261
  return 0
263
262
  }
package/src/cli/help.ts CHANGED
@@ -1,22 +1,15 @@
1
- import {
2
- CORE_VERBS,
3
- detectColors,
4
- errorDocument,
5
- maskedLine,
6
- nearest,
7
- paint,
8
- refusalNotice,
9
- ui,
10
- wantsJson,
11
- type ColorSupport,
12
- } from '../util/index.js'
1
+ import { CORE_VERBS, errorDocument, maskedLine, nearest, wantsJson } from '../util/index.js'
2
+ import * as ui from '../ui/index.js'
3
+ import { detectColors, paint, refusalNotice, type ColorSupport } from '../ui/index.js'
13
4
 
14
5
  export async function printHelp(
15
6
  pluginCommands: readonly string[] = [],
16
7
  verb?: string,
17
8
  ): Promise<void> {
18
9
  const text = verb === undefined ? helpText(pluginCommands) : verbHelpText(verb)
19
- process.stdout.write(paintHelp(text, helpColors(process.stdout, detectColors), paint))
10
+ // The reference a script greps: painted on a terminal only, and strips
11
+ // back to `text` exactly, so it goes out as written.
12
+ ui.data('out', paintHelp(text, helpColors(process.stdout, detectColors), paint))
20
13
  }
21
14
 
22
15
  /**
@@ -31,9 +24,6 @@ export function helpColors(
31
24
  return { enabled: stream.isTTY === true && detect(stream as NodeJS.WriteStream).enabled }
32
25
  }
33
26
 
34
- const VERB = '#06b6d4' // cyan-500 — the orchestrator's accent
35
- const TERM = '#22d3ee' // cyan-400 — a flag, a selector, an example
36
-
37
27
  /**
38
28
  * The reference with ANSI accents, line by line and never reflowed, so the
39
29
  * painted text strips back to `text` exactly: section headings bold (their
@@ -48,13 +38,12 @@ export function paintHelp(text: string, colors: ColorSupport, paint: Paint): str
48
38
  if (line === '') return line
49
39
  if (i === 0) {
50
40
  const [name, ...rest] = line.split(' ')
51
- return [paint(VERB, name!, colors, { bold: true }), ...rest].join(' ')
41
+ return [paint('info', name!, colors, true), ...rest].join(' ')
52
42
  }
53
43
  const heading = /^([A-Z][^:]*?)( \(for [^)]*\))?:$/.exec(line)
54
44
  if (heading !== null) {
55
- const qualifier =
56
- heading[2] === undefined ? '' : paint('', heading[2], colors, { dim: true })
57
- return `${paint('', heading[1]!, colors, { bold: true })}${qualifier}${paint('', ':', colors, { bold: true })}`
45
+ const qualifier = heading[2] === undefined ? '' : paint('muted', heading[2], colors)
46
+ return `${paint('strong', heading[1]!, colors)}${qualifier}${paint('strong', ':', colors)}`
58
47
  }
59
48
  // ` term description`: a term is a flag, a `vx` form, a selector.
60
49
  const row = /^(\s+)(\S(?:.*?\S)?)(\s{2,}\S.*)?$/.exec(line)
@@ -66,11 +55,11 @@ export function paintHelp(text: string, colors: ColorSupport, paint: Paint): str
66
55
  if (description === '' && !(CORE_VERBS as readonly string[]).includes(verb)) return line
67
56
  const head = `vx ${verb}`
68
57
  const tail = term!.slice(head.length)
69
- return `${indent}${paint(VERB, head, colors, { bold: true })}${tail === '' ? '' : paint(TERM, tail, colors)}${description}`
58
+ return `${indent}${paint('info', head, colors, true)}${tail === '' ? '' : paint('term', tail, colors)}${description}`
70
59
  }
71
60
  if (description === '') return line
72
61
  if (/^(-|\(|[\w-]+#)/.test(term!))
73
- return `${indent}${paint(TERM, term!, colors)}${description}`
62
+ return `${indent}${paint('term', term!, colors)}${description}`
74
63
  return line
75
64
  })
76
65
  .join('\n')
@@ -475,7 +464,7 @@ export function refuse(
475
464
  ): number {
476
465
  const line = `vx ${verb}: ${message}`
477
466
  ui.print('err', refusalNotice(`vx ${verb}`, message))
478
- if (wantsJson(args)) process.stdout.write(errorDocument(code, maskedLine(line)))
467
+ if (wantsJson(args)) ui.data('out', errorDocument(code, maskedLine(line)))
479
468
  return 1
480
469
  }
481
470
 
package/src/cli/index.ts CHANGED
@@ -5,21 +5,19 @@ import { VERSION } from '../version.js'
5
5
  import { checkSettingsEnv } from '../settings.js'
6
6
  import { CORE_VERBS, flagHint, printHelp, refuse, refusedWord, seeHelp } from './help.js'
7
7
  import {
8
- detectColors,
9
8
  errorDocument,
10
9
  hideEnvFromTasks,
11
- sayNotice,
12
10
  isFsRefusal,
13
11
  isOutOfFds,
14
12
  isUserError,
15
13
  maskedLine,
16
14
  nearest,
17
- refusalNotice,
18
15
  thrownMessage,
19
- ui,
20
16
  UserError,
21
17
  wantsJson,
22
18
  } from '../util/index.js'
19
+ import * as ui from '../ui/index.js'
20
+ import { sayNotice, refusalNotice } from '../ui/index.js'
23
21
 
24
22
  // Every verb is imported when invoked, `run` included, and so is the
25
23
  // plugin-verb lookup: each pulls in the orchestrator and the workspace
@@ -163,9 +161,7 @@ async function dispatch(command: string | undefined, rest: string[]): Promise<nu
163
161
  )
164
162
  }
165
163
  // Painted, and the same words piped: a script reads `vx <version>`.
166
- process.stdout.write(
167
- `${ui.inline([ui.cmd('vx'), ' ', ui.tone('info', VERSION)], detectColors(process.stdout))}\n`,
168
- )
164
+ ui.print('out', ui.line(ui.cmd('vx'), ' ', ui.tone('info', VERSION)))
169
165
  return 0
170
166
  }
171
167
  case 'run':
@@ -238,8 +234,7 @@ async function dispatch(command: string | undefined, rest: string[]): Promise<nu
238
234
  ): number => {
239
235
  sayNotice(tone, 'vx', text.replace(/^vx: /, '').trimEnd())
240
236
  // `vx --format json` puts the flag where the verb goes.
241
- if (wantsJson([command, ...rest]))
242
- process.stdout.write(errorDocument(code, maskedLine(json)))
237
+ if (wantsJson([command, ...rest])) ui.data('out', errorDocument(code, maskedLine(json)))
243
238
  return 1
244
239
  }
245
240
  const loadNote =
package/src/cli/info.ts CHANGED
@@ -9,7 +9,9 @@ import { type FlakyTask } from '../history/index.js'
9
9
  import { flagHint, formatValue, refuse, refusedWord, seeHelp } from './help.js'
10
10
  import { namedCacheDir, parseCacheDirFlag, warnToStderr } from './workspace-config.js'
11
11
  import { formatBytes } from './format.js'
12
- import { detectColors, MIN_BUN, ui, type ColorSupport } from '../util/index.js'
12
+ import { MIN_BUN } from '../util/index.js'
13
+ import * as ui from '../ui/index.js'
14
+ import { type ColorSupport } from '../ui/index.js'
13
15
  import { envName } from '../settings.js'
14
16
 
15
17
  interface InfoArgs {
@@ -55,15 +57,18 @@ export async function infoCmd(args: readonly string[]): Promise<number> {
55
57
  ...(parsed.cacheDir !== undefined ? { cacheDir: parsed.cacheDir } : {}),
56
58
  warn: warnToStderr,
57
59
  })
58
- process.stdout.write(
59
- parsed.format === 'json'
60
- ? `${JSON.stringify(facts)}\n`
61
- : `${renderInfo(facts, detectColors(process.stdout))}\n`,
62
- )
60
+ if (parsed.format === 'json') ui.data('out', `${JSON.stringify(facts)}\n`)
61
+ else ui.print('out', infoReport(facts))
63
62
  return 0
64
63
  }
65
64
 
65
+ /** `vx info` as text: the report's lines for `colors`. */
66
66
  export function renderInfo(f: InfoFacts, colors: ColorSupport = { enabled: false }): string {
67
+ return infoReport(f).lines(colors).join('\n')
68
+ }
69
+
70
+ /** The `vx info` report: one field a fact. */
71
+ function infoReport(f: InfoFacts): ui.Block {
67
72
  const rows: [string, string][] = [
68
73
  [
69
74
  'vx',
@@ -125,7 +130,7 @@ export function renderInfo(f: InfoFacts, colors: ColorSupport = { enabled: false
125
130
  typeof f.lockError === 'string' ? `refused: ${f.lockError}` : f.lockfile ? 'yes' : 'no',
126
131
  ],
127
132
  ]
128
- return ui.report('vx info', rows).lines(colors).join('\n')
133
+ return ui.report('vx info', rows)
129
134
  }
130
135
 
131
136
  /** `812 (1.2 GB)`, or why the index cannot be read and its fix. */
@@ -195,6 +200,9 @@ function trimCores(n: number): string {
195
200
 
196
201
  /** `2 — app#test (3 of 7 runs failed on unchanged inputs); api#e2e (1 of 4)`, or `none`. */
197
202
  function describeProjects(f: InfoFacts): string {
203
+ // The count is the lock's: with none read, 0 would say the workspace has no project.
204
+ if (f.lockError != null) return '— (the lock is refused)'
205
+ if (!f.lockfile) return '— (no lock: `vx lock`)'
198
206
  return `${f.projects} (${f.tasks} task${f.tasks === 1 ? '' : 's'})`
199
207
  }
200
208
 
package/src/cli/init.ts CHANGED
@@ -23,17 +23,14 @@ import {
23
23
  } from './upgrade.js'
24
24
  import { VERSION } from '../version.js'
25
25
  import {
26
- detectColors,
27
26
  DOT_VX_GITATTRIBUTES,
28
27
  DOT_VX_GITIGNORE,
29
- paint,
30
28
  refuseSymlink,
31
- sayNotice,
32
- SUCCESS,
33
- ui,
34
29
  UserError,
35
30
  writeNoFollow,
36
31
  } from '../util/index.js'
32
+ import * as ui from '../ui/index.js'
33
+ import { sayNotice } from '../ui/index.js'
37
34
  import {
38
35
  findWorkspaceRoot,
39
36
  PROJECT_CONFIG_FILENAMES,
@@ -279,8 +276,9 @@ async function install(
279
276
  let said = ''
280
277
  const decoder = new TextDecoder()
281
278
  for await (const chunk of proc.stderr) {
282
- process.stderr.write(chunk)
283
- said += decoder.decode(chunk, { stream: true })
279
+ const text = decoder.decode(chunk, { stream: true })
280
+ ui.data('err', text)
281
+ said += text
284
282
  }
285
283
  const code = await proc.exited
286
284
  if (code !== 0) {
@@ -351,13 +349,22 @@ async function scaffoldPlugin(seam: string, root: string, args: InitArgs): Promi
351
349
  const factory = /^export function (\w+)/m.exec(template.plugin)![1]!
352
350
  const workspaceFile =
353
351
  WORKSPACE_CONFIG_FILENAMES.find((n) => existsSync(path.join(root, n))) ?? '.vx/workspace.ts'
354
- const c = detectColors(process.stdout)
355
- const dim = (t: string): string => paint('', t, c, { dim: true })
356
- const bold = (t: string): string => paint('', t, c, { bold: true })
357
- process.stdout.write(
358
- `${paint(SUCCESS, args.dry ? 'would write' : 'wrote', c, { bold: true })} ${files.map(([rel]) => bold(rel)).join(' and ')}\n` +
359
- `${dim('Declare it in')} ${workspaceFile}${dim(':')} ${bold(`import { ${factory} } from '../plugins/${seam}.ts'`)}${dim(', then')} ${bold(`plugins: [${factory}(…)]`)}\n` +
360
- `${dim('Test it:')} ${bold(`bun test plugins/${seam}.test.ts`)} ${dim('(needs @vzn/vx installed)')}\n`,
352
+ const names = files.flatMap(([rel], i) => [...(i > 0 ? [' and '] : []), ui.strong(rel)])
353
+ ui.print('out', ui.line(ui.tone('ok', args.dry ? 'would write' : 'wrote', true), ' ', ...names))
354
+ ui.print(
355
+ 'out',
356
+ ui.line(
357
+ 'Declare it in ',
358
+ ui.path(workspaceFile),
359
+ ': ',
360
+ ui.cmd(`import { ${factory} } from '../plugins/${seam}.ts'`),
361
+ ', then ',
362
+ ui.cmd(`plugins: [${factory}(…)]`),
363
+ ),
364
+ )
365
+ ui.print(
366
+ 'out',
367
+ ui.line('Test it: ', ui.cmd(`bun test plugins/${seam}.test.ts`), ' (needs @vzn/vx installed)'),
361
368
  )
362
369
  return 0
363
370
  }
@@ -460,7 +467,8 @@ async function starter(
460
467
  const declare = `declare a task in ${kept.find((f) => f.startsWith('vx.config.')) ?? config}, then vx lock && vx run <task>`
461
468
  const next = `${noGit ? 'git init, ' : ''}${hasScripts(pkgRoot) ? `bunx @vzn/vx-migrate${args.mjs ? ' --mjs' : ''} to make package.json scripts tasks, or ${declare}` : declare}`
462
469
  if (args.json === true) {
463
- process.stdout.write(
470
+ ui.data(
471
+ 'out',
464
472
  `${JSON.stringify({ dry: true, source: 'no turbo.json or nx.json', files, kept, replaced, todos: [], notes: [], next, installs })}\n`,
465
473
  )
466
474
  return 0