@vzn/vx 0.0.645 → 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 +39 -50
- 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 +127 -5
- package/schemas/plan.json +28 -2
- package/schemas/show.json +10 -1
- package/skills/vx/SKILL.md +19 -3
- package/src/bin.ts +54 -13
- package/src/cache/archive.ts +324 -23
- package/src/cache/cache.ts +330 -65
- package/src/cache/git-inputs.ts +274 -97
- package/src/cache/index.ts +7 -2
- package/src/cache/inputs.ts +515 -83
- package/src/cache/key-fold.ts +15 -6
- package/src/cache/layer.ts +74 -11
- package/src/cache/layered-cache.ts +154 -59
- package/src/cache/linked-inputs.ts +1 -1
- package/src/cache/output-index.ts +44 -14
- package/src/cache/plugin-cache.ts +49 -23
- package/src/cache/remote-hits.ts +72 -0
- package/src/cache/remote-lane.ts +100 -15
- package/src/cache/run-history.ts +77 -23
- package/src/cache/run-store.ts +11 -10
- package/src/cache/schema.ts +42 -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/core-alias.ts +5 -2
- package/src/cli/docs.ts +42 -16
- package/src/cli/help.ts +62 -25
- package/src/cli/index.ts +79 -45
- package/src/cli/info.ts +12 -6
- package/src/cli/init.ts +222 -34
- package/src/cli/last.ts +443 -47
- package/src/cli/lock.ts +197 -52
- package/src/cli/plan-format.ts +91 -49
- package/src/cli/plugin-commands.ts +160 -11
- package/src/cli/plugin-templates.ts +13 -13
- package/src/cli/registry.ts +209 -0
- package/src/cli/run.ts +111 -41
- package/src/cli/select.ts +148 -49
- package/src/cli/show.ts +59 -16
- package/src/cli/task-verb.ts +16 -6
- package/src/cli/upgrade.ts +155 -54
- 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 +262 -15
- package/src/exec/index.ts +8 -2
- package/src/exec/kill-tree.ts +15 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/proc-sample.ts +70 -6
- package/src/exec/runner.ts +256 -53
- package/src/exec/sandbox-binds.ts +7 -3
- package/src/exec/sandbox-paths.ts +1 -1
- package/src/exec/sandbox-runtime.ts +222 -28
- 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 +326 -65
- 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 +268 -50
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +142 -45
- package/src/index.ts +42 -10
- 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 +6 -0
- package/src/orchestrator/execute-task.ts +155 -42
- 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 +86 -0
- package/src/orchestrator/hit-restore.ts +227 -122
- package/src/orchestrator/index.ts +20 -3
- package/src/orchestrator/local-floor.ts +198 -0
- package/src/orchestrator/local-shortcircuit.ts +51 -8
- package/src/orchestrator/lockfile-claim.ts +96 -16
- package/src/orchestrator/miss-save.ts +52 -12
- 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 +97 -28
- package/src/orchestrator/persistent.ts +16 -4
- package/src/orchestrator/placement.ts +257 -14
- package/src/orchestrator/plan.ts +64 -7
- package/src/orchestrator/plugin-host.ts +230 -40
- package/src/orchestrator/plugin.ts +150 -37
- package/src/orchestrator/prepare.ts +178 -32
- 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 +300 -0
- package/src/orchestrator/run-live.ts +294 -0
- package/src/orchestrator/run-lock.ts +13 -7
- package/src/orchestrator/run-records.ts +35 -23
- 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 +409 -79
- package/src/orchestrator/sandbox-fix.ts +7 -5
- package/src/orchestrator/sandbox-request.ts +78 -22
- 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 +206 -0
- package/src/orchestrator/task-hash.ts +89 -14
- package/src/orchestrator/task-log-buffer.ts +72 -36
- package/src/orchestrator/task-process.ts +22 -0
- package/src/orchestrator/telemetry-host.ts +24 -3
- package/src/orchestrator/telemetry.ts +113 -38
- package/src/orchestrator/upstream.ts +15 -0
- package/src/report/events.ts +32 -13
- package/src/report/forecast.ts +36 -35
- package/src/report/framed-output.ts +173 -46
- package/src/report/index.ts +1 -0
- package/src/report/logger.ts +217 -186
- package/src/report/path-links.ts +12 -4
- package/src/report/plain-output.ts +13 -5
- package/src/report/status-line.ts +94 -46
- package/src/report/summary.ts +174 -38
- package/src/report/terminal-signals.ts +13 -2
- package/src/settings.ts +14 -6
- package/src/util/bun-version.ts +4 -1
- package/src/util/colors.ts +20 -3
- 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 +22 -5
- package/src/util/num.ts +8 -0
- package/src/util/paths.ts +121 -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 +65 -4
- 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 +282 -0
- 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 -2
- package/src/workspace/json-data.ts +3 -4
- package/src/workspace/lockfile.ts +57 -8
- package/src/workspace/migration.ts +72 -49
- package/src/workspace/package-graph.ts +5 -4
- package/src/workspace/project-loader.ts +314 -47
- package/src/workspace/repo-id.ts +1 -1
- package/src/workspace/workspace.ts +176 -33
- package/src/cli/foreign-flags.ts +0 -727
package/src/cli/select.ts
CHANGED
|
@@ -4,27 +4,30 @@
|
|
|
4
4
|
// here goes through the staged load, so the answer is the run's.
|
|
5
5
|
|
|
6
6
|
import path from 'node:path'
|
|
7
|
+
import { settingEnv } from '../settings.js'
|
|
7
8
|
import fs from 'node:fs'
|
|
8
9
|
import {
|
|
9
10
|
affectedChanges,
|
|
10
11
|
type AffectedChanges,
|
|
11
12
|
affectedProjects,
|
|
12
|
-
|
|
13
|
+
headBase,
|
|
13
14
|
applyFilters,
|
|
14
15
|
buildPackageGraph,
|
|
15
16
|
configuredBaseBranch,
|
|
16
17
|
defaultAffectedBase,
|
|
18
|
+
findProjectConfigs,
|
|
17
19
|
findWorkspaceRoot,
|
|
18
20
|
type FingerprintClaims,
|
|
19
21
|
lockKey,
|
|
20
22
|
NO_LOCK,
|
|
21
23
|
parseFilter,
|
|
24
|
+
PROJECT_CONFIG_FILENAMES,
|
|
22
25
|
type ProjectMeta,
|
|
23
26
|
readLockfile,
|
|
24
27
|
} from '../workspace/index.js'
|
|
25
28
|
import type { ProjectConfig } from '../config.js'
|
|
26
29
|
import type { ProjectEntry } from '../workspace/index.js'
|
|
27
|
-
import { parseDependencySpec } from '../graph/index.js'
|
|
30
|
+
import { isDefaultBuild, parseDependencySpec } from '../graph/index.js'
|
|
28
31
|
import { declaresInput, workspaceFilesReachInto } from '../cache/index.js'
|
|
29
32
|
import {
|
|
30
33
|
isUserError,
|
|
@@ -32,8 +35,10 @@ import {
|
|
|
32
35
|
maskedCommand,
|
|
33
36
|
maskedLine,
|
|
34
37
|
nearest,
|
|
38
|
+
relPosix,
|
|
35
39
|
UserError,
|
|
36
40
|
sayNotice,
|
|
41
|
+
ui,
|
|
37
42
|
} from '../util/index.js'
|
|
38
43
|
import {
|
|
39
44
|
claimedAffected,
|
|
@@ -41,7 +46,6 @@ import {
|
|
|
41
46
|
fingerprintClaims,
|
|
42
47
|
gitOfDiscovery,
|
|
43
48
|
hasHook,
|
|
44
|
-
isDefaultBuild,
|
|
45
49
|
keepDiscoveryGraph,
|
|
46
50
|
} from '../orchestrator/index.js'
|
|
47
51
|
import {
|
|
@@ -49,6 +53,7 @@ import {
|
|
|
49
53
|
discoverCliProjects,
|
|
50
54
|
loadCliProjects,
|
|
51
55
|
loadCliWorkspace,
|
|
56
|
+
stageCacheDir,
|
|
52
57
|
} from './workspace-config.js'
|
|
53
58
|
|
|
54
59
|
/**
|
|
@@ -69,10 +74,12 @@ export async function workspaceGlobOwners(
|
|
|
69
74
|
loadCliProjects(root, projects, 'all', load),
|
|
70
75
|
/** The changed nested repositories (`AffectedChanges.nested`). */
|
|
71
76
|
nested: readonly string[] = [],
|
|
77
|
+
/** `AffectedChanges.dirLinks`. */
|
|
78
|
+
dirLinks: ReadonlySet<string> = new Set(),
|
|
72
79
|
): Promise<string[]> {
|
|
73
80
|
const declaresMatch = (config: ProjectConfig): boolean => {
|
|
74
81
|
for (const task of Object.values(config.tasks ?? {})) {
|
|
75
|
-
if (changed.some((rel) => declaresInput(task, null, rel))) return true
|
|
82
|
+
if (changed.some((rel) => declaresInput(task, null, rel, dirLinks.has(rel)))) return true
|
|
76
83
|
if (nested.some((dir) => workspaceFilesReachInto(task, dir))) return true
|
|
77
84
|
}
|
|
78
85
|
return false
|
|
@@ -87,7 +94,7 @@ export async function workspaceGlobOwners(
|
|
|
87
94
|
// "nothing affected" and exit 0 without ever reaching the run's own
|
|
88
95
|
// refusal. The sweep reads each entry from the lock: it never evaluates.
|
|
89
96
|
const lock = await readLockfile(root)
|
|
90
|
-
if (lock === null) throw new UserError(NO_LOCK)
|
|
97
|
+
if (lock === null) throw new UserError(NO_LOCK, 'VX_E_LOCK')
|
|
91
98
|
const owners: string[] = []
|
|
92
99
|
for (const meta of projects) {
|
|
93
100
|
if (meta.configPath === null || meta.configPath === '') continue
|
|
@@ -117,19 +124,19 @@ async function workspaceFingerprintClaims(
|
|
|
117
124
|
const claims = fingerprintClaims(ws.plugins)
|
|
118
125
|
const ctx = {
|
|
119
126
|
workspaceRoot: root,
|
|
120
|
-
cacheDir:
|
|
121
|
-
warn: (m: string) =>
|
|
127
|
+
cacheDir: stageCacheDir(ws, load),
|
|
128
|
+
warn: (m: string) => ui.say('warn', maskedLine(m)),
|
|
122
129
|
projects: projects.map((p) => ({ name: p.name, dir: p.dir })),
|
|
123
130
|
}
|
|
124
131
|
return {
|
|
125
132
|
files: new Set(claims.keys()),
|
|
126
133
|
affected: (change) => claimedAffected(claims.get(change.file)!, change, ctx),
|
|
127
|
-
extraFiles: (file, bytes) => claimedExtraFiles(claims.get(file)!, file, bytes),
|
|
134
|
+
extraFiles: (file, bytes) => claimedExtraFiles(claims.get(file)!, file, bytes, ctx),
|
|
128
135
|
}
|
|
129
136
|
}
|
|
130
137
|
|
|
131
|
-
async function loadWorkspaceProjects(cwd: string): Promise<ProjectMeta[]> {
|
|
132
|
-
return await discoverCliProjects(await findWorkspaceRoot(cwd))
|
|
138
|
+
async function loadWorkspaceProjects(cwd: string, load: CliLoadOptions): Promise<ProjectMeta[]> {
|
|
139
|
+
return await discoverCliProjects(await findWorkspaceRoot(cwd), load)
|
|
133
140
|
}
|
|
134
141
|
|
|
135
142
|
export async function findCwdProject(cwd: string): Promise<string | null> {
|
|
@@ -143,19 +150,17 @@ export async function findCwdProject(cwd: string): Promise<string | null> {
|
|
|
143
150
|
*/
|
|
144
151
|
export async function findCwdSelection(
|
|
145
152
|
cwd: string,
|
|
153
|
+
load: CliLoadOptions = {},
|
|
146
154
|
): Promise<{ name: string; discovered: { root: string; projects: ProjectMeta[] } } | null> {
|
|
147
155
|
const root = await findWorkspaceRoot(cwd)
|
|
148
|
-
const projects = await discoverCliProjects(root)
|
|
156
|
+
const projects = await discoverCliProjects(root, load)
|
|
149
157
|
const abs = path.resolve(cwd)
|
|
150
|
-
const within = (dirOf: (p: ProjectMeta) => string): string | null => {
|
|
151
|
-
let best: string | null = null
|
|
152
|
-
let bestLength = -1
|
|
158
|
+
const within = (dirOf: (p: ProjectMeta) => string): { name: string; dir: string } | null => {
|
|
159
|
+
let best: { name: string; dir: string } | null = null
|
|
153
160
|
for (const p of projects) {
|
|
154
161
|
const dir = dirOf(p)
|
|
155
|
-
if ((abs === dir || abs.startsWith(dir + path.sep)) && dir.length >
|
|
156
|
-
best = p.name
|
|
157
|
-
bestLength = dir.length
|
|
158
|
-
}
|
|
162
|
+
if ((abs === dir || abs.startsWith(dir + path.sep)) && dir.length > (best?.dir.length ?? -1))
|
|
163
|
+
best = { name: p.name, dir }
|
|
159
164
|
}
|
|
160
165
|
return best
|
|
161
166
|
}
|
|
@@ -164,7 +169,7 @@ export async function findCwdSelection(
|
|
|
164
169
|
// so inside such a member nothing matched and a run from there was "not
|
|
165
170
|
// inside a project" (item 1025). Only then are the members realpathed:
|
|
166
171
|
// one syscall each, paid only by a run no plain match could place.
|
|
167
|
-
const
|
|
172
|
+
const found =
|
|
168
173
|
within((p) => p.dir) ??
|
|
169
174
|
within((p) => {
|
|
170
175
|
try {
|
|
@@ -173,7 +178,45 @@ export async function findCwdSelection(
|
|
|
173
178
|
return p.dir
|
|
174
179
|
}
|
|
175
180
|
})
|
|
176
|
-
|
|
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
|
+
}
|
|
177
220
|
}
|
|
178
221
|
|
|
179
222
|
export type FilterResolution =
|
|
@@ -218,7 +261,7 @@ export function taskEdgesFrom(staged: ReadonlyMap<string, ProjectEntry>): Map<st
|
|
|
218
261
|
for (const raw of task.dependsOn ?? []) {
|
|
219
262
|
let spec
|
|
220
263
|
try {
|
|
221
|
-
spec = parseDependencySpec(raw)
|
|
264
|
+
spec = parseDependencySpec(raw, 'dependsOn')
|
|
222
265
|
} catch {
|
|
223
266
|
continue
|
|
224
267
|
}
|
|
@@ -244,18 +287,24 @@ function refusalOf(err: unknown): Refusal {
|
|
|
244
287
|
|
|
245
288
|
/**
|
|
246
289
|
* The filter `--affected[=<base>]` stands for: `...[<base>]`, the changed
|
|
247
|
-
* projects and their dependents. An empty base is
|
|
248
|
-
* a pull request's base commit), else the
|
|
290
|
+
* projects and their dependents. An empty base is `VX_AFFECTED_BASE`'s,
|
|
291
|
+
* else a plugin's (`setBranch`: a pull request's base commit), else the
|
|
292
|
+
* merge base with `baseBranch`.
|
|
249
293
|
*/
|
|
250
|
-
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> {
|
|
251
300
|
const root = await findWorkspaceRoot(cwd)
|
|
252
|
-
let base = affected
|
|
301
|
+
let base = affected || ((settingEnv('affectedBase') as string | undefined) ?? '')
|
|
253
302
|
if (base === '') {
|
|
254
303
|
const ws = await loadCliWorkspace(root)
|
|
255
304
|
try {
|
|
256
305
|
base =
|
|
257
306
|
ws.branch.base ??
|
|
258
|
-
(await defaultAffectedBase(root, configuredBaseBranch(ws.workspaceConfig)))
|
|
307
|
+
(await defaultAffectedBase(root, configuredBaseBranch(ws.workspaceConfig), instead))
|
|
259
308
|
} catch (err) {
|
|
260
309
|
if (!isUserError(err)) throw err
|
|
261
310
|
return refusalOf(err)
|
|
@@ -270,9 +319,11 @@ export async function resolveFilters(
|
|
|
270
319
|
load: CliLoadOptions = {},
|
|
271
320
|
/** The raw filter `--affected` became: its diff is kept per path for the run. */
|
|
272
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',
|
|
273
324
|
): Promise<FilterResolution> {
|
|
274
325
|
const root = await findWorkspaceRoot(cwd)
|
|
275
|
-
const projects = await loadWorkspaceProjects(cwd)
|
|
326
|
+
const projects = await loadWorkspaceProjects(cwd, load)
|
|
276
327
|
let parsed: ReturnType<typeof parseFilter>[]
|
|
277
328
|
try {
|
|
278
329
|
parsed = raw.map((r) => parseFilter(r, root))
|
|
@@ -330,8 +381,11 @@ export async function resolveFilters(
|
|
|
330
381
|
workspaceRoot: root,
|
|
331
382
|
since: f.gitSince,
|
|
332
383
|
projects,
|
|
333
|
-
workspaceGlobOwners: (
|
|
334
|
-
|
|
384
|
+
workspaceGlobOwners: (
|
|
385
|
+
changed: readonly string[],
|
|
386
|
+
nested: readonly string[],
|
|
387
|
+
dirLinks: ReadonlySet<string>,
|
|
388
|
+
) => workspaceGlobOwners(root, projects, changed, load, stagedOnce, nested, dirLinks),
|
|
335
389
|
fingerprintClaims: () => workspaceFingerprintClaims(root, projects, load),
|
|
336
390
|
...(git !== undefined
|
|
337
391
|
? {
|
|
@@ -344,6 +398,10 @@ export async function resolveFilters(
|
|
|
344
398
|
for (const [rel, oid] of g.trusted) if (oid.startsWith('120000:')) out.push(rel)
|
|
345
399
|
return out
|
|
346
400
|
},
|
|
401
|
+
flagged: async () => {
|
|
402
|
+
const g = await git.start()
|
|
403
|
+
return g.scopes.includes('.') ? g.flagged : null
|
|
404
|
+
},
|
|
347
405
|
}
|
|
348
406
|
: {}),
|
|
349
407
|
}
|
|
@@ -383,20 +441,21 @@ export async function resolveFilters(
|
|
|
383
441
|
...(tags !== undefined ? { tags } : {}),
|
|
384
442
|
// A `[<since>]` selector matching nothing is the ordinary "nothing
|
|
385
443
|
// changed" outcome, reported below; only a name/path pattern that
|
|
386
|
-
// matched
|
|
387
|
-
onNoMatch: (f) => {
|
|
388
|
-
if (
|
|
389
|
-
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)
|
|
390
448
|
},
|
|
391
449
|
// A pattern that matched, with nothing on the walk it asked for: the
|
|
392
450
|
// fact to say, where "no projects matched" read as a typo (item 1030).
|
|
393
451
|
onEmptyWalk: (f, matched) => {
|
|
394
452
|
if (f.negate) return
|
|
395
453
|
const which = matched.join(', ')
|
|
454
|
+
const several = matched.length > 1
|
|
396
455
|
emptyWalks.push(
|
|
397
456
|
f.onlyDeps
|
|
398
|
-
? `filter "${f.raw}" matched ${which}, which depends on no project`
|
|
399
|
-
: `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'}`,
|
|
400
459
|
)
|
|
401
460
|
},
|
|
402
461
|
})
|
|
@@ -408,18 +467,23 @@ export async function resolveFilters(
|
|
|
408
467
|
// Every pattern matched and an exclusion took them all back: "no
|
|
409
468
|
// projects matched filter(s): one, !one" read as a typo of `one`.
|
|
410
469
|
const negations = parsed.filter((f) => f.negate).map((f) => f.raw)
|
|
411
|
-
|
|
412
|
-
|
|
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
|
+
) {
|
|
413
477
|
if (includes.every((f) => emptyDiffs.has(f))) {
|
|
414
|
-
const refs = includes.map((f) => f.gitSince).join(', ')
|
|
478
|
+
const refs = [...new Set(includes.map((f) => f.gitSince))].join(', ')
|
|
415
479
|
// A base that IS HEAD (a single-branch clone whose `origin/HEAD` is
|
|
416
480
|
// the branch under test) can never mark anything affected, and this
|
|
417
481
|
// exit-0 note was the only sign (CI persona, 2026-09-16). Say so.
|
|
418
|
-
const self = includes.
|
|
482
|
+
const self = includes.map((f) => headBase(root, f.gitSince!)).find((b) => b !== undefined)
|
|
419
483
|
const hint =
|
|
420
484
|
self === undefined
|
|
421
485
|
? ''
|
|
422
|
-
: ` — ${self
|
|
486
|
+
: ` — ${self} is HEAD itself: compare with the branch you merge into (--affected=origin/main) or the previous commit (--affected=HEAD~1)`
|
|
423
487
|
return { empty: `nothing affected since ${refs}${hint}` }
|
|
424
488
|
}
|
|
425
489
|
// The diff matched and a walk or an exclusion left nothing: still the
|
|
@@ -437,18 +501,19 @@ export async function resolveFilters(
|
|
|
437
501
|
}
|
|
438
502
|
// One line, not a warning per pattern and then an error saying the same:
|
|
439
503
|
// the patterns are in the error, and the nearest project name is the
|
|
440
|
-
// hint a typo needs.
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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}` }
|
|
444
509
|
}
|
|
445
510
|
// Something matched, so the run proceeds; a pattern that matched nothing
|
|
446
511
|
// alongside it is still worth a line — it is probably a typo.
|
|
447
512
|
for (const f of unmatched)
|
|
448
513
|
sayNotice(
|
|
449
514
|
'warn',
|
|
450
|
-
|
|
451
|
-
`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)}`,
|
|
452
517
|
)
|
|
453
518
|
// Each include is a union: what `--filter other` selected runs its tasks
|
|
454
519
|
// whether or not the `--affected` diff reaches them, and an exclude
|
|
@@ -502,7 +567,7 @@ export async function pickTask(
|
|
|
502
567
|
/** The projects `--filter` / `--affected` selected; every project when absent. */
|
|
503
568
|
only?: ReadonlySet<string>,
|
|
504
569
|
): Promise<PickedTask | null | 'interrupted'> {
|
|
505
|
-
const projects = await loadWorkspaceProjects(cwd)
|
|
570
|
+
const projects = await loadWorkspaceProjects(cwd, load)
|
|
506
571
|
// The staged load: a task a `project` plugin gave a config-less package
|
|
507
572
|
// is on the menu, as it is in a run.
|
|
508
573
|
const staged = await loadCliProjects(await findWorkspaceRoot(cwd), projects, 'all', load)
|
|
@@ -597,13 +662,42 @@ export async function pickTask(
|
|
|
597
662
|
}
|
|
598
663
|
}
|
|
599
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
|
+
|
|
600
694
|
/** The hint for the first unmatched pattern: a near tag for a `tag:` one, else a near project name. */
|
|
601
695
|
function didYouMean(
|
|
602
696
|
unmatched: readonly string[],
|
|
603
697
|
projects: Iterable<{ name: string }>,
|
|
604
698
|
tags: ReadonlyMap<string, readonly string[]> | undefined,
|
|
605
699
|
): string {
|
|
606
|
-
const first = unmatched[0]
|
|
700
|
+
const first = unmatched[0] === undefined ? undefined : typedName(unmatched[0])
|
|
607
701
|
if (first?.startsWith('tag:') !== true) return didYouMeanProject(unmatched, projects)
|
|
608
702
|
const known = [...new Set([...(tags?.values() ?? [])].flat())].sort()
|
|
609
703
|
if (known.length === 0) return '. No project declares tags'
|
|
@@ -625,11 +719,16 @@ function didYouMeanProject(
|
|
|
625
719
|
if (bare !== n) byBare.set(bare, [...(byBare.get(bare) ?? []), n])
|
|
626
720
|
}
|
|
627
721
|
for (const pattern of unmatched) {
|
|
628
|
-
const typed = pattern
|
|
722
|
+
const typed = typedName(pattern)
|
|
629
723
|
// Two edits from any two-letter name: `--filter //` hinted `ui` (X-13).
|
|
630
724
|
if (typed === '//') {
|
|
631
725
|
return '. `//` is the workspace root, which is no project here: add a vx.config there and run `vx lock`'
|
|
632
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
|
+
}
|
|
633
732
|
const best = nearest(typed, names)
|
|
634
733
|
if (best !== undefined) return `. Did you mean ${best}?`
|
|
635
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}?)`
|