@vzn/vx 0.0.652 → 0.0.653

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 (167) hide show
  1. package/README.md +2 -2
  2. package/launcher.sh +2 -1
  3. package/package.json +5 -5
  4. package/schemas/error.json +1 -1
  5. package/schemas/show.json +1 -0
  6. package/skills/vx/SKILL.md +3 -3
  7. package/src/bin.ts +25 -3
  8. package/src/cache/archive.ts +196 -31
  9. package/src/cache/cache.ts +863 -225
  10. package/src/cache/checkpointer.ts +102 -0
  11. package/src/cache/file-hashes.ts +2 -1
  12. package/src/cache/fold-memo.ts +24 -4
  13. package/src/cache/fs-clock.ts +26 -0
  14. package/src/cache/git-inputs.ts +188 -22
  15. package/src/cache/history-stream.ts +22 -5
  16. package/src/cache/index.ts +10 -1
  17. package/src/cache/inputs.ts +573 -152
  18. package/src/cache/key-fold.ts +1 -1
  19. package/src/cache/layer.ts +23 -10
  20. package/src/cache/layered-cache.ts +28 -7
  21. package/src/cache/output-index.ts +106 -44
  22. package/src/cache/plugin-cache.ts +2 -2
  23. package/src/cache/policy.ts +6 -2
  24. package/src/cache/remote-lane.ts +3 -1
  25. package/src/cache/run-history.ts +50 -10
  26. package/src/cache/schema.ts +42 -9
  27. package/src/cli/cache.ts +147 -34
  28. package/src/cli/completions.ts +2 -2
  29. package/src/cli/core-alias.ts +9 -2
  30. package/src/cli/docs.ts +4 -2
  31. package/src/cli/help.ts +25 -2
  32. package/src/cli/index.ts +38 -7
  33. package/src/cli/info.ts +4 -3
  34. package/src/cli/init.ts +55 -20
  35. package/src/cli/last.ts +184 -41
  36. package/src/cli/lock.ts +187 -24
  37. package/src/cli/plan-format.ts +24 -21
  38. package/src/cli/plugin-commands.ts +6 -1
  39. package/src/cli/plugin-templates.ts +1 -1
  40. package/src/cli/run.ts +68 -34
  41. package/src/cli/select.ts +160 -64
  42. package/src/cli/show.ts +63 -18
  43. package/src/cli/task-verb.ts +12 -7
  44. package/src/cli/upgrade.ts +210 -76
  45. package/src/cli/watch-filter.ts +88 -15
  46. package/src/cli/watch-fs.ts +34 -47
  47. package/src/cli/watch-judge.ts +72 -8
  48. package/src/cli/watch-set.ts +6 -4
  49. package/src/cli/watch.ts +179 -25
  50. package/src/cli/why.ts +107 -33
  51. package/src/cli/workspace-config.ts +11 -1
  52. package/src/config.ts +9 -0
  53. package/src/exec/env.ts +3 -1
  54. package/src/exec/executor.ts +76 -15
  55. package/src/exec/kill-tree.ts +7 -7
  56. package/src/exec/runner.ts +270 -108
  57. package/src/exec/sandbox-binds.ts +109 -18
  58. package/src/exec/sandbox-runtime.ts +134 -37
  59. package/src/exec/sandbox-violations.ts +250 -46
  60. package/src/exec/spawn-off.ts +10 -5
  61. package/src/exec/warm-shell.ts +29 -7
  62. package/src/graph/folds.ts +89 -16
  63. package/src/graph/index.ts +8 -0
  64. package/src/graph/priorities.ts +8 -5
  65. package/src/graph/scheduler.ts +173 -38
  66. package/src/graph/stand-ins.ts +36 -0
  67. package/src/graph/task-graph.ts +327 -141
  68. package/src/graph/task-status.ts +5 -1
  69. package/src/handoff.ts +45 -10
  70. package/src/history/failure-mode.ts +15 -5
  71. package/src/history/history.ts +103 -30
  72. package/src/history/index.ts +1 -0
  73. package/src/history/metrics.ts +189 -69
  74. package/src/index.ts +2 -1
  75. package/src/orchestrator/admission.ts +13 -9
  76. package/src/orchestrator/affected-tasks.ts +35 -13
  77. package/src/orchestrator/command-windows.ts +8 -5
  78. package/src/orchestrator/deferred-outputs.ts +41 -12
  79. package/src/orchestrator/doctor.ts +21 -9
  80. package/src/orchestrator/download-policy.ts +3 -3
  81. package/src/orchestrator/excluded-keys.ts +14 -2
  82. package/src/orchestrator/execute-args.ts +9 -2
  83. package/src/orchestrator/execute-task.ts +564 -123
  84. package/src/orchestrator/graph-memo.ts +26 -4
  85. package/src/orchestrator/graph-stage.ts +27 -4
  86. package/src/orchestrator/held-servers.ts +36 -14
  87. package/src/orchestrator/hit-restore.ts +136 -87
  88. package/src/orchestrator/index.ts +4 -1
  89. package/src/orchestrator/keyed-projects.ts +25 -0
  90. package/src/orchestrator/local-floor.ts +21 -3
  91. package/src/orchestrator/lockfile-claim.ts +30 -4
  92. package/src/orchestrator/miss-save.ts +33 -53
  93. package/src/orchestrator/options.ts +13 -0
  94. package/src/orchestrator/output-host.ts +16 -4
  95. package/src/orchestrator/output-log.ts +1 -54
  96. package/src/orchestrator/persistent-task.ts +31 -17
  97. package/src/orchestrator/persistent.ts +152 -15
  98. package/src/orchestrator/placement.ts +81 -15
  99. package/src/orchestrator/plan.ts +37 -15
  100. package/src/orchestrator/plugin-host.ts +232 -108
  101. package/src/orchestrator/plugin.ts +53 -27
  102. package/src/orchestrator/prepare.ts +194 -38
  103. package/src/orchestrator/projects.ts +89 -10
  104. package/src/orchestrator/read-only.ts +115 -0
  105. package/src/orchestrator/run-context.ts +6 -2
  106. package/src/orchestrator/run-failures.ts +43 -14
  107. package/src/orchestrator/run-id.ts +13 -2
  108. package/src/orchestrator/run-keys.ts +87 -28
  109. package/src/orchestrator/run-live.ts +16 -13
  110. package/src/orchestrator/run-lock.ts +27 -4
  111. package/src/orchestrator/run-records.ts +12 -10
  112. package/src/orchestrator/run-report.ts +2 -1
  113. package/src/orchestrator/run-stop.ts +17 -9
  114. package/src/orchestrator/run.ts +443 -107
  115. package/src/orchestrator/sandbox-fix.ts +40 -12
  116. package/src/orchestrator/sandbox-request.ts +286 -59
  117. package/src/orchestrator/save-lane.ts +24 -13
  118. package/src/orchestrator/shell-verdict.ts +20 -1
  119. package/src/orchestrator/signals.ts +103 -8
  120. package/src/orchestrator/stable-keys.ts +17 -9
  121. package/src/orchestrator/task-artifacts.ts +15 -2
  122. package/src/orchestrator/task-hash.ts +187 -30
  123. package/src/orchestrator/telemetry-host.ts +25 -6
  124. package/src/orchestrator/telemetry.ts +80 -12
  125. package/src/orchestrator/upstream.ts +3 -1
  126. package/src/report/events.ts +20 -3
  127. package/src/report/forecast.ts +103 -37
  128. package/src/report/framed-output.ts +165 -157
  129. package/src/report/index.ts +2 -1
  130. package/src/report/logger.ts +107 -30
  131. package/src/report/plain-output.ts +13 -9
  132. package/src/report/status-line.ts +28 -55
  133. package/src/report/summary.ts +57 -14
  134. package/src/settings.ts +42 -6
  135. package/src/ui/components.ts +36 -42
  136. package/src/ui/index.ts +3 -1
  137. package/src/ui/live-region.ts +67 -8
  138. package/src/ui/terminal.ts +12 -0
  139. package/src/ui/theme.ts +16 -1
  140. package/src/util/config-frame.ts +17 -3
  141. package/src/util/edit-distance.ts +17 -1
  142. package/src/util/errors.ts +23 -7
  143. package/src/util/foreign-cache-dir.ts +60 -0
  144. package/src/util/git-ignored.ts +59 -5
  145. package/src/util/index.ts +15 -1
  146. package/src/util/paths.ts +50 -6
  147. package/src/util/plain-text.ts +19 -0
  148. package/src/util/secret-mask.ts +57 -1
  149. package/src/util/settle.ts +16 -0
  150. package/src/util/size.ts +12 -2
  151. package/src/util/write-file.ts +86 -9
  152. package/src/workspace/affected.ts +225 -27
  153. package/src/workspace/config-bytecode.ts +23 -4
  154. package/src/workspace/config-eval.ts +90 -16
  155. package/src/workspace/config-imports.ts +96 -41
  156. package/src/workspace/config-schema.ts +126 -25
  157. package/src/workspace/dependencies-lock.ts +7 -14
  158. package/src/workspace/filter.ts +10 -1
  159. package/src/workspace/index.ts +3 -1
  160. package/src/workspace/json-data.ts +34 -8
  161. package/src/workspace/lockfile.ts +86 -37
  162. package/src/workspace/migration.ts +46 -25
  163. package/src/workspace/module-state.ts +62 -0
  164. package/src/workspace/nested-dirs.ts +8 -0
  165. package/src/workspace/project-loader.ts +276 -32
  166. package/src/workspace/repo-id.ts +29 -6
  167. package/src/workspace/workspace.ts +190 -30
