@vzn/vx 0.0.646 → 0.0.648
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/README.md +29 -22
- package/launcher.cjs +9 -2
- package/launcher.sh +2 -2
- package/package.json +5 -5
- package/schemas/cache.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/info.json +16 -2
- package/schemas/last.json +112 -5
- package/schemas/plan.json +28 -2
- package/schemas/show.json +10 -1
- package/skills/vx/SKILL.md +18 -2
- package/src/bin.ts +53 -12
- package/src/cache/archive.ts +324 -23
- package/src/cache/cache.ts +268 -53
- package/src/cache/git-inputs.ts +219 -87
- package/src/cache/index.ts +3 -2
- package/src/cache/inputs.ts +460 -78
- package/src/cache/key-fold.ts +15 -6
- package/src/cache/layer.ts +49 -9
- package/src/cache/layered-cache.ts +123 -58
- package/src/cache/linked-inputs.ts +1 -1
- package/src/cache/output-index.ts +19 -3
- package/src/cache/plugin-cache.ts +49 -23
- package/src/cache/remote-hits.ts +72 -0
- package/src/cache/remote-lane.ts +90 -14
- package/src/cache/run-history.ts +70 -23
- package/src/cache/run-store.ts +11 -10
- package/src/cache/schema.ts +25 -6
- package/src/cache/tar-stream.ts +5 -0
- package/src/cache/umask.ts +1 -2
- package/src/cli/cache.ts +58 -27
- package/src/cli/completions.ts +12 -5
- package/src/cli/docs.ts +42 -16
- package/src/cli/help.ts +58 -19
- package/src/cli/index.ts +79 -33
- package/src/cli/info.ts +12 -6
- package/src/cli/init.ts +186 -31
- package/src/cli/last.ts +439 -45
- package/src/cli/lock.ts +197 -52
- package/src/cli/plan-format.ts +91 -49
- package/src/cli/plugin-commands.ts +70 -14
- package/src/cli/plugin-templates.ts +13 -13
- package/src/cli/registry.ts +209 -0
- package/src/cli/run.ts +109 -34
- package/src/cli/select.ts +143 -46
- package/src/cli/show.ts +59 -16
- package/src/cli/task-verb.ts +16 -6
- package/src/cli/upgrade.ts +151 -49
- package/src/cli/watch-cycle.ts +10 -5
- package/src/cli/watch-filter.ts +71 -3
- package/src/cli/watch-fs.ts +22 -6
- package/src/cli/watch-judge.ts +48 -6
- package/src/cli/watch.ts +163 -42
- package/src/cli/why.ts +211 -73
- package/src/cli/workspace-config.ts +54 -15
- package/src/config.ts +13 -1
- package/src/exec/env.ts +7 -2
- package/src/exec/executor.ts +154 -10
- package/src/exec/index.ts +6 -2
- package/src/exec/kill-tree.ts +15 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/proc-sample.ts +3 -1
- package/src/exec/runner.ts +253 -54
- package/src/exec/sandbox-binds.ts +7 -3
- package/src/exec/sandbox-paths.ts +1 -1
- package/src/exec/sandbox-runtime.ts +211 -24
- package/src/exec/sandbox-violations.ts +52 -20
- package/src/graph/critical-path.ts +79 -0
- package/src/graph/default-build.ts +19 -0
- package/src/graph/definition.ts +38 -0
- package/src/graph/folds.ts +38 -5
- package/src/graph/index.ts +4 -1
- package/src/graph/priorities.ts +74 -8
- package/src/graph/scheduler.ts +270 -60
- package/src/graph/task-graph.ts +228 -35
- package/src/handoff.ts +97 -9
- package/src/history/failure-mode.ts +6 -3
- package/src/history/history.ts +167 -61
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +90 -30
- package/src/index.ts +30 -13
- package/src/orchestrator/admission.ts +10 -6
- package/src/orchestrator/affected-tasks.ts +11 -6
- package/src/orchestrator/deferred-outputs.ts +2 -0
- package/src/orchestrator/doctor.ts +134 -42
- package/src/orchestrator/execute-args.ts +4 -1
- package/src/orchestrator/execute-task.ts +130 -38
- package/src/orchestrator/fingerprint-watch.ts +1 -1
- package/src/orchestrator/graph-memo.ts +140 -0
- package/src/orchestrator/graph-stage.ts +18 -1
- package/src/orchestrator/held-servers.ts +1 -1
- package/src/orchestrator/hit-restore.ts +217 -119
- package/src/orchestrator/index.ts +4 -3
- package/src/orchestrator/local-floor.ts +119 -25
- package/src/orchestrator/local-shortcircuit.ts +28 -4
- package/src/orchestrator/lockfile-claim.ts +96 -16
- package/src/orchestrator/miss-save.ts +42 -8
- package/src/orchestrator/options.ts +8 -6
- package/src/orchestrator/output-host.ts +43 -14
- package/src/orchestrator/output-log.ts +90 -10
- package/src/orchestrator/persistent-task.ts +84 -29
- package/src/orchestrator/persistent.ts +16 -4
- package/src/orchestrator/placement.ts +185 -17
- package/src/orchestrator/plan.ts +64 -7
- package/src/orchestrator/plugin-host.ts +214 -42
- package/src/orchestrator/plugin.ts +113 -37
- package/src/orchestrator/prepare.ts +168 -30
- package/src/orchestrator/projects.ts +65 -40
- package/src/orchestrator/remote-prefetch.ts +119 -25
- package/src/orchestrator/run-artifacts.ts +95 -32
- package/src/orchestrator/run-context.ts +39 -41
- package/src/orchestrator/run-failures.ts +65 -23
- package/src/orchestrator/run-id.ts +6 -3
- package/src/orchestrator/run-keys.ts +149 -13
- package/src/orchestrator/run-live.ts +294 -0
- package/src/orchestrator/run-lock.ts +13 -7
- package/src/orchestrator/run-records.ts +35 -29
- package/src/orchestrator/run-report.ts +3 -3
- package/src/orchestrator/run-steps.ts +58 -22
- package/src/orchestrator/run-stop.ts +22 -5
- package/src/orchestrator/run.ts +331 -88
- package/src/orchestrator/sandbox-fix.ts +7 -5
- package/src/orchestrator/sandbox-request.ts +70 -20
- package/src/orchestrator/shell-verdict.ts +5 -5
- package/src/orchestrator/signals.ts +26 -1
- package/src/orchestrator/stable-keys.ts +92 -8
- package/src/orchestrator/steps.ts +2 -2
- package/src/orchestrator/task-artifacts.ts +90 -26
- package/src/orchestrator/task-hash.ts +78 -13
- package/src/orchestrator/task-log-buffer.ts +72 -36
- package/src/orchestrator/task-process.ts +7 -1
- package/src/orchestrator/telemetry-host.ts +22 -2
- package/src/orchestrator/telemetry.ts +53 -21
- package/src/orchestrator/upstream.ts +15 -0
- package/src/report/events.ts +32 -13
- package/src/report/forecast.ts +31 -9
- package/src/report/framed-output.ts +141 -50
- package/src/report/index.ts +1 -0
- package/src/report/logger.ts +170 -179
- package/src/report/path-links.ts +12 -4
- package/src/report/plain-output.ts +13 -5
- package/src/report/status-line.ts +81 -53
- package/src/report/summary.ts +114 -32
- package/src/report/terminal-signals.ts +13 -2
- package/src/settings.ts +7 -6
- package/src/util/bun-version.ts +4 -1
- package/src/util/colors.ts +4 -2
- package/src/util/config-frame.ts +41 -4
- package/src/{graph → util}/dependency-spec.ts +22 -13
- package/src/util/edit-distance.ts +13 -1
- package/src/util/env-flag.ts +9 -0
- package/src/util/errors.ts +59 -3
- package/src/util/essential-env.ts +57 -0
- package/src/util/git-ignored.ts +1 -1
- package/src/util/hash.ts +35 -0
- package/src/util/hide-env.ts +45 -5
- package/src/util/index.ts +21 -5
- package/src/util/num.ts +8 -0
- package/src/util/paths.ts +91 -21
- package/src/util/report.ts +6 -1
- package/src/util/secret-mask.ts +62 -1
- package/src/util/secrets.ts +7 -2
- package/src/util/size.ts +11 -3
- package/src/util/tail.ts +6 -1
- package/src/util/timing.ts +13 -1
- package/src/util/ui.ts +61 -3
- package/src/util/which.ts +1 -1
- package/src/util/write-file.ts +67 -0
- package/src/workspace/affected.ts +710 -107
- package/src/workspace/bracket-literal.ts +84 -0
- package/src/workspace/canonical-dir.ts +37 -0
- package/src/workspace/config-bytecode.ts +17 -11
- package/src/workspace/config-eval.ts +29 -8
- package/src/workspace/config-imports.ts +199 -58
- package/src/workspace/config-schema.ts +242 -39
- package/src/workspace/filter.ts +37 -8
- package/src/workspace/index.ts +11 -1
- package/src/workspace/json-data.ts +3 -4
- package/src/workspace/lockfile.ts +57 -8
- package/src/workspace/migration.ts +69 -18
- package/src/workspace/package-graph.ts +5 -4
- package/src/workspace/project-loader.ts +226 -37
- package/src/workspace/repo-id.ts +1 -1
- package/src/workspace/workspace.ts +176 -33
package/src/cli/select.ts
CHANGED
|
@@ -10,22 +10,24 @@ import {
|
|
|
10
10
|
affectedChanges,
|
|
11
11
|
type AffectedChanges,
|
|
12
12
|
affectedProjects,
|
|
13
|
-
|
|
13
|
+
headBase,
|
|
14
14
|
applyFilters,
|
|
15
15
|
buildPackageGraph,
|
|
16
16
|
configuredBaseBranch,
|
|
17
17
|
defaultAffectedBase,
|
|
18
|
+
findProjectConfigs,
|
|
18
19
|
findWorkspaceRoot,
|
|
19
20
|
type FingerprintClaims,
|
|
20
21
|
lockKey,
|
|
21
22
|
NO_LOCK,
|
|
22
23
|
parseFilter,
|
|
24
|
+
PROJECT_CONFIG_FILENAMES,
|
|
23
25
|
type ProjectMeta,
|
|
24
26
|
readLockfile,
|
|
25
27
|
} from '../workspace/index.js'
|
|
26
28
|
import type { ProjectConfig } from '../config.js'
|
|
27
29
|
import type { ProjectEntry } from '../workspace/index.js'
|
|
28
|
-
import { parseDependencySpec } from '../graph/index.js'
|
|
30
|
+
import { isDefaultBuild, parseDependencySpec } from '../graph/index.js'
|
|
29
31
|
import { declaresInput, workspaceFilesReachInto } from '../cache/index.js'
|
|
30
32
|
import {
|
|
31
33
|
isUserError,
|
|
@@ -33,8 +35,10 @@ import {
|
|
|
33
35
|
maskedCommand,
|
|
34
36
|
maskedLine,
|
|
35
37
|
nearest,
|
|
38
|
+
relPosix,
|
|
36
39
|
UserError,
|
|
37
40
|
sayNotice,
|
|
41
|
+
ui,
|
|
38
42
|
} from '../util/index.js'
|
|
39
43
|
import {
|
|
40
44
|
claimedAffected,
|
|
@@ -42,7 +46,6 @@ import {
|
|
|
42
46
|
fingerprintClaims,
|
|
43
47
|
gitOfDiscovery,
|
|
44
48
|
hasHook,
|
|
45
|
-
isDefaultBuild,
|
|
46
49
|
keepDiscoveryGraph,
|
|
47
50
|
} from '../orchestrator/index.js'
|
|
48
51
|
import {
|
|
@@ -50,6 +53,7 @@ import {
|
|
|
50
53
|
discoverCliProjects,
|
|
51
54
|
loadCliProjects,
|
|
52
55
|
loadCliWorkspace,
|
|
56
|
+
stageCacheDir,
|
|
53
57
|
} from './workspace-config.js'
|
|
54
58
|
|
|
55
59
|
/**
|
|
@@ -70,10 +74,12 @@ export async function workspaceGlobOwners(
|
|
|
70
74
|
loadCliProjects(root, projects, 'all', load),
|
|
71
75
|
/** The changed nested repositories (`AffectedChanges.nested`). */
|
|
72
76
|
nested: readonly string[] = [],
|
|
77
|
+
/** `AffectedChanges.dirLinks`. */
|
|
78
|
+
dirLinks: ReadonlySet<string> = new Set(),
|
|
73
79
|
): Promise<string[]> {
|
|
74
80
|
const declaresMatch = (config: ProjectConfig): boolean => {
|
|
75
81
|
for (const task of Object.values(config.tasks ?? {})) {
|
|
76
|
-
if (changed.some((rel) => declaresInput(task, null, rel))) return true
|
|
82
|
+
if (changed.some((rel) => declaresInput(task, null, rel, dirLinks.has(rel)))) return true
|
|
77
83
|
if (nested.some((dir) => workspaceFilesReachInto(task, dir))) return true
|
|
78
84
|
}
|
|
79
85
|
return false
|
|
@@ -88,7 +94,7 @@ export async function workspaceGlobOwners(
|
|
|
88
94
|
// "nothing affected" and exit 0 without ever reaching the run's own
|
|
89
95
|
// refusal. The sweep reads each entry from the lock: it never evaluates.
|
|
90
96
|
const lock = await readLockfile(root)
|
|
91
|
-
if (lock === null) throw new UserError(NO_LOCK)
|
|
97
|
+
if (lock === null) throw new UserError(NO_LOCK, 'VX_E_LOCK')
|
|
92
98
|
const owners: string[] = []
|
|
93
99
|
for (const meta of projects) {
|
|
94
100
|
if (meta.configPath === null || meta.configPath === '') continue
|
|
@@ -118,19 +124,19 @@ async function workspaceFingerprintClaims(
|
|
|
118
124
|
const claims = fingerprintClaims(ws.plugins)
|
|
119
125
|
const ctx = {
|
|
120
126
|
workspaceRoot: root,
|
|
121
|
-
cacheDir:
|
|
122
|
-
warn: (m: string) =>
|
|
127
|
+
cacheDir: stageCacheDir(ws, load),
|
|
128
|
+
warn: (m: string) => ui.say('warn', maskedLine(m)),
|
|
123
129
|
projects: projects.map((p) => ({ name: p.name, dir: p.dir })),
|
|
124
130
|
}
|
|
125
131
|
return {
|
|
126
132
|
files: new Set(claims.keys()),
|
|
127
133
|
affected: (change) => claimedAffected(claims.get(change.file)!, change, ctx),
|
|
128
|
-
extraFiles: (file, bytes) => claimedExtraFiles(claims.get(file)!, file, bytes),
|
|
134
|
+
extraFiles: (file, bytes) => claimedExtraFiles(claims.get(file)!, file, bytes, ctx),
|
|
129
135
|
}
|
|
130
136
|
}
|
|
131
137
|
|
|
132
|
-
async function loadWorkspaceProjects(cwd: string): Promise<ProjectMeta[]> {
|
|
133
|
-
return await discoverCliProjects(await findWorkspaceRoot(cwd))
|
|
138
|
+
async function loadWorkspaceProjects(cwd: string, load: CliLoadOptions): Promise<ProjectMeta[]> {
|
|
139
|
+
return await discoverCliProjects(await findWorkspaceRoot(cwd), load)
|
|
134
140
|
}
|
|
135
141
|
|
|
136
142
|
export async function findCwdProject(cwd: string): Promise<string | null> {
|
|
@@ -144,19 +150,17 @@ export async function findCwdProject(cwd: string): Promise<string | null> {
|
|
|
144
150
|
*/
|
|
145
151
|
export async function findCwdSelection(
|
|
146
152
|
cwd: string,
|
|
153
|
+
load: CliLoadOptions = {},
|
|
147
154
|
): Promise<{ name: string; discovered: { root: string; projects: ProjectMeta[] } } | null> {
|
|
148
155
|
const root = await findWorkspaceRoot(cwd)
|
|
149
|
-
const projects = await discoverCliProjects(root)
|
|
156
|
+
const projects = await discoverCliProjects(root, load)
|
|
150
157
|
const abs = path.resolve(cwd)
|
|
151
|
-
const within = (dirOf: (p: ProjectMeta) => string): string | null => {
|
|
152
|
-
let best: string | null = null
|
|
153
|
-
let bestLength = -1
|
|
158
|
+
const within = (dirOf: (p: ProjectMeta) => string): { name: string; dir: string } | null => {
|
|
159
|
+
let best: { name: string; dir: string } | null = null
|
|
154
160
|
for (const p of projects) {
|
|
155
161
|
const dir = dirOf(p)
|
|
156
|
-
if ((abs === dir || abs.startsWith(dir + path.sep)) && dir.length >
|
|
157
|
-
best = p.name
|
|
158
|
-
bestLength = dir.length
|
|
159
|
-
}
|
|
162
|
+
if ((abs === dir || abs.startsWith(dir + path.sep)) && dir.length > (best?.dir.length ?? -1))
|
|
163
|
+
best = { name: p.name, dir }
|
|
160
164
|
}
|
|
161
165
|
return best
|
|
162
166
|
}
|
|
@@ -165,7 +169,7 @@ export async function findCwdSelection(
|
|
|
165
169
|
// so inside such a member nothing matched and a run from there was "not
|
|
166
170
|
// inside a project" (item 1025). Only then are the members realpathed:
|
|
167
171
|
// one syscall each, paid only by a run no plain match could place.
|
|
168
|
-
const
|
|
172
|
+
const found =
|
|
169
173
|
within((p) => p.dir) ??
|
|
170
174
|
within((p) => {
|
|
171
175
|
try {
|
|
@@ -174,7 +178,45 @@ export async function findCwdSelection(
|
|
|
174
178
|
return p.dir
|
|
175
179
|
}
|
|
176
180
|
})
|
|
177
|
-
|
|
181
|
+
await refuseUnlockedConfig(root, abs, found?.dir ?? null)
|
|
182
|
+
return found === null ? null : { name: found.name, discovered: { root, projects } }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* A config `vx lock` would find between the cwd and the project placing it
|
|
187
|
+
* (the root, when none does): a config written after the lock is no project
|
|
188
|
+
* yet, so a run from its directory ran the locked project above it (the
|
|
189
|
+
* root's `build` for a new member) or said "not inside a project". One
|
|
190
|
+
* readdir per directory between them, none from a project's own; the
|
|
191
|
+
* listing that judges a config found is paid only when one is.
|
|
192
|
+
*/
|
|
193
|
+
async function refuseUnlockedConfig(
|
|
194
|
+
root: string,
|
|
195
|
+
abs: string,
|
|
196
|
+
placed: string | null,
|
|
197
|
+
): Promise<void> {
|
|
198
|
+
const top = placed ?? root
|
|
199
|
+
if (abs !== top && !abs.startsWith(top + path.sep)) return
|
|
200
|
+
for (let dir = abs; ; dir = path.dirname(dir)) {
|
|
201
|
+
if (dir === placed) return
|
|
202
|
+
let names: string[]
|
|
203
|
+
try {
|
|
204
|
+
names = fs.readdirSync(dir)
|
|
205
|
+
} catch {
|
|
206
|
+
names = []
|
|
207
|
+
}
|
|
208
|
+
const file = PROJECT_CONFIG_FILENAMES.find((n) => names.includes(n))
|
|
209
|
+
if (file !== undefined) {
|
|
210
|
+
const { configs } = await findProjectConfigs(root)
|
|
211
|
+
const config = configs.find((c) => c.dir === dir)
|
|
212
|
+
if (config !== undefined) {
|
|
213
|
+
throw new UserError(
|
|
214
|
+
`${relPosix(root, config.configPath)} is not in the lock yet — run \`vx lock\``,
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (dir === root || dir === path.dirname(dir)) return
|
|
219
|
+
}
|
|
178
220
|
}
|
|
179
221
|
|
|
180
222
|
export type FilterResolution =
|
|
@@ -219,7 +261,7 @@ export function taskEdgesFrom(staged: ReadonlyMap<string, ProjectEntry>): Map<st
|
|
|
219
261
|
for (const raw of task.dependsOn ?? []) {
|
|
220
262
|
let spec
|
|
221
263
|
try {
|
|
222
|
-
spec = parseDependencySpec(raw)
|
|
264
|
+
spec = parseDependencySpec(raw, 'dependsOn')
|
|
223
265
|
} catch {
|
|
224
266
|
continue
|
|
225
267
|
}
|
|
@@ -249,7 +291,12 @@ function refusalOf(err: unknown): Refusal {
|
|
|
249
291
|
* else a plugin's (`setBranch`: a pull request's base commit), else the
|
|
250
292
|
* merge base with `baseBranch`.
|
|
251
293
|
*/
|
|
252
|
-
export async function affectedFilterFor(
|
|
294
|
+
export async function affectedFilterFor(
|
|
295
|
+
cwd: string,
|
|
296
|
+
affected: string,
|
|
297
|
+
/** What the verb does instead on the base branch (`defaultAffectedBase`). */
|
|
298
|
+
instead?: string,
|
|
299
|
+
): Promise<string | Refusal> {
|
|
253
300
|
const root = await findWorkspaceRoot(cwd)
|
|
254
301
|
let base = affected || ((settingEnv('affectedBase') as string | undefined) ?? '')
|
|
255
302
|
if (base === '') {
|
|
@@ -257,7 +304,7 @@ export async function affectedFilterFor(cwd: string, affected: string): Promise<
|
|
|
257
304
|
try {
|
|
258
305
|
base =
|
|
259
306
|
ws.branch.base ??
|
|
260
|
-
(await defaultAffectedBase(root, configuredBaseBranch(ws.workspaceConfig)))
|
|
307
|
+
(await defaultAffectedBase(root, configuredBaseBranch(ws.workspaceConfig), instead))
|
|
261
308
|
} catch (err) {
|
|
262
309
|
if (!isUserError(err)) throw err
|
|
263
310
|
return refusalOf(err)
|
|
@@ -272,9 +319,11 @@ export async function resolveFilters(
|
|
|
272
319
|
load: CliLoadOptions = {},
|
|
273
320
|
/** The raw filter `--affected` became: its diff is kept per path for the run. */
|
|
274
321
|
affected?: string,
|
|
322
|
+
/** The verb a warning names: `vx show` and `vx watch` select as a run does. */
|
|
323
|
+
verb: 'run' | 'show' | 'watch' = 'run',
|
|
275
324
|
): Promise<FilterResolution> {
|
|
276
325
|
const root = await findWorkspaceRoot(cwd)
|
|
277
|
-
const projects = await loadWorkspaceProjects(cwd)
|
|
326
|
+
const projects = await loadWorkspaceProjects(cwd, load)
|
|
278
327
|
let parsed: ReturnType<typeof parseFilter>[]
|
|
279
328
|
try {
|
|
280
329
|
parsed = raw.map((r) => parseFilter(r, root))
|
|
@@ -332,8 +381,11 @@ export async function resolveFilters(
|
|
|
332
381
|
workspaceRoot: root,
|
|
333
382
|
since: f.gitSince,
|
|
334
383
|
projects,
|
|
335
|
-
workspaceGlobOwners: (
|
|
336
|
-
|
|
384
|
+
workspaceGlobOwners: (
|
|
385
|
+
changed: readonly string[],
|
|
386
|
+
nested: readonly string[],
|
|
387
|
+
dirLinks: ReadonlySet<string>,
|
|
388
|
+
) => workspaceGlobOwners(root, projects, changed, load, stagedOnce, nested, dirLinks),
|
|
337
389
|
fingerprintClaims: () => workspaceFingerprintClaims(root, projects, load),
|
|
338
390
|
...(git !== undefined
|
|
339
391
|
? {
|
|
@@ -346,6 +398,10 @@ export async function resolveFilters(
|
|
|
346
398
|
for (const [rel, oid] of g.trusted) if (oid.startsWith('120000:')) out.push(rel)
|
|
347
399
|
return out
|
|
348
400
|
},
|
|
401
|
+
flagged: async () => {
|
|
402
|
+
const g = await git.start()
|
|
403
|
+
return g.scopes.includes('.') ? g.flagged : null
|
|
404
|
+
},
|
|
349
405
|
}
|
|
350
406
|
: {}),
|
|
351
407
|
}
|
|
@@ -385,20 +441,21 @@ export async function resolveFilters(
|
|
|
385
441
|
...(tags !== undefined ? { tags } : {}),
|
|
386
442
|
// A `[<since>]` selector matching nothing is the ordinary "nothing
|
|
387
443
|
// changed" outcome, reported below; only a name/path pattern that
|
|
388
|
-
// matched
|
|
389
|
-
onNoMatch: (f) => {
|
|
390
|
-
if (
|
|
391
|
-
else
|
|
444
|
+
// matched no project in the workspace, ref or not, is a probable typo.
|
|
445
|
+
onNoMatch: (f, emptyDiff) => {
|
|
446
|
+
if (emptyDiff) emptyDiffs.add(f)
|
|
447
|
+
else unmatched.push(f.raw)
|
|
392
448
|
},
|
|
393
449
|
// A pattern that matched, with nothing on the walk it asked for: the
|
|
394
450
|
// fact to say, where "no projects matched" read as a typo (item 1030).
|
|
395
451
|
onEmptyWalk: (f, matched) => {
|
|
396
452
|
if (f.negate) return
|
|
397
453
|
const which = matched.join(', ')
|
|
454
|
+
const several = matched.length > 1
|
|
398
455
|
emptyWalks.push(
|
|
399
456
|
f.onlyDeps
|
|
400
|
-
? `filter "${f.raw}" matched ${which}, which depends on no project`
|
|
401
|
-
: `filter "${f.raw}" matched ${which}, and no project depends on it`,
|
|
457
|
+
? `filter "${f.raw}" matched ${which}, which ${several ? 'depend' : 'depends'} on no project`
|
|
458
|
+
: `filter "${f.raw}" matched ${which}, and no project depends on ${several ? 'them' : 'it'}`,
|
|
402
459
|
)
|
|
403
460
|
},
|
|
404
461
|
})
|
|
@@ -410,18 +467,23 @@ export async function resolveFilters(
|
|
|
410
467
|
// Every pattern matched and an exclusion took them all back: "no
|
|
411
468
|
// projects matched filter(s): one, !one" read as a typo of `one`.
|
|
412
469
|
const negations = parsed.filter((f) => f.negate).map((f) => f.raw)
|
|
413
|
-
|
|
414
|
-
|
|
470
|
+
// With no include the base is every project: there is no "other filter".
|
|
471
|
+
const excluded = `${negations.join(', ')} excluded every project${includes.length > 0 ? ' the other filters matched' : ''}`
|
|
472
|
+
if (
|
|
473
|
+
unmatched.length === 0 &&
|
|
474
|
+
includes.length > 0 &&
|
|
475
|
+
includes.every((f) => f.gitSince !== undefined)
|
|
476
|
+
) {
|
|
415
477
|
if (includes.every((f) => emptyDiffs.has(f))) {
|
|
416
|
-
const refs = includes.map((f) => f.gitSince).join(', ')
|
|
478
|
+
const refs = [...new Set(includes.map((f) => f.gitSince))].join(', ')
|
|
417
479
|
// A base that IS HEAD (a single-branch clone whose `origin/HEAD` is
|
|
418
480
|
// the branch under test) can never mark anything affected, and this
|
|
419
481
|
// exit-0 note was the only sign (CI persona, 2026-09-16). Say so.
|
|
420
|
-
const self = includes.
|
|
482
|
+
const self = includes.map((f) => headBase(root, f.gitSince!)).find((b) => b !== undefined)
|
|
421
483
|
const hint =
|
|
422
484
|
self === undefined
|
|
423
485
|
? ''
|
|
424
|
-
: ` — ${self
|
|
486
|
+
: ` — ${self} is HEAD itself: compare with the branch you merge into (--affected=origin/main) or the previous commit (--affected=HEAD~1)`
|
|
425
487
|
return { empty: `nothing affected since ${refs}${hint}` }
|
|
426
488
|
}
|
|
427
489
|
// The diff matched and a walk or an exclusion left nothing: still the
|
|
@@ -439,18 +501,19 @@ export async function resolveFilters(
|
|
|
439
501
|
}
|
|
440
502
|
// One line, not a warning per pattern and then an error saying the same:
|
|
441
503
|
// the patterns are in the error, and the nearest project name is the
|
|
442
|
-
// hint a typo needs.
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
504
|
+
// hint a typo needs. Only the ones that matched nothing: `app !app apq` named
|
|
505
|
+
// app, and `--affected` a `...[<base>]` nobody typed.
|
|
506
|
+
const hint =
|
|
507
|
+
(await unlockedHint(root, parsed, unmatched)) ?? didYouMean(unmatched, projects, tags)
|
|
508
|
+
return { error: `no projects matched filter(s): ${unmatched.join(', ')}${hint}` }
|
|
446
509
|
}
|
|
447
510
|
// Something matched, so the run proceeds; a pattern that matched nothing
|
|
448
511
|
// alongside it is still worth a line — it is probably a typo.
|
|
449
512
|
for (const f of unmatched)
|
|
450
513
|
sayNotice(
|
|
451
514
|
'warn',
|
|
452
|
-
|
|
453
|
-
`filter "${f}" matched no projects${didYouMean([f], projects, tags)}`,
|
|
515
|
+
`vx ${verb}`,
|
|
516
|
+
`filter "${f}" matched no projects${(await unlockedHint(root, parsed, [f])) ?? didYouMean([f], projects, tags)}`,
|
|
454
517
|
)
|
|
455
518
|
// Each include is a union: what `--filter other` selected runs its tasks
|
|
456
519
|
// whether or not the `--affected` diff reaches them, and an exclude
|
|
@@ -504,7 +567,7 @@ export async function pickTask(
|
|
|
504
567
|
/** The projects `--filter` / `--affected` selected; every project when absent. */
|
|
505
568
|
only?: ReadonlySet<string>,
|
|
506
569
|
): Promise<PickedTask | null | 'interrupted'> {
|
|
507
|
-
const projects = await loadWorkspaceProjects(cwd)
|
|
570
|
+
const projects = await loadWorkspaceProjects(cwd, load)
|
|
508
571
|
// The staged load: a task a `project` plugin gave a config-less package
|
|
509
572
|
// is on the menu, as it is in a run.
|
|
510
573
|
const staged = await loadCliProjects(await findWorkspaceRoot(cwd), projects, 'all', load)
|
|
@@ -599,13 +662,42 @@ export async function pickTask(
|
|
|
599
662
|
}
|
|
600
663
|
}
|
|
601
664
|
|
|
665
|
+
/**
|
|
666
|
+
* `. packages/c/vx.config.ts is not in the lock yet — run \`vx lock\``
|
|
667
|
+
* for an unmatched `./dir` filter naming a directory `vx lock` would make
|
|
668
|
+
* a project: a config written after the lock is none yet, and "Projects:
|
|
669
|
+
* …" without it read as a typo of the path. Asked on the refusal's path only.
|
|
670
|
+
*/
|
|
671
|
+
async function unlockedHint(
|
|
672
|
+
root: string,
|
|
673
|
+
parsed: readonly ReturnType<typeof parseFilter>[],
|
|
674
|
+
unmatched: readonly string[],
|
|
675
|
+
): Promise<string | undefined> {
|
|
676
|
+
const dirs = parsed
|
|
677
|
+
.filter((f) => f.isPath && f.pathGlob === undefined && unmatched.includes(f.raw))
|
|
678
|
+
.map((f) => f.matcher)
|
|
679
|
+
if (dirs.length === 0) return undefined
|
|
680
|
+
const { configs } = await findProjectConfigs(root)
|
|
681
|
+
const config = configs.find((c) => dirs.includes(c.dir))
|
|
682
|
+
if (config === undefined) return undefined
|
|
683
|
+
return `. ${relPosix(root, config.configPath)} is not in the lock yet — run \`vx lock\``
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/** A pattern as a name: its `[<since>]` and graph operators aside. */
|
|
687
|
+
// `^\.\.\.` anchored in one alternation with `!` kept `!...apq`'s dots.
|
|
688
|
+
const typedName = (pattern: string): string =>
|
|
689
|
+
pattern
|
|
690
|
+
.replace(/\[[^\]]*\]$/, '')
|
|
691
|
+
.replace(/^!/, '')
|
|
692
|
+
.replace(/^\.\.\.|\.\.\.$|\^/g, '')
|
|
693
|
+
|
|
602
694
|
/** The hint for the first unmatched pattern: a near tag for a `tag:` one, else a near project name. */
|
|
603
695
|
function didYouMean(
|
|
604
696
|
unmatched: readonly string[],
|
|
605
697
|
projects: Iterable<{ name: string }>,
|
|
606
698
|
tags: ReadonlyMap<string, readonly string[]> | undefined,
|
|
607
699
|
): string {
|
|
608
|
-
const first = unmatched[0]
|
|
700
|
+
const first = unmatched[0] === undefined ? undefined : typedName(unmatched[0])
|
|
609
701
|
if (first?.startsWith('tag:') !== true) return didYouMeanProject(unmatched, projects)
|
|
610
702
|
const known = [...new Set([...(tags?.values() ?? [])].flat())].sort()
|
|
611
703
|
if (known.length === 0) return '. No project declares tags'
|
|
@@ -627,11 +719,16 @@ function didYouMeanProject(
|
|
|
627
719
|
if (bare !== n) byBare.set(bare, [...(byBare.get(bare) ?? []), n])
|
|
628
720
|
}
|
|
629
721
|
for (const pattern of unmatched) {
|
|
630
|
-
const typed = pattern
|
|
722
|
+
const typed = typedName(pattern)
|
|
631
723
|
// Two edits from any two-letter name: `--filter //` hinted `ui` (X-13).
|
|
632
724
|
if (typed === '//') {
|
|
633
725
|
return '. `//` is the workspace root, which is no project here: add a vx.config there and run `vx lock`'
|
|
634
726
|
}
|
|
727
|
+
// Nx's `--projects=a,b`: the edit distance hinted the first name alone.
|
|
728
|
+
const parts = pattern.split(',').filter((p) => p !== '')
|
|
729
|
+
if (parts.length > 1) {
|
|
730
|
+
return `. A filter is one pattern: ${parts.map((p) => `--filter ${p}`).join(' ')}`
|
|
731
|
+
}
|
|
635
732
|
const best = nearest(typed, names)
|
|
636
733
|
if (best !== undefined) return `. Did you mean ${best}?`
|
|
637
734
|
const bare = byBare.has(typed) ? typed : nearest(typed, byBare.keys())
|
package/src/cli/show.ts
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
import path from 'node:path'
|
|
10
10
|
import type { ProjectConfig, TaskConfig } from '../config.js'
|
|
11
11
|
import { declaredTask } from '../graph/index.js'
|
|
12
|
-
import {
|
|
12
|
+
import { isDefaultBuild } from '../graph/index.js'
|
|
13
|
+
import { affectedTaskProjects } from '../orchestrator/index.js'
|
|
13
14
|
import { flagHint, formatValue, refuse, seeHelp } from './help.js'
|
|
14
15
|
import {
|
|
15
16
|
type ColorSupport,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
nearMatches,
|
|
19
20
|
relPosix,
|
|
20
21
|
secretMask,
|
|
22
|
+
type SecretMask,
|
|
21
23
|
UserError,
|
|
22
24
|
sayNotice,
|
|
23
25
|
ui,
|
|
@@ -32,6 +34,7 @@ import {
|
|
|
32
34
|
import {
|
|
33
35
|
type AffectedChanges,
|
|
34
36
|
buildPackageGraph,
|
|
37
|
+
EMPTY_LOCK,
|
|
35
38
|
findWorkspaceRoot,
|
|
36
39
|
type ProjectEntry,
|
|
37
40
|
type ProjectMeta,
|
|
@@ -55,17 +58,16 @@ export function parseShowArgs(args: readonly string[]): ShowArgs {
|
|
|
55
58
|
let format: string | undefined
|
|
56
59
|
if (a === '--format') format = args[++i] ?? ''
|
|
57
60
|
else if (a.startsWith('--format=')) format = a.slice('--format='.length)
|
|
58
|
-
else if (a === '--filter') {
|
|
59
|
-
const v = args[++i]
|
|
61
|
+
else if (a === '--filter' || a.startsWith('--filter=')) {
|
|
62
|
+
const v = a === '--filter' ? args[++i] : a.slice('--filter='.length)
|
|
60
63
|
// `--filter --format json` took `--format` as the pattern; no filter opens with `--`.
|
|
61
|
-
if (v === undefined || v === '' || v.startsWith('--'))
|
|
64
|
+
if (v === undefined || v === '' || (a === '--filter' && v.startsWith('--')))
|
|
62
65
|
return {
|
|
63
66
|
...out,
|
|
64
67
|
error: `--filter requires a value: a project name, glob or path${seeHelp('show')}`,
|
|
65
68
|
}
|
|
66
69
|
out.filters.push(v)
|
|
67
|
-
} else if (a
|
|
68
|
-
else if (a === '--affected') out.affected = ''
|
|
70
|
+
} else if (a === '--affected') out.affected = ''
|
|
69
71
|
else if (a.startsWith('--affected=')) out.affected = a.slice('--affected='.length)
|
|
70
72
|
else if (a.startsWith('-'))
|
|
71
73
|
return { ...out, error: `unknown flag: ${a}${flagHint('show', a)}${seeHelp('show')}` }
|
|
@@ -116,6 +118,12 @@ export async function showCmd(args: readonly string[]): Promise<number> {
|
|
|
116
118
|
? parsed.target
|
|
117
119
|
: parsed.target.slice(0, hashAt)
|
|
118
120
|
const taskName = hashAt === -1 ? undefined : parsed.target!.slice(hashAt + 1)
|
|
121
|
+
if (projectName === '' && taskName !== undefined && taskName !== '') {
|
|
122
|
+
throw new UserError(
|
|
123
|
+
`vx show: missing project name before '#' in "${parsed.target}" (vx show ${taskName} shows it in every project)`,
|
|
124
|
+
'VX_E_USAGE',
|
|
125
|
+
)
|
|
126
|
+
}
|
|
119
127
|
if (taskName === '') {
|
|
120
128
|
const list = projectName === '' ? 'vx show' : `vx show ${projectName}`
|
|
121
129
|
throw new UserError(
|
|
@@ -152,12 +160,16 @@ export async function showCmd(args: readonly string[]): Promise<number> {
|
|
|
152
160
|
const raw = [...parsed.filters]
|
|
153
161
|
let affected: string | undefined
|
|
154
162
|
if (parsed.affected !== undefined) {
|
|
155
|
-
const f = await affectedFilterFor(
|
|
163
|
+
const f = await affectedFilterFor(
|
|
164
|
+
process.cwd(),
|
|
165
|
+
parsed.affected,
|
|
166
|
+
'drop --affected to list them all',
|
|
167
|
+
)
|
|
156
168
|
if (typeof f === 'object') throw new UserError(`vx show: ${f.error}`, f.code ?? 'VX_E_USAGE')
|
|
157
169
|
affected = f
|
|
158
170
|
raw.unshift(f)
|
|
159
171
|
}
|
|
160
|
-
const r = await resolveFilters(process.cwd(), raw, {}, affected)
|
|
172
|
+
const r = await resolveFilters(process.cwd(), raw, {}, affected, 'show')
|
|
161
173
|
if ('error' in r) throw new UserError(`vx show: ${r.error}`, r.code ?? 'VX_E_USAGE')
|
|
162
174
|
if ('empty' in r) sayNotice('info', 'vx show', r.empty)
|
|
163
175
|
selected = new Set('empty' in r ? [] : r.names)
|
|
@@ -173,7 +185,7 @@ export async function showCmd(args: readonly string[]): Promise<number> {
|
|
|
173
185
|
sayNotice(
|
|
174
186
|
'info',
|
|
175
187
|
'vx show',
|
|
176
|
-
|
|
188
|
+
`no projects: ${EMPTY_LOCK.charAt(0).toLowerCase()}${EMPTY_LOCK.slice(1)}`,
|
|
177
189
|
)
|
|
178
190
|
const shown = metas.filter((m) => inSelection(m.name))
|
|
179
191
|
process.stdout.write(renderList(root, shown, projects, parsed.format, colorsOut()))
|
|
@@ -220,6 +232,17 @@ export async function showCmd(args: readonly string[]): Promise<number> {
|
|
|
220
232
|
)
|
|
221
233
|
shown = shown.filter((p) => kept.has(p.name))
|
|
222
234
|
}
|
|
235
|
+
// An empty list printed nothing and exited 0, where the run says why.
|
|
236
|
+
if (shown.length === 0 && parsed.format === 'pretty') {
|
|
237
|
+
const n = declaring.length
|
|
238
|
+
sayNotice(
|
|
239
|
+
'info',
|
|
240
|
+
'vx show',
|
|
241
|
+
byDiff !== undefined
|
|
242
|
+
? `no affected project's ${projectName} runs (${n} declare${n === 1 ? 's' : ''} it)`
|
|
243
|
+
: `no selected project declares ${projectName} (${n} outside the selection ${n === 1 ? 'does' : 'do'})`,
|
|
244
|
+
)
|
|
245
|
+
}
|
|
223
246
|
process.stdout.write(renderTaskAcross(root, shown, projectName!, parsed.format, colorsOut()))
|
|
224
247
|
return 0
|
|
225
248
|
}
|
|
@@ -280,10 +303,26 @@ function projectDir(root: string, meta: ProjectMeta): string {
|
|
|
280
303
|
* description) (L-11).
|
|
281
304
|
*/
|
|
282
305
|
function shownTask(task: TaskConfig): TaskConfig {
|
|
283
|
-
const
|
|
306
|
+
const env = task.exec?.env
|
|
307
|
+
const secrets =
|
|
308
|
+
env?.define === undefined && (env?.secret ?? []).length === 0
|
|
309
|
+
? envMask()
|
|
310
|
+
: secretMask([process.env, env?.define], env?.secret)
|
|
284
311
|
return secrets === null ? task : (maskStrings(task, secrets.mask) as TaskConfig)
|
|
285
312
|
}
|
|
286
313
|
|
|
314
|
+
let envOnly: SecretMask | null | undefined
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* The mask of a task that defines and names no variable: vx's own env's,
|
|
318
|
+
* built once. Built per task, its pattern was 10 of `vx show build`'s
|
|
319
|
+
* 58 ms over 100 projects.
|
|
320
|
+
*/
|
|
321
|
+
function envMask(): SecretMask | null {
|
|
322
|
+
if (envOnly === undefined) envOnly = secretMask([process.env])
|
|
323
|
+
return envOnly
|
|
324
|
+
}
|
|
325
|
+
|
|
287
326
|
function maskStrings(value: unknown, mask: (text: string) => string): unknown {
|
|
288
327
|
if (typeof value === 'string') return mask(value)
|
|
289
328
|
if (Array.isArray(value)) return value.map((v) => maskStrings(v, mask))
|
|
@@ -327,11 +366,7 @@ function renderList(
|
|
|
327
366
|
}
|
|
328
367
|
})
|
|
329
368
|
if (format === 'json') {
|
|
330
|
-
return `${JSON.stringify(
|
|
331
|
-
rows.map(({ name, dir, tags, tasks }) => ({ name, dir, tags, tasks })),
|
|
332
|
-
null,
|
|
333
|
-
2,
|
|
334
|
-
)}\n`
|
|
369
|
+
return `${JSON.stringify(rows.map(({ name, dir, tags, tasks }) => ({ name, dir, tags, tasks })))}\n`
|
|
335
370
|
}
|
|
336
371
|
if (rows.length === 0) return ''
|
|
337
372
|
const anyTags = rows.some((r) => r.tags.length > 0)
|
|
@@ -435,8 +470,9 @@ function taskReport(name: string, dir: string, taskName: string, task: TaskConfi
|
|
|
435
470
|
function taskRows(raw: TaskConfig): ui.FieldRow[] {
|
|
436
471
|
const task = shownTask(raw)
|
|
437
472
|
const rows: ui.FieldRow[] = []
|
|
473
|
+
// An empty list is a value: a bare label read as a cut-off line.
|
|
438
474
|
const list = (xs: readonly string[] | undefined): string | undefined =>
|
|
439
|
-
xs === undefined ? undefined : xs.join(', ')
|
|
475
|
+
xs === undefined ? undefined : xs.length === 0 ? 'none' : xs.join(', ')
|
|
440
476
|
const add = (label: string, value: string | number | boolean | undefined): void => {
|
|
441
477
|
if (value !== undefined) rows.push([label, String(value)])
|
|
442
478
|
}
|
|
@@ -457,6 +493,7 @@ function taskRows(raw: TaskConfig): ui.FieldRow[] {
|
|
|
457
493
|
)
|
|
458
494
|
add('env.secret', list(exec?.env?.secret))
|
|
459
495
|
add('remote', exec?.remote === undefined ? undefined : String(exec.remote))
|
|
496
|
+
add('platforms', list(exec?.platforms))
|
|
460
497
|
add('sandbox', exec?.sandbox === undefined ? undefined : JSON.stringify(exec.sandbox))
|
|
461
498
|
const persistent = exec?.persistent
|
|
462
499
|
if (persistent !== undefined) {
|
|
@@ -479,6 +516,12 @@ function taskRows(raw: TaskConfig): ui.FieldRow[] {
|
|
|
479
516
|
if (cache !== undefined) {
|
|
480
517
|
add('outputs.files', list(cache.outputs.files))
|
|
481
518
|
add('outputs.workspaceFiles', list(cache.outputs.workspaceFiles))
|
|
519
|
+
add(
|
|
520
|
+
'outputs.clean',
|
|
521
|
+
cache.outputs.clean === undefined ? undefined : cache.outputs.clean ? 'yes' : 'no',
|
|
522
|
+
)
|
|
523
|
+
add('inputStability.strategy', cache.inputStability?.strategy)
|
|
524
|
+
add('inputStability.severity', cache.inputStability?.severity)
|
|
482
525
|
}
|
|
483
526
|
return rows
|
|
484
527
|
}
|
package/src/cli/task-verb.ts
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
// does it. It stays a refusal: a verb that runs whatever task shares its
|
|
5
5
|
// name would change meaning the day a plugin declares that verb.
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import type { TaskConfig } from '../config.js'
|
|
8
|
+
import { isDefaultBuild } from '../graph/index.js'
|
|
9
|
+
import { nxProjectTarget, projectNamed } from '../orchestrator/index.js'
|
|
8
10
|
import { findWorkspaceRoot } from '../workspace/index.js'
|
|
9
11
|
import { nearest } from '../util/index.js'
|
|
10
12
|
import { findCwdProject } from './select.js'
|
|
@@ -26,19 +28,27 @@ export async function taskVerbHint(
|
|
|
26
28
|
if (projects === null) return null
|
|
27
29
|
const tasks = new Set<string>()
|
|
28
30
|
// The default build is no task `vx run` takes by name (X-145).
|
|
31
|
+
const declared = (task: TaskConfig | undefined): boolean =>
|
|
32
|
+
task !== undefined && !isDefaultBuild(task)
|
|
29
33
|
for (const p of projects.values())
|
|
30
|
-
for (const [t, task] of Object.entries(p.config.tasks ?? {}))
|
|
31
|
-
if (!isDefaultBuild(task)) tasks.add(t)
|
|
34
|
+
for (const [t, task] of Object.entries(p.config.tasks ?? {})) if (declared(task)) tasks.add(t)
|
|
32
35
|
const task = tasks.has(command) ? command : typo ? nearest(command, tasks) : undefined
|
|
33
36
|
if (task === undefined) return null
|
|
34
37
|
// Nx's `nx build app`: the word after the target is its project.
|
|
35
38
|
const project = rest[0] === undefined ? undefined : projectNamed(rest[0], projects)
|
|
39
|
+
// A bare `vx run <task>` runs the cwd project's alone, and refuses where
|
|
40
|
+
// that project does not declare it.
|
|
41
|
+
const declaresHere = async (): Promise<boolean> => {
|
|
42
|
+
const here = await findCwdProject(cwd)
|
|
43
|
+
const own = here === null ? undefined : projects.get(here)?.config.tasks
|
|
44
|
+
return own !== undefined && Object.hasOwn(own, task) && declared(own[task])
|
|
45
|
+
}
|
|
36
46
|
const run =
|
|
37
47
|
project !== undefined
|
|
38
48
|
? `vx run ${task} --filter ${project}`
|
|
39
|
-
: (await
|
|
40
|
-
? `vx run ${task}
|
|
41
|
-
: `vx run ${task}`
|
|
49
|
+
: (await declaresHere())
|
|
50
|
+
? `vx run ${task}`
|
|
51
|
+
: `vx run ${task} --all`
|
|
42
52
|
return task === command
|
|
43
53
|
? `\`${command}\` is a task here, not a command (did you mean ${run}?)`
|
|
44
54
|
: `unknown command: ${command}, a typo of the task \`${task}\` (did you mean ${run}?)`
|