@vzn/vx 0.0.636 → 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/README.md +39 -11
- package/docs/caching.md +246 -213
- package/docs/cli.md +791 -705
- package/docs/execution.md +68 -76
- package/docs/features.md +47 -42
- package/docs/patterns.md +26 -25
- package/docs/schema.md +392 -205
- package/docs/security.md +76 -39
- package/launcher.sh +43 -0
- package/package.json +5 -5
- package/schemas/cache.json +41 -19
- package/schemas/info.json +23 -11
- package/schemas/last.json +48 -2
- package/schemas/lock.json +1 -1
- package/schemas/plan.json +4 -4
- package/schemas/show.json +5 -3
- package/schemas/why.json +15 -1
- package/skills/vx/SKILL.md +28 -13
- package/src/bin.ts +30 -9
- package/src/cache/archive.ts +11 -0
- package/src/cache/cache.ts +124 -53
- package/src/cache/chained-cache.ts +4 -0
- package/src/cache/git-inputs.ts +277 -63
- package/src/cache/index.ts +15 -1
- package/src/cache/inputs.ts +102 -36
- package/src/cache/key-fold.ts +8 -7
- package/src/cache/layer.ts +15 -18
- package/src/cache/layered-cache.ts +20 -102
- package/src/cache/plugin-cache.ts +248 -0
- package/src/cache/policy.ts +32 -27
- package/src/cache/run-history.ts +104 -14
- package/src/cache/run-store.ts +77 -0
- package/src/cache/schema.ts +58 -31
- package/src/cache/scoped-cache.ts +237 -0
- package/src/cli/cache.ts +173 -34
- package/src/cli/completions.ts +11 -7
- package/src/cli/core-alias.ts +1 -1
- package/src/cli/foreign-flags.ts +13 -12
- package/src/cli/help.ts +110 -70
- package/src/cli/index.ts +67 -24
- package/src/cli/info.ts +43 -27
- package/src/cli/init.ts +115 -407
- package/src/cli/last.ts +157 -9
- package/src/cli/lock.ts +356 -122
- package/src/cli/plan-format.ts +7 -5
- package/src/cli/plugin-commands.ts +81 -14
- package/src/cli/plugin-templates.ts +11 -11
- package/src/cli/run.ts +116 -160
- package/src/cli/select.ts +50 -47
- package/src/cli/show.ts +83 -59
- package/src/cli/task-verb.ts +6 -13
- package/src/cli/upgrade.ts +415 -149
- package/src/cli/watch-cycle.ts +9 -2
- package/src/cli/watch-filter.ts +39 -61
- package/src/cli/watch-fs.ts +29 -16
- package/src/cli/watch-judge.ts +18 -4
- package/src/cli/watch-set.ts +16 -42
- package/src/cli/watch.ts +227 -230
- package/src/cli/why.ts +38 -18
- package/src/cli/workspace-config.ts +33 -70
- package/src/config.ts +169 -70
- package/src/exec/env.ts +1 -31
- package/src/exec/executor.ts +1 -5
- package/src/exec/kill-tree.ts +2 -1
- package/src/exec/sandbox-runtime.ts +12 -1
- package/src/exec/sandbox-violations.ts +9 -1
- package/src/graph/dependency-spec.ts +1 -1
- package/src/graph/index.ts +1 -0
- package/src/graph/priorities.ts +1 -1
- package/src/graph/scheduler.ts +27 -7
- package/src/graph/task-graph.ts +76 -24
- package/src/handoff.ts +169 -0
- package/src/index.ts +35 -58
- package/src/orchestrator/affected-tasks.ts +15 -8
- package/src/orchestrator/command-windows.ts +44 -0
- package/src/orchestrator/deferred-outputs.ts +12 -10
- package/src/orchestrator/doctor.ts +98 -110
- package/src/orchestrator/download-policy.ts +3 -3
- package/src/orchestrator/events.ts +19 -3
- package/src/orchestrator/excluded-keys.ts +0 -2
- package/src/orchestrator/execute-task.ts +147 -69
- package/src/orchestrator/failure-mode.ts +19 -10
- package/src/orchestrator/fingerprint-watch.ts +18 -15
- package/src/orchestrator/forecast.ts +10 -2
- package/src/orchestrator/framed-output.ts +9 -5
- package/src/orchestrator/graph-stage.ts +5 -5
- package/src/orchestrator/history.ts +126 -30
- package/src/orchestrator/hit-restore.ts +15 -3
- package/src/orchestrator/index.ts +20 -5
- package/src/orchestrator/local-shortcircuit.ts +2 -1
- package/src/orchestrator/lockfile-claim.ts +79 -20
- package/src/orchestrator/logger.ts +80 -41
- package/src/orchestrator/metrics.ts +78 -9
- package/src/orchestrator/miss-save.ts +3 -0
- package/src/orchestrator/options.ts +42 -29
- package/src/orchestrator/output-host.ts +72 -0
- package/src/orchestrator/plan.ts +6 -8
- package/src/orchestrator/plugin-host.ts +100 -66
- package/src/orchestrator/plugin.ts +173 -67
- package/src/orchestrator/prepare.ts +163 -78
- package/src/orchestrator/projects.ts +105 -99
- package/src/orchestrator/remote-prefetch.ts +11 -6
- package/src/orchestrator/run-context.ts +25 -6
- package/src/orchestrator/run-failures.ts +17 -8
- package/src/orchestrator/run-lock.ts +1 -1
- package/src/orchestrator/run-records.ts +1 -1
- package/src/orchestrator/run-report.ts +9 -9
- package/src/orchestrator/run-settings.ts +48 -0
- package/src/orchestrator/run.ts +244 -119
- package/src/orchestrator/sandbox-fix.ts +39 -20
- package/src/orchestrator/sandbox-request.ts +84 -13
- package/src/orchestrator/signals.ts +1 -1
- package/src/orchestrator/stable-keys.ts +13 -11
- package/src/orchestrator/status-line.ts +49 -7
- package/src/orchestrator/summary.ts +115 -106
- package/src/orchestrator/task-hash.ts +12 -83
- package/src/orchestrator/telemetry.ts +4 -4
- package/src/settings.ts +428 -0
- package/src/{orchestrator → util}/colors.ts +5 -0
- package/src/util/config-frame.ts +31 -2
- package/src/util/errors.ts +33 -1
- package/src/util/essential-env.ts +31 -0
- package/src/util/hide-env.ts +107 -0
- package/src/util/index.ts +16 -3
- package/src/util/paths.ts +10 -0
- package/src/util/refusal-notice.ts +111 -0
- package/src/util/report.ts +31 -0
- package/src/util/settle.ts +49 -30
- package/src/util/size.ts +23 -4
- package/src/util/timing.ts +3 -2
- package/src/util/ui.ts +186 -0
- package/src/util/ulid.ts +1 -1
- package/src/util/verbs.ts +0 -15
- package/src/workspace/affected.ts +84 -232
- package/src/workspace/config-eval.ts +15 -20
- package/src/workspace/config-imports.ts +78 -3
- package/src/workspace/config-schema.ts +209 -268
- package/src/workspace/dependency-rules.ts +65 -0
- package/src/workspace/filter.ts +2 -2
- package/src/workspace/fingerprint.ts +11 -121
- package/src/workspace/index.ts +20 -27
- package/src/workspace/load-reads.ts +4 -4
- package/src/workspace/lockfile.ts +96 -140
- package/src/workspace/migration.ts +307 -215
- package/src/workspace/package-graph.ts +30 -216
- package/src/workspace/project-loader.ts +80 -285
- package/src/workspace/repo-id.ts +2 -1
- package/src/workspace/workspace.ts +363 -831
- package/src/cache/config-evals.ts +0 -130
- package/src/workspace/config-cache.ts +0 -661
- package/src/workspace/migrate-scripts.ts +0 -1121
package/src/cli/select.ts
CHANGED
|
@@ -14,11 +14,9 @@ import {
|
|
|
14
14
|
buildPackageGraph,
|
|
15
15
|
defaultAffectedBase,
|
|
16
16
|
findWorkspaceRoot,
|
|
17
|
-
type LoadReads,
|
|
18
17
|
type FingerprintClaims,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
loadWorkspace,
|
|
18
|
+
lockKey,
|
|
19
|
+
NO_LOCK,
|
|
22
20
|
parseFilter,
|
|
23
21
|
type ProjectMeta,
|
|
24
22
|
readLockfile,
|
|
@@ -34,6 +32,7 @@ import {
|
|
|
34
32
|
maskedLine,
|
|
35
33
|
nearest,
|
|
36
34
|
UserError,
|
|
35
|
+
sayNotice,
|
|
37
36
|
} from '../util/index.js'
|
|
38
37
|
import {
|
|
39
38
|
claimedAffected,
|
|
@@ -49,15 +48,15 @@ import {
|
|
|
49
48
|
loadCliProjects,
|
|
50
49
|
loadCliWorkspace,
|
|
51
50
|
} from './workspace-config.js'
|
|
51
|
+
import { resolveSetting } from '../settings.js'
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
54
|
* The projects whose tasks declare a `cache.inputs.workspaceFiles` glob
|
|
55
55
|
* matching a changed path, one inside another project included (item
|
|
56
56
|
* 954): `--affected` selects them, since the glob is that task's input. Through the run
|
|
57
57
|
* path's staged load, so a glob a `project` plugin gave a config-less
|
|
58
|
-
* package counts;
|
|
59
|
-
*
|
|
60
|
-
* do load, one by one — a broken out-of-scope config does not fail a
|
|
58
|
+
* package counts; read from the lock as a run does. A load that fails drops to the lock's
|
|
59
|
+
* entries, one by one — a project the lock does not hold does not fail a
|
|
61
60
|
* scoped run, and must not fail its selection either.
|
|
62
61
|
*/
|
|
63
62
|
export async function workspaceGlobOwners(
|
|
@@ -85,26 +84,23 @@ export async function workspaceGlobOwners(
|
|
|
85
84
|
} catch {
|
|
86
85
|
// Fall through to the per-file sweep.
|
|
87
86
|
}
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (load.frozen === true && (await readLockfile(root)) === null) {
|
|
94
|
-
throw new UserError(FROZEN_WITHOUT_LOCK)
|
|
95
|
-
}
|
|
87
|
+
// No lock is refused here, before the tolerant sweep below could answer
|
|
88
|
+
// "nothing affected" and exit 0 without ever reaching the run's own
|
|
89
|
+
// refusal. The sweep reads each entry from the lock: it never evaluates.
|
|
90
|
+
const lock = await readLockfile(root)
|
|
91
|
+
if (lock === null) throw new UserError(NO_LOCK)
|
|
96
92
|
const owners: string[] = []
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
93
|
+
for (const meta of projects) {
|
|
94
|
+
if (meta.configPath === null || meta.configPath === '') continue
|
|
95
|
+
const dir = lockKey(root, meta.dir)
|
|
96
|
+
if (!Object.hasOwn(lock.projects, dir)) continue
|
|
97
|
+
try {
|
|
98
|
+
if (declaresMatch(lock.projects[dir]!.config)) owners.push(meta.name)
|
|
99
|
+
} catch {
|
|
100
|
+
// A hand-edited entry that will not read cannot be shown to declare a
|
|
101
|
+
// matching glob; surfacing it here would fail a run the load tolerates.
|
|
102
|
+
}
|
|
103
|
+
}
|
|
108
104
|
return owners
|
|
109
105
|
}
|
|
110
106
|
|
|
@@ -133,9 +129,7 @@ async function workspaceFingerprintClaims(
|
|
|
133
129
|
}
|
|
134
130
|
|
|
135
131
|
async function loadWorkspaceProjects(cwd: string): Promise<ProjectMeta[]> {
|
|
136
|
-
|
|
137
|
-
const root = await findWorkspaceRoot(cwd, reads)
|
|
138
|
-
return await discoverCliProjects(await loadWorkspace(root, reads))
|
|
132
|
+
return await discoverCliProjects(await findWorkspaceRoot(cwd))
|
|
139
133
|
}
|
|
140
134
|
|
|
141
135
|
export async function findCwdProject(cwd: string): Promise<string | null> {
|
|
@@ -150,9 +144,8 @@ export async function findCwdProject(cwd: string): Promise<string | null> {
|
|
|
150
144
|
export async function findCwdSelection(
|
|
151
145
|
cwd: string,
|
|
152
146
|
): Promise<{ name: string; discovered: { root: string; projects: ProjectMeta[] } } | null> {
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
const projects = await discoverCliProjects(await loadWorkspace(root, reads))
|
|
147
|
+
const root = await findWorkspaceRoot(cwd)
|
|
148
|
+
const projects = await discoverCliProjects(root)
|
|
156
149
|
const abs = path.resolve(cwd)
|
|
157
150
|
const within = (dirOf: (p: ProjectMeta) => string): string | null => {
|
|
158
151
|
let best: string | null = null
|
|
@@ -251,14 +244,17 @@ function refusalOf(err: unknown): Refusal {
|
|
|
251
244
|
|
|
252
245
|
/**
|
|
253
246
|
* The filter `--affected[=<base>]` stands for: `...[<base>]`, the changed
|
|
254
|
-
* projects and their dependents. An empty base is
|
|
255
|
-
*
|
|
247
|
+
* projects and their dependents. An empty base is `run.affectedBase`
|
|
248
|
+
* (its variable, then the workspace's, which a plugin's `config` stage may set from nx.json's
|
|
256
249
|
* `defaultBase` or `TURBO_SCM_BASE`), then the guess.
|
|
257
250
|
*/
|
|
258
251
|
export async function affectedFilterFor(cwd: string, affected: string): Promise<string | Refusal> {
|
|
259
252
|
const root = await findWorkspaceRoot(cwd)
|
|
260
253
|
let base = affected
|
|
261
|
-
if (base === '')
|
|
254
|
+
if (base === '') {
|
|
255
|
+
const ws = (await loadCliWorkspace(root)).workspaceConfig
|
|
256
|
+
base = (resolveSetting('run.affectedBase', ws).value as string | undefined) ?? ''
|
|
257
|
+
}
|
|
262
258
|
if (base === '') {
|
|
263
259
|
try {
|
|
264
260
|
base = await defaultAffectedBase(root)
|
|
@@ -339,7 +335,6 @@ export async function resolveFilters(
|
|
|
339
335
|
workspaceGlobOwners: (changed: readonly string[], nested: readonly string[]) =>
|
|
340
336
|
workspaceGlobOwners(root, projects, changed, load, stagedOnce, nested),
|
|
341
337
|
fingerprintClaims: () => workspaceFingerprintClaims(root, projects, load),
|
|
342
|
-
taskEdges: async () => edges ?? taskEdgesFrom(await stagedOnce()),
|
|
343
338
|
...(git !== undefined ? { untracked: async () => (await git.start()).untracked } : {}),
|
|
344
339
|
}
|
|
345
340
|
let names: Set<string>
|
|
@@ -440,8 +435,10 @@ export async function resolveFilters(
|
|
|
440
435
|
// Something matched, so the run proceeds; a pattern that matched nothing
|
|
441
436
|
// alongside it is still worth a line — it is probably a typo.
|
|
442
437
|
for (const f of unmatched)
|
|
443
|
-
|
|
444
|
-
|
|
438
|
+
sayNotice(
|
|
439
|
+
'warn',
|
|
440
|
+
'vx run',
|
|
441
|
+
`filter "${f}" matched no projects${didYouMean([f], projects, tags)}`,
|
|
445
442
|
)
|
|
446
443
|
// Each include is a union: what `--filter other` selected runs its tasks
|
|
447
444
|
// whether or not the `--affected` diff reaches them, and an exclude
|
|
@@ -517,14 +514,18 @@ export async function pickTask(
|
|
|
517
514
|
}
|
|
518
515
|
}
|
|
519
516
|
if (entries.length === 0 && only !== undefined) {
|
|
520
|
-
|
|
521
|
-
|
|
517
|
+
sayNotice(
|
|
518
|
+
'error',
|
|
519
|
+
'vx run',
|
|
520
|
+
`no tasks declared in the selected projects (${[...only].sort().join(', ')})`,
|
|
522
521
|
)
|
|
523
522
|
return null
|
|
524
523
|
}
|
|
525
524
|
if (entries.length === 0) {
|
|
526
|
-
|
|
527
|
-
'
|
|
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`',
|
|
528
529
|
)
|
|
529
530
|
return null
|
|
530
531
|
}
|
|
@@ -568,13 +569,15 @@ export async function pickTask(
|
|
|
568
569
|
if (!(err instanceof Error) || err.name !== 'AbortError') throw err
|
|
569
570
|
if (interrupted) return 'interrupted'
|
|
570
571
|
out.write('\n')
|
|
571
|
-
|
|
572
|
+
sayNotice('info', 'vx run', 'no task picked')
|
|
572
573
|
return null
|
|
573
574
|
}
|
|
574
575
|
const n = Number(answer)
|
|
575
576
|
if (!Number.isInteger(n) || n < 1 || n > entries.length) {
|
|
576
|
-
|
|
577
|
-
|
|
577
|
+
sayNotice(
|
|
578
|
+
'error',
|
|
579
|
+
'vx run',
|
|
580
|
+
`invalid selection: ${answer} (type a number from 1 to ${entries.length})`,
|
|
578
581
|
)
|
|
579
582
|
return null
|
|
580
583
|
}
|
|
@@ -604,7 +607,7 @@ function didYouMeanProject(
|
|
|
604
607
|
projects: Iterable<{ name: string }>,
|
|
605
608
|
): string {
|
|
606
609
|
const names = [...projects].map((p) => p.name)
|
|
607
|
-
// A scoped name typed without its scope (`vx-
|
|
610
|
+
// A scoped name typed without its scope (`vx-otel` for `@vzn/vx-otel`) is
|
|
608
611
|
// many edits from the whole name and none from the part after the `/`.
|
|
609
612
|
const byBare = new Map<string, string[]>()
|
|
610
613
|
for (const n of names) {
|
|
@@ -615,7 +618,7 @@ function didYouMeanProject(
|
|
|
615
618
|
const typed = pattern.replace(/^!|\.\.\.$|^\.\.\.|\^/g, '')
|
|
616
619
|
// Two edits from any two-letter name: `--filter //` hinted `ui` (X-13).
|
|
617
620
|
if (typed === '//') {
|
|
618
|
-
return
|
|
621
|
+
return '. `//` is the workspace root, which is no project here: add a vx.config there and run `vx lock`'
|
|
619
622
|
}
|
|
620
623
|
const best = nearest(typed, names)
|
|
621
624
|
if (best !== undefined) return `. Did you mean ${best}?`
|
package/src/cli/show.ts
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
|
-
// `vx show [target]` — introspect the workspace's
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// line per project. `<project>`: every task's
|
|
6
|
-
//
|
|
7
|
-
//
|
|
1
|
+
// `vx show [target]` — introspect the workspace's resolved configs: what a
|
|
2
|
+
// run would see here, now. Configs load through the same path a run uses
|
|
3
|
+
// (`loadProjects`): from the lock, with the plugin `config` and `project`
|
|
4
|
+
// stages applied, so a package a plugin gives tasks to shows them. No lock
|
|
5
|
+
// is `NO_LOCK`. No target: one line per project. `<project>`: every task's
|
|
6
|
+
// resolved config. `<pkg>#<task>`: one task. `<task>`: that task in every
|
|
7
|
+
// project declaring it.
|
|
8
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
12
|
import { affectedTaskProjects, isDefaultBuild } from '../orchestrator/index.js'
|
|
13
13
|
import { flagHint, formatValue, refuse, seeHelp } from './help.js'
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
type ColorSupport,
|
|
16
|
+
detectColors,
|
|
17
|
+
listed,
|
|
18
|
+
nearMatches,
|
|
19
|
+
relPosix,
|
|
20
|
+
secretMask,
|
|
21
|
+
UserError,
|
|
22
|
+
sayNotice,
|
|
23
|
+
ui,
|
|
24
|
+
} from '../util/index.js'
|
|
15
25
|
import { affectedFilterFor, resolveFilters } from './select.js'
|
|
16
26
|
import {
|
|
17
27
|
discoverCliProjects,
|
|
@@ -23,11 +33,10 @@ import {
|
|
|
23
33
|
type AffectedChanges,
|
|
24
34
|
buildPackageGraph,
|
|
25
35
|
findWorkspaceRoot,
|
|
26
|
-
type LoadReads,
|
|
27
|
-
loadWorkspace,
|
|
28
36
|
type ProjectEntry,
|
|
29
37
|
type ProjectMeta,
|
|
30
38
|
} from '../workspace/index.js'
|
|
39
|
+
import { runRules } from '../settings.js'
|
|
31
40
|
|
|
32
41
|
interface ShowArgs {
|
|
33
42
|
target?: string
|
|
@@ -83,9 +92,8 @@ export function parseShowArgs(args: readonly string[]): ShowArgs {
|
|
|
83
92
|
export async function showCmd(args: readonly string[]): Promise<number> {
|
|
84
93
|
const parsed = parseShowArgs(args)
|
|
85
94
|
if (parsed.error) return refuse('show', args, parsed.error, 'VX_E_USAGE')
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
const metas = await discoverCliProjects(await loadWorkspace(root, reads))
|
|
95
|
+
const root = await findWorkspaceRoot(process.cwd())
|
|
96
|
+
const metas = await discoverCliProjects(root)
|
|
89
97
|
const byName = new Map(metas.map((m) => [m.name, m]))
|
|
90
98
|
|
|
91
99
|
// `//#task` is Turbo's root package's task: the root project here (D-39),
|
|
@@ -149,20 +157,20 @@ export async function showCmd(args: readonly string[]): Promise<number> {
|
|
|
149
157
|
affected = f
|
|
150
158
|
raw.unshift(f)
|
|
151
159
|
}
|
|
152
|
-
const r = await resolveFilters(process.cwd(), raw, {
|
|
160
|
+
const r = await resolveFilters(process.cwd(), raw, {}, affected)
|
|
153
161
|
if ('error' in r) throw new UserError(`vx show: ${r.error}`, r.code ?? 'VX_E_USAGE')
|
|
154
|
-
if ('empty' in r)
|
|
162
|
+
if ('empty' in r) sayNotice('info', 'vx show', r.empty)
|
|
155
163
|
selected = new Set('empty' in r ? [] : r.names)
|
|
156
164
|
if ('names' in r && r.affected !== undefined)
|
|
157
165
|
byDiff = { changes: r.affected, outright: r.outright }
|
|
158
166
|
}
|
|
159
167
|
const inSelection = (name: string): boolean => selected === undefined || selected.has(name)
|
|
160
168
|
|
|
161
|
-
const projects = await loadCliProjects(root, metas, scope, {
|
|
169
|
+
const projects = await loadCliProjects(root, metas, scope, { closure: true })
|
|
162
170
|
|
|
163
171
|
if (parsed.target === undefined) {
|
|
164
172
|
const shown = metas.filter((m) => inSelection(m.name))
|
|
165
|
-
process.stdout.write(renderList(root, shown, projects, parsed.format))
|
|
173
|
+
process.stdout.write(renderList(root, shown, projects, parsed.format, colorsOut()))
|
|
166
174
|
return 0
|
|
167
175
|
}
|
|
168
176
|
|
|
@@ -200,13 +208,13 @@ export async function showCmd(args: readonly string[]): Promise<number> {
|
|
|
200
208
|
plugins: ws.plugins,
|
|
201
209
|
workspaceRoot: root,
|
|
202
210
|
cacheDir: ws.cacheDir,
|
|
203
|
-
rules: ws.workspaceConfig
|
|
211
|
+
rules: runRules(ws.workspaceConfig),
|
|
204
212
|
warn: warnToStderr,
|
|
205
213
|
}),
|
|
206
214
|
)
|
|
207
215
|
shown = shown.filter((p) => kept.has(p.name))
|
|
208
216
|
}
|
|
209
|
-
process.stdout.write(renderTaskAcross(root, shown, projectName!, parsed.format))
|
|
217
|
+
process.stdout.write(renderTaskAcross(root, shown, projectName!, parsed.format, colorsOut()))
|
|
210
218
|
return 0
|
|
211
219
|
}
|
|
212
220
|
|
|
@@ -217,7 +225,7 @@ export async function showCmd(args: readonly string[]): Promise<number> {
|
|
|
217
225
|
|
|
218
226
|
if (taskName === undefined) {
|
|
219
227
|
process.stdout.write(
|
|
220
|
-
renderProject(meta.name, dir, config, meta.configPath !== null, parsed.format),
|
|
228
|
+
renderProject(meta.name, dir, config, meta.configPath !== null, parsed.format, colorsOut()),
|
|
221
229
|
)
|
|
222
230
|
return 0
|
|
223
231
|
}
|
|
@@ -229,7 +237,7 @@ export async function showCmd(args: readonly string[]): Promise<number> {
|
|
|
229
237
|
'VX_E_UNKNOWN_TASK',
|
|
230
238
|
)
|
|
231
239
|
}
|
|
232
|
-
process.stdout.write(renderTask(meta.name, dir, taskName, task, parsed.format))
|
|
240
|
+
process.stdout.write(renderTask(meta.name, dir, taskName, task, parsed.format, colorsOut()))
|
|
233
241
|
return 0
|
|
234
242
|
}
|
|
235
243
|
|
|
@@ -286,11 +294,20 @@ function shownConfig(config: ProjectConfig | null): ProjectConfig | null {
|
|
|
286
294
|
}
|
|
287
295
|
}
|
|
288
296
|
|
|
297
|
+
const colorsOut = (): ColorSupport => detectColors(process.stdout)
|
|
298
|
+
const PLAIN: ColorSupport = { enabled: false }
|
|
299
|
+
|
|
300
|
+
/** One block of several, with no blank line between them. */
|
|
301
|
+
const joined = (blocks: readonly ui.Block[]): ui.Block => ({
|
|
302
|
+
lines: (c) => blocks.flatMap((b) => b.lines(c)),
|
|
303
|
+
})
|
|
304
|
+
|
|
289
305
|
function renderList(
|
|
290
306
|
root: string,
|
|
291
307
|
metas: readonly ProjectMeta[],
|
|
292
308
|
projects: ReadonlyMap<string, ProjectEntry>,
|
|
293
309
|
format: 'pretty' | 'json',
|
|
310
|
+
colors: ColorSupport = PLAIN,
|
|
294
311
|
): string {
|
|
295
312
|
const rows = metas.map((meta) => {
|
|
296
313
|
const tasks = projects.get(meta.name)?.config.tasks ?? {}
|
|
@@ -311,9 +328,8 @@ function renderList(
|
|
|
311
328
|
)}\n`
|
|
312
329
|
}
|
|
313
330
|
if (rows.length === 0) return ''
|
|
314
|
-
const
|
|
315
|
-
const
|
|
316
|
-
const lines = rows.map((r) => {
|
|
331
|
+
const anyTags = rows.some((r) => r.tags.length > 0)
|
|
332
|
+
const cells = rows.map((r) => {
|
|
317
333
|
const n = r.tasks.length
|
|
318
334
|
const count = `${n} task${n === 1 ? '' : 's'}`
|
|
319
335
|
// A package with no config file has only tasks a plugin gave it, or the
|
|
@@ -321,14 +337,14 @@ function renderList(
|
|
|
321
337
|
const tasks = r.configured
|
|
322
338
|
? count
|
|
323
339
|
: n === 0
|
|
324
|
-
? '
|
|
340
|
+
? 'no vx config'
|
|
325
341
|
: r.onlyDefault
|
|
326
|
-
? `${count} (no vx config
|
|
327
|
-
: `${count} (no vx config
|
|
328
|
-
|
|
329
|
-
return `${r.name.padEnd(nameW)} ${r.dir.padEnd(dirW)} ${tasks}${tags}`
|
|
342
|
+
? `${count}, default build (no vx config)`
|
|
343
|
+
: `${count} from plugins (no vx config)`
|
|
344
|
+
return [ui.id(r.name), r.dir, tasks, ...(anyTags ? [r.tags.join(', ')] : [])]
|
|
330
345
|
})
|
|
331
|
-
|
|
346
|
+
const columns = ['project', 'dir', 'tasks', ...(anyTags ? ['tags'] : [])]
|
|
347
|
+
return ui.render([ui.table(columns, cells)], colors)
|
|
332
348
|
}
|
|
333
349
|
|
|
334
350
|
function renderProject(
|
|
@@ -337,23 +353,27 @@ function renderProject(
|
|
|
337
353
|
config: ProjectConfig | null,
|
|
338
354
|
hasConfigFile: boolean,
|
|
339
355
|
format: 'pretty' | 'json',
|
|
356
|
+
colors: ColorSupport = PLAIN,
|
|
340
357
|
): string {
|
|
341
358
|
if (format === 'json') {
|
|
342
359
|
// Round-trip so the printed object is exactly the JSON form of the
|
|
343
360
|
// resolved config (drops `undefined` fields).
|
|
344
361
|
const shown = shownConfig(config)
|
|
345
|
-
return `${JSON.stringify(
|
|
362
|
+
return `${JSON.stringify({ name, dir, config: shown })}\n`
|
|
346
363
|
}
|
|
347
364
|
const tags = config?.tags ?? []
|
|
348
365
|
const tasks = Object.entries(config?.tasks ?? {})
|
|
349
|
-
const head
|
|
366
|
+
const head: ui.FieldRow[] = [
|
|
367
|
+
['dir', dir],
|
|
368
|
+
...(tags.length > 0 ? [['tags', tags.join(', ')] as const] : []),
|
|
350
369
|
// A config-less package a dependant reaches has the default `build` (X-9).
|
|
351
|
-
hasConfigFile
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
370
|
+
...(hasConfigFile ? [] : [['config', 'none'] as const]),
|
|
371
|
+
...(tasks.length === 0 && hasConfigFile ? [['tasks', 'none declared'] as const] : []),
|
|
372
|
+
]
|
|
373
|
+
const blocks = tasks.map(([taskName, task]) =>
|
|
374
|
+
joined([ui.text(ui.id(`${name}#${taskName}`)), ui.fields(taskRows(task))]),
|
|
375
|
+
)
|
|
376
|
+
return ui.render([ui.report(name, head), ...blocks], colors)
|
|
357
377
|
}
|
|
358
378
|
|
|
359
379
|
function renderTask(
|
|
@@ -362,12 +382,13 @@ function renderTask(
|
|
|
362
382
|
taskName: string,
|
|
363
383
|
task: TaskConfig,
|
|
364
384
|
format: 'pretty' | 'json',
|
|
385
|
+
colors: ColorSupport = PLAIN,
|
|
365
386
|
): string {
|
|
366
387
|
if (format === 'json') {
|
|
367
388
|
const obj = { name, dir, task: taskName, config: shownTask(task) }
|
|
368
|
-
return `${JSON.stringify(
|
|
389
|
+
return `${JSON.stringify(obj)}\n`
|
|
369
390
|
}
|
|
370
|
-
return
|
|
391
|
+
return ui.render([taskReport(name, dir, taskName, task)], colors)
|
|
371
392
|
}
|
|
372
393
|
|
|
373
394
|
function renderTaskAcross(
|
|
@@ -375,6 +396,7 @@ function renderTaskAcross(
|
|
|
375
396
|
declaring: readonly ProjectEntry[],
|
|
376
397
|
taskName: string,
|
|
377
398
|
format: 'pretty' | 'json',
|
|
399
|
+
colors: ColorSupport,
|
|
378
400
|
): string {
|
|
379
401
|
if (format === 'json') {
|
|
380
402
|
const list = declaring.map((p) => ({
|
|
@@ -383,20 +405,30 @@ function renderTaskAcross(
|
|
|
383
405
|
task: taskName,
|
|
384
406
|
config: shownTask(p.config.tasks![taskName]!),
|
|
385
407
|
}))
|
|
386
|
-
return `${JSON.stringify(
|
|
408
|
+
return `${JSON.stringify(list)}\n`
|
|
387
409
|
}
|
|
388
|
-
return
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
410
|
+
if (declaring.length === 0) return ''
|
|
411
|
+
return ui.render(
|
|
412
|
+
[
|
|
413
|
+
joined(
|
|
414
|
+
declaring.map((p) =>
|
|
415
|
+
taskReport(p.name, relPosix(root, p.dir) || '.', taskName, p.config.tasks![taskName]!),
|
|
416
|
+
),
|
|
417
|
+
),
|
|
418
|
+
],
|
|
419
|
+
colors,
|
|
420
|
+
)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** `─ app#build ───`, the project's dir, then every field the run reads. */
|
|
424
|
+
function taskReport(name: string, dir: string, taskName: string, task: TaskConfig): ui.Block {
|
|
425
|
+
return ui.report(`${name}#${taskName}`, [['dir', dir], ...taskRows(task)])
|
|
394
426
|
}
|
|
395
427
|
|
|
396
428
|
/** Every field the run reads, in the order the schema declares them. */
|
|
397
|
-
function
|
|
429
|
+
function taskRows(raw: TaskConfig): ui.FieldRow[] {
|
|
398
430
|
const task = shownTask(raw)
|
|
399
|
-
const rows: [
|
|
431
|
+
const rows: ui.FieldRow[] = []
|
|
400
432
|
const list = (xs: readonly string[] | undefined): string | undefined =>
|
|
401
433
|
xs === undefined ? undefined : xs.join(', ')
|
|
402
434
|
const add = (label: string, value: string | number | boolean | undefined): void => {
|
|
@@ -406,7 +438,7 @@ function taskBlock(taskName: string, raw: TaskConfig): string {
|
|
|
406
438
|
const exec = task.exec
|
|
407
439
|
rows.push(['command', exec?.command ?? '(group)'])
|
|
408
440
|
add('dependsOn', list(task.dependsOn))
|
|
409
|
-
add('timeout', exec?.timeout
|
|
441
|
+
add('timeout', exec?.timeout)
|
|
410
442
|
add('retries', exec?.retries)
|
|
411
443
|
add('env.passThrough', list(exec?.env?.passThrough))
|
|
412
444
|
add(
|
|
@@ -439,13 +471,5 @@ function taskBlock(taskName: string, raw: TaskConfig): string {
|
|
|
439
471
|
add('outputs.files', list(cache.outputs.files))
|
|
440
472
|
add('outputs.workspaceFiles', list(cache.outputs.workspaceFiles))
|
|
441
473
|
}
|
|
442
|
-
|
|
443
|
-
// A multi-line value (a generated `vx_script` wrapper, a heredoc)
|
|
444
|
-
// continues under its first line, not at column 0 where it reads as
|
|
445
|
-
// the next task's header.
|
|
446
|
-
const indent = `\n${' '.repeat(labelW + 4)}`
|
|
447
|
-
const body = rows.map(
|
|
448
|
-
([label, value]) => ` ${`${label}:`.padEnd(labelW + 1)} ${value.replaceAll('\n', indent)}`,
|
|
449
|
-
)
|
|
450
|
-
return `${taskName}\n${body.join('\n')}\n`
|
|
474
|
+
return rows
|
|
451
475
|
}
|
package/src/cli/task-verb.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// name would change meaning the day a plugin declares that verb.
|
|
6
6
|
|
|
7
7
|
import { isDefaultBuild, nxProjectTarget, projectNamed } from '../orchestrator/index.js'
|
|
8
|
-
import { findWorkspaceRoot
|
|
8
|
+
import { findWorkspaceRoot } from '../workspace/index.js'
|
|
9
9
|
import { nearest } from '../util/index.js'
|
|
10
10
|
import { findCwdProject } from './select.js'
|
|
11
11
|
import { discoverCliProjects, loadCliProjects } from './workspace-config.js'
|
|
@@ -21,7 +21,7 @@ export async function taskVerbHint(
|
|
|
21
21
|
cwd: string,
|
|
22
22
|
typo = false,
|
|
23
23
|
): Promise<string | null> {
|
|
24
|
-
if (command.includes('#')) return `\`${command}\` is a task
|
|
24
|
+
if (command.includes('#')) return `\`${command}\` is a task (did you mean vx run ${command}?)`
|
|
25
25
|
const projects = await workspaceProjects(cwd)
|
|
26
26
|
if (projects === null) return null
|
|
27
27
|
const tasks = new Set<string>()
|
|
@@ -40,8 +40,8 @@ export async function taskVerbHint(
|
|
|
40
40
|
? `vx run ${task} --all`
|
|
41
41
|
: `vx run ${task}`
|
|
42
42
|
return task === command
|
|
43
|
-
? `\`${command}\` is a task here, not a command
|
|
44
|
-
: `unknown command: ${command}
|
|
43
|
+
? `\`${command}\` is a task here, not a command (did you mean ${run}?)`
|
|
44
|
+
: `unknown command: ${command}, a typo of the task \`${task}\` (did you mean ${run}?)`
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/**
|
|
@@ -54,7 +54,7 @@ export async function nxTargetHint(tasks: readonly string[], cwd: string): Promi
|
|
|
54
54
|
if (projects === null) return null
|
|
55
55
|
const specs = tasks.map((t) => nxProjectTarget(t, projects))
|
|
56
56
|
if (specs.some((s) => s === undefined)) return null
|
|
57
|
-
return `\`${tasks.join(' ')}\` is Nx's project:target
|
|
57
|
+
return `\`${tasks.join(' ')}\` is Nx's project:target (did you mean vx run ${specs.join(' ')}?)`
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/**
|
|
@@ -79,14 +79,7 @@ async function workspaceProjects(
|
|
|
79
79
|
): Promise<Awaited<ReturnType<typeof loadCliProjects>> | null> {
|
|
80
80
|
try {
|
|
81
81
|
const root = await findWorkspaceRoot(cwd)
|
|
82
|
-
return await loadCliProjects(
|
|
83
|
-
root,
|
|
84
|
-
await discoverCliProjects(await loadWorkspace(root)),
|
|
85
|
-
'all',
|
|
86
|
-
{
|
|
87
|
-
noCreate: true,
|
|
88
|
-
},
|
|
89
|
-
)
|
|
82
|
+
return await loadCliProjects(root, await discoverCliProjects(root))
|
|
90
83
|
} catch {
|
|
91
84
|
// No workspace, or a config that does not load: the caller's plain line stands.
|
|
92
85
|
return null
|