package/src/cli/show.ts CHANGED
@@ -6,11 +6,12 @@
6
6
  // resolved config. `<pkg>#<task>`: one task. `<task>`: that task in every
7
7
  // project declaring it.
8
8
 
9
+ import { existsSync } from 'node:fs'
9
10
  import path from 'node:path'
10
11
  import type { ProjectConfig, TaskConfig } from '../config.js'
11
12
  import { declaredTask } from '../graph/index.js'
12
13
  import { isDefaultBuild } from '../graph/index.js'
13
- import { affectedTaskProjects } from '../orchestrator/index.js'
14
+ import { affectedTaskProjects, projectAtDir, unlockedDir } from '../orchestrator/index.js'
14
15
  import { flagHint, formatValue, refuse, seeHelp } from './help.js'
15
16
  import {
16
17
  listed,
@@ -22,7 +23,7 @@ import {
22
23
  } from '../util/index.js'
23
24
  import * as ui from '../ui/index.js'
24
25
  import { sayNotice } from '../ui/index.js'
25
- import { affectedFilterFor, resolveFilters } from './select.js'
26
+ import { type AffectedFilter, affectedFilterFor, resolveFilters } from './select.js'
26
27
  import {
27
28
  discoverCliProjects,
28
29
  loadCliProjects,
@@ -34,6 +35,7 @@ import {
34
35
  buildPackageGraph,
35
36
  EMPTY_LOCK,
36
37
  findWorkspaceRoot,
38
+ PROJECT_CONFIG_FILENAMES,
37
39
  type ProjectEntry,
38
40
  type ProjectMeta,
39
41
  } from '../workspace/index.js'
@@ -113,6 +115,22 @@ export async function showCmd(args: readonly string[]): Promise<number> {
113
115
  }
114
116
  parsed.target = `${rootMeta.name}${parsed.target.slice(2)}`
115
117
  }
118
+ // A project's directory typed for its name: by edit distance, `p/a`
119
+ // hinted `p/b`, another project.
120
+ const atDir = (typed: string, suffix: string): string | undefined => {
121
+ const name = projectAtDir(typed, root, metas)
122
+ return name === undefined ? undefined : ` — did you mean ${name}${suffix}?`
123
+ }
124
+ // A directory whose config came after the lock: the line `vx run` gives,
125
+ // not "unknown project" and a guess.
126
+ const refuseUnlocked = async (typed: string): Promise<void> => {
127
+ const config = await unlockedDir(typed, metas, root)
128
+ if (config === undefined) return
129
+ throw new UserError(
130
+ `${parsed.target}: ${relPosix(root, config)} is not in the lock yet — run \`vx lock\``,
131
+ 'VX_E_LOCK',
132
+ )
133
+ }
116
134
  const hashAt = parsed.target?.indexOf('#') ?? -1
117
135
  const projectName =
118
136
  parsed.target === undefined
@@ -136,8 +154,9 @@ export async function showCmd(args: readonly string[]): Promise<number> {
136
154
  }
137
155
  if (projectName !== undefined && (taskName !== undefined || byName.has(projectName))) {
138
156
  if (!byName.has(projectName)) {
157
+ await refuseUnlocked(projectName)
139
158
  throw new UserError(
140
- `vx show: unknown project: "${projectName}"${suggest(projectName, [...byName.keys()], '', 'projects')}`,
159
+ `vx show: unknown project: "${projectName}"${atDir(projectName, `#${taskName}`) ?? suggest(projectName, [...byName.keys()], '', 'projects')}`,
141
160
  'VX_E_UNKNOWN_PROJECT',
142
161
  )
143
162
  }
@@ -161,16 +180,16 @@ export async function showCmd(args: readonly string[]): Promise<number> {
161
180
  )
162
181
  }
163
182
  const raw = [...parsed.filters]
164
- let affected: string | undefined
183
+ let affected: AffectedFilter | undefined
165
184
  if (parsed.affected !== undefined) {
166
185
  const f = await affectedFilterFor(
167
186
  process.cwd(),
168
187
  parsed.affected,
169
188
  'drop --affected to list them all',
170
189
  )
171
- if (typeof f === 'object') throw new UserError(`vx show: ${f.error}`, f.code ?? 'VX_E_USAGE')
190
+ if ('error' in f) throw new UserError(`vx show: ${f.error}`, f.code ?? 'VX_E_USAGE')
172
191
  affected = f
173
- raw.unshift(f)
192
+ raw.unshift(f.filter)
174
193
  }
175
194
  const r = await resolveFilters(process.cwd(), raw, {}, affected, 'show')
176
195
  if ('error' in r) throw new UserError(`vx show: ${r.error}`, r.code ?? 'VX_E_USAGE')
@@ -203,13 +222,14 @@ export async function showCmd(args: readonly string[]): Promise<number> {
203
222
  declares(declaredTask(p.config, projectName!)),
204
223
  )
205
224
  if (declaring.length === 0) {
225
+ await refuseUnlocked(projectName!)
206
226
  const names = new Set<string>()
207
227
  for (const p of projects.values())
208
228
  for (const [t, task] of Object.entries(p.config.tasks ?? {}))
209
229
  if (declares(task)) names.add(t)
210
230
  // `nx show projects` lists them; here a bare `vx show` does.
211
231
  const nx = projectName === 'projects' ? ' (`nx show projects` is `vx show` here)' : ''
212
- const unknown = `vx show: unknown project or task: "${projectName}"${nx}${suggest(projectName!, [...byName.keys(), ...names], '', 'projects and tasks')}`
232
+ const unknown = `vx show: unknown project or task: "${projectName}"${nx}${atDir(projectName!, '') ?? suggest(projectName!, [...byName.keys(), ...names], '', 'projects and tasks')}`
213
233
  const base =
214
234
  parsed.spacedBase === projectName
215
235
  ? `${unknown.endsWith('.') ? '' : '.'} \`--affected\` takes its base with '=': --affected=${projectName}`
@@ -258,7 +278,7 @@ export async function showCmd(args: readonly string[]): Promise<number> {
258
278
  const dir = projectDir(root, meta)
259
279
 
260
280
  if (taskName === undefined) {
261
- emit(renderProject(meta.name, dir, config, meta.configPath !== null, parsed.format))
281
+ emit(renderProject(meta.name, dir, config, configNote(meta), parsed.format))
262
282
  return 0
263
283
  }
264
284
 
@@ -369,7 +389,9 @@ function renderList(
369
389
  tags: projects.get(meta.name)?.config.tags ?? [],
370
390
  tasks: Object.keys(tasks),
371
391
  onlyDefault: Object.values(tasks).every((t) => isDefaultBuild(t)),
392
+ hasDefault: Object.values(tasks).some((t) => isDefaultBuild(t)),
372
393
  configured: meta.configPath !== null,
394
+ note: configNote(meta),
373
395
  }
374
396
  })
375
397
  if (format === 'json') {
@@ -383,23 +405,38 @@ function renderList(
383
405
  // A package with no config file has only tasks a plugin gave it, or the
384
406
  // default `build` a configured dependant's closure gave it.
385
407
  const tasks = r.configured
386
- ? count
387
- : n === 0
388
- ? 'no vx config'
389
- : r.onlyDefault
390
- ? `${count}, default build (no vx config)`
391
- : `${count} from plugins (no vx config)`
408
+ ? r.tasks.length > 0 && r.hasDefault
409
+ ? `${count}, default build`
410
+ : count
411
+ : r.note !== null && r.note !== 'none'
412
+ ? r.note
413
+ : n === 0
414
+ ? 'no vx config'
415
+ : r.onlyDefault
416
+ ? `${count}, default build (no vx config)`
417
+ : `${count} from plugins (no vx config)`
392
418
  return [ui.id(r.name), r.dir, tasks, ...(anyTags ? [r.tags.join(', ')] : [])]
393
419
  })
394
420
  const columns = ['project', 'dir', 'tasks', ...(anyTags ? ['tags'] : [])]
395
421
  return [ui.section('Projects', String(rows.length), ui.table(columns, cells))]
396
422
  }
