@vzn/vx 0.0.638 → 0.0.640

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 (134) hide show
  1. package/README.md +1 -1
  2. package/docs/caching.md +152 -95
  3. package/docs/cli.md +225 -372
  4. package/docs/execution.md +22 -47
  5. package/docs/features.md +14 -16
  6. package/docs/patterns.md +6 -8
  7. package/docs/schema.md +268 -215
  8. package/docs/security.md +1 -1
  9. package/package.json +5 -5
  10. package/schemas/last.json +7 -4
  11. package/schemas/show.json +14 -14
  12. package/skills/vx/SKILL.md +10 -7
  13. package/src/cache/cache.ts +44 -11
  14. package/src/cache/file-hashes.ts +7 -12
  15. package/src/cache/fs-clock.ts +62 -0
  16. package/src/cache/git-inputs.ts +8 -2
  17. package/src/cache/index.ts +5 -2
  18. package/src/cache/inputs.ts +154 -83
  19. package/src/cache/key-fold.ts +23 -3
  20. package/src/cache/layer.ts +33 -18
  21. package/src/cache/layered-cache.ts +108 -315
  22. package/src/cache/linked-inputs.ts +87 -0
  23. package/src/cache/plugin-cache.ts +2 -10
  24. package/src/cache/remote-lane.ts +383 -0
  25. package/src/cache/run-history.ts +24 -8
  26. package/src/cache/run-store.ts +4 -4
  27. package/src/cache/schema.ts +6 -4
  28. package/src/cache/scoped-cache.ts +7 -0
  29. package/src/cli/completions.ts +2 -2
  30. package/src/cli/foreign-flags.ts +11 -23
  31. package/src/cli/help.ts +6 -31
  32. package/src/cli/info.ts +2 -1
  33. package/src/cli/init.ts +23 -11
  34. package/src/cli/last.ts +30 -4
  35. package/src/cli/plan-format.ts +5 -58
  36. package/src/cli/plugin-commands.ts +2 -2
  37. package/src/cli/plugin-templates.ts +2 -2
  38. package/src/cli/run.ts +23 -185
  39. package/src/cli/select.ts +16 -6
  40. package/src/cli/show.ts +15 -6
  41. package/src/cli/upgrade.ts +215 -55
  42. package/src/cli/watch-cycle.ts +2 -1
  43. package/src/cli/watch-filter.ts +22 -6
  44. package/src/cli/watch-judge.ts +49 -5
  45. package/src/cli/watch-set.ts +18 -8
  46. package/src/cli/watch.ts +103 -16
  47. package/src/cli/why.ts +9 -7
  48. package/src/config.ts +19 -21
  49. package/src/exec/executor.ts +5 -5
  50. package/src/exec/sandbox-runtime.ts +1 -1
  51. package/src/exec/sandbox-violations.ts +3 -1
  52. package/src/graph/dependency-spec.ts +2 -2
  53. package/src/graph/folds.ts +236 -0
  54. package/src/graph/index.ts +17 -2
  55. package/src/graph/priorities.ts +1 -1
  56. package/src/graph/scheduler.ts +45 -31
  57. package/src/graph/task-graph.ts +190 -58
  58. package/src/graph/task-status.ts +95 -0
  59. package/src/{orchestrator → history}/failure-mode.ts +1 -1
  60. package/src/{orchestrator → history}/history.ts +1 -1
  61. package/src/history/index.ts +48 -0
  62. package/src/{orchestrator → history}/metrics.ts +7 -20
  63. package/src/{orchestrator → history}/miss-reason.ts +1 -1
  64. package/src/index.ts +11 -29
  65. package/src/orchestrator/affected-tasks.ts +20 -19
  66. package/src/orchestrator/doctor.ts +1 -1
  67. package/src/orchestrator/download-policy.ts +16 -22
  68. package/src/orchestrator/excluded-keys.ts +4 -5
  69. package/src/orchestrator/execute-args.ts +80 -0
  70. package/src/orchestrator/execute-task.ts +50 -411
  71. package/src/orchestrator/fingerprint-watch.ts +2 -2
  72. package/src/orchestrator/index.ts +2 -73
  73. package/src/orchestrator/keyed-projects.ts +12 -51
  74. package/src/orchestrator/local-shortcircuit.ts +1 -1
  75. package/src/orchestrator/miss-save.ts +33 -3
  76. package/src/orchestrator/options.ts +1 -36
  77. package/src/orchestrator/output-host.ts +4 -3
  78. package/src/orchestrator/persistent-task.ts +321 -0
  79. package/src/orchestrator/persistent.ts +130 -3
  80. package/src/orchestrator/placement.ts +5 -5
  81. package/src/orchestrator/plan.ts +19 -6
  82. package/src/orchestrator/plugin-host.ts +129 -69
  83. package/src/orchestrator/plugin.ts +18 -8
  84. package/src/orchestrator/prepare.ts +40 -34
  85. package/src/orchestrator/projects.ts +7 -10
  86. package/src/orchestrator/remote-prefetch.ts +14 -1
  87. package/src/orchestrator/run-artifacts.ts +5 -177
  88. package/src/orchestrator/run-context.ts +55 -0
  89. package/src/orchestrator/run-failures.ts +15 -3
  90. package/src/orchestrator/run-records.ts +35 -3
  91. package/src/orchestrator/run-report.ts +151 -171
  92. package/src/orchestrator/run-settings.ts +1 -16
  93. package/src/orchestrator/run-steps.ts +57 -0
  94. package/src/orchestrator/run-stop.ts +153 -0
  95. package/src/orchestrator/run.ts +293 -604
  96. package/src/orchestrator/sandbox-fix.ts +1 -17
  97. package/src/orchestrator/sandbox-request.ts +2 -2
  98. package/src/orchestrator/signals.ts +14 -6
  99. package/src/orchestrator/stable-keys.ts +164 -27
  100. package/src/orchestrator/steps.ts +70 -0
  101. package/src/orchestrator/task-hash.ts +34 -25
  102. package/src/orchestrator/task-log-buffer.ts +1 -1
  103. package/src/orchestrator/task-process.ts +94 -0
  104. package/src/orchestrator/telemetry-host.ts +4 -4
  105. package/src/orchestrator/telemetry.ts +14 -86
  106. package/src/orchestrator/upstream.ts +4 -181
  107. package/src/{orchestrator → report}/events.ts +41 -8
  108. package/src/{orchestrator → report}/forecast.ts +35 -2
  109. package/src/{orchestrator → report}/framed-output.ts +40 -34
  110. package/src/report/index.ts +36 -0
  111. package/src/{orchestrator → report}/logger.ts +158 -20
  112. package/src/{orchestrator → report}/status-line.ts +88 -7
  113. package/src/{orchestrator → report}/summary.ts +126 -35
  114. package/src/report/tally.ts +83 -0
  115. package/src/settings.ts +12 -39
  116. package/src/util/colors.ts +44 -4
  117. package/src/util/git-ignored.ts +143 -0
  118. package/src/util/index.ts +19 -4
  119. package/src/util/paths.ts +1 -1
  120. package/src/util/report.ts +13 -5
  121. package/src/util/settle.ts +14 -39
  122. package/src/util/tail.ts +1 -1
  123. package/src/util/ui.ts +17 -6
  124. package/src/workspace/affected.ts +81 -2
  125. package/src/workspace/config-imports.ts +1 -1
  126. package/src/workspace/config-schema.ts +142 -143
  127. package/src/workspace/lockfile.ts +1 -1
  128. package/src/workspace/migration.ts +19 -3
  129. package/schemas/summary.json +0 -133
  130. package/src/cache/chained-cache.ts +0 -280
  131. package/src/orchestrator/tally.ts +0 -112
  132. /package/src/{orchestrator → report}/path-links.ts +0 -0
  133. /package/src/{orchestrator → report}/plain-output.ts +0 -0
  134. /package/src/{orchestrator → report}/terminal-signals.ts +0 -0
