@vzn/vx 0.0.646 → 0.0.649
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/why.ts
CHANGED
|
@@ -2,24 +2,46 @@
|
|
|
2
2
|
// terminal. The runs table names the run-over-run hash change, and
|
|
3
3
|
// `entry_inputs` (the input-fingerprint moat) names the exact cache-key
|
|
4
4
|
// components that differ. Read-only over cache.db plus a stat of each
|
|
5
|
-
// moved input file
|
|
5
|
+
// moved input file, no re-hash. Configs are evaluated only when a moved
|
|
6
|
+
// upstream is a group: it records no run, so the graph names what it gathers.
|
|
6
7
|
|
|
7
8
|
import { Cache } from '../cache/index.js'
|
|
8
|
-
import { flagHint, formatValue, seeHelp } from './help.js'
|
|
9
|
-
import { splitTaskId } from '../graph/index.js'
|
|
9
|
+
import { anchoredSpecError, flagHint, formatValue, seeHelp } from './help.js'
|
|
10
|
+
import { buildTaskGraph, isGroupTask, splitTaskId } from '../graph/index.js'
|
|
10
11
|
import {
|
|
11
12
|
cacheKeyDiff,
|
|
12
13
|
explainCacheKey,
|
|
13
14
|
inputWriters,
|
|
14
15
|
latestRunId,
|
|
15
16
|
rootCauses,
|
|
17
|
+
upstreamTask,
|
|
16
18
|
whyDidThisRerun,
|
|
17
19
|
} from '../history/index.js'
|
|
18
20
|
import { resolveRunId } from '../orchestrator/index.js'
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
+
import {
|
|
22
|
+
detectColors,
|
|
23
|
+
ERROR,
|
|
24
|
+
MASKED,
|
|
25
|
+
nearMatches,
|
|
26
|
+
paint,
|
|
27
|
+
printable,
|
|
28
|
+
refusal,
|
|
29
|
+
SUCCESS,
|
|
30
|
+
ui,
|
|
31
|
+
UserError,
|
|
32
|
+
WARN,
|
|
33
|
+
} from '../util/index.js'
|
|
34
|
+
import { runRules } from '../settings.js'
|
|
35
|
+
import { buildPackageGraph, findWorkspaceRoot } from '../workspace/index.js'
|
|
21
36
|
import { findCwdProject } from './select.js'
|
|
22
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
cliCacheDir,
|
|
39
|
+
discoverCliProjects,
|
|
40
|
+
loadCliProjects,
|
|
41
|
+
loadCliWorkspace,
|
|
42
|
+
parseCacheDirFlag,
|
|
43
|
+
rootSpelled,
|
|
44
|
+
} from './workspace-config.js'
|
|
23
45
|
|
|
24
46
|
interface WhyArgs {
|
|
25
47
|
target?: string
|
|
@@ -47,7 +69,8 @@ export const WHAT_TO_DO: Readonly<Record<string, string>> = {
|
|
|
47
69
|
config:
|
|
48
70
|
"the task's evaluated config changed (its vx.config or a file it imports); `vx show <that task>` prints it as it is now",
|
|
49
71
|
upstream: "a dependency's key moved; `vx why <that task>` says why",
|
|
50
|
-
plugin: "a key plugin's material changed; that plugin decides what
|
|
72
|
+
plugin: "a key plugin's material changed; that plugin decides what goes into the key",
|
|
73
|
+
platform: 'it ran on another platform; each of its `exec.platforms` keeps its own entry',
|
|
51
74
|
format:
|
|
52
75
|
'vx keys tasks differently since an upgrade; every cached task misses once, nothing to fix',
|
|
53
76
|
}
|
|
@@ -65,8 +88,13 @@ export function parseWhyArgs(args: readonly string[]): WhyArgs {
|
|
|
65
88
|
// got two different diagnoses depending on how it was typed.
|
|
66
89
|
if (a === '--run' || a.startsWith('--run=')) {
|
|
67
90
|
const rv = a === '--run' ? args[++i] : a.slice(6)
|
|
68
|
-
|
|
69
|
-
|
|
91
|
+
// A run id is hex: one opening with `-` is the next flag, swallowed.
|
|
92
|
+
const flag = a === '--run' && rv?.startsWith('-') === true ? `got flag ${rv}; ` : ''
|
|
93
|
+
if (rv === undefined || rv === '' || flag !== '')
|
|
94
|
+
return {
|
|
95
|
+
...out,
|
|
96
|
+
error: `--run requires a run id (${flag}\`vx last --list\` shows them)${seeHelp('why')}`,
|
|
97
|
+
}
|
|
70
98
|
out.runId = rv
|
|
71
99
|
continue
|
|
72
100
|
}
|
|
@@ -78,7 +106,7 @@ export function parseWhyArgs(args: readonly string[]): WhyArgs {
|
|
|
78
106
|
}
|
|
79
107
|
const cd = parseCacheDirFlag(args, i)
|
|
80
108
|
if (cd !== null) {
|
|
81
|
-
if ('error' in cd) return { ...out, error: cd.error }
|
|
109
|
+
if ('error' in cd) return { ...out, error: `${cd.error}${seeHelp('why')}` }
|
|
82
110
|
out.cacheDir = cd.cacheDir
|
|
83
111
|
i = cd.next
|
|
84
112
|
continue
|
|
@@ -90,6 +118,13 @@ export function parseWhyArgs(args: readonly string[]): WhyArgs {
|
|
|
90
118
|
// Empty is no task: read as a query it matched every recorded one.
|
|
91
119
|
if (a !== '') out.target = a
|
|
92
120
|
}
|
|
121
|
+
if (out.target !== undefined) {
|
|
122
|
+
const error = anchoredSpecError(
|
|
123
|
+
out.target,
|
|
124
|
+
(task) => `vx why ${task} finds the project that ran it`,
|
|
125
|
+
)
|
|
126
|
+
if (error !== undefined) return { ...out, error }
|
|
127
|
+
}
|
|
93
128
|
return out
|
|
94
129
|
}
|
|
95
130
|
|
|
@@ -141,6 +176,25 @@ async function resolveTarget(
|
|
|
141
176
|
.query('SELECT 1 FROM runs WHERE project = ? AND task = ? ORDER BY id DESC LIMIT 1')
|
|
142
177
|
.get(project, task)
|
|
143
178
|
if (hit !== null) return target
|
|
179
|
+
} else {
|
|
180
|
+
// A bare name reads only its own task's rows: the DISTINCT over every
|
|
181
|
+
// pair sorted the whole history (24 of 26 ms at 58k rows), and its list
|
|
182
|
+
// serves only the miss's suggestions below.
|
|
183
|
+
const matches = (
|
|
184
|
+
db.query('SELECT DISTINCT project FROM runs WHERE task = ? ORDER BY project').all(target) as {
|
|
185
|
+
project: string
|
|
186
|
+
}[]
|
|
187
|
+
).map((r) => `${r.project}#${target}`)
|
|
188
|
+
if (matches.length === 1) return matches[0]!
|
|
189
|
+
if (matches.length > 1) {
|
|
190
|
+
const here = await cwdProject()
|
|
191
|
+
if (here !== null && matches.includes(`${here}#${target}`)) return `${here}#${target}`
|
|
192
|
+
throw new UserError(
|
|
193
|
+
`vx why: "${target}" ran in ${matches.length} projects — pick one:\n` +
|
|
194
|
+
matches.map((m) => ` ${m}`).join('\n'),
|
|
195
|
+
'VX_E_AMBIGUOUS',
|
|
196
|
+
)
|
|
197
|
+
}
|
|
144
198
|
}
|
|
145
199
|
const pairs = db
|
|
146
200
|
.query('SELECT DISTINCT project, task FROM runs ORDER BY project, task')
|
|
@@ -162,17 +216,6 @@ async function resolveTarget(
|
|
|
162
216
|
'VX_E_NO_HISTORY',
|
|
163
217
|
)
|
|
164
218
|
}
|
|
165
|
-
const matches = ids.filter((id) => id.endsWith(`#${target}`))
|
|
166
|
-
if (matches.length === 1) return matches[0]!
|
|
167
|
-
if (matches.length > 1) {
|
|
168
|
-
const here = await cwdProject()
|
|
169
|
-
if (here !== null && matches.includes(`${here}#${target}`)) return `${here}#${target}`
|
|
170
|
-
throw new UserError(
|
|
171
|
-
`vx why: "${target}" ran in ${matches.length} projects — pick one:\n` +
|
|
172
|
-
matches.map((m) => ` ${m}`).join('\n'),
|
|
173
|
-
'VX_E_AMBIGUOUS',
|
|
174
|
-
)
|
|
175
|
-
}
|
|
176
219
|
throw new UserError(
|
|
177
220
|
`vx why: no recorded runs for task "${target}"${suggest(target, ids, invoked)}`,
|
|
178
221
|
'VX_E_NO_HISTORY',
|
|
@@ -181,27 +224,38 @@ async function resolveTarget(
|
|
|
181
224
|
|
|
182
225
|
const fmtWhen = (ms: number): string => new Date(ms).toISOString()
|
|
183
226
|
|
|
184
|
-
/** `//#task`, Turbo's root package, as the root project's id: the name discovery gives it (D-39). */
|
|
185
|
-
async function rootSpelled(root: string, target: string): Promise<string> {
|
|
186
|
-
if (!target.startsWith('//#')) return target
|
|
187
|
-
const name = await discoverCliProjects(root).then(
|
|
188
|
-
(metas) => metas.find((m) => m.dir === root)?.name,
|
|
189
|
-
() => undefined,
|
|
190
|
-
)
|
|
191
|
-
return name === undefined ? target : `${name}#${target.slice(3)}`
|
|
192
|
-
}
|
|
193
|
-
|
|
194
227
|
/** ` · up-to-date` or ` · restored` on a hit, as the run's summary said it; nothing otherwise. */
|
|
195
228
|
function restoredWord(r: { restored: boolean | null }): string {
|
|
196
229
|
return r.restored === null ? '' : r.restored ? ' · restored' : ' · up-to-date'
|
|
197
230
|
}
|
|
198
231
|
|
|
232
|
+
/**
|
|
233
|
+
* One run of the task, the same for `this run` and `previous`. A hit's
|
|
234
|
+
* status (`cache-hit`, `cache-hit-remote`) already names it and its tier,
|
|
235
|
+
* and `up-to-date` / `restored` says what it did to the disk; only an
|
|
236
|
+
* executed run needs the word. A skipped task never ran and has no key: it
|
|
237
|
+
* read `skipped · executed · key ` (item 898).
|
|
238
|
+
*/
|
|
239
|
+
function runFacts(r: {
|
|
240
|
+
startedAt: number
|
|
241
|
+
status: string
|
|
242
|
+
cacheHit: boolean | null
|
|
243
|
+
restored: boolean | null
|
|
244
|
+
hash: string
|
|
245
|
+
}): string {
|
|
246
|
+
const ran = r.cacheHit === false && r.status !== 'skipped'
|
|
247
|
+
return (
|
|
248
|
+
`${fmtWhen(r.startedAt)} · ${r.status}${restoredWord(r)}${ran ? ' · executed' : ''}` +
|
|
249
|
+
(r.hash === '' ? ' · no key' : ` · key ${r.hash}`)
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
|
|
199
253
|
export async function whyCmd(args: readonly string[]): Promise<number> {
|
|
200
254
|
const parsed = parseWhyArgs(args)
|
|
201
255
|
if (parsed.error !== undefined) throw new UserError(`vx why: ${parsed.error}`, 'VX_E_USAGE')
|
|
202
256
|
if (parsed.target === undefined) {
|
|
203
257
|
throw new UserError(
|
|
204
|
-
|
|
258
|
+
`vx why: <task> required (e.g. vx why app#build, or vx why build)${seeHelp('why')}`,
|
|
205
259
|
'VX_E_USAGE',
|
|
206
260
|
)
|
|
207
261
|
}
|
|
@@ -215,7 +269,13 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
|
|
|
215
269
|
)
|
|
216
270
|
let runId: string | null
|
|
217
271
|
if (parsed.runId !== undefined) {
|
|
218
|
-
|
|
272
|
+
// A stopped run keeps rows but no header (item 854), and `vx last
|
|
273
|
+
// --log` prints its id, so the whole id is taken from its rows too.
|
|
274
|
+
runId =
|
|
275
|
+
resolveRunId(db, parsed.runId, 'vx why') ??
|
|
276
|
+
(db.query('SELECT 1 FROM runs WHERE run_id = ? LIMIT 1').get(parsed.runId) !== null
|
|
277
|
+
? parsed.runId
|
|
278
|
+
: null)
|
|
219
279
|
// An unknown id read as a known run missing the task (X-18).
|
|
220
280
|
if (runId === null) {
|
|
221
281
|
throw new UserError(
|
|
@@ -232,8 +292,9 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
|
|
|
232
292
|
if (parsed.format === 'json') {
|
|
233
293
|
process.stdout.write(`${JSON.stringify({ taskId, why: null, diff: null, explanation })}\n`)
|
|
234
294
|
} else {
|
|
295
|
+
const c = detectColors(process.stdout)
|
|
235
296
|
process.stdout.write(
|
|
236
|
-
`${taskId}: recorded runs carry no run id — showing the latest cache entry instead\n` +
|
|
297
|
+
`${ui.inline(ui.id(taskId), c)}: recorded runs carry no run id — showing the latest cache entry instead\n` +
|
|
237
298
|
(explanation.latestEntry !== null
|
|
238
299
|
? ` hash ${explanation.latestEntry.hash} · $ ${explanation.latestEntry.command}\n`
|
|
239
300
|
: ' (no cache entry either)\n'),
|
|
@@ -253,7 +314,15 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
|
|
|
253
314
|
// An upstream change is never the cause, only its carrier: follow
|
|
254
315
|
// each moved dependency in this run down to the tasks whose own
|
|
255
316
|
// inputs moved, so one `vx why` names the file, not the next task.
|
|
256
|
-
|
|
317
|
+
// A group records no run: the graph names what it gathers.
|
|
318
|
+
const unrecorded = diff.entries.filter(
|
|
319
|
+
(e) =>
|
|
320
|
+
e.kind === 'upstream' &&
|
|
321
|
+
e.change === 'changed' &&
|
|
322
|
+
!cacheKeyDiff(db, runId, upstreamTask(e.name)).found,
|
|
323
|
+
)
|
|
324
|
+
const groups = unrecorded.length > 0 ? await groupMembers(root, taskId) : new Map()
|
|
325
|
+
const roots = rootCauses(db, runId, taskId, diff.entries, groups)
|
|
257
326
|
const writers = inputWriters(db, root, taskId, diff.entries, roots)
|
|
258
327
|
|
|
259
328
|
if (parsed.format === 'json') {
|
|
@@ -262,24 +331,22 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
|
|
|
262
331
|
}
|
|
263
332
|
|
|
264
333
|
const lines: string[] = []
|
|
334
|
+
const c = detectColors(process.stdout)
|
|
335
|
+
const dim = (text: string): string => paint('', text, c, { dim: true })
|
|
336
|
+
const head = (text: string): string => paint('', text, c, { bold: true })
|
|
265
337
|
const t = why.thisRun!
|
|
266
|
-
lines.push(`${taskId} — run ${runId}`)
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
)
|
|
276
|
-
if (why.previousRun == null) {
|
|
277
|
-
lines.push(' previous (none — first recorded run of this task)')
|
|
338
|
+
lines.push(`${ui.inline(ui.id(taskId), c)}${dim(` — run ${runId}`)}`)
|
|
339
|
+
lines.push(` ${dim('this run')} ${runFacts(t)}`)
|
|
340
|
+
if (why.previousRun == null && t.hash === '') {
|
|
341
|
+
// Only keyed runs are compared: a persistent task's tenth run read
|
|
342
|
+
// as its first, and the note saying why went unprinted.
|
|
343
|
+
lines.push(` ${dim('previous')} (none with a cache key)`)
|
|
344
|
+
lines.push(` ${dim('verdict')} ${why.note}`)
|
|
345
|
+
} else if (why.previousRun == null) {
|
|
346
|
+
lines.push(` ${dim('previous')} (none — first recorded run of this task)`)
|
|
278
347
|
} else {
|
|
279
348
|
const p = why.previousRun
|
|
280
|
-
lines.push(
|
|
281
|
-
` previous ${fmtWhen(p.startedAt)} · ${p.status}${restoredWord(p)} · key ${p.hash}`,
|
|
282
|
-
)
|
|
349
|
+
lines.push(` ${dim('previous')} ${runFacts(p)}`)
|
|
283
350
|
// The key moved and the diff says by what: the verdict names it, so
|
|
284
351
|
// an env-only change reads as one line, not "inputs differ" above a
|
|
285
352
|
// table to scan.
|
|
@@ -288,9 +355,13 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
|
|
|
288
355
|
e.name === e.kind ? e.kind : `${e.kind} ${printable(e.name)}`,
|
|
289
356
|
)
|
|
290
357
|
lines.push(
|
|
291
|
-
` verdict ${
|
|
358
|
+
` ${dim('verdict')} ${
|
|
292
359
|
why.hashChanged === true && moved.length > 0
|
|
293
|
-
?
|
|
360
|
+
? paint(
|
|
361
|
+
WARN,
|
|
362
|
+
`cache key changed: ${moved.slice(0, 3).join(', ')}${moved.length > 3 ? ` and ${moved.length - 3} more` : ''}`,
|
|
363
|
+
c,
|
|
364
|
+
)
|
|
294
365
|
: why.note
|
|
295
366
|
}`,
|
|
296
367
|
)
|
|
@@ -299,7 +370,7 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
|
|
|
299
370
|
if (diff.entries.length > 0) {
|
|
300
371
|
lines.push('')
|
|
301
372
|
lines.push(
|
|
302
|
-
` what changed (${diff.entries.length} component${diff.entries.length === 1 ? '' : 's'}, ${diff.unchangedCount} unchanged):`,
|
|
373
|
+
` ${head('what changed')}${dim(` (${diff.entries.length} component${diff.entries.length === 1 ? '' : 's'}, ${diff.unchangedCount} unchanged)`)}:`,
|
|
303
374
|
)
|
|
304
375
|
const kindW = Math.max(...diff.entries.map((e) => e.kind.length), 4)
|
|
305
376
|
for (const e of diff.entries) {
|
|
@@ -313,11 +384,11 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
|
|
|
313
384
|
? `- ${e.before}`
|
|
314
385
|
: `${e.before} → ${e.after}`
|
|
315
386
|
lines.push(
|
|
316
|
-
` ${e.change.padEnd(7)} ${e.kind.padEnd(kindW)} ${printable(e.name)} ${beforeAfter}`.trimEnd(),
|
|
387
|
+
` ${paint(CHANGE_HUE[e.change] ?? WARN, e.change.padEnd(7), c)} ${dim(e.kind.padEnd(kindW))} ${printable(e.name)} ${dim(beforeAfter)}`.trimEnd(),
|
|
317
388
|
)
|
|
318
389
|
}
|
|
319
390
|
if (roots.length > 0) {
|
|
320
|
-
lines.push('', '
|
|
391
|
+
lines.push('', ` ${head('root cause')}:`)
|
|
321
392
|
for (const r of roots.slice(0, ROOTS_SHOWN)) {
|
|
322
393
|
const what = r.entries.map((e) =>
|
|
323
394
|
e.name === e.kind ? e.kind : `${e.kind} ${printable(e.name)}`,
|
|
@@ -329,40 +400,47 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
|
|
|
329
400
|
if (roots.length > ROOTS_SHOWN) lines.push(` and ${roots.length - ROOTS_SHOWN} more`)
|
|
330
401
|
}
|
|
331
402
|
if (writers.length > 0) {
|
|
332
|
-
lines.push('', '
|
|
403
|
+
lines.push('', ` ${head('changed during a run')}:`)
|
|
333
404
|
for (const w of writers) {
|
|
334
|
-
lines.push(` ${printable(w.file)} while ${w.writers.join(', ')} ran`)
|
|
405
|
+
lines.push(` ${printable(w.file)} ${dim(`while ${w.writers.join(', ')} ran`)}`)
|
|
335
406
|
}
|
|
336
407
|
}
|
|
337
408
|
const rootKinds = roots.flatMap((r) => r.entries.map((e) => e.kind))
|
|
409
|
+
// An upstream row names the task to ask next: the command, not a
|
|
410
|
+
// placeholder to fill in from the row above. A group records no run,
|
|
411
|
+
// so `vx why` on one refuses: its members are asked instead.
|
|
412
|
+
const upstream = upstreamTodo(
|
|
413
|
+
diff.entries.filter((e) => e.kind === 'upstream'),
|
|
414
|
+
unrecorded,
|
|
415
|
+
)
|
|
338
416
|
const kinds = [...new Set([...diff.entries.map((e) => e.kind), ...rootKinds])].filter(
|
|
339
|
-
(k) =>
|
|
417
|
+
(k) =>
|
|
418
|
+
k in WHAT_TO_DO && !(k === 'upstream' && (roots.length > 0 || upstream.length === 0)),
|
|
340
419
|
)
|
|
341
420
|
if (kinds.length > 0) {
|
|
342
|
-
lines.push('', '
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
const
|
|
421
|
+
lines.push('', ` ${head('what to do')}:`)
|
|
422
|
+
// Its own column: a root cause's kind and the `written` row are
|
|
423
|
+
// not in the table above, and overflowed its width.
|
|
424
|
+
const todoW = Math.max(kindW, ...kinds.map((k) => k.length), writers.length > 0 ? 7 : 0)
|
|
346
425
|
for (const k of kinds) {
|
|
426
|
+
if (k === 'upstream') {
|
|
427
|
+
for (const todo of upstream) lines.push(` ${dim(k.padEnd(todoW))} ${todo}`)
|
|
428
|
+
continue
|
|
429
|
+
}
|
|
347
430
|
const todo =
|
|
348
|
-
k === '
|
|
349
|
-
? WHAT_TO_DO[k]!.replace(
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
)
|
|
353
|
-
: k === 'config'
|
|
354
|
-
? WHAT_TO_DO[k]!.replace('`vx show <that task>`', `\`vx show ${taskId}\``)
|
|
355
|
-
: WHAT_TO_DO[k]
|
|
356
|
-
lines.push(` ${k.padEnd(kindW)} ${todo}`)
|
|
431
|
+
k === 'config'
|
|
432
|
+
? WHAT_TO_DO[k]!.replace('`vx show <that task>`', `\`vx show ${taskId}\``)
|
|
433
|
+
: WHAT_TO_DO[k]
|
|
434
|
+
lines.push(` ${dim(k.padEnd(todoW))} ${todo}`)
|
|
357
435
|
}
|
|
358
436
|
if (writers.length > 0) {
|
|
359
437
|
// A task re-writing a file a key reads moves that key every run:
|
|
360
438
|
// the miss is right, and only moving the write ends it.
|
|
361
|
-
lines.push(` ${'written'.padEnd(
|
|
439
|
+
lines.push(` ${dim('written'.padEnd(todoW))} ${WRITTEN_TODO}`)
|
|
362
440
|
}
|
|
363
441
|
}
|
|
364
442
|
} else if (why.hashChanged === true) {
|
|
365
|
-
lines.push(` detail ${diff.note}`)
|
|
443
|
+
lines.push(` ${dim('detail')} ${diff.note}`)
|
|
366
444
|
}
|
|
367
445
|
process.stdout.write(`${lines.join('\n')}\n`)
|
|
368
446
|
return 0
|
|
@@ -373,5 +451,65 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
|
|
|
373
451
|
|
|
374
452
|
const ROOTS_SHOWN = 5
|
|
375
453
|
|
|
454
|
+
const CHANGE_HUE: Record<string, string> = { added: SUCCESS, removed: ERROR, changed: WARN }
|
|
455
|
+
|
|
456
|
+
type DiffEntry = ReturnType<typeof cacheKeyDiff>['entries'][number]
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* The upstream advice: a moved key names the task to ask next, a group
|
|
460
|
+
* (no run of its own) the verb that names what it gathers, and an edge
|
|
461
|
+
* added or removed says the graph changed, since no key moved.
|
|
462
|
+
*/
|
|
463
|
+
function upstreamTodo(upstream: readonly DiffEntry[], unrecorded: readonly DiffEntry[]): string[] {
|
|
464
|
+
const out: string[] = []
|
|
465
|
+
const groups = new Set(unrecorded.map((e) => e.name))
|
|
466
|
+
const moved = [
|
|
467
|
+
...new Set(
|
|
468
|
+
upstream
|
|
469
|
+
.filter((e) => e.change === 'changed' && !groups.has(e.name))
|
|
470
|
+
.map((e) => upstreamTask(e.name)),
|
|
471
|
+
),
|
|
472
|
+
]
|
|
473
|
+
if (moved.length > 0)
|
|
474
|
+
out.push(
|
|
475
|
+
WHAT_TO_DO['upstream']!.replace(
|
|
476
|
+
'`vx why <that task>`',
|
|
477
|
+
moved.map((n) => `\`vx why ${n}\``).join(', '),
|
|
478
|
+
),
|
|
479
|
+
)
|
|
480
|
+
for (const g of groups)
|
|
481
|
+
out.push(
|
|
482
|
+
`${g} is a group and records no run; \`vx show ${g}\` names what it gathers, \`vx why\` on the one whose key moved says why`,
|
|
483
|
+
)
|
|
484
|
+
const edges = upstream.filter((e) => e.change !== 'changed')
|
|
485
|
+
if (edges.length > 0)
|
|
486
|
+
out.push(
|
|
487
|
+
`a dependency joined or left the graph (${edges.map((e) => `${e.name} ${e.change}`).join(', ')}): its \`dependsOn\` or a project edge changed, which re-keys the task by design`,
|
|
488
|
+
)
|
|
489
|
+
return out
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Each group in `taskId`'s graph with the tasks it gathers. Empty when a
|
|
494
|
+
* config no longer loads: the advice then names the group to `vx show`.
|
|
495
|
+
*/
|
|
496
|
+
async function groupMembers(root: string, taskId: string): Promise<Map<string, string[]>> {
|
|
497
|
+
try {
|
|
498
|
+
const metas = await discoverCliProjects(root)
|
|
499
|
+
const projects = await loadCliProjects(root, metas, 'all', { closure: true })
|
|
500
|
+
const [project, task] = splitTaskId(taskId)
|
|
501
|
+
const nodes = buildTaskGraph({
|
|
502
|
+
projects,
|
|
503
|
+
packageGraph: buildPackageGraph(metas),
|
|
504
|
+
requested: [{ project, task }],
|
|
505
|
+
workspaceRoot: root,
|
|
506
|
+
rules: runRules((await loadCliWorkspace(root)).workspaceConfig),
|
|
507
|
+
})
|
|
508
|
+
return new Map([...nodes.values()].filter(isGroupTask).map((n) => [n.id, n.deps]))
|
|
509
|
+
} catch {
|
|
510
|
+
return new Map()
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
376
514
|
const WRITTEN_TODO =
|
|
377
515
|
'a task that writes a file a key reads re-keys it every run; give that write its own task with the file as an output, or keep the file out of inputs.files'
|
|
@@ -30,25 +30,29 @@ interface CliWorkspace {
|
|
|
30
30
|
/** What a plugin's `config` stage said of the branch: its cache scope and base commit. */
|
|
31
31
|
branch: BranchFacts
|
|
32
32
|
cacheDir: string
|
|
33
|
-
/**
|
|
34
|
-
|
|
33
|
+
/**
|
|
34
|
+
* The shared store a run opens `cacheDir` with (`resolveStoreRoot`), asked
|
|
35
|
+
* on first call: with no remote, the answer is a git spawn, and a verb that
|
|
36
|
+
* never opens the store (help, show, last) paid it.
|
|
37
|
+
*/
|
|
38
|
+
storeRoot: () => Promise<string | null>
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
export const warnToStderr = (message: string): void => {
|
|
38
42
|
sayLine('warn', message)
|
|
39
43
|
}
|
|
40
44
|
|
|
41
|
-
export async function loadCliWorkspace(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
)
|
|
45
|
+
export async function loadCliWorkspace(
|
|
46
|
+
workspaceRoot: string,
|
|
47
|
+
warn: (message: string) => void = warnToStderr,
|
|
48
|
+
): Promise<CliWorkspace> {
|
|
49
|
+
const { workspaceConfig, plugins, branch } = await loadWorkspacePlugins(workspaceRoot, warn)
|
|
46
50
|
return {
|
|
47
51
|
workspaceConfig,
|
|
48
52
|
plugins,
|
|
49
53
|
branch,
|
|
50
54
|
cacheDir: resolveCacheDir(workspaceRoot, workspaceConfig),
|
|
51
|
-
storeRoot: resolveStoreRoot(workspaceRoot, workspaceConfig),
|
|
55
|
+
storeRoot: () => resolveStoreRoot(workspaceRoot, workspaceConfig),
|
|
52
56
|
}
|
|
53
57
|
}
|
|
54
58
|
|
|
@@ -57,9 +61,35 @@ export async function loadCliWorkspace(workspaceRoot: string): Promise<CliWorksp
|
|
|
57
61
|
* plugin `discover` stage, with the cache dir the run uses. No lock is
|
|
58
62
|
* `NO_LOCK`.
|
|
59
63
|
*/
|
|
60
|
-
export async function discoverCliProjects(
|
|
61
|
-
|
|
62
|
-
|
|
64
|
+
export async function discoverCliProjects(
|
|
65
|
+
root: string,
|
|
66
|
+
load: CliLoadOptions = {},
|
|
67
|
+
): Promise<ProjectMeta[]> {
|
|
68
|
+
const warn = load.warn ?? warnToStderr
|
|
69
|
+
const ws = await loadCliWorkspace(root, warn)
|
|
70
|
+
return await discoverProjects(root, lockReader(root), ws.plugins, stageCacheDir(ws, load), warn)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The cache dir a plugin stage is told: `--cache-dir` resolved against the
|
|
75
|
+
* working directory as the run resolves it (prepare.ts), else the
|
|
76
|
+
* workspace's. The run reuses what these stages answered.
|
|
77
|
+
*/
|
|
78
|
+
export function stageCacheDir(ws: { cacheDir: string }, load: CliLoadOptions): string {
|
|
79
|
+
return load.cacheDir === undefined ? ws.cacheDir : path.resolve(process.cwd(), load.cacheDir)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* `//#task`, Turbo's root package, as the root project's id: the name
|
|
84
|
+
* discovery gives it (D-39), for a verb that reads history by task id.
|
|
85
|
+
*/
|
|
86
|
+
export async function rootSpelled(root: string, target: string): Promise<string> {
|
|
87
|
+
if (!target.startsWith('//#')) return target
|
|
88
|
+
const name = await discoverCliProjects(root).then(
|
|
89
|
+
(metas) => metas.find((m) => m.dir === root)?.name,
|
|
90
|
+
() => undefined,
|
|
91
|
+
)
|
|
92
|
+
return name === undefined ? target : `${name}#${target.slice(3)}`
|
|
63
93
|
}
|
|
64
94
|
|
|
65
95
|
/**
|
|
@@ -93,7 +123,9 @@ export function parseCacheDirFlag(
|
|
|
93
123
|
*/
|
|
94
124
|
export function namedCacheDir(override: string): string {
|
|
95
125
|
const dir = path.resolve(process.cwd(), override)
|
|
96
|
-
if (!existsSync(dir))
|
|
126
|
+
if (!existsSync(dir)) {
|
|
127
|
+
throw new UserError(`--cache-dir ${override}: no such directory (${dir})`, 'VX_E_USAGE')
|
|
128
|
+
}
|
|
97
129
|
return dir
|
|
98
130
|
}
|
|
99
131
|
|
|
@@ -120,6 +152,12 @@ export interface CliLoadOptions {
|
|
|
120
152
|
* `build` there, and a reader that skipped it showed less than a run.
|
|
121
153
|
*/
|
|
122
154
|
closure?: boolean
|
|
155
|
+
/**
|
|
156
|
+
* Where a plugin's warning goes, stderr by default. `vx watch` re-reads
|
|
157
|
+
* the configs around every cycle, and the cycle's run says each warning
|
|
158
|
+
* itself: printed per read, one cycle said it seven times.
|
|
159
|
+
*/
|
|
160
|
+
warn?: (message: string) => void
|
|
123
161
|
}
|
|
124
162
|
|
|
125
163
|
/**
|
|
@@ -134,9 +172,10 @@ export async function loadCliProjects(
|
|
|
134
172
|
scope: 'all' | readonly string[] = 'all',
|
|
135
173
|
opts: CliLoadOptions = {},
|
|
136
174
|
): Promise<Map<string, ProjectEntry>> {
|
|
137
|
-
const
|
|
175
|
+
const warn = opts.warn ?? warnToStderr
|
|
176
|
+
const ws = await loadCliWorkspace(workspaceRoot, warn)
|
|
138
177
|
const { plugins } = ws
|
|
139
|
-
const cacheDir = opts
|
|
178
|
+
const cacheDir = stageCacheDir(ws, opts)
|
|
140
179
|
const loaded = await loadProjects({
|
|
141
180
|
workspaceRoot,
|
|
142
181
|
cacheDir,
|
|
@@ -147,7 +186,7 @@ export async function loadCliProjects(
|
|
|
147
186
|
? { closure: true as const, packageGraph: buildPackageGraph([...metas]) }
|
|
148
187
|
: { closure: false as const }),
|
|
149
188
|
configs: lockOfDiscovery(metas) ?? lockReader(workspaceRoot),
|
|
150
|
-
warn
|
|
189
|
+
warn,
|
|
151
190
|
})
|
|
152
191
|
return loaded.projects
|
|
153
192
|
}
|
package/src/config.ts
CHANGED
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export type Duration = `${number}${'ms' | 's' | 'm' | 'h' | 'd'}`
|
|
8
8
|
|
|
9
|
+
/** The machines a task can name in `exec.platforms`: `os-arch`, as Bun's compile targets name them. */
|
|
10
|
+
export const PLATFORMS = ['linux-x64', 'linux-arm64', 'darwin-x64', 'darwin-arm64'] as const
|
|
11
|
+
|
|
12
|
+
export type Platform = (typeof PLATFORMS)[number]
|
|
13
|
+
|
|
9
14
|
/**
|
|
10
15
|
* The workspace: settings in groups (`run`, `cache`, `lock`, `watch`) and the
|
|
11
16
|
* plugins. Each setting's environment variable and flag derive from its
|
|
@@ -329,7 +334,7 @@ export interface TaskConfig {
|
|
|
329
334
|
* INVALIDATES a task, `allow` says what it may TOUCH, and deriving one
|
|
330
335
|
* from the other coupled them both ways — a declaration added for
|
|
331
336
|
* caching silently widened the sandbox, and a path the task needed had
|
|
332
|
-
* to be laundered through the cache key to get it (
|
|
337
|
+
* to be laundered through the cache key to get it (2026-09-05).
|
|
333
338
|
* A declared `cache.outputs` is NOT a write grant; the request
|
|
334
339
|
* `sandbox-request.ts` builds carries no write of its own and binds
|
|
335
340
|
* `allow.write` alone. This comment claimed the opposite — here and on
|
|
@@ -464,6 +469,13 @@ export interface ExecConfig {
|
|
|
464
469
|
* state the machine has, exactly as they did before the field existed.
|
|
465
470
|
*/
|
|
466
471
|
remote?: boolean | 'only'
|
|
472
|
+
/**
|
|
473
|
+
* The machines this task can run on, any of them. On one that is not
|
|
474
|
+
* listed the task is skipped, never failed, and so are its dependants;
|
|
475
|
+
* an executor that routes by platform (`TaskExecutor.platforms`) runs
|
|
476
|
+
* it on a worker that is. Omitted → anywhere.
|
|
477
|
+
*/
|
|
478
|
+
platforms?: readonly Platform[]
|
|
467
479
|
/** Environment exposed to the child process. */
|
|
468
480
|
env?: ExecEnv
|
|
469
481
|
/**
|
package/src/exec/env.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
-
import { ESSENTIAL_ENV } from '../util/index.js'
|
|
3
|
+
import { ESSENTIAL_ENV, withoutWorkspaceBins } from '../util/index.js'
|
|
4
4
|
|
|
5
5
|
// Build the environment exposed to a task.
|
|
6
6
|
//
|
|
@@ -34,6 +34,8 @@ export interface BuildEnvOptions {
|
|
|
34
34
|
* One holding `path.delimiter` is left out: PATH cannot name it.
|
|
35
35
|
*/
|
|
36
36
|
binPaths?: readonly string[]
|
|
37
|
+
/** When set, the PATH taken from `source` drops each `node_modules/.bin` inside it. */
|
|
38
|
+
workspaceRoot?: string
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
export function buildIsolatedEnv(opts: BuildEnvOptions): NodeJS.ProcessEnv {
|
|
@@ -47,6 +49,9 @@ export function buildIsolatedEnv(opts: BuildEnvOptions): NodeJS.ProcessEnv {
|
|
|
47
49
|
const value = opts.source[name]
|
|
48
50
|
if (value !== undefined) out[name] = value
|
|
49
51
|
}
|
|
52
|
+
if (opts.workspaceRoot !== undefined && out['PATH'] !== undefined) {
|
|
53
|
+
out['PATH'] = withoutWorkspaceBins(out['PATH'], opts.workspaceRoot)
|
|
54
|
+
}
|
|
50
55
|
for (const [name, value] of Object.entries(opts.define)) {
|
|
51
56
|
out[name] = value
|
|
52
57
|
}
|
|
@@ -73,7 +78,7 @@ export function buildIsolatedEnv(opts: BuildEnvOptions): NodeJS.ProcessEnv {
|
|
|
73
78
|
* A tool that runs other scripts reads it to call the same manager back
|
|
74
79
|
* (npm-run-all's `run-s`, `run-p`) and falls back to `npm` without it:
|
|
75
80
|
* solidjs/solid's `npm-run-all -nl build:*` needed a global npm under vx,
|
|
76
|
-
* where `pnpm run build` passed (
|
|
81
|
+
* where `pnpm run build` passed (2026-10-06). It names a binary on
|
|
77
82
|
* this machine, so it is not in the key, as PATH is not.
|
|
78
83
|
*/
|
|
79
84
|
export const PM_EXEC_ENV = 'npm_execpath'
|