@vzn/vx 0.0.637 → 0.0.638
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/docs/caching.md +41 -14
- package/docs/cli.md +115 -73
- package/docs/execution.md +1 -1
- package/docs/features.md +6 -5
- package/docs/schema.md +24 -15
- package/package.json +5 -5
- package/schemas/plan.json +2 -2
- package/skills/vx/SKILL.md +13 -2
- package/src/bin.ts +13 -7
- package/src/cache/cache.ts +21 -2
- package/src/cache/chained-cache.ts +4 -0
- package/src/cache/index.ts +9 -1
- package/src/cache/inputs.ts +0 -24
- package/src/cache/layer.ts +7 -1
- package/src/cache/layered-cache.ts +4 -0
- package/src/cache/plugin-cache.ts +248 -0
- package/src/cache/policy.ts +9 -0
- package/src/cache/run-history.ts +68 -0
- package/src/cache/run-store.ts +77 -0
- package/src/cache/schema.ts +38 -9
- package/src/cache/scoped-cache.ts +15 -2
- package/src/cli/cache.ts +33 -16
- package/src/cli/completions.ts +9 -5
- package/src/cli/help.ts +16 -7
- package/src/cli/index.ts +25 -11
- package/src/cli/info.ts +6 -5
- package/src/cli/init.ts +14 -15
- package/src/cli/last.ts +14 -7
- package/src/cli/lock.ts +39 -27
- package/src/cli/plan-format.ts +5 -3
- package/src/cli/plugin-commands.ts +77 -6
- package/src/cli/plugin-templates.ts +1 -1
- package/src/cli/run.ts +14 -12
- package/src/cli/select.ts +18 -9
- package/src/cli/show.ts +66 -40
- package/src/cli/task-verb.ts +4 -4
- package/src/cli/upgrade.ts +20 -11
- package/src/cli/watch-filter.ts +7 -2
- package/src/cli/watch-judge.ts +6 -1
- package/src/cli/watch.ts +44 -20
- package/src/cli/why.ts +10 -1
- package/src/cli/workspace-config.ts +2 -2
- package/src/graph/scheduler.ts +8 -0
- package/src/graph/task-graph.ts +32 -4
- package/src/index.ts +11 -1
- package/src/orchestrator/affected-tasks.ts +8 -3
- package/src/orchestrator/doctor.ts +1 -1
- package/src/orchestrator/execute-task.ts +26 -43
- package/src/orchestrator/failure-mode.ts +19 -10
- package/src/orchestrator/framed-output.ts +1 -2
- package/src/orchestrator/history.ts +60 -4
- package/src/orchestrator/index.ts +2 -1
- package/src/orchestrator/logger.ts +9 -2
- package/src/orchestrator/metrics.ts +7 -3
- package/src/orchestrator/plugin-host.ts +14 -4
- package/src/orchestrator/plugin.ts +80 -8
- package/src/orchestrator/prepare.ts +51 -7
- package/src/orchestrator/projects.ts +3 -0
- package/src/orchestrator/remote-prefetch.ts +9 -5
- package/src/orchestrator/run-records.ts +1 -1
- package/src/orchestrator/run.ts +126 -27
- package/src/orchestrator/status-line.ts +1 -1
- package/src/orchestrator/summary.ts +69 -44
- package/src/orchestrator/telemetry.ts +1 -1
- package/src/settings.ts +12 -1
- package/src/{orchestrator → util}/colors.ts +5 -0
- package/src/util/config-frame.ts +31 -2
- package/src/util/errors.ts +32 -0
- package/src/util/index.ts +8 -1
- package/src/util/refusal-notice.ts +111 -0
- package/src/util/report.ts +31 -0
- package/src/util/ui.ts +186 -0
- package/src/workspace/affected.ts +20 -23
- package/src/workspace/dependency-rules.ts +12 -4
- package/src/workspace/migration.ts +87 -91
- package/src/workspace/workspace.ts +5 -4
package/src/cli/last.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
shortRunId,
|
|
25
25
|
taskLog,
|
|
26
26
|
} from '../orchestrator/index.js'
|
|
27
|
-
import { formatElapsed, splitTaskId, UserError } from '../util/index.js'
|
|
27
|
+
import { formatElapsed, refusal, splitTaskId, UserError, sayNotice, ui } from '../util/index.js'
|
|
28
28
|
import { findWorkspaceRoot } from '../workspace/index.js'
|
|
29
29
|
import { cliCacheDir, parseCacheDirFlag } from './workspace-config.js'
|
|
30
30
|
|
|
@@ -242,7 +242,7 @@ async function printStats(
|
|
|
242
242
|
return 0
|
|
243
243
|
}
|
|
244
244
|
if (stats.length === 0) {
|
|
245
|
-
|
|
245
|
+
ui.print('out', ui.status('info', 'vx last', 'no runs yet'))
|
|
246
246
|
return 0
|
|
247
247
|
}
|
|
248
248
|
const pct = (r: number): string => `${Math.round(r * 100)}%`
|
|
@@ -468,8 +468,10 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
468
468
|
return 0
|
|
469
469
|
}
|
|
470
470
|
if (log.source === null) {
|
|
471
|
-
|
|
472
|
-
|
|
471
|
+
sayNotice(
|
|
472
|
+
'info',
|
|
473
|
+
'vx last',
|
|
474
|
+
`${log.taskId} ended ${log.status}. vx keeps output only for failed and cached tasks`,
|
|
473
475
|
)
|
|
474
476
|
}
|
|
475
477
|
process.stdout.write(log.output)
|
|
@@ -486,9 +488,8 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
486
488
|
return 0
|
|
487
489
|
}
|
|
488
490
|
if (invocations.length === 0) {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
)
|
|
491
|
+
const none = parsed.failed === true && !noRuns() ? 'no failed runs' : 'no runs yet'
|
|
492
|
+
ui.print('out', ui.status('info', 'vx last', none))
|
|
492
493
|
return 0
|
|
493
494
|
}
|
|
494
495
|
const lines = formatRunList(
|
|
@@ -504,6 +505,12 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
504
505
|
: parsed.failed === true
|
|
505
506
|
? (failedInvocations(db, 1)[0] ?? null)
|
|
506
507
|
: (listInvocations(db, { limit: 1 })[0] ?? null)
|
|
508
|
+
if ((inv === null || inv === undefined) && noRuns()) {
|
|
509
|
+
throw refusal(
|
|
510
|
+
{ tone: 'info', headline: 'no runs yet', fix: 'vx run' },
|
|
511
|
+
{ code: 'VX_E_NO_HISTORY', message: 'vx last: no recorded runs yet — run something first' },
|
|
512
|
+
)
|
|
513
|
+
}
|
|
507
514
|
if (inv === null || inv === undefined) {
|
|
508
515
|
throw new UserError(
|
|
509
516
|
noRuns()
|
package/src/cli/lock.ts
CHANGED
|
@@ -17,11 +17,14 @@ import {
|
|
|
17
17
|
} from '../orchestrator/index.js'
|
|
18
18
|
import { resolveSetting, runRules } from '../settings.js'
|
|
19
19
|
import {
|
|
20
|
-
|
|
20
|
+
refusalNotice,
|
|
21
21
|
DOT_VX_GITATTRIBUTES,
|
|
22
22
|
isUserError,
|
|
23
23
|
relPosix,
|
|
24
|
+
type RefusalParts,
|
|
24
25
|
secretMask,
|
|
26
|
+
ui,
|
|
27
|
+
sayNotice,
|
|
25
28
|
} from '../util/index.js'
|
|
26
29
|
import {
|
|
27
30
|
findProjectConfigs,
|
|
@@ -84,7 +87,7 @@ export async function lockCmd(args: readonly string[]): Promise<number> {
|
|
|
84
87
|
|
|
85
88
|
/**
|
|
86
89
|
* Projects with tasks and no vx.config: a plugin's tasks, which load live
|
|
87
|
-
* in every run, so both verbs name them — `
|
|
90
|
+
* in every run, so both verbs name them — `0 project configs`
|
|
88
91
|
* on a turbo() repo read like an audit of something (2026-09-16). One with
|
|
89
92
|
* no task is left out: solidjs/solid's solid-ssr, which has no script Turbo
|
|
90
93
|
* runs, read as a project whose tasks come live from the plugin `project` stage.
|
|
@@ -100,13 +103,14 @@ function bareProjects(all: readonly ProjectMeta[], tasked: ReadonlySet<string>):
|
|
|
100
103
|
export async function lockWorkspace(
|
|
101
104
|
root: string,
|
|
102
105
|
warn: (message: string) => void = warnToStderr,
|
|
106
|
+
say: (line: string) => void = (line) => ui.print('out', ui.status('ok', 'vx lock', line)),
|
|
103
107
|
): Promise<number> {
|
|
104
108
|
const check = await audit(root, warn)
|
|
105
109
|
if (check.broken !== null) {
|
|
106
|
-
|
|
110
|
+
notWritten(check.broken, check.brokenParts)
|
|
107
111
|
return 1
|
|
108
112
|
}
|
|
109
|
-
return await writeLock(root, check)
|
|
113
|
+
return await writeLock(root, check, say)
|
|
110
114
|
}
|
|
111
115
|
|
|
112
116
|
/**
|
|
@@ -159,6 +163,7 @@ async function audit(root: string, warn: (message: string) => void): Promise<Gra
|
|
|
159
163
|
closure: false,
|
|
160
164
|
configs: lock,
|
|
161
165
|
warn,
|
|
166
|
+
lock: true,
|
|
162
167
|
})
|
|
163
168
|
for (const entry of projects.values()) resolved.set(entry.name, entry.config)
|
|
164
169
|
const names = new Set<string>()
|
|
@@ -193,7 +198,8 @@ async function audit(root: string, warn: (message: string) => void): Promise<Gra
|
|
|
193
198
|
rules,
|
|
194
199
|
})
|
|
195
200
|
} catch (err) {
|
|
196
|
-
if (isUserError(err))
|
|
201
|
+
if (isUserError(err))
|
|
202
|
+
return { ...out, broken: err.message, ...(err.parts ? { brokenParts: err.parts } : {}) }
|
|
197
203
|
throw err
|
|
198
204
|
}
|
|
199
205
|
return out
|
|
@@ -202,6 +208,7 @@ async function audit(root: string, warn: (message: string) => void): Promise<Gra
|
|
|
202
208
|
interface GraphCheck {
|
|
203
209
|
/** A graph rule's refusal, or a config that does not load; null when sound. */
|
|
204
210
|
broken: string | null
|
|
211
|
+
brokenParts?: RefusalParts
|
|
205
212
|
/** The lock the configs evaluate to here. */
|
|
206
213
|
fresh: Lockfile
|
|
207
214
|
/** Every project, as a run discovers them from `fresh`. */
|
|
@@ -219,23 +226,25 @@ function configured(all: readonly ProjectMeta[]): ConfiguredMeta[] {
|
|
|
219
226
|
}
|
|
220
227
|
|
|
221
228
|
/**
|
|
222
|
-
* `vx init`'s last step: the lock, as `vx lock` writes it
|
|
223
|
-
*
|
|
224
|
-
* written configs in place.
|
|
229
|
+
* `vx init`'s last step: the lock, as `vx lock` writes it, its line
|
|
230
|
+
* returned for the report's `lock` row. A refusal (a graph rule, a config
|
|
231
|
+
* that does not load) is printed and leaves the written configs in place.
|
|
225
232
|
*/
|
|
226
|
-
export async function lockAfterMigration(root: string): Promise<number> {
|
|
233
|
+
export async function lockAfterMigration(root: string): Promise<{ code: number; said: string }> {
|
|
234
|
+
let said = ''
|
|
227
235
|
try {
|
|
228
|
-
|
|
236
|
+
const code = await lockWorkspace(root, warnToStderr, (line) => (said = line))
|
|
237
|
+
return { code, said }
|
|
229
238
|
} catch (err) {
|
|
230
239
|
if (!isUserError(err)) throw err
|
|
231
|
-
|
|
232
|
-
return 1
|
|
240
|
+
notWritten(err.message, err.parts)
|
|
241
|
+
return { code: 1, said }
|
|
233
242
|
}
|
|
234
243
|
}
|
|
235
244
|
|
|
236
|
-
/**
|
|
237
|
-
function
|
|
238
|
-
|
|
245
|
+
/** The lock refused: the refusal's own parts, or its config line and frame, or its message. */
|
|
246
|
+
function notWritten(message: string, parts: RefusalParts | undefined): void {
|
|
247
|
+
ui.print('err', refusalNotice('vx lock', message, parts, { headline: 'lock not written' }))
|
|
239
248
|
}
|
|
240
249
|
|
|
241
250
|
/** Names shown per note: a turbo() repo can have hundreds of bare projects. */
|
|
@@ -295,7 +304,11 @@ async function writeIfMissing(file: string, text: string): Promise<void> {
|
|
|
295
304
|
}
|
|
296
305
|
}
|
|
297
306
|
|
|
298
|
-
async function writeLock(
|
|
307
|
+
async function writeLock(
|
|
308
|
+
root: string,
|
|
309
|
+
check: GraphCheck,
|
|
310
|
+
say: (line: string) => void,
|
|
311
|
+
): Promise<number> {
|
|
299
312
|
const metas = configured(check.all)
|
|
300
313
|
const bare = bareProjects(check.all, check.tasked)
|
|
301
314
|
// By project name, as the refusal names them.
|
|
@@ -310,11 +323,12 @@ async function writeLock(root: string, check: GraphCheck): Promise<number> {
|
|
|
310
323
|
for (const l of secretsIn(Object.fromEntries(reviews.map((r) => [r.name, r.config]))))
|
|
311
324
|
if (!named.has(l.at)) leaks.push(l)
|
|
312
325
|
if (leaks.length > 0) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
326
|
+
notWritten('', {
|
|
327
|
+
headline: 'these configs evaluated to a secret value',
|
|
328
|
+
what: leaks.map((l) => l.at),
|
|
329
|
+
why: 'the lock is committed, so the secret would be too',
|
|
330
|
+
fix: `let the shell expand it ($${leaks[0]!.name} in the command, the name in exec.env.passThrough) instead of reading process.env in the config`,
|
|
331
|
+
})
|
|
318
332
|
return 1
|
|
319
333
|
}
|
|
320
334
|
await writeLockfile(root, check.fresh)
|
|
@@ -323,9 +337,7 @@ async function writeLock(root: string, check: GraphCheck): Promise<number> {
|
|
|
323
337
|
writeIfMissing(path.join(root, '.vx', '.gitattributes'), DOT_VX_GITATTRIBUTES),
|
|
324
338
|
])
|
|
325
339
|
const n = metas.length
|
|
326
|
-
|
|
327
|
-
`vx lock: locked ${n} project config${n === 1 ? '' : 's'} → ${LOCKFILE_NAME}${bareNote(bare)}\n`,
|
|
328
|
-
)
|
|
340
|
+
say(`${n} project config${n === 1 ? '' : 's'} → ${LOCKFILE_NAME}${bareNote(bare)}`)
|
|
329
341
|
return 0
|
|
330
342
|
}
|
|
331
343
|
|
|
@@ -389,7 +401,7 @@ async function checkLock(root: string, json: boolean): Promise<number> {
|
|
|
389
401
|
const lock = await readLockfile(root)
|
|
390
402
|
if (!lock) {
|
|
391
403
|
const problem = `no ${LOCKFILE_NAME} at ${root} — run \`vx lock\` first`
|
|
392
|
-
if (!json)
|
|
404
|
+
if (!json) sayNotice('error', 'vx lock --check', problem)
|
|
393
405
|
return answer([problem])
|
|
394
406
|
}
|
|
395
407
|
const failures: string[] = []
|
|
@@ -451,12 +463,12 @@ async function checkLock(root: string, json: boolean): Promise<number> {
|
|
|
451
463
|
}
|
|
452
464
|
if (json) return answer(failures)
|
|
453
465
|
if (failures.length > 0) {
|
|
454
|
-
for (const f of failures)
|
|
466
|
+
for (const f of failures) sayNotice('error', 'vx lock --check', f)
|
|
455
467
|
return 1
|
|
456
468
|
}
|
|
457
469
|
const n = metas.length
|
|
458
470
|
const audited = `${n} project${n === 1 ? '' : 's'}`
|
|
459
471
|
const skipped = bare.length === 0 ? '' : `; not audited, no vx.config: ${bareNames(bare)}`
|
|
460
|
-
|
|
472
|
+
ui.print('out', ui.status('ok', 'vx lock --check', `up to date (${audited}${skipped})`))
|
|
461
473
|
return 0
|
|
462
474
|
}
|
package/src/cli/plan-format.ts
CHANGED
|
@@ -118,9 +118,11 @@ function describeReason(r: AffectedReason): string {
|
|
|
118
118
|
? `a ^ edge passes changed package ${r.project}`
|
|
119
119
|
: r.kind === 'input'
|
|
120
120
|
? `${r.file} changed (an input)`
|
|
121
|
-
: r.
|
|
122
|
-
? `${r.file} changed (
|
|
123
|
-
:
|
|
121
|
+
: r.kind === 'workspace'
|
|
122
|
+
? `${r.file} changed (workspace-wide)`
|
|
123
|
+
: r.file !== undefined
|
|
124
|
+
? `${r.file} changed (in its project)`
|
|
125
|
+
: 'its project changed as a whole'
|
|
124
126
|
return r.via === undefined ? cause : `${cause}, via ${r.via.join(' ← ')}`
|
|
125
127
|
}
|
|
126
128
|
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
// reports an unknown command as it always did. Core's verbs are matched
|
|
8
8
|
// before this is asked, so a plugin cannot shadow `run`.
|
|
9
9
|
|
|
10
|
+
import type { CachePolicy, PluginCache, PluginCacheRemote } from '../cache/index.js'
|
|
11
|
+
import type { WorkspaceConfig } from '../config.js'
|
|
10
12
|
import type { VxPlugin, PluginCommand, CommandContext } from '../orchestrator/index.js'
|
|
11
|
-
import { findWorkspaceRoot } from '../workspace/index.js'
|
|
13
|
+
import { cacheScopeEnvError, findWorkspaceRoot, isCacheScope } from '../workspace/index.js'
|
|
12
14
|
import { machineParallelism, maskedLine } from '../util/index.js'
|
|
13
15
|
import { resolveSetting } from '../settings.js'
|
|
14
16
|
|
|
@@ -39,10 +41,15 @@ export interface UnresolvedPluginCommand {
|
|
|
39
41
|
loadError: string
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
async function workspacePlugins(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
async function workspacePlugins(cwd: string): Promise<
|
|
45
|
+
| {
|
|
46
|
+
workspaceRoot: string
|
|
47
|
+
cacheDir: string
|
|
48
|
+
storeRoot: Promise<string | null>
|
|
49
|
+
workspaceConfig: WorkspaceConfig | null
|
|
50
|
+
concurrency: number
|
|
51
|
+
plugins: readonly VxPlugin[]
|
|
52
|
+
}
|
|
46
53
|
| UnresolvedPluginCommand
|
|
47
54
|
| null
|
|
48
55
|
> {
|
|
@@ -62,6 +69,8 @@ async function workspacePlugins(
|
|
|
62
69
|
return {
|
|
63
70
|
workspaceRoot,
|
|
64
71
|
cacheDir: ws.cacheDir,
|
|
72
|
+
storeRoot: ws.storeRoot,
|
|
73
|
+
workspaceConfig: ws.workspaceConfig,
|
|
65
74
|
// What run.ts takes when no --concurrency is given.
|
|
66
75
|
concurrency:
|
|
67
76
|
(resolveSetting('run.concurrency', ws.workspaceConfig).value as number | undefined) ??
|
|
@@ -98,13 +107,75 @@ export async function resolvePluginCommand(
|
|
|
98
107
|
vx: (await import('./upgrade.js')).isCompiledBinary()
|
|
99
108
|
? [process.execPath]
|
|
100
109
|
: [process.execPath, Bun.main],
|
|
101
|
-
warn
|
|
110
|
+
warn,
|
|
111
|
+
cache: lazyCache(() => verbCache(ws, plugin.name)),
|
|
102
112
|
},
|
|
103
113
|
}
|
|
104
114
|
}
|
|
105
115
|
return { declaredVerbs: [...declaredVerbs] }
|
|
106
116
|
}
|
|
107
117
|
|
|
118
|
+
const warn = (m: string): void => {
|
|
119
|
+
process.stderr.write(`${maskedLine(m)}\n`)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Read-only: a verb reads what runs stored, and only a run's policy may write. */
|
|
123
|
+
const VERB_POLICY: CachePolicy = {
|
|
124
|
+
localRead: true,
|
|
125
|
+
localWrite: false,
|
|
126
|
+
remoteRead: true,
|
|
127
|
+
remoteWrite: false,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The verb's `ctx.cache`: the plugin's entries in the local store, then each
|
|
132
|
+
* remote the workspace's cache plugins supply, under the run's scope. Opened
|
|
133
|
+
* on first use, so a verb that never reads it opens nothing.
|
|
134
|
+
*/
|
|
135
|
+
async function verbCache(
|
|
136
|
+
ws: Exclude<Awaited<ReturnType<typeof workspacePlugins>>, UnresolvedPluginCommand | null>,
|
|
137
|
+
plugin: string,
|
|
138
|
+
): Promise<PluginCache> {
|
|
139
|
+
const [{ Cache, pluginCache }, { resolveCache }] = await Promise.all([
|
|
140
|
+
import('../cache/index.js'),
|
|
141
|
+
import('../orchestrator/index.js'),
|
|
142
|
+
])
|
|
143
|
+
const envScope = process.env['VX_CACHE_SCOPE'] || undefined
|
|
144
|
+
if (envScope !== undefined && !isCacheScope(envScope)) throw cacheScopeEnvError()
|
|
145
|
+
const scope = envScope ?? ws.workspaceConfig?.cache?.scope
|
|
146
|
+
const localCache = new Cache(
|
|
147
|
+
ws.cacheDir,
|
|
148
|
+
{ read: true, write: false },
|
|
149
|
+
ws.workspaceRoot,
|
|
150
|
+
undefined,
|
|
151
|
+
'open',
|
|
152
|
+
await ws.storeRoot,
|
|
153
|
+
)
|
|
154
|
+
const remotes: PluginCacheRemote[] = []
|
|
155
|
+
await resolveCache(ws.plugins, {
|
|
156
|
+
workspaceRoot: ws.workspaceRoot,
|
|
157
|
+
cacheDir: ws.cacheDir,
|
|
158
|
+
warn,
|
|
159
|
+
scope,
|
|
160
|
+
localCache,
|
|
161
|
+
policy: VERB_POLICY,
|
|
162
|
+
remotes,
|
|
163
|
+
})
|
|
164
|
+
return pluginCache(plugin, { db: localCache.dbHandle(), policy: VERB_POLICY, remotes, scope })
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function lazyCache(open: () => Promise<PluginCache>): PluginCache {
|
|
168
|
+
let opened: Promise<PluginCache> | undefined
|
|
169
|
+
const cache = (): Promise<PluginCache> => (opened ??= open())
|
|
170
|
+
return {
|
|
171
|
+
get: async (key) => (await cache()).get(key),
|
|
172
|
+
put: async (key, bytes) => (await cache()).put(key, bytes),
|
|
173
|
+
has: async (key) => (await cache()).has(key),
|
|
174
|
+
hasMany: async (keys) => (await cache()).hasMany(keys),
|
|
175
|
+
getRun: async (name) => (await cache()).getRun(name),
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
108
179
|
/** The plugin verbs the workspace around `cwd` declares, in declaration order; none when it does not load. */
|
|
109
180
|
export async function pluginVerbs(cwd = process.cwd()): Promise<string[]> {
|
|
110
181
|
const ws = await workspacePlugins(cwd).catch(() => null)
|
|
@@ -16,7 +16,7 @@ export const PLUGIN_TEMPLATES: Readonly<Record<string, { plugin: string; test: s
|
|
|
16
16
|
commands: {
|
|
17
17
|
plugin:
|
|
18
18
|
"// `commands`: a CLI verb. `vx cache-dir` prints where this workspace's cache\n// lives. The verb's return value is the exit code.\nimport { definePlugin, type VxPlugin } from '@vzn/vx'\n\nexport function cacheDirVerb(): VxPlugin {\n return definePlugin(import.meta, {\n commands: {\n 'cache-dir': {\n description: \"print this workspace's cache directory\",\n run(argv, ctx) {\n if (argv.length > 0) {\n console.error(`vx cache-dir: takes no arguments, got ${argv.join(' ')}`)\n return 2\n }\n console.log(ctx.cacheDir)\n return 0\n },\n },\n },\n })\n}\n",
|
|
19
|
-
test: "// A test for the example `commands` plugin beside it: the verb is called\n// the way vx calls it, with the context vx hands it. `bun test` runs it.\nimport { expect, it, spyOn } from 'bun:test'\nimport { cacheDirVerb } from './commands.ts'\n\nconst verb = cacheDirVerb().commands!['cache-dir']!\nconst ctx = {\n workspaceRoot: '/ws',\n cacheDir: '/ws/.vx/cache',\n warn() {},\n concurrency: 1,\n vx: ['vx'],\n}\n\nit('prints the cache directory; an argument is exit 2', async () => {\n const out = spyOn(console, 'log').mockImplementation(() => {})\n const err = spyOn(console, 'error').mockImplementation(() => {})\n try {\n expect(await verb.run([], ctx)).toBe(0)\n expect(out.mock.calls).toEqual([['/ws/.vx/cache']])\n expect(await verb.run(['x'], ctx)).toBe(2)\n expect(err.mock.calls).toEqual([['vx cache-dir: takes no arguments, got x']])\n } finally {\n out.mockRestore()\n err.mockRestore()\n }\n})\n",
|
|
19
|
+
test: "// A test for the example `commands` plugin beside it: the verb is called\n// the way vx calls it, with the context vx hands it. `bun test` runs it.\nimport { expect, it, spyOn } from 'bun:test'\nimport { cacheDirVerb } from './commands.ts'\n\nconst verb = cacheDirVerb().commands!['cache-dir']!\nconst ctx = {\n workspaceRoot: '/ws',\n cacheDir: '/ws/.vx/cache',\n warn() {},\n concurrency: 1,\n vx: ['vx'],\n cache: {\n get: async () => null,\n put: async () => {},\n has: async () => null,\n hasMany: async () => null,\n getRun: async () => null,\n },\n}\n\nit('prints the cache directory; an argument is exit 2', async () => {\n const out = spyOn(console, 'log').mockImplementation(() => {})\n const err = spyOn(console, 'error').mockImplementation(() => {})\n try {\n expect(await verb.run([], ctx)).toBe(0)\n expect(out.mock.calls).toEqual([['/ws/.vx/cache']])\n expect(await verb.run(['x'], ctx)).toBe(2)\n expect(err.mock.calls).toEqual([['vx cache-dir: takes no arguments, got x']])\n } finally {\n out.mockRestore()\n err.mockRestore()\n }\n})\n",
|
|
20
20
|
},
|
|
21
21
|
executor: {
|
|
22
22
|
plugin:
|
package/src/cli/run.ts
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
} from '../cache/index.js'
|
|
24
24
|
import { affectedFilterFor, findCwdSelection, pickTask, resolveFilters } from './select.js'
|
|
25
25
|
import { nxTargetHint, taskNamesHere } from './task-verb.js'
|
|
26
|
-
import { parseDecimalInt } from '../util/index.js'
|
|
26
|
+
import { parseDecimalInt, sayNotice, sayLine } from '../util/index.js'
|
|
27
27
|
import { kindHint, parseSettingValue, type Setting, SETTINGS, settingRefusal } from '../settings.js'
|
|
28
28
|
import { formatGraphDot, formatPlanJson, formatPlanText } from './plan-format.js'
|
|
29
29
|
|
|
@@ -556,7 +556,7 @@ export async function resolveRunOptions(
|
|
|
556
556
|
// scope. An empty project list keeps the bare tasks unselected
|
|
557
557
|
// while the anchored ones still run.
|
|
558
558
|
if (anchoredTasks.length === 0) return { nothingSelected: resolved.empty }
|
|
559
|
-
|
|
559
|
+
sayNotice('info', 'vx run', `${resolved.empty} — running ${anchoredTasks.join(', ')} only`)
|
|
560
560
|
projects = []
|
|
561
561
|
} else {
|
|
562
562
|
projects = resolved.names
|
|
@@ -665,7 +665,7 @@ export async function runCmd(args: readonly string[]): Promise<number> {
|
|
|
665
665
|
// Every run needs git: refused here, not after the user has chosen.
|
|
666
666
|
const refusal = gitRefusal(await findWorkspaceRoot(cwd))
|
|
667
667
|
if (refusal !== undefined) {
|
|
668
|
-
|
|
668
|
+
sayLine('error', refusal.message, 'vx run')
|
|
669
669
|
return 1
|
|
670
670
|
}
|
|
671
671
|
const load = parsed.cacheDir !== undefined ? { cacheDir: parsed.cacheDir } : {}
|
|
@@ -679,7 +679,7 @@ export async function runCmd(args: readonly string[]): Promise<number> {
|
|
|
679
679
|
if ('error' in selected)
|
|
680
680
|
return refuse('run', args, selected.error, selected.code ?? 'VX_E_USAGE')
|
|
681
681
|
if ('empty' in selected) {
|
|
682
|
-
|
|
682
|
+
sayNotice('info', 'vx run', selected.empty)
|
|
683
683
|
return 0
|
|
684
684
|
}
|
|
685
685
|
only = new Set(selected.names)
|
|
@@ -695,7 +695,7 @@ export async function runCmd(args: readonly string[]): Promise<number> {
|
|
|
695
695
|
const resolved = await resolveRunOptions(parsed, cwd, tasks, 'run', scopeOut)
|
|
696
696
|
if ('error' in resolved) return refuse('run', args, resolved.error, resolved.code ?? 'VX_E_USAGE')
|
|
697
697
|
if ('nothingSelected' in resolved) {
|
|
698
|
-
|
|
698
|
+
sayNotice('info', 'vx run', resolved.nothingSelected)
|
|
699
699
|
return 0
|
|
700
700
|
}
|
|
701
701
|
const opts = resolved
|
|
@@ -715,8 +715,10 @@ export async function runCmd(args: readonly string[]): Promise<number> {
|
|
|
715
715
|
if (parsed.dry !== undefined || parsed.graph !== undefined) {
|
|
716
716
|
const plan = await planRun(opts)
|
|
717
717
|
if (plan.unresolvedTasks !== undefined && plan.unresolvedTasks.length > 0) {
|
|
718
|
-
|
|
719
|
-
|
|
718
|
+
sayNotice(
|
|
719
|
+
'error',
|
|
720
|
+
'vx run',
|
|
721
|
+
`no projects declare task(s): ${plan.unresolvedTasks.join(', ')}.${plan.unresolvedHint ?? ''}`,
|
|
720
722
|
)
|
|
721
723
|
return 1
|
|
722
724
|
}
|
|
@@ -724,14 +726,14 @@ export async function runCmd(args: readonly string[]): Promise<number> {
|
|
|
724
726
|
// The run's own line: a dry run that guessed from the scope alone
|
|
725
727
|
// said no affected project declared a task one did (X-141).
|
|
726
728
|
if (plan.noneAffected !== undefined) {
|
|
727
|
-
|
|
729
|
+
sayLine('info', plan.noneAffected, 'vx run')
|
|
728
730
|
return 0
|
|
729
731
|
}
|
|
730
732
|
if (opts.selectedByDiff === true) {
|
|
731
|
-
|
|
733
|
+
sayNotice('info', 'vx run', `no affected project declares task(s): ${tasks.join(', ')}`)
|
|
732
734
|
return 0
|
|
733
735
|
}
|
|
734
|
-
|
|
736
|
+
sayNotice('error', 'vx run', `no projects declare task(s): ${tasks.join(', ')}`)
|
|
735
737
|
return 1
|
|
736
738
|
}
|
|
737
739
|
if (parsed.graph !== undefined) {
|
|
@@ -747,7 +749,7 @@ export async function runCmd(args: readonly string[]): Promise<number> {
|
|
|
747
749
|
await Bun.write(target, out)
|
|
748
750
|
} catch (err) {
|
|
749
751
|
const message = err instanceof Error ? err.message : String(err)
|
|
750
|
-
|
|
752
|
+
sayNotice('error', 'vx run', `failed to write graph to ${target}: ${message}`)
|
|
751
753
|
return 1
|
|
752
754
|
}
|
|
753
755
|
}
|
|
@@ -813,7 +815,7 @@ export async function runCmd(args: readonly string[]): Promise<number> {
|
|
|
813
815
|
// Same contract as --summarize / --profile: the run already
|
|
814
816
|
// happened, so a write failure is reported, not fatal.
|
|
815
817
|
const message = err instanceof Error ? err.message : String(err)
|
|
816
|
-
|
|
818
|
+
sayNotice('warn', 'vx run', `failed to write report to ${target}: ${message}`)
|
|
817
819
|
}
|
|
818
820
|
}
|
|
819
821
|
}
|
package/src/cli/select.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
maskedLine,
|
|
33
33
|
nearest,
|
|
34
34
|
UserError,
|
|
35
|
+
sayNotice,
|
|
35
36
|
} from '../util/index.js'
|
|
36
37
|
import {
|
|
37
38
|
claimedAffected,
|
|
@@ -434,8 +435,10 @@ export async function resolveFilters(
|
|
|
434
435
|
// Something matched, so the run proceeds; a pattern that matched nothing
|
|
435
436
|
// alongside it is still worth a line — it is probably a typo.
|
|
436
437
|
for (const f of unmatched)
|
|
437
|
-
|
|
438
|
-
|
|
438
|
+
sayNotice(
|
|
439
|
+
'warn',
|
|
440
|
+
'vx run',
|
|
441
|
+
`filter "${f}" matched no projects${didYouMean([f], projects, tags)}`,
|
|
439
442
|
)
|
|
440
443
|
// Each include is a union: what `--filter other` selected runs its tasks
|
|
441
444
|
// whether or not the `--affected` diff reaches them, and an exclude
|
|
@@ -511,14 +514,18 @@ export async function pickTask(
|
|
|
511
514
|
}
|
|
512
515
|
}
|
|
513
516
|
if (entries.length === 0 && only !== undefined) {
|
|
514
|
-
|
|
515
|
-
|
|
517
|
+
sayNotice(
|
|
518
|
+
'error',
|
|
519
|
+
'vx run',
|
|
520
|
+
`no tasks declared in the selected projects (${[...only].sort().join(', ')})`,
|
|
516
521
|
)
|
|
517
522
|
return null
|
|
518
523
|
}
|
|
519
524
|
if (entries.length === 0) {
|
|
520
|
-
|
|
521
|
-
'
|
|
525
|
+
sayNotice(
|
|
526
|
+
'error',
|
|
527
|
+
'vx run',
|
|
528
|
+
'no tasks declared in any project. Declare one under `tasks` in a vx.config, or run `vx init` to write them from package.json scripts, then `vx lock`',
|
|
522
529
|
)
|
|
523
530
|
return null
|
|
524
531
|
}
|
|
@@ -562,13 +569,15 @@ export async function pickTask(
|
|
|
562
569
|
if (!(err instanceof Error) || err.name !== 'AbortError') throw err
|
|
563
570
|
if (interrupted) return 'interrupted'
|
|
564
571
|
out.write('\n')
|
|
565
|
-
|
|
572
|
+
sayNotice('info', 'vx run', 'no task picked')
|
|
566
573
|
return null
|
|
567
574
|
}
|
|
568
575
|
const n = Number(answer)
|
|
569
576
|
if (!Number.isInteger(n) || n < 1 || n > entries.length) {
|
|
570
|
-
|
|
571
|
-
|
|
577
|
+
sayNotice(
|
|
578
|
+
'error',
|
|
579
|
+
'vx run',
|
|
580
|
+
`invalid selection: ${answer} (type a number from 1 to ${entries.length})`,
|
|
572
581
|
)
|
|
573
582
|
return null
|
|
574
583
|
}
|