397
423
 
424
+ /**
425
+ * Null for a project the lock holds a config for; else `none`, or a config
426
+ * written after the lock: "no vx config" sent a user who had just written
427
+ * one looking for a typo.
428
+ */
429
+ function configNote(meta: ProjectMeta): string | null {
430
+ if (meta.configPath !== null) return null
431
+ const file = PROJECT_CONFIG_FILENAMES.find((n) => existsSync(path.join(meta.dir, n)))
432
+ return file === undefined ? 'none' : `${file} not locked — run vx lock`
433
+ }
434
+
398
435
  function renderProject(
399
436
  name: string,
400
437
  dir: string,
401
438
  config: ProjectConfig | null,
402
- hasConfigFile: boolean,
439
+ note: string | null,
403
440
  format: 'pretty' | 'json',
404
441
  ): Shown {
405
442
  if (format === 'json') {
@@ -414,11 +451,16 @@ function renderProject(
414
451
  ['dir', dir],
415
452
  ...(tags.length > 0 ? [['tags', tags.join(', ')] as const] : []),
416
453
  // A config-less package a dependant reaches has the default `build` (X-9).
417
- ...(hasConfigFile ? [] : [['config', 'none'] as const]),
418
- ...(tasks.length === 0 && hasConfigFile ? [['tasks', 'none declared'] as const] : []),
454
+ ...(note === null ? [] : [['config', note] as const]),
455
+ ...(tasks.length === 0 && note === null ? [['tasks', 'none declared'] as const] : []),
419
456
  ]
420
457
  const blocks = tasks.map(([taskName, task]) =>
421
- joined([ui.text(ui.id(`${name}#${taskName}`)), ui.fields(taskRows(task))]),
458
+ joined([
459
+ isDefaultBuild(task)
460
+ ? ui.line(' ', ui.id(`${name}#${taskName}`), ui.dim(' default'))
461
+ : ui.text(ui.id(`${name}#${taskName}`)),
462
+ ui.fields(taskRows(task)),
463
+ ]),
422
464
  )
423
465
  return [ui.section(name, ui.fields(head), ...blocks)]
424
466
  }
@@ -516,5 +558,8 @@ function taskRows(raw: TaskConfig): ui.FieldRow[] {
516
558
  add('outputs.workspaceFiles', list(outputs.workspaceFiles))
517
559
  add('outputs.clean', outputs.clean === undefined ? undefined : outputs.clean ? 'yes' : 'no')
518
560
  }
561
+ // The first question `vx why` raises ("declares no `cache: true`"), which
562
+ // only --format json answered.
563
+ if (task.cache === true) add('cache', 'yes')
519
564
  return rows
520
565
  }
