@vzn/vx 0.0.28 → 0.0.30
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.
|
|
3
|
+
"version": "0.0.30",
|
|
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.
|
|
21
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
22
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
23
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
20
|
+
"@vzn/vx-linux-x64": "0.0.30",
|
|
21
|
+
"@vzn/vx-linux-arm64": "0.0.30",
|
|
22
|
+
"@vzn/vx-darwin-x64": "0.0.30",
|
|
23
|
+
"@vzn/vx-darwin-arm64": "0.0.30"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/cli/run.ts
CHANGED
|
@@ -461,6 +461,7 @@ export async function resolveRunOptions(
|
|
|
461
461
|
const anchoredTasks = tasks.filter((t) => t.includes('#'))
|
|
462
462
|
let projects: string[] | undefined
|
|
463
463
|
let staged: RunOptions['staged']
|
|
464
|
+
let selectedByDiff = false
|
|
464
465
|
if (bareTasks.length === 0) {
|
|
465
466
|
projects = undefined
|
|
466
467
|
} else if (filterStrings.length > 0) {
|
|
@@ -480,6 +481,7 @@ export async function resolveRunOptions(
|
|
|
480
481
|
projects = []
|
|
481
482
|
} else {
|
|
482
483
|
projects = resolved.names
|
|
484
|
+
selectedByDiff = resolved.byDiff
|
|
483
485
|
// The graph walk staged every config; the run reuses those entries
|
|
484
486
|
// instead of evaluating and staging them a second time.
|
|
485
487
|
staged = resolved.staged
|
|
@@ -515,6 +517,7 @@ export async function resolveRunOptions(
|
|
|
515
517
|
opts.excludeDependencies = parsed.excludeDependencies
|
|
516
518
|
}
|
|
517
519
|
if (projects !== undefined) opts.projects = projects
|
|
520
|
+
if (selectedByDiff) opts.selectedByDiff = true
|
|
518
521
|
if (staged !== undefined) opts.staged = staged
|
|
519
522
|
if (parsed.retries !== undefined) opts.retries = parsed.retries
|
|
520
523
|
if (parsed.timeout !== undefined) opts.timeout = parsed.timeout
|
|
@@ -582,6 +585,10 @@ export async function runCmd(args: readonly string[]): Promise<number> {
|
|
|
582
585
|
return 1
|
|
583
586
|
}
|
|
584
587
|
if (plan.tasks.length === 0) {
|
|
588
|
+
if (opts.selectedByDiff === true) {
|
|
589
|
+
process.stderr.write(`vx run: no affected project declares task(s): ${tasks.join(', ')}.\n`)
|
|
590
|
+
return 0
|
|
591
|
+
}
|
|
585
592
|
process.stderr.write(`vx run: no projects declare task(s): ${tasks.join(', ')}.\n`)
|
|
586
593
|
return 1
|
|
587
594
|
}
|
package/src/cli/select.ts
CHANGED
|
@@ -127,6 +127,8 @@ export async function findCwdProject(cwd: string): Promise<string | null> {
|
|
|
127
127
|
export type FilterResolution =
|
|
128
128
|
| {
|
|
129
129
|
names: string[]
|
|
130
|
+
/** An include filter was a git diff (`[ref]`, `--affected`): `RunOptions.selectedByDiff`. */
|
|
131
|
+
byDiff: boolean
|
|
130
132
|
/** The staged load the graph walk needed, for the run to reuse (`RunOptions.staged`). */
|
|
131
133
|
staged?: ReadonlyMap<string, ProjectEntry>
|
|
132
134
|
}
|
|
@@ -269,7 +271,11 @@ export async function resolveFilters(
|
|
|
269
271
|
staged = undefined
|
|
270
272
|
}
|
|
271
273
|
}
|
|
272
|
-
return {
|
|
274
|
+
return {
|
|
275
|
+
names: [...selected].sort(),
|
|
276
|
+
byDiff: parsed.some((f) => !f.negate && f.gitSince !== undefined),
|
|
277
|
+
...(staged !== undefined ? { staged } : {}),
|
|
278
|
+
}
|
|
273
279
|
}
|
|
274
280
|
|
|
275
281
|
export interface PickedTask {
|
|
@@ -20,6 +20,15 @@ export interface RunOptions {
|
|
|
20
20
|
*/
|
|
21
21
|
tasks: readonly string[]
|
|
22
22
|
projects?: string[]
|
|
23
|
+
/**
|
|
24
|
+
* `projects` came from a git diff (`--affected`, a `[ref]` filter), so
|
|
25
|
+
* which projects hold a bare task name depends on what changed: a name
|
|
26
|
+
* the changed projects do not declare is not a typo when the workspace
|
|
27
|
+
* declares it elsewhere, and a run with nothing left to do is clean.
|
|
28
|
+
* Judged against the scope, a docs-only commit turned
|
|
29
|
+
* `vx run test --affected` red (item 1024).
|
|
30
|
+
*/
|
|
31
|
+
selectedByDiff?: boolean
|
|
23
32
|
/**
|
|
24
33
|
* Configs a selection pass in this same process already loaded and
|
|
25
34
|
* staged with the same `cacheDir` and `frozen` (the CLI walks every
|
|
@@ -277,7 +277,10 @@ export async function applyScheduleHooks(
|
|
|
277
277
|
* `admit` stage, built once per run into the predicate the scheduler asks
|
|
278
278
|
* at every local dispatch: every plugin that answers must admit. A plugin
|
|
279
279
|
* that throws is reported once and admits from then on — a policy never
|
|
280
|
-
* breaks a run —
|
|
280
|
+
* breaks a run — and one that refuses while nothing local is running is
|
|
281
|
+
* overridden with a word, since only a completion asks again: that
|
|
282
|
+
* refusal stalled the run for good (item 1023). So the predicate is never
|
|
283
|
+
* the reason a task hangs.
|
|
281
284
|
* Undefined when no plugin answers, which keeps the scheduler on its
|
|
282
285
|
* count-only path.
|
|
283
286
|
*/
|
|
@@ -290,6 +293,7 @@ export function buildAdmission(
|
|
|
290
293
|
const answering = plugins.filter((p) => p.admit !== undefined)
|
|
291
294
|
if (answering.length === 0) return undefined
|
|
292
295
|
const broken = new Set<VxPlugin>()
|
|
296
|
+
const overridden = new Set<VxPlugin>()
|
|
293
297
|
return (id, running) => {
|
|
294
298
|
// The scheduler asks only about ids in this same map, so the lookup
|
|
295
299
|
// always lands: an unknown-id arm here was deleted with the whole core
|
|
@@ -302,7 +306,14 @@ export function buildAdmission(
|
|
|
302
306
|
for (const plugin of answering) {
|
|
303
307
|
if (broken.has(plugin)) continue
|
|
304
308
|
try {
|
|
305
|
-
if (plugin.admit!(task, ctx)
|
|
309
|
+
if (plugin.admit!(task, ctx) !== false) continue
|
|
310
|
+
if (running.size > 0) return false
|
|
311
|
+
if (!overridden.has(plugin)) {
|
|
312
|
+
overridden.add(plugin)
|
|
313
|
+
warn(
|
|
314
|
+
`plugin '${plugin.name}' refused ${id} in admit with nothing running; admitting it, since no completion would ask again`,
|
|
315
|
+
)
|
|
316
|
+
}
|
|
306
317
|
} catch (err) {
|
|
307
318
|
broken.add(plugin)
|
|
308
319
|
const m = err instanceof Error ? err.message : String(err)
|
|
@@ -131,11 +131,14 @@ export interface PreparedRun {
|
|
|
131
131
|
* user's task names were resolved against `projects`. Typically
|
|
132
132
|
* a typo'd task name; `run()` treats this as a CI footgun and
|
|
133
133
|
* returns NOT-ok.
|
|
134
|
+
* - `'none-affected'` — as `'no-tasks-declared'`, but the scope
|
|
135
|
+
* came from a diff and every name is declared somewhere else in the
|
|
136
|
+
* workspace: nothing changed that runs them, a clean outcome.
|
|
134
137
|
* - `'empty-graph'` — `requested` was non-empty but the
|
|
135
138
|
* graph builder still produced no nodes. Defensive; unreachable
|
|
136
139
|
* under current `buildTaskGraph` semantics.
|
|
137
140
|
*/
|
|
138
|
-
empty: null | 'no-tasks-declared' | 'empty-graph'
|
|
141
|
+
empty: null | 'no-tasks-declared' | 'none-affected' | 'empty-graph'
|
|
139
142
|
}
|
|
140
143
|
|
|
141
144
|
/**
|
|
@@ -286,7 +289,16 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
286
289
|
: [...projects.keys()]
|
|
287
290
|
|
|
288
291
|
const requested = expandRequested(options.tasks, candidateProjects, projects)
|
|
289
|
-
|
|
292
|
+
let unresolvedTasks = unresolvedRequests(options.tasks, candidateProjects, projects)
|
|
293
|
+
if (
|
|
294
|
+
options.selectedByDiff === true &&
|
|
295
|
+
projects.size < projectsWithConfigs.length &&
|
|
296
|
+
unresolvedTasks.some((t) => !t.includes('#'))
|
|
297
|
+
) {
|
|
298
|
+
unresolvedTasks = await declaredNowhere(unresolvedTasks, () =>
|
|
299
|
+
loadProjects({ ...loadArgs, seeds: 'all', closure: false, staged: projects }),
|
|
300
|
+
)
|
|
301
|
+
}
|
|
290
302
|
|
|
291
303
|
// Cache seam precedence: an EXPLICITLY injected remote layer
|
|
292
304
|
// (RunOptions.remoteCache — a distribution agent or daemon that already
|
|
@@ -369,7 +381,10 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
369
381
|
gitFilesCache,
|
|
370
382
|
hashCache,
|
|
371
383
|
workspaceProjectCount: projectMetas.length,
|
|
372
|
-
empty:
|
|
384
|
+
empty:
|
|
385
|
+
options.selectedByDiff === true && unresolvedTasks.length === 0
|
|
386
|
+
? 'none-affected'
|
|
387
|
+
: 'no-tasks-declared',
|
|
373
388
|
}
|
|
374
389
|
}
|
|
375
390
|
|
|
@@ -460,6 +475,29 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
460
475
|
}
|
|
461
476
|
}
|
|
462
477
|
|
|
478
|
+
/**
|
|
479
|
+
* The names in `unresolved` no project in the whole workspace declares: an
|
|
480
|
+
* anchored `pkg#task` stays as it is, a bare name leaves when a config the
|
|
481
|
+
* diff's scope left out declares it. `load` evaluates those configs; one
|
|
482
|
+
* failing to load leaves every name as it was, so the guard still speaks.
|
|
483
|
+
*/
|
|
484
|
+
async function declaredNowhere(
|
|
485
|
+
unresolved: readonly string[],
|
|
486
|
+
load: () => Promise<LoadedProjects>,
|
|
487
|
+
): Promise<string[]> {
|
|
488
|
+
let all: LoadedProjects
|
|
489
|
+
try {
|
|
490
|
+
all = await load()
|
|
491
|
+
} catch {
|
|
492
|
+
return [...unresolved]
|
|
493
|
+
}
|
|
494
|
+
const declared = new Set<string>()
|
|
495
|
+
for (const p of all.projects.values()) {
|
|
496
|
+
for (const t of Object.keys(p.config.tasks ?? {})) declared.add(t)
|
|
497
|
+
}
|
|
498
|
+
return unresolved.filter((t) => t.includes('#') || !declared.has(t))
|
|
499
|
+
}
|
|
500
|
+
|
|
463
501
|
/**
|
|
464
502
|
* Refuses the first `^name` in `unproven` that no project in the whole
|
|
465
503
|
* workspace declares. `load` evaluates the configs a scoped run left out;
|
package/src/orchestrator/run.ts
CHANGED
|
@@ -259,6 +259,12 @@ async function runOnBus(
|
|
|
259
259
|
prepared.cache.close()
|
|
260
260
|
return { ok: false, outcomes: [] }
|
|
261
261
|
}
|
|
262
|
+
if (prepared.empty === 'none-affected') {
|
|
263
|
+
log.status(`No affected project declares task(s): ${options.tasks.join(', ')}.`)
|
|
264
|
+
await teardown()
|
|
265
|
+
prepared.cache.close()
|
|
266
|
+
return { ok: true, outcomes: [] }
|
|
267
|
+
}
|
|
262
268
|
if (prepared.empty !== null) {
|
|
263
269
|
// `no-tasks-declared` is almost always a typo in CI; we surface
|
|
264
270
|
// a clear message and return NOT-ok so the script exits 1.
|