@vzn/vx 0.0.16 → 0.0.21
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/LICENSE +1 -1
- package/README.md +135 -124
- package/index.ts +10 -0
- package/launcher.mjs +10 -11
- package/package.json +11 -7
- package/src/bin.ts +14 -4
- package/src/cache/archive.ts +659 -0
- package/src/cache/cache.ts +769 -1101
- package/src/cache/chained-cache.ts +194 -0
- package/src/cache/config-evals.ts +89 -0
- package/src/cache/file-hashes.ts +262 -0
- package/src/cache/git-inputs.ts +637 -0
- package/src/cache/index.ts +24 -5
- package/src/cache/inputs.ts +373 -456
- package/src/cache/layer.ts +604 -0
- package/src/cache/layered-cache.ts +96 -43
- package/src/cache/output-index.ts +224 -0
- package/src/cache/policy.ts +84 -0
- package/src/cache/run-history.ts +147 -0
- package/src/cache/tar-stream.ts +358 -0
- package/src/cache/zstd.ts +160 -0
- package/src/cli/cache.ts +75 -21
- package/src/cli/completions.ts +104 -0
- package/src/cli/core-alias.ts +25 -0
- package/src/cli/help.ts +195 -128
- package/src/cli/index.ts +116 -46
- package/src/cli/info.ts +122 -63
- package/src/cli/init.ts +75 -0
- package/src/cli/last.ts +172 -0
- package/src/cli/lock.ts +2 -1
- package/src/cli/plan-format.ts +72 -6
- package/src/cli/plugin-commands.ts +103 -0
- package/src/cli/run.ts +245 -271
- package/src/cli/select.ts +332 -0
- package/src/cli/show.ts +144 -69
- package/src/cli/upgrade.ts +9 -7
- package/src/cli/watch.ts +858 -110
- package/src/cli/why.ts +223 -0
- package/src/cli/workspace-config.ts +126 -0
- package/src/config.ts +187 -150
- package/src/exec/env.ts +18 -1
- package/src/exec/executor.ts +224 -0
- package/src/exec/index.ts +24 -2
- package/src/exec/local-executor.ts +41 -0
- package/src/exec/runner.ts +166 -40
- package/src/exec/sandbox-binds.ts +182 -0
- package/src/exec/sandbox-paths.ts +50 -0
- package/src/exec/sandbox-runtime.ts +642 -260
- package/src/exec/sandbox-violations.ts +280 -0
- package/src/graph/index.ts +4 -10
- package/src/graph/scheduler.ts +232 -203
- package/src/graph/task-graph.ts +222 -8
- package/src/index.ts +146 -153
- package/src/orchestrator/admission.ts +137 -0
- package/src/orchestrator/deferred-outputs.ts +147 -0
- package/src/orchestrator/doctor.ts +255 -0
- package/src/orchestrator/download-policy.ts +171 -0
- package/src/orchestrator/events.ts +72 -8
- package/src/orchestrator/execute-task.ts +374 -549
- package/src/orchestrator/failure-mode.ts +202 -0
- package/src/orchestrator/framed-output.ts +126 -69
- package/src/orchestrator/history.ts +126 -59
- package/src/orchestrator/hit-restore.ts +221 -0
- package/src/orchestrator/index.ts +77 -93
- package/src/orchestrator/local-shortcircuit.ts +22 -0
- package/src/orchestrator/lockfile-claim.ts +281 -0
- package/src/orchestrator/logger.ts +208 -35
- package/src/orchestrator/metrics.ts +102 -1388
- package/src/orchestrator/miss-save.ts +169 -0
- package/src/orchestrator/options.ts +55 -34
- package/src/orchestrator/persistent.ts +79 -0
- package/src/orchestrator/placement.ts +182 -0
- package/src/orchestrator/plan.ts +144 -1
- package/src/orchestrator/plugin-host.ts +357 -120
- package/src/orchestrator/plugin.ts +298 -58
- package/src/orchestrator/prepare.ts +202 -106
- package/src/orchestrator/projects.ts +259 -0
- package/src/orchestrator/remote-prefetch.ts +12 -8
- package/src/orchestrator/run-artifacts.ts +63 -16
- package/src/orchestrator/run-context.ts +167 -29
- package/src/orchestrator/run-records.ts +149 -0
- package/src/orchestrator/run-report.ts +74 -47
- package/src/orchestrator/run.ts +534 -417
- package/src/orchestrator/sandbox-request.ts +243 -0
- package/src/orchestrator/save-lane.ts +47 -0
- package/src/orchestrator/signals.ts +111 -0
- package/src/orchestrator/stable-keys.ts +129 -31
- package/src/orchestrator/status-line.ts +49 -9
- package/src/orchestrator/summary.ts +90 -26
- package/src/orchestrator/tally.ts +78 -35
- package/src/orchestrator/task-hash.ts +111 -15
- package/src/orchestrator/task-log-buffer.ts +288 -0
- package/src/orchestrator/telemetry-host.ts +34 -6
- package/src/orchestrator/telemetry.ts +182 -36
- package/src/orchestrator/upstream.ts +72 -31
- package/src/util/cgroup.ts +160 -0
- package/src/util/edit-distance.ts +70 -0
- package/src/util/errors.ts +15 -1
- package/src/util/index.ts +15 -2
- package/src/util/num.ts +37 -0
- package/src/util/paths.ts +64 -0
- package/src/util/settle.ts +67 -0
- package/src/util/size.ts +5 -1
- package/src/util/tail.ts +66 -0
- package/src/util/timing.ts +63 -0
- package/src/util/verbs.ts +39 -0
- package/src/workspace/affected.ts +250 -23
- package/src/workspace/config-cache.ts +313 -0
- package/src/workspace/config-eval.ts +217 -0
- package/src/workspace/config-imports.ts +162 -0
- package/src/workspace/config-schema.ts +837 -0
- package/src/workspace/filter.ts +65 -10
- package/src/workspace/fingerprint.ts +53 -4
- package/src/workspace/index.ts +42 -3
- package/src/workspace/lockfile.ts +25 -9
- package/src/workspace/migrate-scripts.ts +134 -0
- package/src/workspace/migration.ts +314 -0
- package/src/workspace/nested-dirs.ts +21 -7
- package/src/workspace/package-graph.ts +114 -41
- package/src/workspace/project-loader.ts +224 -450
- package/src/workspace/workspace.ts +298 -72
- package/src/cache/cas-backend.ts +0 -97
- package/src/cache/digest.ts +0 -47
- package/src/cache/tar.ts +0 -343
- package/src/cli/backend.ts +0 -23
- package/src/cli/mcp-rpc.ts +0 -292
- package/src/cli/mcp.ts +0 -92
- package/src/cli/migrate-nx.ts +0 -450
- package/src/cli/migrate-turbo.ts +0 -364
- package/src/cli/migrate.ts +0 -219
- package/src/orchestrator/devframe-surface.ts +0 -74
- package/src/orchestrator/predict.ts +0 -106
- package/src/orchestrator/protocol.ts +0 -156
- package/src/orchestrator/resources.ts +0 -59
- package/src/orchestrator/run-state.ts +0 -145
- package/src/orchestrator/verify.ts +0 -164
- package/src/orchestrator/wire-render.ts +0 -92
- package/src/orchestrator/wire.ts +0 -212
package/src/cli/why.ts
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// `vx why [pkg#task | task]` — answer "why did this task re-run?" from the
|
|
2
|
+
// terminal. The same persisted data the MCP `whyDidThisRerun` tool reads:
|
|
3
|
+
// the runs table names the
|
|
4
|
+
// run-over-run hash change, and `entry_inputs` (the input-fingerprint moat)
|
|
5
|
+
// names the exact cache-key components that differ. Read-only over cache.db —
|
|
6
|
+
// no config evaluation, no re-hash.
|
|
7
|
+
|
|
8
|
+
import { Cache, noteSchemaReset } from '../cache/index.js'
|
|
9
|
+
import { seeHelp } from './help.js'
|
|
10
|
+
import { splitTaskId } from '../graph/index.js'
|
|
11
|
+
import {
|
|
12
|
+
cacheKeyDiff,
|
|
13
|
+
explainCacheKeyQuery as explainCacheKey,
|
|
14
|
+
whyDidThisRerunQuery as whyDidThisRerun,
|
|
15
|
+
} from '../orchestrator/index.js'
|
|
16
|
+
import { nearMatches, UserError } from '../util/index.js'
|
|
17
|
+
import { findWorkspaceRoot } from '../workspace/index.js'
|
|
18
|
+
import { cliCacheDir, parseCacheDirFlag, warnToStderr } from './workspace-config.js'
|
|
19
|
+
|
|
20
|
+
interface WhyArgs {
|
|
21
|
+
target?: string
|
|
22
|
+
runId?: string
|
|
23
|
+
format: 'pretty' | 'json'
|
|
24
|
+
/** `--cache-dir`: read the history a run with the same flag wrote. */
|
|
25
|
+
cacheDir?: string
|
|
26
|
+
error?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function parseWhyArgs(args: readonly string[]): WhyArgs {
|
|
30
|
+
const out: WhyArgs = { format: 'pretty' }
|
|
31
|
+
for (let i = 0; i < args.length; i++) {
|
|
32
|
+
const a = args[i]!
|
|
33
|
+
// Match on the flag NAME first. Reading the value into the same
|
|
34
|
+
// `undefined` that means "not this flag" conflated the two: a trailing
|
|
35
|
+
// `--run` (value omitted) consumed a non-existent argv slot, fell through,
|
|
36
|
+
// and was reported as `unknown flag: --run` — false, since `--run` is very
|
|
37
|
+
// much known, and silent about the real mistake. The `=` spelling of the
|
|
38
|
+
// same mistake already said `invalid --run: empty`, so one omitted value
|
|
39
|
+
// got two different diagnoses depending on how it was typed.
|
|
40
|
+
if (a === '--run' || a.startsWith('--run=')) {
|
|
41
|
+
const rv = a === '--run' ? args[++i] : a.slice(6)
|
|
42
|
+
if (rv === undefined || rv === '') return { ...out, error: 'invalid --run: empty' }
|
|
43
|
+
out.runId = rv
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
if (a === '--format' || a.startsWith('--format=')) {
|
|
47
|
+
const fv = a === '--format' ? args[++i] : a.slice(9)
|
|
48
|
+
if (fv !== 'pretty' && fv !== 'json') {
|
|
49
|
+
// An omitted value reads as empty, not as the literal word "undefined".
|
|
50
|
+
return {
|
|
51
|
+
...out,
|
|
52
|
+
error: `invalid --format: ${fv ?? ''} (expected pretty | json)`,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
out.format = fv
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
const cd = parseCacheDirFlag(args, i)
|
|
59
|
+
if (cd !== null) {
|
|
60
|
+
if ('error' in cd) return { ...out, error: cd.error }
|
|
61
|
+
out.cacheDir = cd.cacheDir
|
|
62
|
+
i = cd.next
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
if (a.startsWith('-')) return { ...out, error: `unknown flag: ${a}${seeHelp('why')}` }
|
|
66
|
+
if (out.target !== undefined) return { ...out, error: `unexpected argument: ${a}` }
|
|
67
|
+
out.target = a
|
|
68
|
+
}
|
|
69
|
+
return out
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A bare query is a TASK name, so it is matched against the task half of
|
|
74
|
+
* every recorded id (the same rule `vx run` hints with) and the hint is
|
|
75
|
+
* the runnable `project#task`; an anchored query is matched whole.
|
|
76
|
+
*/
|
|
77
|
+
function suggest(query: string, ids: readonly string[]): string {
|
|
78
|
+
let hits: string[]
|
|
79
|
+
if (query.includes('#')) {
|
|
80
|
+
hits = nearMatches(query, ids)
|
|
81
|
+
} else {
|
|
82
|
+
const byTask = new Map<string, string[]>()
|
|
83
|
+
for (const id of ids) {
|
|
84
|
+
const task = splitTaskId(id)[1]
|
|
85
|
+
byTask.set(task, [...(byTask.get(task) ?? []), id])
|
|
86
|
+
}
|
|
87
|
+
hits = nearMatches(query, byTask.keys()).flatMap((t) => byTask.get(t) ?? [])
|
|
88
|
+
}
|
|
89
|
+
return hits.length > 0 ? ` — did you mean ${hits.slice(0, 3).join(', ')}?` : ''
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Resolve a positional target to a full `project#task` id against the runs
|
|
94
|
+
* table. A `pkg#task` form is used as-is; a bare task name matches every
|
|
95
|
+
* project that ran it — unique → resolved, several → error listing them.
|
|
96
|
+
*/
|
|
97
|
+
function resolveTarget(cache: Cache, target: string): string {
|
|
98
|
+
const db = cache.dbHandle()
|
|
99
|
+
if (target.includes('#')) {
|
|
100
|
+
// An exact id needs one row, and its newest is where a scan from the
|
|
101
|
+
// top of the table finds it first — the full DISTINCT walk below is for
|
|
102
|
+
// the error's suggestions and the bare-name form only.
|
|
103
|
+
const [project, task] = splitTaskId(target)
|
|
104
|
+
const hit = db
|
|
105
|
+
.query('SELECT 1 FROM runs WHERE project = ? AND task = ? ORDER BY id DESC LIMIT 1')
|
|
106
|
+
.get(project, task)
|
|
107
|
+
if (hit !== null) return target
|
|
108
|
+
}
|
|
109
|
+
const pairs = db
|
|
110
|
+
.query('SELECT DISTINCT project, task FROM runs ORDER BY project, task')
|
|
111
|
+
.all() as Array<{ project: string; task: string }>
|
|
112
|
+
const ids = pairs.map((p) => `${p.project}#${p.task}`)
|
|
113
|
+
if (target.includes('#')) {
|
|
114
|
+
throw new UserError(`vx why: no recorded runs for "${target}"${suggest(target, ids)}`)
|
|
115
|
+
}
|
|
116
|
+
const matches = ids.filter((id) => id.endsWith(`#${target}`))
|
|
117
|
+
if (matches.length === 1) return matches[0]!
|
|
118
|
+
if (matches.length > 1) {
|
|
119
|
+
throw new UserError(
|
|
120
|
+
`vx why: "${target}" ran in ${matches.length} projects — pick one:\n` +
|
|
121
|
+
matches.map((m) => ` ${m}`).join('\n'),
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
throw new UserError(`vx why: no recorded runs for task "${target}"${suggest(target, ids)}`)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The latest recorded run of a task (run_id may be NULL on very old rows). */
|
|
128
|
+
function latestRunId(cache: Cache, taskId: string): string | null {
|
|
129
|
+
const [project, task] = splitTaskId(taskId)
|
|
130
|
+
const row = cache
|
|
131
|
+
.dbHandle()
|
|
132
|
+
.query(
|
|
133
|
+
`SELECT run_id AS runId FROM runs WHERE project = ? AND task = ?
|
|
134
|
+
ORDER BY started_at DESC LIMIT 1`,
|
|
135
|
+
)
|
|
136
|
+
.get(project, task) as { runId: string | null } | undefined
|
|
137
|
+
return row?.runId ?? null
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const fmtWhen = (ms: number): string => new Date(ms).toISOString()
|
|
141
|
+
|
|
142
|
+
export async function whyCmd(args: readonly string[]): Promise<number> {
|
|
143
|
+
const parsed = parseWhyArgs(args)
|
|
144
|
+
if (parsed.error !== undefined) throw new UserError(`vx why: ${parsed.error}`)
|
|
145
|
+
if (parsed.target === undefined) {
|
|
146
|
+
throw new UserError('vx why: <task> required (e.g. vx why app#build, or vx why build)')
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const root = await findWorkspaceRoot(process.cwd())
|
|
150
|
+
const cache = new Cache(await cliCacheDir(root, parsed.cacheDir))
|
|
151
|
+
noteSchemaReset(cache, warnToStderr)
|
|
152
|
+
try {
|
|
153
|
+
const db = cache.dbHandle()
|
|
154
|
+
const taskId = resolveTarget(cache, parsed.target)
|
|
155
|
+
const runId = parsed.runId ?? latestRunId(cache, taskId)
|
|
156
|
+
|
|
157
|
+
if (runId === null) {
|
|
158
|
+
// Runs exist (resolveTarget passed) but predate run ids — fall back to
|
|
159
|
+
// the persisted entry metadata so the verb still says something useful.
|
|
160
|
+
const explanation = explainCacheKey(db, taskId)
|
|
161
|
+
if (parsed.format === 'json') {
|
|
162
|
+
process.stdout.write(`${JSON.stringify({ taskId, why: null, diff: null, explanation })}\n`)
|
|
163
|
+
} else {
|
|
164
|
+
process.stdout.write(
|
|
165
|
+
`${taskId}: recorded runs carry no run id — showing the latest cache entry instead\n` +
|
|
166
|
+
(explanation.latestEntry !== null
|
|
167
|
+
? ` hash ${explanation.latestEntry.hash} · $ ${explanation.latestEntry.command}\n`
|
|
168
|
+
: ' (no cache entry either)\n'),
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
return 0
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const why = whyDidThisRerun(db, runId, taskId)
|
|
175
|
+
if (!why.found) {
|
|
176
|
+
throw new UserError(`vx why: run ${runId} has no row for ${taskId}`)
|
|
177
|
+
}
|
|
178
|
+
const diff = cacheKeyDiff(db, runId, taskId)
|
|
179
|
+
|
|
180
|
+
if (parsed.format === 'json') {
|
|
181
|
+
process.stdout.write(`${JSON.stringify({ taskId, runId, why, diff })}\n`)
|
|
182
|
+
return 0
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const lines: string[] = []
|
|
186
|
+
const t = why.thisRun!
|
|
187
|
+
lines.push(`${taskId} — run ${runId}`)
|
|
188
|
+
lines.push(
|
|
189
|
+
` this run ${fmtWhen(t.startedAt)} · ${t.status}` +
|
|
190
|
+
`${t.cacheHit === true ? ' · cache hit' : t.cacheHit === false ? ' · executed' : ''} · key ${t.hash}`,
|
|
191
|
+
)
|
|
192
|
+
if (why.previousRun == null) {
|
|
193
|
+
lines.push(' previous (none — first recorded run of this task)')
|
|
194
|
+
} else {
|
|
195
|
+
const p = why.previousRun
|
|
196
|
+
lines.push(` previous ${fmtWhen(p.startedAt)} · ${p.status} · key ${p.hash}`)
|
|
197
|
+
lines.push(` verdict ${why.note}`)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (diff.entries.length > 0) {
|
|
201
|
+
lines.push('')
|
|
202
|
+
lines.push(
|
|
203
|
+
` what changed (${diff.entries.length} component${diff.entries.length === 1 ? '' : 's'}, ${diff.unchangedCount} unchanged):`,
|
|
204
|
+
)
|
|
205
|
+
const kindW = Math.max(...diff.entries.map((e) => e.kind.length), 4)
|
|
206
|
+
for (const e of diff.entries) {
|
|
207
|
+
const beforeAfter =
|
|
208
|
+
e.change === 'added'
|
|
209
|
+
? `+ ${e.after}`
|
|
210
|
+
: e.change === 'removed'
|
|
211
|
+
? `- ${e.before}`
|
|
212
|
+
: `${e.before} → ${e.after}`
|
|
213
|
+
lines.push(` ${e.change.padEnd(7)} ${e.kind.padEnd(kindW)} ${e.name} ${beforeAfter}`)
|
|
214
|
+
}
|
|
215
|
+
} else if (why.hashChanged === true) {
|
|
216
|
+
lines.push(` detail ${diff.note}`)
|
|
217
|
+
}
|
|
218
|
+
process.stdout.write(`${lines.join('\n')}\n`)
|
|
219
|
+
return 0
|
|
220
|
+
} finally {
|
|
221
|
+
cache.close()
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// The workspace as every verb must see it: the config with the plugin
|
|
2
|
+
// `config` stage applied, and the cache dir derived from THAT. The stage
|
|
3
|
+
// is documented to shape `cacheDir`; a verb that read the file raw opened
|
|
4
|
+
// a directory the run never used — `vx last` found no runs, `vx cache
|
|
5
|
+
// prune` pruned nothing, `vx watch` ignored the wrong path.
|
|
6
|
+
|
|
7
|
+
import path from 'node:path'
|
|
8
|
+
import type { WorkspaceConfig } from '../config.js'
|
|
9
|
+
import { UserError } from '../util/index.js'
|
|
10
|
+
import { Cache, noteSchemaReset } from '../cache/index.js'
|
|
11
|
+
import { loadProjects, loadWorkspacePlugins } from '../orchestrator/index.js'
|
|
12
|
+
import type { VxPlugin } from '../orchestrator/index.js'
|
|
13
|
+
import {
|
|
14
|
+
buildPackageGraph,
|
|
15
|
+
computeWorkspaceFingerprint,
|
|
16
|
+
FROZEN_WITHOUT_LOCK,
|
|
17
|
+
type ProjectEntry,
|
|
18
|
+
type ProjectMeta,
|
|
19
|
+
readLockfile,
|
|
20
|
+
resolveCacheDir,
|
|
21
|
+
} from '../workspace/index.js'
|
|
22
|
+
|
|
23
|
+
export interface CliWorkspace {
|
|
24
|
+
workspaceConfig: WorkspaceConfig | null
|
|
25
|
+
plugins: readonly VxPlugin[]
|
|
26
|
+
cacheDir: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const warnToStderr = (message: string): void => {
|
|
30
|
+
process.stderr.write(`${message}\n`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function loadCliWorkspace(workspaceRoot: string): Promise<CliWorkspace> {
|
|
34
|
+
const { workspaceConfig, plugins } = await loadWorkspacePlugins(workspaceRoot, warnToStderr)
|
|
35
|
+
return { workspaceConfig, plugins, cacheDir: resolveCacheDir(workspaceRoot, workspaceConfig) }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The run path's project-config load (`loadProjects`) for a verb that only
|
|
40
|
+
* reads: the plugin `project` stage applies, and the local cache opens only
|
|
41
|
+
* to serve cached evaluations — a pure config costs a stat, not an
|
|
42
|
+
* evaluation. `scope` is every project or a list of names; no closure, no
|
|
43
|
+
* lock (a verb reads live, as a default run does).
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* `--cache-dir <path>` / `--cache-dir=<path>` for a verb that reads the
|
|
47
|
+
* cache a run wrote — `why`, `last`, `info`, `cache prune`. Same rules as
|
|
48
|
+
* `vx run`'s: a value is required, and the space form refuses a value
|
|
49
|
+
* that looks like a flag (an unquoted empty shell variable would otherwise
|
|
50
|
+
* swallow the next flag as the path). Returns null when `args[i]` is not
|
|
51
|
+
* this flag; `next` is the index of the last argv slot consumed.
|
|
52
|
+
*/
|
|
53
|
+
export function parseCacheDirFlag(
|
|
54
|
+
args: readonly string[],
|
|
55
|
+
i: number,
|
|
56
|
+
): { cacheDir: string; next: number } | { error: string } | null {
|
|
57
|
+
const a = args[i]!
|
|
58
|
+
if (a !== '--cache-dir' && !a.startsWith('--cache-dir=')) return null
|
|
59
|
+
const v = a === '--cache-dir' ? args[i + 1] : a.slice('--cache-dir='.length)
|
|
60
|
+
if (v === undefined || v === '') return { error: '--cache-dir requires a path' }
|
|
61
|
+
if (a === '--cache-dir' && v.startsWith('-')) {
|
|
62
|
+
return { error: `--cache-dir requires a path, got flag: ${v}` }
|
|
63
|
+
}
|
|
64
|
+
return { cacheDir: v, next: a === '--cache-dir' ? i + 1 : i }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The cache directory a reading verb opens: `--cache-dir` resolved against
|
|
69
|
+
* cwd exactly as `vx run` resolves it (prepare.ts), else the workspace's —
|
|
70
|
+
* so `vx run --cache-dir X` and `vx last --cache-dir X` read one history.
|
|
71
|
+
*/
|
|
72
|
+
export async function cliCacheDir(
|
|
73
|
+
workspaceRoot: string,
|
|
74
|
+
override: string | undefined,
|
|
75
|
+
): Promise<string> {
|
|
76
|
+
if (override !== undefined) return path.resolve(process.cwd(), override)
|
|
77
|
+
return (await loadCliWorkspace(workspaceRoot)).cacheDir
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The run flags a verb's staged load must honour to see what the run sees. */
|
|
81
|
+
export interface CliLoadOptions {
|
|
82
|
+
/** `--cache-dir`: where the cached evaluations live. */
|
|
83
|
+
cacheDir?: string
|
|
84
|
+
/** `--frozen`: configs come from `vx-lock.json`, not evaluation. */
|
|
85
|
+
frozen?: boolean
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function loadCliProjects(
|
|
89
|
+
workspaceRoot: string,
|
|
90
|
+
metas: readonly ProjectMeta[],
|
|
91
|
+
scope: 'all' | readonly string[] = 'all',
|
|
92
|
+
opts: CliLoadOptions = {},
|
|
93
|
+
): Promise<Map<string, ProjectEntry>> {
|
|
94
|
+
const ws = await loadCliWorkspace(workspaceRoot)
|
|
95
|
+
const { plugins } = ws
|
|
96
|
+
// The cached evaluations live where the run's do: a verb given
|
|
97
|
+
// `--cache-dir` must not open (and create) the workspace's default one.
|
|
98
|
+
const cacheDir = opts.cacheDir ?? ws.cacheDir
|
|
99
|
+
// A frozen run reads its configs from the lock; what it SELECTS must be
|
|
100
|
+
// decided from the same configs, or an env-dependent `workspaceFiles`
|
|
101
|
+
// glob could select live what the run then treats otherwise.
|
|
102
|
+
const lock = opts.frozen === true ? await readLockfile(workspaceRoot) : null
|
|
103
|
+
if (opts.frozen === true && lock === null) throw new UserError(FROZEN_WITHOUT_LOCK)
|
|
104
|
+
const cache = new Cache(cacheDir)
|
|
105
|
+
noteSchemaReset(cache, warnToStderr)
|
|
106
|
+
try {
|
|
107
|
+
const loaded = await loadProjects({
|
|
108
|
+
workspaceRoot,
|
|
109
|
+
cacheDir,
|
|
110
|
+
plugins,
|
|
111
|
+
projectMetas: metas,
|
|
112
|
+
packageGraph: buildPackageGraph([...metas]),
|
|
113
|
+
seeds: scope,
|
|
114
|
+
closure: false,
|
|
115
|
+
lock,
|
|
116
|
+
evalCache: {
|
|
117
|
+
store: cache,
|
|
118
|
+
workspaceFingerprint: await computeWorkspaceFingerprint(workspaceRoot),
|
|
119
|
+
},
|
|
120
|
+
warn: warnToStderr,
|
|
121
|
+
})
|
|
122
|
+
return loaded.projects
|
|
123
|
+
} finally {
|
|
124
|
+
cache.close()
|
|
125
|
+
}
|
|
126
|
+
}
|