package/src/cli/run.ts CHANGED
@@ -1,18 +1,14 @@
1
- import { appendFileSync, mkdirSync } from 'node:fs'
2
- import path from 'node:path'
3
1
  import { isatty } from 'node:tty'
4
2
  import { cacheScopeError, findWorkspaceRoot, isCacheScope } from '../workspace/index.js'
5
3
  import { translateForeign } from './foreign-flags.js'
6
- import { flagHint, formatValue, refuse, seeHelp } from './help.js'
4
+ import { flagHint, refuse, seeHelp } from './help.js'
7
5
  import {
8
6
  planRun,
9
- formatRunReportMarkdown,
10
- projectOutcome,
11
7
  run as runOrchestrator,
12
8
  shellQuote,
13
9
  type RunOptions,
14
10
  } from '../orchestrator/index.js'
15
- import type { ContinueMode, TaskOutcome } from '../graph/index.js'
11
+ import type { ContinueMode } from '../graph/index.js'
16
12
  import {
17
13
  applyCacheFlags,
18
14
  type CacheFlags,
@@ -25,7 +21,7 @@ import { affectedFilterFor, findCwdSelection, pickTask, resolveFilters } from '.
25
21
  import { nxTargetHint, taskNamesHere } from './task-verb.js'
26
22
  import { parseDecimalInt, sayNotice, sayLine } from '../util/index.js'
27
23
  import { kindHint, parseSettingValue, type Setting, SETTINGS, settingRefusal } from '../settings.js'
28
- import { formatGraphDot, formatPlanJson, formatPlanText } from './plan-format.js'
24
+ import { formatPlanJson, formatPlanText } from './plan-format.js'
29
25
 
30
26
  export interface RunArgs {
31
27
  /** Failure propagation (`--continue[=never|deps-ok|always]`). */
@@ -71,10 +67,6 @@ export interface RunArgs {
71
67
  forwardArgs: string[]
72
68
  verbosity: number
73
69
  dry: 'text' | 'json' | undefined
74
- /** `--format json`: the run's result document on stdout, every other line on stderr. */
75
- format: 'pretty' | 'json'
76
- graph: string | undefined
77
- summarize: string | undefined
78
70
  profile: string | undefined
79
71
  /**
80
72
  * `--affected[=<base>]`. When undefined the flag wasn't passed.
@@ -82,16 +74,6 @@ export interface RunArgs {
82
74
  * `defaultAffectedBase`). Any other string is an explicit git ref.
83
75
  */
84
76
  affected: string | undefined
85
- /** `--tag k=v` pairs (repeatable). Empty key is a parse error. */
86
- tags: Record<string, string>
87
- /** `--report[=markdown]`. Undefined when the flag wasn't passed. */
88
- report: 'markdown' | undefined
89
- /**
90
- * `--report-file=<path>`. Undefined when the flag wasn't passed. Writes
91
- * the same markdown as `--report`, but to a file — because redirecting
92
- * vx's stdout captures the whole run log, not just the report.
93
- */
94
- reportFile: string | undefined
95
77
  error?: string
96
78
  }
97
79
 
@@ -130,9 +112,6 @@ const SETTING_OPTIONS = [
130
112
  ['run.timeout', 'timeout'],
131
113
  ['run.retries', 'retries'],
132
114
  ['run.outputLogs', 'outputLogs'],
133
- ['run.killGrace', 'killGrace'],
134
- ['run.teardownTimeout', 'teardownTimeout'],
135
- ['run.readyNotice', 'readyNotice'],
136
115
  ['cache.download', 'download'],
137
116
  ] as const satisfies readonly (readonly [string, keyof RunOptions])[]
138
117
 
@@ -150,14 +129,8 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
150
129
  forwardArgs: [],
151
130
  verbosity: 0,
152
131
  dry: undefined,
153
- format: 'pretty',
154
- graph: undefined,
155
- summarize: undefined,
156
132
  profile: undefined,
157
133
  affected: undefined,
158
- tags: {},
159
- report: undefined,
160
- reportFile: undefined,
161
134
  }
162
135
 
163
136
  // A Turbo or Nx spelling becomes vx's, or a refusal naming vx's.
@@ -301,58 +274,18 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
301
274
  return { ...out, error: `--dry must be text or json (got ${fmt})` }
302
275
  }
