@vzn/vx 0.0.651 → 0.0.653
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 +3 -4
- package/launcher.sh +2 -1
- package/package.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/show.json +15 -20
- package/skills/vx/SKILL.md +3 -3
- package/src/bin.ts +37 -3
- package/src/cache/archive.ts +197 -32
- package/src/cache/cache.ts +882 -230
- package/src/cache/checkpointer.ts +102 -0
- package/src/cache/file-hashes.ts +2 -1
- package/src/cache/fold-memo.ts +24 -4
- package/src/cache/fs-clock.ts +26 -0
- package/src/cache/git-inputs.ts +188 -22
- package/src/cache/history-stream.ts +151 -0
- package/src/cache/history-writer.ts +76 -0
- package/src/cache/index.ts +12 -1
- package/src/cache/inputs.ts +616 -135
- package/src/cache/key-fold.ts +1 -1
- package/src/cache/layer.ts +29 -16
- package/src/cache/layered-cache.ts +28 -7
- package/src/cache/lockfile-memo.ts +2 -1
- package/src/cache/output-index.ts +106 -44
- package/src/cache/plugin-cache.ts +2 -2
- package/src/cache/policy.ts +6 -2
- package/src/cache/remote-lane.ts +3 -1
- package/src/cache/run-history.ts +94 -25
- package/src/cache/schema.ts +46 -10
- package/src/cli/cache.ts +149 -36
- package/src/cli/completions.ts +2 -2
- package/src/cli/core-alias.ts +9 -2
- package/src/cli/docs.ts +4 -2
- package/src/cli/help.ts +27 -4
- package/src/cli/index.ts +62 -7
- package/src/cli/info.ts +5 -4
- package/src/cli/init.ts +55 -20
- package/src/cli/last.ts +218 -43
- package/src/cli/lock.ts +187 -24
- package/src/cli/plan-format.ts +24 -21
- package/src/cli/plugin-commands.ts +6 -1
- package/src/cli/plugin-templates.ts +36 -45
- package/src/cli/run.ts +68 -34
- package/src/cli/select.ts +160 -64
- package/src/cli/show.ts +70 -28
- package/src/cli/task-verb.ts +12 -7
- package/src/cli/upgrade.ts +210 -76
- package/src/cli/watch-filter.ts +88 -15
- package/src/cli/watch-fs.ts +34 -47
- package/src/cli/watch-judge.ts +73 -9
- package/src/cli/watch-set.ts +10 -8
- package/src/cli/watch.ts +190 -33
- package/src/cli/why.ts +107 -33
- package/src/cli/workspace-config.ts +11 -1
- package/src/config.ts +56 -30
- package/src/exec/env.ts +3 -1
- package/src/exec/executor.ts +116 -16
- package/src/exec/index.ts +1 -0
- package/src/exec/kill-tree.ts +7 -7
- package/src/exec/runner.ts +270 -108
- package/src/exec/sandbox-binds.ts +109 -18
- package/src/exec/sandbox-paths.ts +3 -1
- package/src/exec/sandbox-runtime.ts +446 -64
- package/src/exec/sandbox-violations.ts +250 -46
- package/src/exec/spawn-off.ts +10 -5
- package/src/exec/task-proxy.ts +93 -0
- package/src/exec/warm-shell.ts +29 -7
- package/src/graph/definition.ts +16 -3
- package/src/graph/folds.ts +89 -16
- package/src/graph/index.ts +8 -0
- package/src/graph/priorities.ts +8 -5
- package/src/graph/scheduler.ts +176 -39
- package/src/graph/stand-ins.ts +36 -0
- package/src/graph/task-graph.ts +347 -160
- package/src/graph/task-status.ts +5 -1
- package/src/handoff.ts +45 -10
- package/src/history/failure-mode.ts +39 -6
- package/src/history/history.ts +103 -30
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +193 -73
- package/src/index.ts +6 -3
- package/src/orchestrator/admission.ts +14 -10
- package/src/orchestrator/affected-tasks.ts +35 -13
- package/src/orchestrator/command-windows.ts +8 -5
- package/src/orchestrator/deferred-outputs.ts +42 -13
- package/src/orchestrator/doctor.ts +21 -9
- package/src/orchestrator/download-policy.ts +8 -10
- package/src/orchestrator/excluded-keys.ts +15 -3
- package/src/orchestrator/execute-args.ts +9 -2
- package/src/orchestrator/execute-task.ts +612 -138
- package/src/orchestrator/graph-memo.ts +26 -4
- package/src/orchestrator/graph-stage.ts +27 -4
- package/src/orchestrator/held-servers.ts +36 -14
- package/src/orchestrator/hit-restore.ts +141 -93
- package/src/orchestrator/index.ts +4 -1
- package/src/orchestrator/keyed-projects.ts +53 -0
- package/src/orchestrator/local-floor.ts +21 -3
- package/src/orchestrator/local-shortcircuit.ts +2 -2
- package/src/orchestrator/lockfile-claim.ts +30 -4
- package/src/orchestrator/miss-save.ts +60 -61
- package/src/orchestrator/options.ts +13 -0
- package/src/orchestrator/output-host.ts +16 -4
- package/src/orchestrator/output-log.ts +1 -54
- package/src/orchestrator/persistent-task.ts +32 -16
- package/src/orchestrator/persistent.ts +152 -15
- package/src/orchestrator/placement.ts +82 -16
- package/src/orchestrator/plan.ts +39 -17
- package/src/orchestrator/plugin-host.ts +238 -110
- package/src/orchestrator/plugin.ts +59 -27
- package/src/orchestrator/prepare.ts +194 -38
- package/src/orchestrator/projects.ts +89 -10
- package/src/orchestrator/read-only.ts +115 -0
- package/src/orchestrator/remote-prefetch.ts +1 -1
- package/src/orchestrator/run-context.ts +6 -2
- package/src/orchestrator/run-failures.ts +101 -23
- package/src/orchestrator/run-id.ts +13 -2
- package/src/orchestrator/run-keys.ts +87 -28
- package/src/orchestrator/run-live.ts +16 -13
- package/src/orchestrator/run-lock.ts +27 -4
- package/src/orchestrator/run-records.ts +95 -74
- package/src/orchestrator/run-report.ts +3 -2
- package/src/orchestrator/run-stop.ts +17 -9
- package/src/orchestrator/run.ts +531 -122
- package/src/orchestrator/sandbox-fix.ts +40 -12
- package/src/orchestrator/sandbox-request.ts +314 -44
- package/src/orchestrator/save-lane.ts +24 -13
- package/src/orchestrator/shell-verdict.ts +29 -1
- package/src/orchestrator/signals.ts +103 -8
- package/src/orchestrator/stable-keys.ts +34 -27
- package/src/orchestrator/task-artifacts.ts +16 -3
- package/src/orchestrator/task-hash.ts +191 -34
- package/src/orchestrator/telemetry-host.ts +25 -6
- package/src/orchestrator/telemetry.ts +85 -13
- package/src/orchestrator/upstream.ts +3 -1
- package/src/report/events.ts +23 -6
- package/src/report/forecast.ts +103 -37
- package/src/report/framed-output.ts +179 -162
- package/src/report/index.ts +2 -1
- package/src/report/logger.ts +108 -31
- package/src/report/plain-output.ts +15 -9
- package/src/report/status-line.ts +28 -55
- package/src/report/summary.ts +61 -18
- package/src/settings.ts +50 -14
- package/src/ui/components.ts +36 -42
- package/src/ui/index.ts +3 -1
- package/src/ui/live-region.ts +67 -8
- package/src/ui/terminal.ts +12 -0
- package/src/ui/theme.ts +16 -1
- package/src/util/config-frame.ts +142 -6
- package/src/util/edit-distance.ts +17 -1
- package/src/util/errors.ts +23 -7
- package/src/util/foreign-cache-dir.ts +60 -0
- package/src/util/git-ignored.ts +59 -5
- package/src/util/index.ts +15 -1
- package/src/util/paths.ts +51 -7
- package/src/util/plain-text.ts +19 -0
- package/src/util/secret-mask.ts +57 -1
- package/src/util/settle.ts +18 -1
- package/src/util/size.ts +12 -2
- package/src/util/tail.ts +4 -1
- package/src/util/write-file.ts +86 -9
- package/src/workspace/affected.ts +225 -27
- package/src/workspace/bracket-literal.ts +2 -2
- package/src/workspace/config-bytecode.ts +23 -4
- package/src/workspace/config-eval.ts +90 -16
- package/src/workspace/config-imports.ts +96 -41
- package/src/workspace/config-schema.ts +192 -86
- package/src/workspace/dependencies-lock.ts +7 -14
- package/src/workspace/filter.ts +18 -2
- package/src/workspace/index.ts +3 -1
- package/src/workspace/json-data.ts +34 -8
- package/src/workspace/lockfile.ts +86 -37
- package/src/workspace/migration.ts +58 -31
- package/src/workspace/module-state.ts +62 -0
- package/src/workspace/nested-dirs.ts +8 -0
- package/src/workspace/project-loader.ts +276 -32
- package/src/workspace/repo-id.ts +29 -6
- package/src/workspace/workspace.ts +190 -30
|
@@ -31,7 +31,9 @@ import {
|
|
|
31
31
|
} from '../util/index.js'
|
|
32
32
|
import * as ui from '../ui/index.js'
|
|
33
33
|
import type { ProjectMeta } from '../workspace/index.js'
|
|
34
|
-
import type { EventBus, Logger, RunStartInfo } from '../report/index.js'
|
|
34
|
+
import type { EventBus, Logger, RunEvent, RunStartInfo } from '../report/index.js'
|
|
35
|
+
import { readOnlyEvents } from './read-only.js'
|
|
36
|
+
import { unlessStopped } from './signals.js'
|
|
35
37
|
import type { CiContext } from './run-context.js'
|
|
36
38
|
import type { CacheFacts, TaskArtifacts } from './task-artifacts.js'
|
|
37
39
|
import type { TelemetryContext, TelemetrySink } from './telemetry.js'
|
|
@@ -121,7 +123,7 @@ export interface VxPlugin {
|
|
|
121
123
|
* feature flag, anything the declared inputs cannot see. Runs once per
|
|
122
124
|
* task per run, before any key is derived. Return undefined to add
|
|
123
125
|
* nothing. Values must be deterministic for the same inputs or the key
|
|
124
|
-
* never hits.
|
|
126
|
+
* never hits. The task is read-only (a write throws): `graph` edits tasks.
|
|
125
127
|
*/
|
|
126
128
|
key?(
|
|
127
129
|
task: TaskNode,
|
|
@@ -153,6 +155,7 @@ export interface VxPlugin {
|
|
|
153
155
|
* The run waits for this answer before its first task, so it should
|
|
154
156
|
* come from what is at hand (local history); better weights that need
|
|
155
157
|
* a fetch go to `ctx.refine` when they arrive, without holding the run.
|
|
158
|
+
* The nodes are read-only (a write throws): their keys are taken.
|
|
156
159
|
*/
|
|
157
160
|
schedule?(
|
|
158
161
|
nodes: ReadonlyMap<string, TaskNode>,
|
|
@@ -170,7 +173,8 @@ export interface VxPlugin {
|
|
|
170
173
|
* hold no local resources: never asked, never in `ctx.running`. When
|
|
171
174
|
* several plugins answer, all must admit. Core keeps no notion of what a task needs: a plugin that
|
|
172
175
|
* packs memory or CPU learns or declares the numbers itself
|
|
173
|
-
* (`@vzn/vx-history` packs what past executions used).
|
|
176
|
+
* (`@vzn/vx-history` packs what past executions used). The task and
|
|
177
|
+
* `ctx.running` are read-only (a write throws, and admits from then on).
|
|
174
178
|
*/
|
|
175
179
|
admit?(task: TaskNode, ctx: AdmitContext): boolean
|
|
176
180
|
|
|
@@ -234,9 +238,10 @@ export interface VxPlugin {
|
|
|
234
238
|
* renderer in place. The first plugin, in declaration order, that
|
|
235
239
|
* returns one renders the run; the rest are not asked. Not consulted
|
|
236
240
|
* when an embedder passed its own `log`. A logger that throws or rejects
|
|
237
|
-
* hands the rest of the run back to core's renderer with one warning
|
|
238
|
-
*
|
|
239
|
-
* over with one.
|
|
241
|
+
* hands the rest of the run back to core's renderer with one warning, and
|
|
242
|
+
* core draws the event it was handed; a hook that does not answer within
|
|
243
|
+
* the teardown bound (3 s) is passed over with one. The nodes and
|
|
244
|
+
* outcomes it is handed are read-only: a write throws.
|
|
240
245
|
*/
|
|
241
246
|
output?(ctx: OutputContext): Logger | undefined | Promise<Logger | undefined>
|
|
242
247
|
|
|
@@ -268,7 +273,8 @@ export interface VxPlugin {
|
|
|
268
273
|
* task that saved no entry, and a failed task's log and the files its
|
|
269
274
|
* outputs left when `cache.upload.failures` covers the run. Read
|
|
270
275
|
* them with `ctx.cache.getRun(name)`. Core keeps no manifest of its own; a
|
|
271
|
-
* plugin that wants one writes it from here (`ctx.cache.put`).
|
|
276
|
+
* plugin that wants one writes it from here (`ctx.cache.put`). Frozen:
|
|
277
|
+
* every teardown reads the same report.
|
|
272
278
|
*/
|
|
273
279
|
export interface RunReport {
|
|
274
280
|
/** The `run_id` the run's history rows and telemetry carry. */
|
|
@@ -317,6 +323,8 @@ export interface PluginSetupContext extends BaseContext, PluginSecrets {
|
|
|
317
323
|
/**
|
|
318
324
|
* Subscribe a lifecycle handler; it leaves with the run. A throw or a
|
|
319
325
|
* rejection warns once and switches the plugin's handlers off for the run.
|
|
326
|
+
* What a handler is handed is read-only: a write throws, so a handler
|
|
327
|
+
* can never turn a failed task into a pass.
|
|
320
328
|
*/
|
|
321
329
|
on<K extends PluginHookName>(hook: K, handler: PluginHookHandlers[K]): void
|
|
322
330
|
}
|
|
@@ -397,7 +405,11 @@ export interface ConfigContext extends WorkspaceHookContext {
|
|
|
397
405
|
export interface DiscoverContext extends WorkspaceHookContext {
|
|
398
406
|
/** Where vx's cache lives: a plugin may keep what it read to answer here. */
|
|
399
407
|
readonly cacheDir: string
|
|
400
|
-
/**
|
|
408
|
+
/**
|
|
409
|
+
* The projects found so far: the lock's, then earlier plugins'. Read-only,
|
|
410
|
+
* entries too (a write throws): a project is added by returning it. The
|
|
411
|
+
* same object `project` hooks get as `ctx.projects`.
|
|
412
|
+
*/
|
|
401
413
|
readonly projects: readonly ProjectMeta[]
|
|
402
414
|
/**
|
|
403
415
|
* What `git status` lists in the workspace, workspace-relative: modified,
|
|
@@ -436,10 +448,11 @@ export interface ProjectHookContext extends BaseContext {
|
|
|
436
448
|
* not — as the `discover` stage left them. A plugin whose mapping needs
|
|
437
449
|
* the whole workspace (a `dependsOn` is only valid against every
|
|
438
450
|
* package's scripts at once) reads it here instead of walking the workspace a
|
|
439
|
-
* second time. The same array is handed to every visit of a run and a
|
|
451
|
+
* second time. The same array (`discover`'s) is handed to every visit of a run and a
|
|
440
452
|
* new one to the next run — a plugin instance outlives a run under `vx
|
|
441
453
|
* watch`, so a per-run memo keys on this array's identity, never on the
|
|
442
|
-
* process.
|
|
454
|
+
* process. Frozen, entries too: a hook edits its own project's config,
|
|
455
|
+
* never which projects there are or where they live.
|
|
443
456
|
*/
|
|
444
457
|
readonly projects: readonly ProjectMeta[]
|
|
445
458
|
/**
|
|
@@ -603,6 +616,12 @@ export interface ScheduleHookContext extends BaseContext {
|
|
|
603
616
|
* in a plan.
|
|
604
617
|
*/
|
|
605
618
|
onRefine(listener: () => void): void
|
|
619
|
+
/**
|
|
620
|
+
* Name tasks a shared record knows as flaky (a history server's): one
|
|
621
|
+
* that fails gets the one retry a known flake gets, as the local
|
|
622
|
+
* record's do. Ids not in the run are ignored; ignored in a plan.
|
|
623
|
+
*/
|
|
624
|
+
flaky(taskIds: Iterable<string>): void
|
|
606
625
|
}
|
|
607
626
|
|
|
608
627
|
/** A `cache` hook's context: the workspace and the run's scope, never the local store or the policy, which core owns. */
|
|
@@ -650,7 +669,8 @@ export interface ExecutorContext extends BaseContext {
|
|
|
650
669
|
export interface PluginContext extends PluginSetupContext {
|
|
651
670
|
/**
|
|
652
671
|
* The run event bus. A plugin can subscribe directly if its needs exceed
|
|
653
|
-
* the hooks; the subscription ends with the run, as a hook's does.
|
|
672
|
+
* the hooks; the subscription ends with the run, as a hook's does. The
|
|
673
|
+
* events a subscriber gets are read-only, as a hook's are.
|
|
654
674
|
*/
|
|
655
675
|
readonly bus: EventBus
|
|
656
676
|
}
|
|
@@ -664,16 +684,16 @@ export type PluginHookName =
|
|
|
664
684
|
| 'onRunStatus'
|
|
665
685
|
| 'onRunEnd'
|
|
666
686
|
|
|
667
|
-
// A Record so the compiler holds it to `PluginHookName`
|
|
668
|
-
// missing here, or one the type lacks, does not compile.
|
|
669
|
-
const
|
|
670
|
-
onRunStart:
|
|
671
|
-
onTaskStart:
|
|
672
|
-
onTaskStdout:
|
|
673
|
-
onTaskStderr:
|
|
674
|
-
onTaskComplete:
|
|
675
|
-
onRunStatus:
|
|
676
|
-
onRunEnd:
|
|
687
|
+
// Each hook's event. A Record so the compiler holds it to `PluginHookName`
|
|
688
|
+
// both ways: a name missing here, or one the type lacks, does not compile.
|
|
689
|
+
const HOOK_EVENTS: Record<PluginHookName, RunEvent['kind']> = {
|
|
690
|
+
onRunStart: 'run:start',
|
|
691
|
+
onTaskStart: 'task:start',
|
|
692
|
+
onTaskStdout: 'task:stdout',
|
|
693
|
+
onTaskStderr: 'task:stderr',
|
|
694
|
+
onTaskComplete: 'task:complete',
|
|
695
|
+
onRunStatus: 'run:status',
|
|
696
|
+
onRunEnd: 'run:end',
|
|
677
697
|
}
|
|
678
698
|
|
|
679
699
|
export interface PluginHookHandlers {
|
|
@@ -892,6 +912,8 @@ export interface InstallPluginsArgs {
|
|
|
892
912
|
secrets?: SecretBroker
|
|
893
913
|
/** Each setup as the step `<plugin>:setup`; default: untimed. */
|
|
894
914
|
step?: <T>(id: string, work: () => Promise<T>) => Promise<T>
|
|
915
|
+
/** The run's held signals: a stop ends a setup's wait, which then throws. */
|
|
916
|
+
signal?: AbortSignal | undefined
|
|
895
917
|
}
|
|
896
918
|
|
|
897
919
|
/**
|
|
@@ -926,6 +948,9 @@ export async function installPlugins(args: InstallPluginsArgs): Promise<() => vo
|
|
|
926
948
|
const disposers: Array<() => void> = []
|
|
927
949
|
// By object, not name: one package can export two plugins.
|
|
928
950
|
const disabled = new Set<Plugin>()
|
|
951
|
+
// Made at the first event a plugin hears: a run no handler hears pays nothing.
|
|
952
|
+
let readOnly: ((event: RunEvent) => RunEvent) | undefined
|
|
953
|
+
const viewOf = (event: RunEvent): RunEvent => (readOnly ??= readOnlyEvents())(event)
|
|
929
954
|
|
|
930
955
|
for (const plugin of plugins) {
|
|
931
956
|
if (typeof plugin.name !== 'string' || plugin.name.length === 0) {
|
|
@@ -970,7 +995,7 @@ export async function installPlugins(args: InstallPluginsArgs): Promise<() => vo
|
|
|
970
995
|
if (disabled.has(plugin)) return
|
|
971
996
|
let ret: unknown
|
|
972
997
|
try {
|
|
973
|
-
ret = subscriber(event)
|
|
998
|
+
ret = subscriber(viewOf(event))
|
|
974
999
|
} catch (err) {
|
|
975
1000
|
fail('a bus subscriber', err)
|
|
976
1001
|
return
|
|
@@ -984,9 +1009,9 @@ export async function installPlugins(args: InstallPluginsArgs): Promise<() => vo
|
|
|
984
1009
|
on(hook, handler) {
|
|
985
1010
|
// A misspelt name ('taskComplete') subscribed a handler that never
|
|
986
1011
|
// ran, and no word of it; a config file has no type-checker (H-16).
|
|
987
|
-
if (!Object.hasOwn(
|
|
1012
|
+
if (!Object.hasOwn(HOOK_EVENTS, hook)) {
|
|
988
1013
|
throw new Error(
|
|
989
|
-
`ctx.on: unknown hook '${String(hook)}' (one of ${Object.keys(
|
|
1014
|
+
`ctx.on: unknown hook '${String(hook)}' (one of ${Object.keys(HOOK_EVENTS).join(', ')})`,
|
|
990
1015
|
)
|
|
991
1016
|
}
|
|
992
1017
|
// Taken, it threw at the first event and switched the whole plugin off.
|
|
@@ -996,8 +1021,9 @@ export async function installPlugins(args: InstallPluginsArgs): Promise<() => vo
|
|
|
996
1021
|
)
|
|
997
1022
|
}
|
|
998
1023
|
const onFail = (err: unknown): void => fail(hook, err)
|
|
999
|
-
const dispose = bus.subscribe((
|
|
1024
|
+
const dispose = bus.subscribe((live) => {
|
|
1000
1025
|
if (disabled.has(plugin)) return
|
|
1026
|
+
const event = HOOK_EVENTS[hook] === live.kind ? viewOf(live) : live
|
|
1001
1027
|
// Not awaited: the bus is synchronous and plugin work runs off the
|
|
1002
1028
|
// critical path. A handler's rejection is caught like its throw.
|
|
1003
1029
|
let ret: unknown
|
|
@@ -1042,12 +1068,18 @@ export async function installPlugins(args: InstallPluginsArgs): Promise<() => vo
|
|
|
1042
1068
|
}
|
|
1043
1069
|
|
|
1044
1070
|
try {
|
|
1045
|
-
|
|
1046
|
-
|
|
1071
|
+
const setup = async (): Promise<void> => plugin.setup?.(ctx)
|
|
1072
|
+
await unlessStopped(
|
|
1073
|
+
args.step === undefined ? setup() : args.step(`${plugin.name}:setup`, setup),
|
|
1074
|
+
args.signal,
|
|
1075
|
+
)
|
|
1047
1076
|
} catch (err) {
|
|
1048
1077
|
// Earlier plugins' subscriptions too: the caller never gets the
|
|
1049
1078
|
// disposer, and a bus that outlives the run would keep them.
|
|
1050
1079
|
for (const d of disposers) d()
|
|
1080
|
+
// A stop is no refusal of this plugin's: its setup may hold what
|
|
1081
|
+
// its teardown releases, so it is torn down with every other.
|
|
1082
|
+
if (args.signal?.aborted === true) throw err
|
|
1051
1083
|
throw new PluginSetupError(
|
|
1052
1084
|
plugin,
|
|
1053
1085
|
// Masked at the source, as plugin-host.ts's `safe()` does (L-11).
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { existsSync, realpathSync } from 'node:fs'
|
|
11
11
|
import path from 'node:path'
|
|
12
|
-
import type { WorkspaceConfig } from '../config.js'
|
|
12
|
+
import type { InputStability, WorkspaceConfig } from '../config.js'
|
|
13
13
|
import {
|
|
14
14
|
beginRun,
|
|
15
15
|
mark,
|
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
import {
|
|
48
48
|
buildPackageGraph,
|
|
49
49
|
computeNestedProjectDirs,
|
|
50
|
+
findProjectConfigs,
|
|
50
51
|
findWorkspaceRoot,
|
|
51
52
|
configuredBaseBranch,
|
|
52
53
|
type LoadReads,
|
|
@@ -83,24 +84,26 @@ import {
|
|
|
83
84
|
discoverProjects,
|
|
84
85
|
gitOfDiscovery,
|
|
85
86
|
graphOfDiscovery,
|
|
87
|
+
ignoredConfigRefusal,
|
|
86
88
|
loadProjects,
|
|
87
89
|
loadWorkspacePlugins,
|
|
88
90
|
lockOfDiscovery,
|
|
89
91
|
lockReader,
|
|
90
92
|
type LoadedProjects,
|
|
93
|
+
nestedWorkspacesOf,
|
|
91
94
|
} from './projects.js'
|
|
92
95
|
import { keptByAffected } from './affected-tasks.js'
|
|
93
96
|
import { applyGraphStage } from './graph-stage.js'
|
|
94
97
|
import { keyExcludedDependencies } from './excluded-keys.js'
|
|
95
98
|
import { FingerprintWatch } from './fingerprint-watch.js'
|
|
96
99
|
import type { VxPlugin } from './plugin.js'
|
|
97
|
-
import { memoGraph, type GraphMemo } from './graph-memo.js'
|
|
100
|
+
import { graphCounts, memoGraph, type GraphMemo } from './graph-memo.js'
|
|
98
101
|
import { askIgnoredOutputs, gatedOutputs } from './stable-keys.js'
|
|
99
102
|
import { cacheRefs, currentBranch, resolveCacheScope } from './run-context.js'
|
|
100
103
|
import { createHashCache, type HashCache } from './task-hash.js'
|
|
101
104
|
import { type Logger, warnOf } from '../report/index.js'
|
|
102
105
|
import type { RunOptions } from './options.js'
|
|
103
|
-
import { resolveSetting, runRules } from '../settings.js'
|
|
106
|
+
import { resolveSetting, runRules, settingEnv } from '../settings.js'
|
|
104
107
|
|
|
105
108
|
export interface PreparedRun {
|
|
106
109
|
workspaceRoot: string
|
|
@@ -207,6 +210,12 @@ export interface PreparedRun {
|
|
|
207
210
|
* `computeTaskHash` call so the same config object isn't re-hashed.
|
|
208
211
|
*/
|
|
209
212
|
hashCache: HashCache
|
|
213
|
+
/**
|
|
214
|
+
* Each task's count of the tasks downstream of it (`graphCounts`), when
|
|
215
|
+
* nothing after the graph build can edit `nodes`: counted while git's
|
|
216
|
+
* blob-size check runs, so `run` reads it instead of counting.
|
|
217
|
+
*/
|
|
218
|
+
dependents?: ReadonlyMap<string, number>
|
|
210
219
|
/**
|
|
211
220
|
* Reason `nodes` is empty. `null` when the prepared run is ready to
|
|
212
221
|
* execute. Either:
|
|
@@ -237,6 +246,26 @@ export interface PreparedRun {
|
|
|
237
246
|
* empty plan).
|
|
238
247
|
*/
|
|
239
248
|
export async function prepareRun(options: RunOptions, log: Logger): Promise<PreparedRun> {
|
|
249
|
+
return prepareRunLocked(options, log)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* `prepareRun`, with `beforeOpen` run once the workspace root is known and
|
|
254
|
+
* before the cache opens: a run takes its lock there (run.ts), so it never
|
|
255
|
+
* holds the store while it waits for a `vx cache clean` that holds the lock,
|
|
256
|
+
* and holds a cache directory with no store (`sharedCacheDir`) shared.
|
|
257
|
+
* `countDependents`: a run's, which schedules by them (`PreparedRun.dependents`);
|
|
258
|
+
* a plan never reads them. `planning`: a plan's (`--dry`). With no remote
|
|
259
|
+
* to probe it opens the cache to read only: no directory, index,
|
|
260
|
+
* `.gitignore` or row is made.
|
|
261
|
+
*/
|
|
262
|
+
export async function prepareRunLocked(
|
|
263
|
+
options: RunOptions,
|
|
264
|
+
log: Logger,
|
|
265
|
+
beforeOpen?: (workspaceRoot: string, sharedCacheDir: string | undefined) => Promise<void>,
|
|
266
|
+
countDependents = false,
|
|
267
|
+
planning = false,
|
|
268
|
+
): Promise<PreparedRun> {
|
|
240
269
|
beginRun()
|
|
241
270
|
mark('startup')
|
|
242
271
|
// A claimed root file is read once. A watch cycle is a new run and reads
|
|
@@ -301,8 +330,10 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
301
330
|
? path.resolve(options.cwd, options.cacheDir)
|
|
302
331
|
: resolveCacheDir(workspaceRoot, workspaceConfig)
|
|
303
332
|
// Before any racy window is judged: a file system behind this process's
|
|
304
|
-
// clock dated a mid-command edit before the command (fs-clock.ts).
|
|
305
|
-
|
|
333
|
+
// clock dated a mid-command edit before the command (fs-clock.ts). A plan
|
|
334
|
+
// keys as the run does but may make no cache dir: it measures in `.vx`,
|
|
335
|
+
// on the workspace's file system, which the workspace file makes exist.
|
|
336
|
+
measureFsClockLag(planning ? path.join(workspaceRoot, '.vx') : cacheDir)
|
|
306
337
|
// Asked now, read when the cache opens: a repository with no remote is
|
|
307
338
|
// named by its root commit, a git spawn the discovery below overlaps.
|
|
308
339
|
const storeRoot = options.cacheDir ? null : resolveStoreRoot(workspaceRoot, workspaceConfig)
|
|
@@ -363,20 +394,51 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
363
394
|
options.remoteReadOnlyByDefault === true &&
|
|
364
395
|
!(options.cache === undefined && namesRemote(workspaceConfig?.cache?.policy))
|
|
365
396
|
const wanted: CachePolicy = readOnly ? { ...asked, remoteWrite: false } : asked
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
397
|
+
const resolvedStore = await storeRoot
|
|
398
|
+
// A cache directory with no store may be another workspace's too: the
|
|
399
|
+
// run holds it shared (`holdCacheDir`).
|
|
400
|
+
await beforeOpen?.(workspaceRoot, resolvedStore === null ? cacheDir : undefined)
|
|
401
|
+
// A plan with no remote to probe writes nothing: it reads the index as the
|
|
402
|
+
// run would, one of another schema as the empty one the run's reset
|
|
403
|
+
// leaves (`'preview'`), and upserts no file-hash memo. One with a remote
|
|
404
|
+
// keeps the hits its probe finds, for the `vx run` that follows (an
|
|
405
|
+
// index made for them if need be): that run GETs them before its own
|
|
406
|
+
// probe answers (2026-10-23).
|
|
407
|
+
const planReadOnly =
|
|
408
|
+
planning &&
|
|
409
|
+
(!wanted.remoteRead ||
|
|
410
|
+
(options.remoteCache === undefined && !plugins.some((p) => p.cache !== undefined)))
|
|
411
|
+
const openCache = (): Cache =>
|
|
412
|
+
new Cache(
|
|
413
|
+
cacheDir,
|
|
414
|
+
{ read: wanted.localRead, write: wanted.localWrite && !planReadOnly },
|
|
415
|
+
workspaceRoot,
|
|
416
|
+
options.artifactCeiling,
|
|
417
|
+
planReadOnly ? 'preview' : 'open',
|
|
418
|
+
resolvedStore,
|
|
419
|
+
{
|
|
420
|
+
embeddings: (resolveSetting('cache.store.embeddings', workspaceConfig).value ??
|
|
421
|
+
DEFAULT_STORE.embeddings) as number,
|
|
422
|
+
decompress: (resolveSetting('cache.store.decompress', workspaceConfig).value ??
|
|
423
|
+
DEFAULT_STORE.decompress) as boolean,
|
|
424
|
+
},
|
|
425
|
+
)
|
|
426
|
+
// A `vx cache clean` in another checkout holds the shared store alone
|
|
427
|
+
// until it has deleted it (`attachStore`), as this run's hold keeps a
|
|
428
|
+
// clean waiting: the run waits its turn, then opens the store there now.
|
|
429
|
+
let localCache: Cache
|
|
430
|
+
for (let said = false; ; await Bun.sleep(50)) {
|
|
431
|
+
try {
|
|
432
|
+
localCache = openCache()
|
|
433
|
+
break
|
|
434
|
+
} catch (err) {
|
|
435
|
+
if (resolvedStore == null || (err as { code?: unknown }).code !== 'SQLITE_BUSY') throw err
|
|
436
|
+
}
|
|
437
|
+
if (!said) {
|
|
438
|
+
said = true
|
|
439
|
+
warnOf(log)(`[vx] waiting for a vx cache clean of ${resolvedStore} to finish…`)
|
|
440
|
+
}
|
|
441
|
+
}
|
|
380
442
|
const unwritable = localCache.readOnlyNotice()
|
|
381
443
|
if (unwritable !== null) warnOf(log)(`[vx] ${unwritable}`)
|
|
382
444
|
const policy: CachePolicy = unwritable === null ? wanted : { ...wanted, localWrite: false }
|
|
@@ -456,6 +518,9 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
456
518
|
// a parent's glob that folded (or cleaned) them crossed a boundary the
|
|
457
519
|
// selection drew (X-57).
|
|
458
520
|
const nestedDirsByProject = computeNestedProjectDirs(projectMetas)
|
|
521
|
+
const nestedWorkspaces = nestedWorkspacesOf(projectMetas)
|
|
522
|
+
if (nestedWorkspaces.length > 0)
|
|
523
|
+
outputWalls.set(nestedDirsByProject, computeNestedProjectDirs(projectMetas, nestedWorkspaces))
|
|
459
524
|
|
|
460
525
|
const candidateProjects = options.projects
|
|
461
526
|
? options.projects.filter((p) => projects.has(p))
|
|
@@ -471,7 +536,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
471
536
|
// nothing, exit 0.
|
|
472
537
|
if (options.selectedByDiff === true && candidateProjects.length === 0)
|
|
473
538
|
unresolvedTasks = [...new Set([...tasks.filter((t) => !t.includes('#')), ...unresolvedTasks])]
|
|
474
|
-
refuseUnlockedConfig(tasks, projects, projectMetas, workspaceRoot)
|
|
539
|
+
await refuseUnlockedConfig(tasks, projects, projectMetas, workspaceRoot)
|
|
475
540
|
// A scope that loaded no candidate judged no bare name: each is unresolved.
|
|
476
541
|
if (options.projects !== undefined && options.selectedByDiff !== true)
|
|
477
542
|
refuseUnlockedScope(
|
|
@@ -514,11 +579,15 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
514
579
|
// short name for `@scope/pkg`) was measured against nothing and got no
|
|
515
580
|
// "did you mean". Only a failing run pays for the rest of the workspace.
|
|
516
581
|
let hintProjects: ReadonlyMap<string, ProjectEntry> = projects
|
|
582
|
+
// So does a bare name where no project in scope declares a task: "no
|
|
583
|
+
// project declares a task yet" must be true of the workspace, not of the
|
|
584
|
+
// root project a run from the root selects.
|
|
517
585
|
if (
|
|
518
586
|
unresolvedTasks.some((t) => {
|
|
519
587
|
const at = t.indexOf('#')
|
|
520
588
|
return at > 0 && !projects.has(t.slice(0, at))
|
|
521
|
-
})
|
|
589
|
+
}) ||
|
|
590
|
+
(unresolvedTasks.some((t) => !t.includes('#')) && !declaresAny(projects))
|
|
522
591
|
) {
|
|
523
592
|
try {
|
|
524
593
|
hintProjects = (
|
|
@@ -587,7 +656,14 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
587
656
|
// cycle under `vx watch` (item 1029). run() owns both only once this
|
|
588
657
|
// returns.
|
|
589
658
|
try {
|
|
590
|
-
const hashCache = createHashCache(
|
|
659
|
+
const hashCache = createHashCache(
|
|
660
|
+
localCache.foldMemo(),
|
|
661
|
+
resolveSetting('outputs.clean', workspaceConfig).value as boolean | undefined,
|
|
662
|
+
{
|
|
663
|
+
run: settingEnv('inputs.stability.strategy') as InputStability['strategy'],
|
|
664
|
+
workspace: workspaceConfig?.inputs?.stability?.strategy,
|
|
665
|
+
},
|
|
666
|
+
)
|
|
591
667
|
const fingerprintWatch = new FingerprintWatch(
|
|
592
668
|
workspaceRoot,
|
|
593
669
|
claimed,
|
|
@@ -699,11 +775,12 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
699
775
|
options.affected,
|
|
700
776
|
projects,
|
|
701
777
|
packageGraph,
|
|
778
|
+
workspaceRoot,
|
|
702
779
|
{
|
|
703
780
|
named: new Set(tasks.filter((t) => t.includes('#'))),
|
|
704
781
|
outright: new Set(options.selectedOutright),
|
|
705
782
|
},
|
|
706
|
-
options.affectedReasons && {
|
|
783
|
+
options.affectedReasons && { reasons: options.affectedReasons },
|
|
707
784
|
)
|
|
708
785
|
if (kept.length === 0) return emptyRun('none-affected')
|
|
709
786
|
if (before !== null) {
|
|
@@ -790,7 +867,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
790
867
|
const settleIgnored = askIgnoredOutputs(gatedOutputs(nodes, workspaceRoot), workspaceRoot)
|
|
791
868
|
const enumeration = await enumerating
|
|
792
869
|
localCache.holdBlobSizes()
|
|
793
|
-
|
|
870
|
+
const applied = applyGitEnumeration(
|
|
794
871
|
enumeration,
|
|
795
872
|
workspaceRoot,
|
|
796
873
|
projectDirs,
|
|
@@ -798,6 +875,19 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
798
875
|
usesWorkspaceInputs,
|
|
799
876
|
localCache,
|
|
800
877
|
)
|
|
878
|
+
// The dependents count while the blob-size check's `cat-file` runs: the
|
|
879
|
+
// thread waited ~80 ms on it in a cold 8,002-task run, and counting
|
|
880
|
+
// after it, before the first task, took 40 ms (2026-11-07). Only when
|
|
881
|
+
// nothing below edits the nodes: a graph or key hook, or
|
|
882
|
+
// `--exclude-dependencies`.
|
|
883
|
+
const excluding =
|
|
884
|
+
options.excludeDependencies !== undefined &&
|
|
885
|
+
(options.excludeDependencies === 'all' || options.excludeDependencies.length > 0)
|
|
886
|
+
const dependents =
|
|
887
|
+
!countDependents || (hasGraphHook && !hooksFirst) || hasHook(plugins, 'key') || excluding
|
|
888
|
+
? undefined
|
|
889
|
+
: graphCounts(nodes, hashCache.folds, hashCache.graph)
|
|
890
|
+
await applied
|
|
801
891
|
await settleIgnored(gitFilesCache)
|
|
802
892
|
mark('git enumeration')
|
|
803
893
|
if (hasGraphHook && !hooksFirst) await graphStage()
|
|
@@ -909,6 +999,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
909
999
|
nestedDirsByProject,
|
|
910
1000
|
gitFilesCache,
|
|
911
1001
|
hashCache,
|
|
1002
|
+
...(dependents !== undefined ? { dependents } : {}),
|
|
912
1003
|
workspaceProjectCount: projectMetas.length,
|
|
913
1004
|
empty: nodes.size === 0 ? 'empty-graph' : null,
|
|
914
1005
|
}
|
|
@@ -938,6 +1029,13 @@ async function declaredNowhere(
|
|
|
938
1029
|
return undeclaredIn(unresolved, all.projects)
|
|
939
1030
|
}
|
|
940
1031
|
|
|
1032
|
+
/** Whether any project in `projects` declares a task: its default `build` is none. */
|
|
1033
|
+
function declaresAny(projects: LoadedProjects['projects']): boolean {
|
|
1034
|
+
for (const p of projects.values())
|
|
1035
|
+
for (const task of Object.values(p.config.tasks ?? {})) if (!isDefaultBuild(task)) return true
|
|
1036
|
+
return false
|
|
1037
|
+
}
|
|
1038
|
+
|
|
941
1039
|
/**
|
|
942
1040
|
* The bare names in `unresolved` no project in `projects` declares; `a#b`
|
|
943
1041
|
* forms pass through. Every project has a `build`, its default one if
|
|
@@ -1008,6 +1106,23 @@ function pruneToClosure(nodes: Map<string, TaskNode>, roots: ReadonlySet<string>
|
|
|
1008
1106
|
export const namesRemote = (spec: string | undefined): boolean =>
|
|
1009
1107
|
spec !== undefined && /remote:[rw]/.test(spec)
|
|
1010
1108
|
|
|
1109
|
+
/** Each run's output walls, by its `nestedDirsByProject`, when it has a nested workspace. */
|
|
1110
|
+
const outputWalls = new WeakMap<ReadonlyMap<string, string[]>, ReadonlyMap<string, string[]>>()
|
|
1111
|
+
|
|
1112
|
+
/**
|
|
1113
|
+
* Where each project's outputs stop: its nested projects and every nested
|
|
1114
|
+
* workspace under it. Its inputs stop at the projects alone: a task may
|
|
1115
|
+
* read a nested workspace's files (a git worktree checked out under
|
|
1116
|
+
* `src/` holds one), so folding them costs a miss, and leaving them out
|
|
1117
|
+
* a stale hit; cleaning them deleted another workspace's work. The map
|
|
1118
|
+
* itself when there is no nested workspace.
|
|
1119
|
+
*/
|
|
1120
|
+
export function outputWallsOf(
|
|
1121
|
+
nestedDirsByProject: ReadonlyMap<string, string[]>,
|
|
1122
|
+
): ReadonlyMap<string, string[]> {
|
|
1123
|
+
return outputWalls.get(nestedDirsByProject) ?? nestedDirsByProject
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1011
1126
|
/**
|
|
1012
1127
|
* The workspace's `cache.policy`, parsed: the schema checks only its shape,
|
|
1013
1128
|
* since the grammar lives in the cache module the workspace module may not
|
|
@@ -1022,7 +1137,7 @@ export function workspaceCachePolicy(
|
|
|
1022
1137
|
return parseCachePolicy(spec)
|
|
1023
1138
|
} catch (err) {
|
|
1024
1139
|
throw new UserError(
|
|
1025
|
-
`.vx/workspace: \`cache.policy
|
|
1140
|
+
`.vx/workspace: \`cache.policy\` ${err instanceof Error ? err.message : String(err)}`,
|
|
1026
1141
|
'VX_E_CONFIG',
|
|
1027
1142
|
)
|
|
1028
1143
|
}
|
|
@@ -1031,13 +1146,17 @@ export function workspaceCachePolicy(
|
|
|
1031
1146
|
/** A config no locked project has, looked for only when the lock holds none. */
|
|
1032
1147
|
function unlockedConfigOf(
|
|
1033
1148
|
configured: readonly unknown[],
|
|
1034
|
-
metas: ReadonlyArray<{ dir: string }>,
|
|
1149
|
+
metas: ReadonlyArray<{ name: string; dir: string }>,
|
|
1035
1150
|
workspaceRoot: string,
|
|
1036
1151
|
): string | undefined {
|
|
1037
1152
|
if (configured.length > 0) return undefined
|
|
1038
1153
|
for (const m of metas) {
|
|
1039
1154
|
const config = PROJECT_CONFIG_FILENAMES.find((n) => existsSync(path.join(m.dir, n)))
|
|
1040
|
-
if (config
|
|
1155
|
+
if (config === undefined) continue
|
|
1156
|
+
const file = path.join(m.dir, config)
|
|
1157
|
+
// `vx lock` never reads it: the hint would send the user to it for nothing.
|
|
1158
|
+
if (ignoredConfigRefusal(workspaceRoot, m, file) === undefined)
|
|
1159
|
+
return relPosix(workspaceRoot, file)
|
|
1041
1160
|
}
|
|
1042
1161
|
return undefined
|
|
1043
1162
|
}
|
|
@@ -1048,12 +1167,12 @@ function unlockedConfigOf(
|
|
|
1048
1167
|
* one that runs nothing: ran, `c#build` was 0 tasks, exit 0, where a
|
|
1049
1168
|
* renamed task gets "run `vx lock`" (Growth, lock drift).
|
|
1050
1169
|
*/
|
|
1051
|
-
function refuseUnlockedConfig(
|
|
1170
|
+
async function refuseUnlockedConfig(
|
|
1052
1171
|
tasks: readonly string[],
|
|
1053
1172
|
projects: ReadonlyMap<string, ProjectEntry>,
|
|
1054
1173
|
metas: ReadonlyArray<{ name: string; dir: string; configPath?: string | null }>,
|
|
1055
1174
|
workspaceRoot: string,
|
|
1056
|
-
): void {
|
|
1175
|
+
): Promise<void> {
|
|
1057
1176
|
for (const spec of tasks) {
|
|
1058
1177
|
const at = spec.indexOf('#')
|
|
1059
1178
|
if (at < 0) continue
|
|
@@ -1061,12 +1180,13 @@ function refuseUnlockedConfig(
|
|
|
1061
1180
|
const entry = projects.get(name)
|
|
1062
1181
|
// No entry: a package the lock holds no project for, whose config came after it.
|
|
1063
1182
|
if (entry !== undefined && !isDefaultBuild(entry.config.tasks?.[spec.slice(at + 1)])) continue
|
|
1064
|
-
const config =
|
|
1183
|
+
const config =
|
|
1184
|
+
unlockedConfig(name, metas) ??
|
|
1185
|
+
(metas.some((m) => m.name === name)
|
|
1186
|
+
? undefined
|
|
1187
|
+
: await unlockedDir(name, metas, workspaceRoot))
|
|
1065
1188
|
if (config === undefined) continue
|
|
1066
|
-
throw new UserError(
|
|
1067
|
-
`${spec}: ${relPosix(workspaceRoot, config)} is not in the lock yet — run \`vx lock\``,
|
|
1068
|
-
'VX_E_LOCK',
|
|
1069
|
-
)
|
|
1189
|
+
throw new UserError(`${spec}: ${notLockedYet(workspaceRoot, config, metas, name)}`, 'VX_E_LOCK')
|
|
1070
1190
|
}
|
|
1071
1191
|
}
|
|
1072
1192
|
|
|
@@ -1089,13 +1209,28 @@ function refuseUnlockedScope(
|
|
|
1089
1209
|
for (const name of selected) {
|
|
1090
1210
|
const config = unlockedConfig(name, metas)
|
|
1091
1211
|
if (config === undefined) continue
|
|
1092
|
-
throw new UserError(
|
|
1093
|
-
`${spec}: ${relPosix(workspaceRoot, config)} is not in the lock yet — run \`vx lock\``,
|
|
1094
|
-
'VX_E_LOCK',
|
|
1095
|
-
)
|
|
1212
|
+
throw new UserError(`${spec}: ${notLockedYet(workspaceRoot, config, metas, name)}`, 'VX_E_LOCK')
|
|
1096
1213
|
}
|
|
1097
1214
|
}
|
|
1098
1215
|
|
|
1216
|
+
/**
|
|
1217
|
+
* Why the lock holds no `config`: git ignores it, in a directory a
|
|
1218
|
+
* `discover` plugin made a project, which `vx lock` cannot fix; else it
|
|
1219
|
+
* came after the lock.
|
|
1220
|
+
*/
|
|
1221
|
+
function notLockedYet(
|
|
1222
|
+
workspaceRoot: string,
|
|
1223
|
+
config: string,
|
|
1224
|
+
metas: ReadonlyArray<{ name: string; dir: string }>,
|
|
1225
|
+
name: string,
|
|
1226
|
+
): string {
|
|
1227
|
+
const meta = metas.find((m) => m.name === name)
|
|
1228
|
+
return (
|
|
1229
|
+
(meta === undefined ? undefined : ignoredConfigRefusal(workspaceRoot, meta, config)) ??
|
|
1230
|
+
`${relPosix(workspaceRoot, config)} is not in the lock yet — run \`vx lock\``
|
|
1231
|
+
)
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1099
1234
|
/** The config file in a project's directory the lock holds none for. */
|
|
1100
1235
|
function unlockedConfig(
|
|
1101
1236
|
name: string,
|
|
@@ -1108,6 +1243,27 @@ function unlockedConfig(
|
|
|
1108
1243
|
return config === undefined ? undefined : path.join(meta.dir, config)
|
|
1109
1244
|
}
|
|
1110
1245
|
|
|
1246
|
+
/**
|
|
1247
|
+
* The config `vx lock` would find in the directory a name no project has
|
|
1248
|
+
* reads as (`packages/d`, `c`): with no discover plugin a config written
|
|
1249
|
+
* after the lock makes no project, so `unlockedConfig` never sees it.
|
|
1250
|
+
* Asked only for a name that matches nothing, a run or `vx show` about to
|
|
1251
|
+
* be refused; the listing is paid only when the directory holds a config.
|
|
1252
|
+
*/
|
|
1253
|
+
export async function unlockedDir(
|
|
1254
|
+
name: string,
|
|
1255
|
+
metas: ReadonlyArray<{ dir: string }>,
|
|
1256
|
+
workspaceRoot: string,
|
|
1257
|
+
): Promise<string | undefined> {
|
|
1258
|
+
const dir = path.resolve(workspaceRoot, name)
|
|
1259
|
+
// A project's directory typed for its name is that project, hinted as such.
|
|
1260
|
+
if (!dir.startsWith(workspaceRoot + path.sep) || metas.some((m) => m.dir === dir))
|
|
1261
|
+
return undefined
|
|
1262
|
+
if (!PROJECT_CONFIG_FILENAMES.some((n) => existsSync(path.join(dir, n)))) return undefined
|
|
1263
|
+
const { configs } = await findProjectConfigs(workspaceRoot)
|
|
1264
|
+
return configs.find((c) => c.dir === dir)?.configPath
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1111
1267
|
/**
|
|
1112
1268
|
* Each remote's `open`, in declared order, for the ones the policy lets read
|
|
1113
1269
|
* or write. One at a time: two that ask on the terminal must not interleave.
|