@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
@@ -5,11 +5,12 @@
5
5
  // whichever tool wrote it. Core knows no source format here: a mapper returns a
6
6
  // `MigrationPlan` and this file does the rest.
7
7
 
8
- import { existsSync, readFileSync } from 'node:fs'
8
+ import { existsSync, readFileSync, realpathSync } 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
12
  import {
13
+ checkIgnoreSync,
13
14
  DOT_VX_GITIGNORE,
14
15
  executablePath,
15
16
  gitEnv,
@@ -22,6 +23,31 @@ import * as ui from '../ui/index.js'
22
23
  import { VERSION } from '../version.js'
23
24
  import { WORKSPACE_CONFIG_FILENAMES } from './project-loader.js'
24
25
 
26
+ /**
27
+ * The nearest existing directory at or above `dir` (under `root`) and its
28
+ * real path when that path is outside `realRoot`; none when it is inside.
29
+ */
30
+ function linkOut(
31
+ root: string,
32
+ realRoot: string,
33
+ dir: string,
34
+ ): [link: string | undefined, real: string] {
35
+ for (let at = dir; ; at = path.dirname(at)) {
36
+ let real: string
37
+ try {
38
+ real = realpathSync(at)
39
+ } catch {
40
+ // quiet: not made yet; mkdir makes it a real directory under its parent
41
+ if (at === root) return [undefined, realRoot]
42
+ continue
43
+ }
44
+ const rel = path.relative(realRoot, real)
45
+ return rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)
46
+ ? [at, real]
47
+ : [undefined, real]
48
+ }
49
+ }
50
+
25
51
  /** The root config `vx init` writes outside a Turbo or Nx repo: no task yet. */
26
52
  export function starterProject(format: MigrationFormat): string {
27
53
  return format === 'ts'
@@ -246,8 +272,9 @@ export interface ApplyMigrationArgs {
246
272
  * the migrated workspace has its .vx/projects.lock.json and its graph rules checked
247
273
  * (2026-10-14). On success `said` is its line, shown as the
248
274
  * report's `lock` row; a refusal (a graph rule, a plugin not installed
249
- * yet) prints above the report and the migration still succeeds: its
250
- * files are written, and `vx lock` is the step to re-run.
275
+ * yet) prints above the report, the files stay written, `vx lock` is the
276
+ * step to re-run, and the exit is 1: a run reads only the lock, so the
277
+ * migration is not done (as `vx init`'s own path exits).
251
278
  */
252
279
  lock?: (root: string) => Promise<{ code: number; said: string }>
253
280
  /** Rows of what the tool did before the write, e.g. `['ran', 'bun add -d …']`. */
@@ -352,11 +379,23 @@ export async function applyMigration(args: ApplyMigrationArgs): Promise<number>
352
379
 
353
380
  // A plan is a mapper's reading of the repo's own files (an Nx node's
354
381
  // `root`, a workspace glob): one that names `../x` wrote outside it.
382
+ // A member directory linked out of the root is spelled inside it, so the
383
+ // real directory is asked too: core's discovery never loads a config
384
+ // there. `.vx` is not a project: its files are refused outside the
385
+ // root at the write (`refuseSymlink`).
386
+ const realRoot = realpathSync(root)
355
387
  for (const f of files) {
356
388
  const rel = relPosix(root, f.abs)
357
389
  if (rel === '..' || rel.startsWith('../') || path.isAbsolute(rel)) {
358
390
  throw new UserError(`${f.relPath}: outside the workspace root`)
359
391
  }
392
+ if (rel.startsWith('.vx/')) continue
393
+ const [link, real] = linkOut(root, realRoot, path.dirname(f.abs))
394
+ if (link !== undefined) {
395
+ throw new UserError(
396
+ `${f.relPath}: outside the workspace root (${relPosix(root, link)} resolves to ${real}); vx loads no project there`,
397
+ )
398
+ }
360
399
  }
361
400
 
362
401
  if (!dry && !force) {
@@ -396,8 +435,8 @@ export async function applyMigration(args: ApplyMigrationArgs): Promise<number>
396
435
  ui.print('out', ui.excerpt(f.relPath, f.contents))
397
436
  }
398
437
  } else if (!dry) {
399
- for (const f of files) refuseSymlink(f.abs)
400
- for (const f of files) await writeNoFollow(f.abs, f.contents)
438
+ for (const f of files) refuseSymlink(f.abs, root)
439
+ for (const f of files) await writeNoFollow(f.abs, f.contents, root)
401
440
  for (const f of replaced) await unlink(f)
402
441
  // The lock that follows loads its plugins from the file just written.
403
442
  workspaceFileWritten()
@@ -538,7 +577,7 @@ export async function applyMigration(args: ApplyMigrationArgs): Promise<number>
538
577
  lock === undefined || lock.code === 0 ? next : `fix the above, then vx lock and ${next}`
539
578
  rows.push(null, ['next', said])
540
579
  ui.print('out', ui.report(`${verb} ${VERSION}`, rows))
541
- return 0
580
+ return lock === undefined || lock.code === 0 ? 0 : 1
542
581
  }
543
582
 
544
583
  /**
@@ -590,25 +629,7 @@ function ignoreAdvice(root: string, ignored: readonly string[], dry: boolean): s
590
629
 
591
630
  /** The paths (root-relative) git's ignore rules drop; none outside a work tree. */