303
276
  out.dry = fmt
304
- } else if (a === '--format' || a?.startsWith('--format=')) {
305
- const f = formatValue(a === '--format' ? before[++i] : a.slice('--format='.length), verb)
306
- if (typeof f !== 'string') return { ...out, error: f.error }
307
- out.format = f
308
- } else if (a === '--graph') {
309
- out.graph = ''
310
- } else if (a?.startsWith('--graph=')) {
311
- out.graph = a.slice('--graph='.length)
312
- } else if (a === '--summarize') {
313
- out.summarize = ''
314
- } else if (a?.startsWith('--summarize=')) {
315
- out.summarize = a.slice('--summarize='.length)
316
277
  } else if (a === '--profile') {
317
278
  out.profile = 'profile.json'
318
279
  } else if (a?.startsWith('--profile=')) {
319
280
  // An optional-value flag: an empty `=` means "no value given", so it
320
281
  // takes the same documented default as the bare form. Resolving ''
321
282
  // against cwd would name the cwd DIRECTORY and die with EISDIR after
322
- // the whole run — and `--summarize=` already degrades to its default.
283
+ // the whole run.
323
284
  out.profile = a.slice('--profile='.length) || 'profile.json'
324
285
  } else if (a === '--affected') {
325
286
  out.affected = ''
326
287
  } else if (a?.startsWith('--affected=')) {
327
288
  out.affected = a.slice('--affected='.length)
328
- } else if (a === '--tag' || a?.startsWith('--tag=')) {
329
- const raw = a === '--tag' ? before[++i] : a.slice('--tag='.length)
330
- if (raw === undefined) return { ...out, error: `${a} requires a value (k=v)` }
331
- // Split on the FIRST `=` so values may contain `=` (e.g. a URL).
332
- const eq = raw.indexOf('=')
333
- if (eq <= 0) return { ...out, error: `--tag must be k=v (got ${raw})` }
334
- out.tags[raw.slice(0, eq)] = raw.slice(eq + 1)
335
- } else if (a === '--report-file' || a?.startsWith('--report-file=')) {
336
- const v = a === '--report-file' ? before[++i] : a.slice('--report-file='.length)
337
- if (v === undefined || v === '') return { ...out, error: `--report-file requires a path` }
338
- // Same reasoning as --cache-dir: a path is an arbitrary string, so
339
- // nothing about its shape rejects a swallowed flag. A path starting
340
- // with `-` needs the `=` form.
341
- if (a === '--report-file' && v.startsWith('-')) {
342
- return {
343
- ...out,
344
- error: `--report-file requires a path (got flag ${v}; a path that starts with - needs --report-file=${v})`,
345
- }
346
- }
347
- out.reportFile = v
348
- } else if (a === '--report') {
349
- out.report = 'markdown'
350
- } else if (a?.startsWith('--report=')) {
351
- const fmt = a.slice('--report='.length)
352
- if (fmt !== 'markdown') {
353
- return { ...out, error: `--report must be markdown (got ${fmt})` }
354
- }
355
- out.report = fmt
356
289
  } else if (verb === 'watch' && a === '--poll') {
357
290
  out.poll = true
358
291
  } else if (a !== undefined && settingFlagOf(a) !== undefined) {
@@ -400,39 +333,10 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
400
333
  out.cache = applyCacheFlags(cachePolicy, flags)
401
334
  if (!cacheFlag && envSpec === undefined) out.cacheFlags = flags
402
335
 
403
- if (out.dry !== undefined && out.graph !== undefined) {
404
- return { ...out, error: '--dry and --graph are mutually exclusive' }
405
- }
406
- // Every flag that promises a file or a section about the run: the
407
- // planning branch returns before any of them is written, and
408
- // `--report-file="$GITHUB_STEP_SUMMARY" --dry` exited 0 having written
409
- // nothing (item 992).
410
- const needsRun =
411
- out.summarize !== undefined ||
412
- out.profile !== undefined ||
413
- out.report !== undefined ||
414
- out.reportFile !== undefined
415
- const runOnly = '--summarize / --profile / --report / --report-file need a real run'
416
- if (out.dry !== undefined && needsRun) {
417
- return { ...out, error: `--dry skips execution; ${runOnly}` }
418
- }
419
- if (out.graph !== undefined && needsRun) {
420
- return { ...out, error: `--graph skips execution; ${runOnly}` }
421
- }
422
- if (out.format === 'json') {
423
- // Each of these prints something else on stdout, which the document owns.
424
- if (out.dry !== undefined)
425
- return { ...out, error: '--format json runs the tasks; --dry=json prints the plan' }
426
- if (out.graph === '')
427
- return {
428
- ...out,
429
- error: '--format json and --graph both print on stdout; give --graph a file',
430
- }
431
- if (out.report === 'markdown')
432
- return {
433
- ...out,
434
- error: '--format json and --report=markdown both print on stdout; use --report-file',
435
- }
336
+ // A profile is about a run: the planning branch returns before it is
337
+ // written (item 992).
338
+ if (out.dry !== undefined && out.profile !== undefined) {
339
+ return { ...out, error: '--dry skips execution; --profile needs a real run' }
436
340
  }
437
341
  return out
438
342
  }
@@ -619,10 +523,7 @@ export async function resolveRunOptions(
619
523
  for (const [path, field] of SETTING_OPTIONS) {
620
524
  if (parsed.settings[path] !== undefined) Object.assign(opts, { [field]: parsed.settings[path] })
621
525
  }
622
- if (parsed.summarize !== undefined) opts.summarize = parsed.summarize
623
- if (parsed.format === 'json') opts.json = true
624
526
  if (parsed.profile !== undefined) opts.profile = parsed.profile
625
- if (Object.keys(parsed.tags).length > 0) opts.tags = parsed.tags
626
527
 
627
528
  return opts
628
529
  }
@@ -646,7 +547,7 @@ export async function runCmd(args: readonly string[]): Promise<number> {
646
547
  if (tasks.length === 0) {
647
548
  // A JSON answer is for a program, which cannot pick from a menu: an
648
549
  // agent on a pty waited at the picker forever.
649
- const forProgram = parsed.format === 'json' || parsed.dry === 'json'
550
+ const forProgram = parsed.dry === 'json'
650
551
  if (!process.stdin.isTTY || forProgram) {
651
552
  const names = await taskNamesHere(cwd)
652
553
  const shown = (names ?? []).slice(0, TASKS_SHOWN)
@@ -710,17 +611,17 @@ export async function runCmd(args: readonly string[]): Promise<number> {
710
611
  ...args.map(shellQuote),
711
612
  ].join(' ')
712
613
 
713
- // Planning paths short-circuit execution. Both build the full task
714
- // graph + probe the cache; the difference is just the formatter.
715
- if (parsed.dry !== undefined || parsed.graph !== undefined) {
614
+ // Planning short-circuits execution: the full task graph + a cache probe.
615
+ if (parsed.dry !== undefined) {
716
616
  const plan = await planRun(opts)
617
+ // Refused as a run refuses: `--dry=json` gets the error document.
717
618
  if (plan.unresolvedTasks !== undefined && plan.unresolvedTasks.length > 0) {
718
- sayNotice(
719
- 'error',
720
- 'vx run',
619
+ return refuse(
620
+ 'run',
621
+ args,
721
622
  `no projects declare task(s): ${plan.unresolvedTasks.join(', ')}.${plan.unresolvedHint ?? ''}`,
623
+ 'VX_E_UNKNOWN_TASK',
722
624
  )
723
- return 1
724
625
  }
725
626
  if (plan.tasks.length === 0) {
726
627
  // The run's own line: a dry run that guessed from the scope alone
@@ -733,27 +634,14 @@ export async function runCmd(args: readonly string[]): Promise<number> {
733
634
  sayNotice('info', 'vx run', `no affected project declares task(s): ${tasks.join(', ')}`)
734
635
  return 0
735
636
  }
736
- sayNotice('error', 'vx run', `no projects declare task(s): ${tasks.join(', ')}`)
737
- return 1
637
+ return refuse(
638
+ 'run',
639
+ args,
640
+ `no projects declare task(s): ${tasks.join(', ')}`,
641
+ 'VX_E_UNKNOWN_TASK',
642
+ )
738
643
  }
739
- if (parsed.graph !== undefined) {
740
- const out = formatGraphDot(plan)
741
- if (parsed.graph === '') {
742
- process.stdout.write(out)
743
- } else {
744
- // The graph is the command's one product, so a path it cannot write
745
- // is exit 1, said in one line as the other output paths say theirs:
746
- // a directory or an unreachable path printed a raw stack (item 993).
747
- const target = path.resolve(cwd, parsed.graph)
748
- try {
749
- await Bun.write(target, out)
750
- } catch (err) {
751
- const message = err instanceof Error ? err.message : String(err)
752
- sayNotice('error', 'vx run', `failed to write graph to ${target}: ${message}`)
753
- return 1
754
- }
755
- }
756
- } else if (parsed.dry === 'json') {
644
+ if (parsed.dry === 'json') {
757
645
  process.stdout.write(formatPlanJson(plan, scopeOut.affectedBase))
758
646
  } else {
759
647
  process.stdout.write(formatPlanText(plan))
@@ -761,25 +649,6 @@ export async function runCmd(args: readonly string[]): Promise<number> {
761
649
  return 0
762
650
  }
763
651
 
764
- // The report prints above the footer: nothing prints below it (owner).
765
- // Rendered once, however many sinks asked for it; a kept server's crash
766
- // after the footer re-renders the file's copy (C-53).
767
- const wantsReport = parsed.report === 'markdown' || parsed.reportFile !== undefined
768
- let reported: { outcomes: readonly TaskOutcome[]; md: string } | undefined
769
- if (wantsReport) {
770
- opts.beforeFooter = (outcomes, ok) => {
771
- reported = {
772
- outcomes,
773
- md: formatRunReportMarkdown({ ok, outcomes: outcomes.map(projectOutcome) }),
774
- }
775
- // `--report` writes stdout. The report ITSELF is machine-clean, but
776
- // stdout is not vx's alone — the status logger writes there too, so
777
- // `--report=markdown >> "$GITHUB_STEP_SUMMARY"` captures every frame,
778
- // meter bar and `::group::` command above the table. `--report-file`
779
- // is the redirect-free form.
780
- return parsed.report === 'markdown' ? reported.md : ''
781
- }
782
- }
783
652
  // A run executes in THIS process, always. Where an individual task's
784
653
  // command runs is the `executor` capability's business (per task, with
785
654
  // the scheduler, cache, retries and telemetry unchanged above it); there
@@ -789,37 +658,6 @@ export async function runCmd(args: readonly string[]): Promise<number> {
789
658
  const unknown = summary.refused.startsWith('no projects declare')
790
659
  return refuse('run', args, summary.refused, unknown ? 'VX_E_UNKNOWN_TASK' : 'VX_E_REFUSED')
791
660
  }
792
- if (wantsReport) {
793
- const md =
794
- reported?.outcomes === summary.outcomes
795
- ? reported.md
796
- : formatRunReportMarkdown({
797
- ok: summary.ok,
798
- outcomes: summary.outcomes.map(projectOutcome),
799
- })
800
- if (parsed.reportFile !== undefined) {
801
- const target = path.resolve(cwd, parsed.reportFile)
802
- try {
803
- // APPEND, not overwrite: the flag exists for `$GITHUB_STEP_SUMMARY`,
804
- // which GitHub documents as append-only and which other steps in the
805
- // same job also write to. Truncating it would silently destroy their
806
- // content, and would make replacing the documented `>>` recipe a
807
- // behaviour change rather than a drop-in.
808
- // Its directory made first, as `--summarize`, `--profile` and
809
- // `--graph` make theirs: `nr/r.md` failed alone of the four (item 993).
810
- // Sync: on a Ctrl-C the signal path exits once stdout drains, and
811
- // an awaited write lost the race every time (E-23).
812
- mkdirSync(path.dirname(target), { recursive: true })
813
- appendFileSync(target, md)
814
- } catch (err) {
815
- // Same contract as --summarize / --profile: the run already
816
- // happened, so a write failure is reported, not fatal.
817
- const message = err instanceof Error ? err.message : String(err)
818
- sayNotice('warn', 'vx run', `failed to write report to ${target}: ${message}`)
819
- }
820
- }
821
- }
822
- if (summary.json !== undefined) process.stdout.write(`${JSON.stringify(summary.json)}\n`)
823
661
  return summary.ok ? 0 : 1
824
662
  }
825
663
 
package/src/cli/select.ts CHANGED
@@ -51,7 +51,7 @@ import {
51
51
  import { resolveSetting } from '../settings.js'
52
52
 
53
53
  /**
54
- * The projects whose tasks declare a `cache.inputs.workspaceFiles` glob
54
+ * The projects whose tasks declare an `inputs.workspaceFiles` glob
55
55
  * matching a changed path, one inside another project included (item
56
56
  * 954): `--affected` selects them, since the glob is that task's input. Through the run
57
57
  * path's staged load, so a glob a `project` plugin gave a config-less
@@ -71,10 +71,8 @@ export async function workspaceGlobOwners(
71
71
  ): Promise<string[]> {
72
72
  const declaresMatch = (config: ProjectConfig): boolean => {
73
73
  for (const task of Object.values(config.tasks ?? {})) {
74
- const cache = task.cache
75
- if (cache === undefined) continue
76
- if (changed.some((rel) => declaresInput(cache, null, rel))) return true
77
- if (nested.some((dir) => workspaceFilesReachInto(cache, dir))) return true
74
+ if (changed.some((rel) => declaresInput(task, null, rel))) return true
75
+ if (nested.some((dir) => workspaceFilesReachInto(task, dir))) return true
78
76
  }
79
77
  return false
80
78
  }
@@ -335,7 +333,19 @@ export async function resolveFilters(
335
333
  workspaceGlobOwners: (changed: readonly string[], nested: readonly string[]) =>
336
334
  workspaceGlobOwners(root, projects, changed, load, stagedOnce, nested),
337
335
  fingerprintClaims: () => workspaceFingerprintClaims(root, projects, load),
338
- ...(git !== undefined ? { untracked: async () => (await git.start()).untracked } : {}),
336
+ ...(git !== undefined
337
+ ? {
338
+ untracked: async () => (await git.start()).untracked,
339
+ // A scoped enumeration lists only its projects: null asks git.
340
+ links: async () => {
341
+ const g = await git.start()
342
+ if (!g.scopes.includes('.')) return null
343
+ const out: string[] = []
344
+ for (const [rel, oid] of g.trusted) if (oid.startsWith('120000:')) out.push(rel)
345
+ return out
346
+ },
347
+ }
348
+ : {}),
339
349
  }
340
350
  let names: Set<string>
341
351
  if (f.raw === affected) {
package/src/cli/show.ts CHANGED
@@ -169,6 +169,12 @@ export async function showCmd(args: readonly string[]): Promise<number> {
169
169
  const projects = await loadCliProjects(root, metas, scope, { closure: true })
170
170
 
171
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
+ )
172
178
  const shown = metas.filter((m) => inSelection(m.name))
173
179
  process.stdout.write(renderList(root, shown, projects, parsed.format, colorsOut()))
174
180
  return 0
@@ -460,14 +466,17 @@ function taskRows(raw: TaskConfig): ui.FieldRow[] {
460
466
  )
461
467
  }
462
468
  add('interactive', exec?.interactive === undefined ? undefined : exec.interactive ? 'yes' : 'no')
469
+ const inputs = task.inputs
470
+ if (inputs !== undefined) {
471
+ add('inputs.files', list(inputs.files))
472
+ add('inputs.workspaceFiles', list(inputs.workspaceFiles))
473
+ add('inputs.env', list(inputs.env))
474
+ add('inputs.tasks', list(inputs.tasks))
475
+ add('inputs.runtime', list(inputs.runtime))
476
+ add('inputs.workspaceRuntime', list(inputs.workspaceRuntime))
477
+ }
463
478
  const cache = task.cache
464
479
  if (cache !== undefined) {
465
- add('inputs.files', list(cache.inputs.files))
466
- add('inputs.workspaceFiles', list(cache.inputs.workspaceFiles))
467
- add('inputs.env', list(cache.inputs.env))
468
- add('inputs.tasks', list(cache.inputs.tasks))
469
- add('inputs.runtime', list(cache.inputs.runtime))
470
- add('inputs.workspaceRuntime', list(cache.inputs.workspaceRuntime))
471
480
  add('outputs.files', list(cache.outputs.files))
472
481
  add('outputs.workspaceFiles', list(cache.outputs.workspaceFiles))
473
482
  }