@@ -68,20 +68,25 @@ export async function nxTargetHint(tasks: readonly string[], cwd: string): Promi
68
68
  }
69
69
 
70
70
  /**
71
- * The task names `vx run <task>` can take here: the cwd project's, else
72
- * every project's, sorted; null when no workspace loads. What a run with
73
- * no task and no terminal for the picker names instead of a guess.
71
+ * The task names `vx run <task>` can take in the asked scope (`asked`: the
72
+ * projects `--filter` / `--affected` selected, or `all`), else the cwd
73
+ * project's, else every project's, sorted; null when no workspace loads.
74
+ * What a run with no task and no terminal for the picker names instead of
75
+ * a guess.
74
76
  */
75
- export async function taskNamesHere(cwd: string): Promise<string[] | null> {
77
+ export async function taskNamesHere(
78
+ cwd: string,
79
+ asked?: ReadonlySet<string> | 'all',
80
+ ): Promise<{ names: string[]; inProject: boolean } | null> {
76
81
  const projects = await workspaceProjects(cwd)
77
82
  if (projects === null) return null
78
- const own = await findCwdProject(cwd)
83
+ const own = asked === undefined ? await findCwdProject(cwd) : null
79
84
  const names = new Set<string>()
80
85
  for (const p of projects.values())
81
- if (own === null || p.name === own)
86
+ if (typeof asked === 'object' ? asked.has(p.name) : own === null || p.name === own)
82
87
  for (const [t, task] of Object.entries(p.config.tasks ?? {}))
83
88
  if (!isDefaultBuild(task)) names.add(t)
84
- return [...names].sort()
89
+ return { names: [...names].sort(), inProject: own !== null }
85
90
  }
86
91
 
87
92
  /**