592
631
  function gitIgnored(root: string, relPaths: readonly string[]): string[] {
593
- if (relPaths.length === 0) return []
594
- try {
595
- const r = Bun.spawnSync({
596
- cmd: [executablePath('git'), 'check-ignore', '--stdin', '-z'],
597
- env: gitEnv(),
598
- cwd: root,
599
- stdin: Buffer.from(relPaths.map((p) => `${p}\0`).join('')),
600
- stdout: 'pipe',
601
- stderr: 'ignore',
602
- })
603
- // 0: some ignored; 1: none; anything else (no work tree): nothing to say.
604
- if (r.exitCode !== 0) return []
605
- return r.stdout
606
- .toString()
607
- .split('\0')
608
- .filter((p) => p !== '')
609
- } catch {
610
- return []
611
- }
632
+ return [...(checkIgnoreSync(root, relPaths) ?? [])]
612
633
  }
613
634
 
614
635
  function inGitWorkTree(root: string): boolean {
@@ -0,0 +1,62 @@
1
+ // What a module's exports hold, to tell whether a config changed a module
2
+ // other configs import. Both functions are self-contained: the config
3
+ // worker embeds them by their source (config-eval.ts), as it does
4
+ // `nonJsonPaths`, and a minified build renames only what each one holds.
5
+
6
+ /** One entry per export: every value reachable from it, in walk order, compared by identity. */
7
+ export type ExportState = Map<PropertyKey, unknown[]>
8
+
9
+ /**
10
+ * Every value each export of `ns` reaches through own properties and Map
11
+ * and Set entries, without calling a getter. Functions and typed arrays
12
+ * are compared by identity only. State a module keeps unexported, behind a
13
+ * function, is not reached.
14
+ */
15
+ export function exportState(ns: object): ExportState {
16
+ const ownKeys = Reflect.ownKeys
17
+ const describe = Object.getOwnPropertyDescriptor
18
+ const protoOf = Object.getPrototypeOf
19
+ const state: ExportState = new Map()
20
+ for (const name of ownKeys(ns)) {
21
+ const out: unknown[] = []
22
+ const seen = new Set<object>()
23
+ const walk = (v: unknown): void => {
24
+ out.push(v)
25
+ if (v === null || typeof v !== 'object' || seen.has(v) || ArrayBuffer.isView(v)) return
26
+ seen.add(v)
27
+ out.push(protoOf(v))
28
+ if (v instanceof Map)
29
+ for (const [k, x] of v) {
30
+ out.push(k)
31
+ walk(x)
32
+ }
33
+ else if (v instanceof Set) for (const x of v) walk(x)
34
+ for (const k of ownKeys(v)) {
35
+ const d = describe(v, k)!
36
+ out.push(k, d.get, d.set, d.writable)
37
+ if ('value' in d) walk(d.value)
38
+ }
39
+ }
40
+ try {
41
+ walk((ns as Record<PropertyKey, unknown>)[name])
42
+ } catch {
43
+ // A binding not yet initialized, or a Proxy's trap that throws.
44
+ out.push('\0threw')
45
+ }
46
+ state.set(name, out)
47
+ }
48
+ return state
49
+ }
50
+
51
+ /** The exports whose state differs between `before` and `after`. */
52
+ export function changedExports(before: ExportState, after: ExportState): string[] {
53
+ const out: string[] = []
54
+ for (const name of new Set([...before.keys(), ...after.keys()])) {
55
+ const a = before.get(name)
56
+ const b = after.get(name)
57
+ let same = a !== undefined && b !== undefined && a.length === b.length
58
+ for (let i = 0; same && i < a!.length; i++) same = Object.is(a![i], b![i])
59
+ if (!same) out.push(String(name))
60
+ }
61
+ return out
62
+ }
@@ -19,9 +19,16 @@ import type { ProjectEntry } from './workspace.js'
19
19
  * once we leave the `p.dir` block entirely. The inner loop is still bounded by
20
20
  * that block (near-O(P log P) on real trees; interlopers are few), not the
21
21
  * full tail.
22
+ *
23
+ * `workspaces`: the absolute roots of nested workspaces (each holds its own
24
+ * `.vx/workspace.*`), a boundary for outputs: each joins the dirs of every
25
+ * project it lies under. An outer output glob that ran into one let the
26
+ * clean before a run delete its files. Never passed for inputs: a task may
27
+ * read them, and an input left out of the key is a stale hit.
22
28
  */
23
29
  export function computeNestedProjectDirs(
24
30
  entries: Array<Pick<ProjectEntry, 'name' | 'dir'>>,
31
+ workspaces: readonly string[] = [],
25
32
  ): Map<string, string[]> {
26
33
  const result = new Map<string, string[]>()
27
34
  if (entries.length === 0) return result
@@ -42,6 +49,7 @@ export function computeNestedProjectDirs(
42
49
  // else: an interloping sibling (`foo-utils` for `foo`) that shares the
43
50
  // string prefix but isn't nested — skip it, descendants may follow.
44
51
  }
52
+ for (const w of workspaces) if (w.startsWith(prefix)) nested.push(w)
45
53
  result.set(p.name, nested)
46
54
  }
47
55
  return result