@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
|
@@ -1,64 +1,172 @@
|
|
|
1
|
-
// Plugin API —
|
|
1
|
+
// Plugin API — the ONE `VxPlugin` contract (docs/design/pipeline-2026-09.md).
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// receives a PluginContext with the bus + workspace metadata and can
|
|
7
|
-
// install lifecycle hooks. Plugins observe; they do not redirect.
|
|
3
|
+
// Users register plugins via defineWorkspace({ plugins }). Behaviour
|
|
4
|
+
// capabilities (`cache`, `executor`) return objects core calls INTO;
|
|
5
|
+
// observe capabilities (`telemetry`, `setup` on the bus) can only read.
|
|
8
6
|
//
|
|
9
|
-
// Crash isolation: a plugin that throws
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// plugin that throws inside a hook is logged + the plugin is disabled
|
|
13
|
-
// for the remainder of the run (mirrors the deleted Observer's
|
|
14
|
-
// makeSafeObserver pattern; surfaces are isolated from execution).
|
|
15
|
-
//
|
|
16
|
-
// Hook return values: today every hook is fire-and-forget (the bus IS
|
|
17
|
-
// the message pipe; hooks just give users an ergonomic place to read
|
|
18
|
-
// it). One write-capable hook is reserved for a future iteration —
|
|
19
|
-
// onCacheLookup returning { skip: true } — but not in v1.
|
|
7
|
+
// Crash isolation: a plugin that throws in a load-bearing hook aborts the
|
|
8
|
+
// run with a clean UserError naming plugin + hook. A plugin that throws
|
|
9
|
+
// inside a bus hook is logged and disabled for the remainder of the run.
|
|
20
10
|
|
|
11
|
+
import { readFileSync } from 'node:fs'
|
|
12
|
+
import path from 'node:path'
|
|
13
|
+
import { fileURLToPath } from 'node:url'
|
|
21
14
|
import type { Cache, CacheLayer, CachePolicy } from '../cache/index.js'
|
|
15
|
+
import { PLUGIN_PACKAGE, type ProjectConfig, type WorkspaceConfig } from '../config.js'
|
|
16
|
+
import type { TaskExecutor } from '../exec/index.js'
|
|
22
17
|
import type { TaskNode, TaskOutcome } from '../graph/index.js'
|
|
23
18
|
import { UserError } from '../util/index.js'
|
|
24
|
-
import type {
|
|
25
|
-
import type {
|
|
19
|
+
import type { ProjectMeta } from '../workspace/index.js'
|
|
20
|
+
import type { EventBus, RunStartInfo } from './events.js'
|
|
26
21
|
import type { TelemetryContext, TelemetrySink } from './telemetry.js'
|
|
27
22
|
|
|
28
23
|
/**
|
|
29
|
-
* A vx plugin. Contributes any subset of
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* vx.workspace.ts via
|
|
24
|
+
* A vx plugin. Contributes any subset of the run-level capabilities —
|
|
25
|
+
* where work runs (executor), which cache is used (cache), who
|
|
26
|
+
* observes the run (telemetry). It never changes WHAT a task is (the
|
|
27
|
+
* command string — principle #3), only where and how that command is
|
|
28
|
+
* executed. Registered explicitly in vx.workspace.ts via
|
|
29
|
+
* defineWorkspace({ plugins: [...] }). No auto-discovery.
|
|
34
30
|
*
|
|
35
31
|
* The old observe-only `Plugin` (`{ name, setup(ctx) }`) is a subset of
|
|
36
32
|
* this shape: a plugin with only `setup` installs and runs exactly as
|
|
37
|
-
* before via `installPlugins`. The
|
|
38
|
-
* `plugin-host.ts
|
|
33
|
+
* before via `installPlugins`. The capabilities are consulted by
|
|
34
|
+
* `plugin-host.ts`; core's own executor and cache are plugins too
|
|
35
|
+
* (src/plugins/), declared by the workspace — there is no fallback
|
|
36
|
+
* outside the list.
|
|
39
37
|
*/
|
|
40
38
|
export interface VxPlugin {
|
|
41
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* The name of the package the plugin was defined in — set by
|
|
41
|
+
* `definePlugin` from the nearest `package.json`, never by the plugin.
|
|
42
|
+
* Heads every warning about the plugin, every `vx info` line, and the
|
|
43
|
+
* key material a `key` hook contributes.
|
|
44
|
+
*/
|
|
42
45
|
readonly name: string
|
|
43
46
|
|
|
44
|
-
// ---
|
|
47
|
+
// --- PIPELINE stages (shape the run before it executes — opt-in) ----------
|
|
48
|
+
//
|
|
49
|
+
// Each receives the object core is about to use and edits it IN PLACE;
|
|
50
|
+
// core re-validates after the last plugin, so a plugin cannot produce what
|
|
51
|
+
// the loader would refuse from a user. Declaration order is the order.
|
|
52
|
+
// Whatever a stage changes reaches the cache key by construction: the key
|
|
53
|
+
// hashes the task config AFTER `project` ran. See
|
|
54
|
+
// docs/design/pipeline-2026-09.md.
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The workspace config, before anything is derived from it (concurrency,
|
|
58
|
+
* cacheDir, timeout, …). `plugins` is already fixed by the time this runs.
|
|
59
|
+
*/
|
|
60
|
+
config?(workspace: WorkspaceConfig, ctx: WorkspaceHookContext): void | Promise<void>
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* One project's validated config, right after it loaded and before the
|
|
64
|
+
* graph is built. Add, remove or edit tasks. Runs for every loaded project
|
|
65
|
+
* on every run — a config's cached evaluation is the user's file, and the
|
|
66
|
+
* hook is applied on top of it live. A package with NO config file is
|
|
67
|
+
* loaded too, as `{ tasks: {} }`, so a plugin can give tasks to packages
|
|
68
|
+
* that never wrote one (`ctx.packageJson` carries their scripts); with no
|
|
69
|
+
* `project` plugin such a package is never visited.
|
|
70
|
+
*/
|
|
71
|
+
project?(config: ProjectConfig, ctx: ProjectHookContext): void | Promise<void>
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The task graph, after `dependsOn` expansion and before scheduling. Add
|
|
75
|
+
* or drop edges (`node.deps`), mark tasks requested, adjust resources. A
|
|
76
|
+
* dep naming a task that is not in the graph, or a cycle, is refused with
|
|
77
|
+
* the plugin's name.
|
|
78
|
+
*/
|
|
79
|
+
graph?(nodes: Map<string, TaskNode>, ctx: GraphHookContext): void | Promise<void>
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Extra cache-key material for one task: a `{ name: value }` record that
|
|
83
|
+
* is folded into the key (and named in `vx why`) — a tool version, a
|
|
84
|
+
* feature flag, anything the declared inputs cannot see. Runs once per
|
|
85
|
+
* task per run, before any key is derived. Return undefined to add
|
|
86
|
+
* nothing. Values must be deterministic for the same inputs or the key
|
|
87
|
+
* never hits.
|
|
88
|
+
*/
|
|
89
|
+
key?(
|
|
90
|
+
task: TaskNode,
|
|
91
|
+
ctx: KeyHookContext,
|
|
92
|
+
):
|
|
93
|
+
| Readonly<Record<string, string>>
|
|
94
|
+
| undefined
|
|
95
|
+
| Promise<Readonly<Record<string, string>> | undefined>
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Workspace-root files this plugin keys on its own, taken OUT of the
|
|
99
|
+
* workspace fingerprint. Core folds every lockfile it knows into every
|
|
100
|
+
* task's key, so one `pnpm install` re-keys the whole workspace. A plugin
|
|
101
|
+
* that reads the lockfile and folds each project's own dependency
|
|
102
|
+
* closure through `key` claims the file here: core leaves it out of the
|
|
103
|
+
* fingerprint every key sees (the config-evaluation cache still keys on
|
|
104
|
+
* it — a config may import a dependency), and `--affected` asks
|
|
105
|
+
* `affected` which projects a change to it touches instead of selecting
|
|
106
|
+
* every project. A file has one claimant; a name core does not fold is
|
|
107
|
+
* refused, since there is nothing to take out.
|
|
108
|
+
*/
|
|
109
|
+
readonly fingerprint?: FingerprintClaim
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Scheduling priorities: task id → weight, higher runs first among READY
|
|
113
|
+
* tasks (merged over the structural baseline, which stays the tie-break).
|
|
114
|
+
* Runs once, after the graph is final. A later plugin's weight for a task
|
|
115
|
+
* overrides an earlier one's. Return undefined to leave the baseline.
|
|
116
|
+
*/
|
|
117
|
+
schedule?(
|
|
118
|
+
nodes: ReadonlyMap<string, TaskNode>,
|
|
119
|
+
ctx: ScheduleHookContext,
|
|
120
|
+
): ReadonlyMap<string, number> | undefined | Promise<ReadonlyMap<string, number> | undefined>
|
|
45
121
|
|
|
46
122
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
123
|
+
* Admission over the worker count: asked for every task about to start
|
|
124
|
+
* on this machine, with the tasks running here right now. Return `false`
|
|
125
|
+
* to hold it until something finishes (it is asked again then). Asked
|
|
126
|
+
* many times per run, so it must be cheap and synchronous; a throw is
|
|
127
|
+
* reported once and the plugin admits from then on — a policy never
|
|
128
|
+
* breaks a run. Restore-tier hits and tasks on an executor pool hold no
|
|
129
|
+
* local resources and are never asked. When several plugins answer, all
|
|
130
|
+
* must admit. Core keeps no notion of what a task needs: a plugin that
|
|
131
|
+
* packs memory or CPU learns or declares the numbers itself
|
|
132
|
+
* (`@vzn/vx-schedule-history` packs what past executions used).
|
|
51
133
|
*/
|
|
52
|
-
|
|
134
|
+
admit?(task: TaskNode, ctx: AdmitContext): boolean
|
|
135
|
+
|
|
136
|
+
// --- CLI verbs (opt-in) ---------------------------------------------------
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Verbs this plugin adds to the `vx` CLI, keyed by name. Consulted only
|
|
140
|
+
* for a verb core does not know — core's own verbs always win — and only
|
|
141
|
+
* when the cwd is inside a workspace that declares the plugin. `vx help`
|
|
142
|
+
* lists them under "Plugin commands". A verb's exit code is the process
|
|
143
|
+
* exit code; a thrown `UserError` prints cleanly, like core's own.
|
|
144
|
+
*/
|
|
145
|
+
readonly commands?: Readonly<Record<string, PluginCommand>>
|
|
146
|
+
|
|
147
|
+
// --- BEHAVIOR capabilities (change WHAT/HOW work runs — opt-in) -----------
|
|
53
148
|
|
|
54
149
|
/**
|
|
55
150
|
* Contribute a cache layer. Returns a CacheLayer wrapping (or replacing)
|
|
56
151
|
* the local Cache, or undefined to decline. Consulted ONCE per prepareRun.
|
|
57
|
-
* Precedence: first non-undefined plugin cache wins
|
|
58
|
-
*
|
|
152
|
+
* Precedence: first non-undefined plugin cache wins, in declaration
|
|
153
|
+
* order, ahead of core's own `.vx/cache` handle at the tail. A layer
|
|
154
|
+
* that WRAPS the local handle subsumes that tail, so it is not written
|
|
155
|
+
* twice.
|
|
59
156
|
*/
|
|
60
157
|
cache?(ctx: CacheContext): CacheLayer | undefined | Promise<CacheLayer | undefined>
|
|
61
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Contribute a task executor — WHERE one task's command runs. Consulted
|
|
161
|
+
* ONCE per run; every contributed executor is kept, in declaration order,
|
|
162
|
+
* and per task the first whose `accepts()` passes executes it. Nothing is
|
|
163
|
+
* appended in front: core's own local executor is always the TAIL, so
|
|
164
|
+
* declining a task hands it back to this machine rather than failing
|
|
165
|
+
* the run. Persistent tasks never reach an executor (local by
|
|
166
|
+
* construction).
|
|
167
|
+
*/
|
|
168
|
+
executor?(ctx: ExecutorContext): TaskExecutor | undefined | Promise<TaskExecutor | undefined>
|
|
169
|
+
|
|
62
170
|
// --- OBSERVE-ONLY capability (cannot change behavior — by construction) ---
|
|
63
171
|
|
|
64
172
|
/**
|
|
@@ -78,15 +186,6 @@ export interface VxPlugin {
|
|
|
78
186
|
| undefined
|
|
79
187
|
| Promise<TelemetrySink | TelemetrySink[] | undefined>
|
|
80
188
|
|
|
81
|
-
/**
|
|
82
|
-
* @deprecated Prefer `telemetry`. Contribute an event sink — a consumer of
|
|
83
|
-
* the serializable WireEvent stream, subscribed for the whole run via
|
|
84
|
-
* wireForwarder. Fire-and-forget; a throwing sink is isolated and cannot
|
|
85
|
-
* break the run. Kept as a back-compat path; `telemetry` is the canonical,
|
|
86
|
-
* analytics-shaped export contract.
|
|
87
|
-
*/
|
|
88
|
-
eventSink?(ctx: EventSinkContext): EventSink | undefined | Promise<EventSink | undefined>
|
|
89
|
-
|
|
90
189
|
/**
|
|
91
190
|
* Optional one-time setup before any capability is consulted (validate the
|
|
92
191
|
* workspace, open a connection, read a token). Throwing aborts the run with
|
|
@@ -98,12 +197,6 @@ export interface VxPlugin {
|
|
|
98
197
|
teardown?(): void | Promise<void>
|
|
99
198
|
}
|
|
100
199
|
|
|
101
|
-
/** A WireEvent consumer. onEvent is fire-and-forget; flush is awaited at teardown. */
|
|
102
|
-
export interface EventSink {
|
|
103
|
-
onEvent(event: WireEvent): void
|
|
104
|
-
flush?(): Promise<void>
|
|
105
|
-
}
|
|
106
|
-
|
|
107
200
|
/** Shared, read-only context every capability factory receives. */
|
|
108
201
|
interface BaseContext {
|
|
109
202
|
readonly workspaceRoot: string
|
|
@@ -113,11 +206,89 @@ interface BaseContext {
|
|
|
113
206
|
}
|
|
114
207
|
|
|
115
208
|
export interface PluginSetupContext extends BaseContext {}
|
|
116
|
-
export interface EventSinkContext extends BaseContext {}
|
|
117
209
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
210
|
+
/** One CLI verb contributed by a plugin. */
|
|
211
|
+
export interface PluginCommand {
|
|
212
|
+
/** One line for `vx help`. */
|
|
213
|
+
readonly description: string
|
|
214
|
+
run(argv: readonly string[], ctx: CommandContext): number | Promise<number>
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export interface CommandContext extends BaseContext {}
|
|
218
|
+
|
|
219
|
+
/** `config` runs before the cache dir is known — it may be what the hook changes. */
|
|
220
|
+
export interface WorkspaceHookContext {
|
|
221
|
+
readonly workspaceRoot: string
|
|
222
|
+
warn(message: string): void
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export interface ProjectHookContext extends BaseContext {
|
|
226
|
+
/** The package name (`package.json#name`). */
|
|
227
|
+
readonly name: string
|
|
228
|
+
/** The project's directory, absolute. */
|
|
229
|
+
readonly dir: string
|
|
230
|
+
/** The parsed `package.json`, read-only. */
|
|
231
|
+
readonly packageJson: Readonly<Record<string, unknown>>
|
|
232
|
+
/**
|
|
233
|
+
* Every package the workspace discovered — the one being visited, the
|
|
234
|
+
* rest of the run's scope and the packages outside it, config file or
|
|
235
|
+
* not — as core read them at startup. A plugin whose mapping needs the
|
|
236
|
+
* whole workspace (a `dependsOn` is only valid against every package's
|
|
237
|
+
* scripts at once) reads it here instead of walking the workspace a
|
|
238
|
+
* second time. The same array is handed to every visit of a run and a
|
|
239
|
+
* new one to the next run — a plugin instance outlives a run under `vx
|
|
240
|
+
* watch`, so a per-run memo keys on this array's identity, never on the
|
|
241
|
+
* process.
|
|
242
|
+
*/
|
|
243
|
+
readonly projects: readonly ProjectMeta[]
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export interface GraphHookContext extends BaseContext {
|
|
247
|
+
/** Task ids the user asked for (the rest were pulled in by `dependsOn`). */
|
|
248
|
+
readonly requested: readonly string[]
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export interface KeyHookContext extends BaseContext {}
|
|
252
|
+
|
|
253
|
+
/** A plugin's claim on workspace fingerprint files — see `VxPlugin.fingerprint`. */
|
|
254
|
+
export interface FingerprintClaim {
|
|
255
|
+
/** Root-relative names from `WORKSPACE_FINGERPRINT_FILES`, e.g. `['pnpm-lock.yaml']`. */
|
|
256
|
+
readonly files: readonly string[]
|
|
257
|
+
/**
|
|
258
|
+
* The projects (package names) a change to a claimed file affects. Called
|
|
259
|
+
* by `--affected` with the file's bytes at the base ref and in the
|
|
260
|
+
* working tree (`null` where it does not exist). Return `undefined` to
|
|
261
|
+
* select every project — the answer a claimant gives when it cannot
|
|
262
|
+
* tell, and what core does for an unclaimed file.
|
|
263
|
+
*/
|
|
264
|
+
affected(
|
|
265
|
+
change: FingerprintChange,
|
|
266
|
+
ctx: FingerprintContext,
|
|
267
|
+
): Iterable<string> | undefined | Promise<Iterable<string> | undefined>
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export interface FingerprintChange {
|
|
271
|
+
/** The claimed file's root-relative name. */
|
|
272
|
+
readonly file: string
|
|
273
|
+
readonly before: Uint8Array | null
|
|
274
|
+
readonly after: Uint8Array | null
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export interface FingerprintContext extends BaseContext {
|
|
278
|
+
/** Every project in the workspace: package name and absolute directory. */
|
|
279
|
+
readonly projects: ReadonlyArray<{ readonly name: string; readonly dir: string }>
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export interface AdmitContext {
|
|
283
|
+
/** The tasks executing on this machine right now, in dispatch order. */
|
|
284
|
+
readonly running: readonly TaskNode[]
|
|
285
|
+
/** The run's worker count — the ceiling the count gate already applies. */
|
|
286
|
+
readonly concurrency: number
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export interface ScheduleHookContext extends BaseContext {
|
|
290
|
+
/** The run's local cache handle — its `dbHandle()` holds the run history a policy can learn from. */
|
|
291
|
+
readonly localCache: Cache
|
|
121
292
|
}
|
|
122
293
|
|
|
123
294
|
export interface CacheContext extends BaseContext {
|
|
@@ -127,6 +298,11 @@ export interface CacheContext extends BaseContext {
|
|
|
127
298
|
readonly policy: CachePolicy
|
|
128
299
|
}
|
|
129
300
|
|
|
301
|
+
export interface ExecutorContext extends BaseContext {
|
|
302
|
+
/** The run's worker count — an executor that paces itself reads it here. */
|
|
303
|
+
readonly concurrency: number
|
|
304
|
+
}
|
|
305
|
+
|
|
130
306
|
export interface PluginContext {
|
|
131
307
|
/** Where the workspace lives on disk. */
|
|
132
308
|
readonly workspaceRoot: string
|
|
@@ -170,12 +346,76 @@ export interface Plugin {
|
|
|
170
346
|
* be installed synchronously inside setup() so no events are missed.
|
|
171
347
|
*
|
|
172
348
|
* OPTIONAL: a capability-only plugin (one that contributes
|
|
173
|
-
* `
|
|
349
|
+
* `cache`/`executor`/`telemetry` but no `setup`) is simply skipped by
|
|
174
350
|
* `installPlugins` — its capabilities are consulted by `plugin-host.ts`.
|
|
175
351
|
*/
|
|
176
352
|
setup?(ctx: PluginContext): void | Promise<void>
|
|
177
353
|
}
|
|
178
354
|
|
|
355
|
+
/** What a plugin author writes: every hook, and no name. */
|
|
356
|
+
export type PluginHooks = Omit<VxPlugin, 'name'>
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Where a plugin is defined — `import.meta` of its module. `dir` is Bun's
|
|
360
|
+
* field; `url` is the standard one, for a module evaluated elsewhere.
|
|
361
|
+
*/
|
|
362
|
+
export interface PluginOrigin {
|
|
363
|
+
readonly dir?: string
|
|
364
|
+
readonly url?: string
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const packageNameByDir = new Map<string, string>()
|
|
368
|
+
|
|
369
|
+
/** The name of the nearest `package.json` above `dir` — the one that owns it. */
|
|
370
|
+
function pluginPackageName(dir: string): string {
|
|
371
|
+
const memo = packageNameByDir.get(dir)
|
|
372
|
+
if (memo !== undefined) return memo
|
|
373
|
+
for (let d = dir; ;) {
|
|
374
|
+
let text: string | undefined
|
|
375
|
+
try {
|
|
376
|
+
text = readFileSync(path.join(d, 'package.json'), 'utf8')
|
|
377
|
+
} catch {
|
|
378
|
+
/* not here; look one level up */
|
|
379
|
+
}
|
|
380
|
+
if (text !== undefined) {
|
|
381
|
+
const name = (JSON.parse(text) as { name?: unknown }).name
|
|
382
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
383
|
+
throw new UserError(
|
|
384
|
+
`definePlugin: ${path.join(d, 'package.json')} has no name — a plugin is a package, and its name is the package's`,
|
|
385
|
+
)
|
|
386
|
+
}
|
|
387
|
+
packageNameByDir.set(dir, name)
|
|
388
|
+
return name
|
|
389
|
+
}
|
|
390
|
+
const parent = path.dirname(d)
|
|
391
|
+
if (parent === d) {
|
|
392
|
+
throw new UserError(
|
|
393
|
+
`definePlugin: no package.json above ${dir} — a plugin is a package, and its name is the package's`,
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
d = parent
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* The one way to make a plugin: `definePlugin(import.meta, { ...hooks })`.
|
|
402
|
+
* The name is read from the package the calling module belongs to — the
|
|
403
|
+
* nearest `package.json` above it — and stamped where the workspace loader
|
|
404
|
+
* checks for it, so a plugin cannot be named anything but its package.
|
|
405
|
+
*/
|
|
406
|
+
export function definePlugin(origin: PluginOrigin, hooks: PluginHooks): VxPlugin {
|
|
407
|
+
if ('name' in hooks) {
|
|
408
|
+
throw new UserError(`definePlugin: a plugin's name is its package name — drop the 'name' field`)
|
|
409
|
+
}
|
|
410
|
+
const dir =
|
|
411
|
+
origin.dir ?? (origin.url !== undefined ? path.dirname(fileURLToPath(origin.url)) : undefined)
|
|
412
|
+
if (dir === undefined) {
|
|
413
|
+
throw new UserError(`definePlugin: the first argument must be the plugin module's import.meta`)
|
|
414
|
+
}
|
|
415
|
+
const name = pluginPackageName(dir)
|
|
416
|
+
return { ...hooks, name, [PLUGIN_PACKAGE]: name } as VxPlugin
|
|
417
|
+
}
|
|
418
|
+
|
|
179
419
|
export interface InstallPluginsArgs {
|
|
180
420
|
plugins: readonly Plugin[]
|
|
181
421
|
workspaceRoot: string
|
|
@@ -195,8 +435,8 @@ export interface InstallPluginsArgs {
|
|
|
195
435
|
* hooks are subscribed before the next plugin's setup runs. Throws if any
|
|
196
436
|
* plugin's setup() throws (the run cannot start with a broken plugin).
|
|
197
437
|
*
|
|
198
|
-
* A plugin without a `setup` is a capability-only plugin (
|
|
199
|
-
* /
|
|
438
|
+
* A plugin without a `setup` is a capability-only plugin (cache
|
|
439
|
+
* / executor / telemetry) — skipped here; those capabilities are consulted by
|
|
200
440
|
* `plugin-host.ts`.
|
|
201
441
|
*/
|
|
202
442
|
export async function installPlugins(args: InstallPluginsArgs): Promise<() => void> {
|
|
@@ -209,7 +449,7 @@ export async function installPlugins(args: InstallPluginsArgs): Promise<() => vo
|
|
|
209
449
|
if (typeof plugin.name !== 'string' || plugin.name.length === 0) {
|
|
210
450
|
throw new UserError('plugin missing `name` field')
|
|
211
451
|
}
|
|
212
|
-
// No setup → a capability-only plugin (
|
|
452
|
+
// No setup → a capability-only plugin (cache / executor / telemetry),
|
|
213
453
|
// consulted by plugin-host.ts; skip the hook install. A setup that's
|
|
214
454
|
// present but not callable is a real authoring error — reject it.
|
|
215
455
|
if (plugin.setup === undefined) continue
|