@vzn/vx 0.0.51 → 0.0.53

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx",
3
- "version": "0.0.51",
3
+ "version": "0.0.53",
4
4
  "description": "An open, extensible monorepo task runner.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -17,10 +17,10 @@
17
17
  "node": ">=18"
18
18
  },
19
19
  "optionalDependencies": {
20
- "@vzn/vx-linux-x64": "0.0.51",
21
- "@vzn/vx-linux-arm64": "0.0.51",
22
- "@vzn/vx-darwin-x64": "0.0.51",
23
- "@vzn/vx-darwin-arm64": "0.0.51"
20
+ "@vzn/vx-linux-x64": "0.0.53",
21
+ "@vzn/vx-linux-arm64": "0.0.53",
22
+ "@vzn/vx-darwin-x64": "0.0.53",
23
+ "@vzn/vx-darwin-arm64": "0.0.53"
24
24
  },
25
25
  "dependencies": {
26
26
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
package/src/cli/run.ts CHANGED
@@ -437,9 +437,10 @@ export async function resolveRunOptions(
437
437
  // changed-only `[<base>]` (item 287). `--filter '[<base>]'` stays the
438
438
  // "only what I touched" form. Merging it into the filter list means the
439
439
  // same code path handles plain filter use, --affected alone, and the combo.
440
- // It goes FIRST: filters apply in argv order, and appended last it came
441
- // after every `!` exclude, so `--affected --filter '!app'` still ran app
442
- // (item 955). An include's place does not change a union.
440
+ // Its place does not change the selection: `applyFilters` takes every
441
+ // include before any exclude (item 979), so `--affected --filter '!app'`
442
+ // drops app from either side (item 955 had put it first for that). First
443
+ // it stays, so a message naming the filters names it first.
443
444
  const filterStrings = [...parsed.filters]
444
445
  if (parsed.affected !== undefined) {
445
446
  const root = await findWorkspaceRoot(cwd)
@@ -461,6 +462,7 @@ export async function resolveRunOptions(
461
462
  const anchoredTasks = tasks.filter((t) => t.includes('#'))
462
463
  let projects: string[] | undefined
463
464
  let staged: RunOptions['staged']
465
+ let discovered: RunOptions['discovered']
464
466
  let selectedByDiff = false
465
467
  if (bareTasks.length === 0) {
466
468
  projects = undefined
@@ -485,6 +487,7 @@ export async function resolveRunOptions(
485
487
  // The graph walk staged every config; the run reuses those entries
486
488
  // instead of evaluating and staging them a second time.
487
489
  staged = resolved.staged
490
+ discovered = resolved.discovered
488
491
  }
489
492
  } else if (parsed.all) {
490
493
  projects = undefined
@@ -519,6 +522,7 @@ export async function resolveRunOptions(
519
522
  if (projects !== undefined) opts.projects = projects
520
523
  if (selectedByDiff) opts.selectedByDiff = true
521
524
  if (staged !== undefined) opts.staged = staged
525
+ if (discovered !== undefined) opts.discovered = discovered
522
526
  if (parsed.retries !== undefined) opts.retries = parsed.retries
523
527
  if (parsed.timeout !== undefined) opts.timeout = parsed.timeout
524
528
  if (parsed.cacheDir !== undefined) opts.cacheDir = parsed.cacheDir
package/src/cli/select.ts CHANGED
@@ -152,6 +152,8 @@ export type FilterResolution =
152
152
  byDiff: boolean
153
153
  /** The staged load the graph walk needed, for the run to reuse (`RunOptions.staged`). */
154
154
  staged?: ReadonlyMap<string, ProjectEntry>
155
+ /** The discovery this pass made, for the run to reuse (`RunOptions.discovered`). */
156
+ discovered: { root: string; projects: ProjectMeta[] }
155
157
  }
156
158
  | { error: string }
157
159
  | { empty: string }
@@ -317,6 +319,7 @@ export async function resolveFilters(
317
319
  names: [...selected].sort(),
318
320
  byDiff: parsed.some((f) => !f.negate && f.gitSince !== undefined),
319
321
  ...(staged !== undefined ? { staged } : {}),
322
+ discovered: { root, projects },
320
323
  }
321
324
  }
322
325
 
package/src/cli/watch.ts CHANGED
@@ -466,9 +466,10 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
466
466
  // run()'s process.argv fallback put the bin's absolute path there, so
467
467
  // `vx last --list` showed `$ /…/bin.ts watch build` beside `$ vx run build`.
468
468
  opts.command = ['vx', 'watch', ...args].join(' ')
469
- // A staged load from the selection pass is one run's worth of configs;
470
- // every cycle after an edit must evaluate live.
469
+ // A staged load and a discovery from the selection pass are one run's
470
+ // worth; every cycle after an edit must load and list live.
471
471
  delete opts.staged
472
+ delete opts.discovered
472
473
  process.once('SIGINT', () => {
473
474
  process.stdout.write('\nvx watch: stopped\n')
474
475
  stop.abort('SIGINT')
package/src/index.ts CHANGED
@@ -15,9 +15,6 @@ export { VERSION } from './version.js'
15
15
  // fractional SQL LIMIT is a datatype mismatch, not a smaller page) should
16
16
  // have one implementation.
17
17
  export { clampInt, UserError, isUserError } from './util/index.js'
18
- // "Did you mean": the hint core's own verbs give for a near-miss name, for a
19
- // plugin verb to give the same one.
20
- export { nearMatches } from './util/index.js'
21
18
  // Every hook a plugin may fill, in pipeline order — the one list the
22
19
  // loader, the host and `vx info` read; exported so a doc pin outside core
23
20
  // (the site's guides) holds its tables to the same list.
@@ -116,7 +113,6 @@ export type { ProjectEntry } from './workspace/index.js'
116
113
  export {
117
114
  applyMigration,
118
115
  foldScriptHooks,
119
- PERSISTENT_TASK_NAMES,
120
116
  PERSISTENT_TODO,
121
117
  quoteTsLiteral,
122
118
  } from './workspace/index.js'
@@ -184,18 +180,15 @@ export type {
184
180
  // that added them found: with only the raw `TaskStatus` union exported, every
185
181
  // consumer rolls its own Set of status literals — and a Set has no
186
182
  // compile-time tripwire when the union gains a member, so it silently answers
187
- // "no" for the new one. `TASK_STATUSES` is the union at runtime, for a
188
- // consumer that needs the list rather than the predicate.
183
+ // "no" for the new one.
189
184
  // `escapeMarkdownCell` is on the façade for the same demonstrated need: a
190
185
  // plugin that renders a run as a markdown table takes the same unvalidated
191
186
  // task names core does, and the cloud job summary shipped without the escape.
192
187
  export {
193
188
  escapeMarkdownCell,
194
189
  TELEMETRY_SCHEMA_VERSION,
195
- deriveCacheSource,
196
190
  isCacheHit,
197
191
  isPassStatus,
198
- TASK_STATUSES,
199
192
  } from './orchestrator/index.js'
200
193
  // `TaskLogBuffer` is on the façade on the same demonstrated need: EVERY
201
194
  // telemetry sink that ships build output has to bound it, and the retention
@@ -8,7 +8,7 @@ import type { TaskOutcome } from '../graph/index.js'
8
8
  import type { EventBus } from './events.js'
9
9
  import type { TelemetrySink } from './telemetry.js'
10
10
  import type { Logger } from './logger.js'
11
- import type { ProjectEntry } from '../workspace/index.js'
11
+ import type { ProjectEntry, ProjectMeta } from '../workspace/index.js'
12
12
 
13
13
  export interface RunOptions {
14
14
  cwd: string
@@ -39,6 +39,13 @@ export interface RunOptions {
39
39
  * it per cycle).
40
40
  */
41
41
  staged?: ReadonlyMap<string, ProjectEntry>
42
+ /**
43
+ * The projects a selection pass in this same process discovered under
44
+ * `root` (`--filter`, `--affected`). The run takes them instead of
45
+ * walking the workspace again when its root is the same. One run only,
46
+ * as `staged` is.
47
+ */
48
+ discovered?: { root: string; projects: ProjectMeta[] }
42
49
  concurrency?: number
43
50
  /**
44
51
  * Cache directory override (`--cache-dir <path>`). Absolute, or relative
@@ -179,7 +179,10 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
179
179
  log.status(m),
180
180
  )
181
181
  mark('workspace config')
182
- const projectMetas = await listProjects(workspace)
182
+ const projectMetas =
183
+ options.discovered?.root === workspaceRoot
184
+ ? options.discovered.projects
185
+ : await listProjects(workspace)
183
186
  mark('discover projects')
184
187
 
185
188
  // SCOPED config loading: configs are programs, and evaluating 1090
@@ -291,14 +294,20 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
291
294
 
292
295
  const requested = expandRequested(options.tasks, candidateProjects, projects)
293
296
  let unresolvedTasks = unresolvedRequests(options.tasks, candidateProjects, projects)
294
- if (
295
- options.selectedByDiff === true &&
296
- projects.size < projectsWithConfigs.length &&
297
- unresolvedTasks.some((t) => !t.includes('#'))
298
- ) {
299
- unresolvedTasks = await declaredNowhere(unresolvedTasks, () =>
300
- loadProjects({ ...loadArgs, seeds: 'all', closure: false, staged: projects }),
301
- )
297
+ if (options.selectedByDiff === true && unresolvedTasks.some((t) => !t.includes('#'))) {
298
+ // A name a loaded project declares is no typo, whether or not the load
299
+ // was whole: an unaffected dependency loaded for the closure declared
300
+ // it, and the run said no project did (C-3). Only what is still
301
+ // unjudged pays for the rest of the workspace.
302
+ unresolvedTasks = undeclaredIn(unresolvedTasks, projects)
303
+ if (
304
+ projects.size < projectsWithConfigs.length &&
305
+ unresolvedTasks.some((t) => !t.includes('#'))
306
+ ) {
307
+ unresolvedTasks = await declaredNowhere(unresolvedTasks, () =>
308
+ loadProjects({ ...loadArgs, seeds: 'all', closure: false, staged: projects }),
309
+ )
310
+ }
302
311
  }
303
312
 
304
313
  // Cache seam precedence: an EXPLICITLY injected remote layer
@@ -527,8 +536,16 @@ async function declaredNowhere(
527
536
  } catch {
528
537
  return [...unresolved]
529
538
  }
539
+ return undeclaredIn(unresolved, all.projects)
540
+ }
541
+
542
+ /** The bare names in `unresolved` no project in `projects` declares; `a#b` forms pass through. */
543
+ function undeclaredIn(
544
+ unresolved: readonly string[],
545
+ projects: LoadedProjects['projects'],
546
+ ): string[] {
530
547
  const declared = new Set<string>()
531
- for (const p of all.projects.values()) {
548
+ for (const p of projects.values()) {
532
549
  for (const t of Object.keys(p.config.tasks ?? {})) declared.add(t)
533
550
  }
534
551
  return unresolved.filter((t) => t.includes('#') || !declared.has(t))
@@ -201,8 +201,6 @@ export const PURE_CORE_EXPORTS: ReadonlySet<string> = new Set([
201
201
  'normalizeGlob',
202
202
  'isLiteralPattern',
203
203
  'PLUGIN_HOOKS',
204
- 'TASK_STATUSES',
205
- 'PERSISTENT_TASK_NAMES',
206
204
  ])
207
205
 
208
206
  /** Whether an `import`/`export … from '@vzn/vx'` statement takes only pure values. */