@uniqbit/mate-core 0.18.4 → 0.18.5

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.
Files changed (44) hide show
  1. package/opencode-plugin/src/add-dir.ts +76 -10
  2. package/opencode-plugin/src/companion.ts +1 -8
  3. package/opencode-plugin/src/server.ts +2 -15
  4. package/package.json +1 -1
  5. package/src/cli/commands/artifact/finish/openspec.ts +40 -8
  6. package/src/cli/commands/doctor.ts +92 -86
  7. package/src/cli/commands/install.ts +0 -8
  8. package/src/cli/commands/studio/terminal-launch.ts +7 -16
  9. package/src/cli/commands/wrap.ts +4 -29
  10. package/src/cli/usage.ts +1 -2
  11. package/src/hooks/session-guidance.ts +2 -20
  12. package/src/lib/orchestrator/adapters/claude.ts +20 -130
  13. package/src/lib/orchestrator/adapters/opencode.ts +7 -24
  14. package/src/lib/orchestrator/config-store.ts +2 -70
  15. package/src/lib/orchestrator/projection-claude-skills.ts +35 -88
  16. package/src/lib/orchestrator/projection-entries.ts +0 -1
  17. package/src/lib/orchestrator/projection-types.ts +0 -2
  18. package/src/lib/orchestrator/session-prepare.ts +4 -82
  19. package/src/lib/orchestrator/wrap-pass.ts +1 -88
  20. package/src/runtime/guidance.ts +0 -17
  21. package/src/runtime/policy.ts +1 -14
  22. package/src/runtime/runtime-document-manifest.ts +0 -17
  23. package/src/runtime/wrap-state.ts +0 -4
  24. package/src/tools/setup/capabilities/openspec.ts +140 -18
  25. package/src/tools/setup/dynamic-plugins/audiences.ts +7 -39
  26. package/src/tools/setup/dynamic-plugins/declarations.ts +1 -1
  27. package/src/tools/setup/dynamic-plugins/hydrate.ts +4 -7
  28. package/src/tools/setup/engine.ts +10 -141
  29. package/src/tools/setup/plugin.ts +4 -19
  30. package/src/tools/setup/plugins/gitignore.ts +1 -7
  31. package/src/tools/setup/providers/claude.ts +2 -27
  32. package/src/tools/setup/providers/opencode.ts +0 -10
  33. package/src/tools/setup/registry.ts +2 -24
  34. package/src/tools/setup/surface-target.ts +4 -13
  35. package/src/tools/setup.ts +6 -9
  36. package/opencode-plugin/src/audience-layer.ts +0 -91
  37. package/src/runtime/audience-layer.ts +0 -84
  38. package/src/runtime/opencode-audience-layer.ts +0 -176
  39. package/src/tools/setup/audience-isolation.fixture.ts +0 -325
  40. package/src/tools/setup/audience-layer.ts +0 -207
  41. package/src/tools/setup/audience-registrations.ts +0 -52
  42. package/src/tools/setup/contribution-ledger.ts +0 -207
  43. package/src/tools/setup/isolation-diagnostics.ts +0 -139
  44. package/src/tools/setup/upgrade-sweep.ts +0 -153
@@ -1,325 +0,0 @@
1
- // oxlint-disable no-await-in-loop
2
- import type { Dirent } from "node:fs";
3
- import fs from "node:fs/promises";
4
- import os from "node:os";
5
- import path from "node:path";
6
-
7
- import type { FrameworkConfig } from "../../lib/orchestrator/types";
8
- import { pluginPackageRoot } from "./dynamic-plugins/paths";
9
- import type {
10
- CapabilityPlugin,
11
- PluginRegistration,
12
- ProviderPlugin,
13
- RuntimeContributions,
14
- } from "./plugin";
15
-
16
- /**
17
- * Audience isolation harness: one companion whose base plugin and audience
18
- * `ba` and `qa` plugins each declare every contribution kind, every value
19
- * carrying its owner's marker. A test runs a pass, then asserts which markers
20
- * a surface holds — a marker found where it must not be is a leak.
21
- */
22
-
23
- export type FixtureOwner = "base" | "ba" | "qa";
24
-
25
- export const AUDIENCE_MARKERS: Record<FixtureOwner, string> = {
26
- base: "BASEMARK7",
27
- ba: "BAMARK7",
28
- qa: "QAMARK7",
29
- };
30
-
31
- export const FIXTURE_AUDIENCES = ["ba", "qa"] as const;
32
-
33
- export function fixturePluginId(owner: FixtureOwner): string {
34
- return `${owner}-plugin`;
35
- }
36
-
37
- export function fixturePackage(owner: FixtureOwner): string {
38
- return `@fixture/${owner}`;
39
- }
40
-
41
- export interface FixtureCapabilityOptions {
42
- apply?: () => Promise<void>;
43
- teardown?: () => Promise<void>;
44
- forProvider?: CapabilityPlugin["forProvider"];
45
- pluginReference?: string;
46
- }
47
-
48
- function contributionsFor(
49
- owner: FixtureOwner,
50
- skillSourceDir: string,
51
- runtime: "claude" | "opencode",
52
- options: FixtureCapabilityOptions,
53
- ): RuntimeContributions {
54
- const marker = AUDIENCE_MARKERS[owner];
55
- const contributions: RuntimeContributions = {
56
- mcpServers: [{ name: `${owner}-mcp`, command: `${marker}-server`, args: ["serve"] }],
57
- guidanceSections: [{ content: `${marker} guidance for ${owner}.` }],
58
- skillTrees: [{ name: `${owner}-skill`, sourceDir: skillSourceDir }],
59
- agentDefinitions: [
60
- {
61
- name: `${owner}-agent`,
62
- content: `---\nname: ${owner}-agent\ndescription: ${owner} agent\n---\n${marker} agent prompt.\n`,
63
- },
64
- ],
65
- permissionEntries: [`Bash(${marker}:*)`],
66
- };
67
- if (runtime === "claude") {
68
- contributions.hookGroups = [
69
- {
70
- event: "PreToolUse",
71
- marker: `${marker}-hook.sh`,
72
- group: { matcher: "Bash", hooks: [{ type: "command", command: `${marker}-hook.sh` }] },
73
- },
74
- ];
75
- }
76
- if (runtime === "opencode" && options.pluginReference) {
77
- const reference = options.pluginReference;
78
- contributions.pluginReferences = [
79
- { reference, isManagedReference: (entry) => entry === reference },
80
- ];
81
- }
82
- return contributions;
83
- }
84
-
85
- /** A capability declaring every contribution kind, each value carrying the owner's marker. */
86
- export function markedCapability(
87
- owner: FixtureOwner,
88
- skillSourceDir: string,
89
- options: FixtureCapabilityOptions = {},
90
- ): CapabilityPlugin {
91
- return {
92
- id: fixturePluginId(owner),
93
- kind: "capability",
94
- label: owner,
95
- description: `${owner} fixture capability`,
96
- defaultSelected: true,
97
- isEnabled: () => true,
98
- apply: options.apply ?? (async () => {}),
99
- teardown: options.teardown ?? (async () => {}),
100
- ...(options.forProvider ? { forProvider: options.forProvider } : {}),
101
- getRuntimeContributions() {
102
- return {
103
- claude: contributionsFor(owner, skillSourceDir, "claude", options),
104
- opencode: contributionsFor(owner, skillSourceDir, "opencode", options),
105
- };
106
- },
107
- };
108
- }
109
-
110
- export function fixtureProvider(id: "claude" | "opencode"): ProviderPlugin {
111
- return {
112
- id,
113
- kind: "provider",
114
- label: id,
115
- description: id,
116
- defaultSelected: true,
117
- isEnabled: () => true,
118
- async apply() {},
119
- async teardown() {},
120
- };
121
- }
122
-
123
- export interface AudienceFixture {
124
- root: string;
125
- companionPath: string;
126
- repoPath: string;
127
- config: FrameworkConfig;
128
- skillSources: Record<FixtureOwner, string>;
129
- /** Providers, the base plugin, and both audiences' plugins, tagged with their source. */
130
- registrations(
131
- options?: Partial<Record<FixtureOwner, FixtureCapabilityOptions>>,
132
- ): PluginRegistration[];
133
- cleanup(): Promise<void>;
134
- }
135
-
136
- const FRAMEWORK_YAML = [
137
- "type: companion",
138
- "allowedAgents:",
139
- " - claude",
140
- " - opencode",
141
- "plugins:",
142
- ' - package: "@fixture/base"',
143
- ' version: "1.0.0"',
144
- "audiences:",
145
- " ba:",
146
- " plugins:",
147
- ' - package: "@fixture/ba"',
148
- ' version: "1.0.0"',
149
- " qa:",
150
- " plugins:",
151
- ' - package: "@fixture/qa"',
152
- ' version: "1.0.0"',
153
- "",
154
- ].join("\n");
155
-
156
- export async function makeAudienceFixture(): Promise<AudienceFixture> {
157
- const root = await fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(), "audience-isolation-")));
158
- const companionPath = path.join(root, "companion");
159
- const repoPath = path.join(root, "repo");
160
- await fs.mkdir(path.join(companionPath, ".mate", "config"), { recursive: true });
161
- await fs.mkdir(repoPath, { recursive: true });
162
- await fs.writeFile(
163
- path.join(companionPath, ".mate", "config", "framework.yaml"),
164
- FRAMEWORK_YAML,
165
- "utf8",
166
- );
167
-
168
- const skillSources = {} as Record<FixtureOwner, string>;
169
- for (const owner of ["base", "ba", "qa"] as const) {
170
- const dir = path.join(root, "skill-sources", `${owner}-skill`);
171
- await fs.mkdir(dir, { recursive: true });
172
- await fs.writeFile(
173
- path.join(dir, "SKILL.md"),
174
- `---\nname: ${owner}-skill\ndescription: ${owner} skill\n---\n${AUDIENCE_MARKERS[owner]} skill body.\n`,
175
- "utf8",
176
- );
177
- skillSources[owner] = dir;
178
- }
179
-
180
- const config: FrameworkConfig = {
181
- type: "companion",
182
- allowedAgents: ["claude", "opencode"],
183
- capabilities: [],
184
- plugins: [{ package: "@fixture/base", version: "1.0.0" }],
185
- audiences: {
186
- ba: { plugins: [{ package: "@fixture/ba", version: "1.0.0" }] },
187
- qa: { plugins: [{ package: "@fixture/qa", version: "1.0.0" }] },
188
- },
189
- } as FrameworkConfig;
190
-
191
- return {
192
- root,
193
- companionPath,
194
- repoPath,
195
- config,
196
- skillSources,
197
- registrations(options = {}) {
198
- return [
199
- { plugin: fixtureProvider("claude"), policy: "required" },
200
- { plugin: fixtureProvider("opencode"), policy: "required" },
201
- {
202
- plugin: markedCapability("base", skillSources.base, options.base),
203
- policy: "required",
204
- audience: null,
205
- packageName: fixturePackage("base"),
206
- },
207
- {
208
- plugin: markedCapability("ba", skillSources.ba, options.ba),
209
- policy: "optional",
210
- audience: "ba",
211
- packageName: fixturePackage("ba"),
212
- },
213
- {
214
- plugin: markedCapability("qa", skillSources.qa, options.qa),
215
- policy: "optional",
216
- audience: "qa",
217
- packageName: fixturePackage("qa"),
218
- },
219
- ];
220
- },
221
- cleanup: () => fs.rm(root, { recursive: true, force: true }),
222
- };
223
- }
224
-
225
- /** Every regular file under `root` (symlinks followed), as relative path → text. Missing root: none. */
226
- export async function readTree(root: string): Promise<Map<string, string>> {
227
- const files = new Map<string, string>();
228
- async function walk(dir: string): Promise<void> {
229
- let entries: Dirent[];
230
- try {
231
- entries = await fs.readdir(dir, { withFileTypes: true });
232
- } catch {
233
- return;
234
- }
235
- for (const entry of entries) {
236
- const full = path.join(dir, entry.name);
237
- const stat = await fs.stat(full).catch(() => null);
238
- if (!stat) continue;
239
- if (stat.isDirectory()) await walk(full);
240
- else if (stat.isFile()) files.set(path.relative(root, full), await fs.readFile(full, "utf8"));
241
- }
242
- }
243
- await walk(root);
244
- return files;
245
- }
246
-
247
- /** `<where>: <marker>` for every marker of `owners` found in `text`. */
248
- export function markerHits(where: string, text: string, owners: readonly FixtureOwner[]): string[] {
249
- return owners
250
- .filter((owner) => text.includes(AUDIENCE_MARKERS[owner]) || ownerIdentityIn(text, owner))
251
- .map((owner) => `${where}: ${owner}`);
252
- }
253
-
254
- /** Identities without the marker (MCP, skill, agent names) still count as the owner's. */
255
- function ownerIdentityIn(text: string, owner: FixtureOwner): boolean {
256
- return [`${owner}-mcp`, `${owner}-skill`, `${owner}-agent`, `${owner}-plugin`].some((identity) =>
257
- text.includes(identity),
258
- );
259
- }
260
-
261
- /** The installed plugin packages: what a plugin is, not what a session loads. */
262
- const PLUGIN_WORKSPACE = `${path.join(".mate", "plugins")}${path.sep}`;
263
-
264
- /** Marker hits of `owners` in every file (path and content) under `root`, the plugin workspace aside. */
265
- export async function markerHitsInTree(
266
- root: string,
267
- owners: readonly FixtureOwner[],
268
- skip: readonly string[] = [],
269
- ): Promise<string[]> {
270
- const hits: string[] = [];
271
- for (const [relative, text] of await readTree(root)) {
272
- if (relative.startsWith(PLUGIN_WORKSPACE)) continue;
273
- if (skip.some((prefix) => relative.startsWith(prefix))) continue;
274
- hits.push(...markerHits(relative, `${relative}\n${text}`, owners));
275
- }
276
- return hits;
277
- }
278
-
279
- /** Marker hits of `owners` in any JSON-serializable value: rendered documents, launch arguments. */
280
- export function markerHitsInValue(
281
- where: string,
282
- value: unknown,
283
- owners: readonly FixtureOwner[],
284
- ): string[] {
285
- return markerHits(where, JSON.stringify(value), owners);
286
- }
287
-
288
- /**
289
- * Installs `owner`'s fixture plugin into the companion's plugin workspace, so
290
- * a pass can load it from its declaration alone. Declares exactly what
291
- * `markedCapability` declares, hook groups and all.
292
- */
293
- export async function installFixturePackage(
294
- fixture: AudienceFixture,
295
- owner: FixtureOwner,
296
- ): Promise<void> {
297
- const name = fixturePackage(owner);
298
- const root = pluginPackageRoot(fixture.companionPath, name);
299
- await fs.mkdir(root, { recursive: true });
300
- await fs.writeFile(
301
- path.join(root, "package.json"),
302
- JSON.stringify({ name, version: "1.0.0", type: "module", main: "index.js" }),
303
- "utf8",
304
- );
305
- const declared = markedCapability(owner, fixture.skillSources[owner]).getRuntimeContributions!(
306
- {} as never,
307
- );
308
- const source = `
309
- const contributions = ${JSON.stringify(declared)};
310
- export default function createPlugin() {
311
- return {
312
- id: ${JSON.stringify(fixturePluginId(owner))},
313
- kind: "capability",
314
- label: ${JSON.stringify(owner)},
315
- description: "installed fixture",
316
- defaultSelected: true,
317
- isEnabled: () => true,
318
- apply: async () => {},
319
- teardown: async () => {},
320
- getRuntimeContributions: () => contributions,
321
- };
322
- }
323
- `;
324
- await fs.writeFile(path.join(root, "index.js"), source, "utf8");
325
- }
@@ -1,207 +0,0 @@
1
- // oxlint-disable no-await-in-loop
2
- import crypto from "node:crypto";
3
- import fs from "node:fs/promises";
4
- import path from "node:path";
5
-
6
- import { getActiveDistribution } from "../../distribution";
7
- import type { FrameworkConfig } from "../../lib/orchestrator/types";
8
- import {
9
- audienceLayerDir,
10
- audienceLayersRoot,
11
- CURRENT_LAYER_LINK,
12
- } from "../../runtime/audience-layer";
13
- import {
14
- dynamicPluginsWorkspaceRoot,
15
- pluginLocalOverridesPath,
16
- pluginPackageRoot,
17
- } from "./dynamic-plugins/paths";
18
- import { collectAudienceInputs, RUNTIME_SURFACE_RECONCILERS } from "./engine";
19
- import type { PluginRegistration, SetupContext } from "./plugin";
20
- import { projectionTarget } from "./surface-target";
21
-
22
- /**
23
- * Writes the active audience's layer through each runtime's Runtime Surface,
24
- * under the pass's layer root only. Refuses any other target, and a pass that
25
- * names no audience.
26
- */
27
- export async function reconcileAudienceLayer(
28
- ctx: SetupContext,
29
- registrations: PluginRegistration[],
30
- ): Promise<void> {
31
- if (projectionTarget(ctx) !== "audience-layer") {
32
- throw new Error("an audience layer is reconciled with the audience-layer target only");
33
- }
34
- for (const [runtimeId, inputs] of await collectAudienceInputs(
35
- ctx,
36
- registrations,
37
- ctx.audience!,
38
- )) {
39
- const reconcile = RUNTIME_SURFACE_RECONCILERS[runtimeId];
40
- if (!reconcile || inputs.length === 0) continue;
41
- await reconcile(ctx, inputs);
42
- }
43
- }
44
-
45
- /** Bumped when the layout a layer pass writes changes, so old layers are re-rendered. */
46
- const LAYER_FORMAT = 1;
47
-
48
- /** A render that never finished, or a superseded layer, is left this long for sessions still reading it. */
49
- const STALE_LAYER_MS = 24 * 60 * 60 * 1000;
50
-
51
- const FINGERPRINT_FILE = ".fingerprint";
52
-
53
- export interface AudienceLayerRequest {
54
- companionPath: string;
55
- config: FrameworkConfig;
56
- audience: string;
57
- /** The Agent Runtimes the layer is rendered for. */
58
- activeProviders: string[];
59
- /** The process's registrations; only the audience's own are rendered. */
60
- registrations: PluginRegistration[];
61
- homeDir?: string;
62
- }
63
-
64
- export interface RenderedAudienceLayer {
65
- audience: string;
66
- /** The complete layer a launch attaches; never changed once in place. */
67
- root: string;
68
- /** False when an unchanged layer was reused. */
69
- rendered: boolean;
70
- }
71
-
72
- function isRecord(value: unknown): value is Record<string, unknown> {
73
- return typeof value === "object" && value !== null && !Array.isArray(value);
74
- }
75
-
76
- /** The installed version of each package, from the committed lockfile, else the installed manifest. */
77
- async function lockedVersions(
78
- companionPath: string,
79
- packages: string[],
80
- ): Promise<Record<string, string | null>> {
81
- const lock = await fs
82
- .readFile(path.join(dynamicPluginsWorkspaceRoot(companionPath), "package-lock.json"), "utf8")
83
- .then((text) => JSON.parse(text) as unknown)
84
- .catch(() => null);
85
- const lockPackages = isRecord(lock) && isRecord(lock.packages) ? lock.packages : {};
86
- const versions: Record<string, string | null> = {};
87
- for (const name of packages.toSorted()) {
88
- const locked = lockPackages[`node_modules/${name}`];
89
- if (isRecord(locked) && typeof locked.version === "string") {
90
- versions[name] = locked.version;
91
- continue;
92
- }
93
- const manifest = await fs
94
- .readFile(path.join(pluginPackageRoot(companionPath, name), "package.json"), "utf8")
95
- .then((text) => JSON.parse(text) as unknown)
96
- .catch(() => null);
97
- versions[name] =
98
- isRecord(manifest) && typeof manifest.version === "string" ? manifest.version : null;
99
- }
100
- return versions;
101
- }
102
-
103
- /**
104
- * What a layer depends on: the audience's declaration, the installed versions
105
- * of its packages, the local plugin overrides, the Mate version and the active
106
- * runtimes. Anything else changing leaves the layer as it is.
107
- */
108
- export async function audienceLayerFingerprint(request: AudienceLayerRequest): Promise<string> {
109
- const entry = request.config.audiences?.[request.audience] ?? null;
110
- const packages = (entry?.plugins ?? []).map((declaration) => declaration.package);
111
- const overrides = await fs
112
- .readFile(pluginLocalOverridesPath(request.companionPath), "utf8")
113
- .catch(() => null);
114
- const inputs = {
115
- format: LAYER_FORMAT,
116
- audience: request.audience,
117
- entry,
118
- versions: await lockedVersions(request.companionPath, packages),
119
- overrides,
120
- mate: getActiveDistribution().config.version,
121
- runtimes: [...new Set(request.activeProviders)].toSorted(),
122
- };
123
- return crypto.createHash("sha256").update(JSON.stringify(inputs)).digest("hex").slice(0, 24);
124
- }
125
-
126
- async function exists(target: string): Promise<boolean> {
127
- return fs
128
- .access(target)
129
- .then(() => true)
130
- .catch(() => false);
131
- }
132
-
133
- /** Points `current` at `fingerprint` with one rename, so a reader never finds it missing. */
134
- async function pointCurrentAt(dir: string, fingerprint: string): Promise<void> {
135
- const link = path.join(dir, CURRENT_LAYER_LINK);
136
- if ((await fs.readlink(link).catch(() => null)) === fingerprint) return;
137
- const staged = path.join(dir, `.${CURRENT_LAYER_LINK}-${crypto.randomUUID()}`);
138
- await fs.symlink(fingerprint, staged);
139
- await fs.rename(staged, link);
140
- }
141
-
142
- /** Superseded layers and abandoned renders, once no session can still be starting from them. */
143
- async function pruneStaleLayers(dir: string, keep: string): Promise<void> {
144
- const now = Date.now();
145
- for (const name of await fs.readdir(dir).catch(() => [])) {
146
- /** Launch settings files live here too; each launch rewrites its own. */
147
- if (name === keep || name === CURRENT_LAYER_LINK || name.startsWith("launch-")) continue;
148
- const target = path.join(dir, name);
149
- const stat = await fs.lstat(target).catch(() => null);
150
- if (!stat || now - stat.mtimeMs < STALE_LAYER_MS) continue;
151
- await fs.rm(target, { recursive: true, force: true });
152
- }
153
- }
154
-
155
- /** Creates the layers root and every level below it for the current user only. */
156
- async function ensurePrivateDir(root: string, dir: string): Promise<void> {
157
- await fs.mkdir(dir, { recursive: true, mode: 0o700 });
158
- for (let level = dir; level.startsWith(root); level = path.dirname(level)) {
159
- await fs.chmod(level, 0o700);
160
- if (level === root) break;
161
- }
162
- }
163
-
164
- /**
165
- * Renders the audience's layer, or reuses it when nothing it depends on
166
- * changed. A render is built in a private staging directory and renamed into a
167
- * directory named by its fingerprint, so a reader sees a complete layer or
168
- * none; concurrent renders of the same inputs agree on one result.
169
- */
170
- export async function renderAudienceLayer(
171
- request: AudienceLayerRequest,
172
- ): Promise<RenderedAudienceLayer> {
173
- const dir = audienceLayerDir(request.companionPath, request.audience, request.homeDir);
174
- const fingerprint = await audienceLayerFingerprint(request);
175
- const root = path.join(dir, fingerprint);
176
-
177
- await ensurePrivateDir(audienceLayersRoot(request.homeDir), dir);
178
- if (await exists(path.join(root, FINGERPRINT_FILE))) {
179
- await pointCurrentAt(dir, fingerprint);
180
- return { audience: request.audience, root, rendered: false };
181
- }
182
-
183
- const staging = await fs.mkdtemp(path.join(dir, ".render-"));
184
- try {
185
- await reconcileAudienceLayer(
186
- {
187
- companionPath: request.companionPath,
188
- config: request.config,
189
- mode: "sync",
190
- activeProviders: request.activeProviders,
191
- target: "audience-layer",
192
- audience: request.audience,
193
- layerRoot: staging,
194
- },
195
- request.registrations,
196
- );
197
- await fs.writeFile(path.join(staging, FINGERPRINT_FILE), `${fingerprint}\n`, "utf8");
198
- await fs.rename(staging, root);
199
- } catch (error) {
200
- await fs.rm(staging, { recursive: true, force: true });
201
- /** Another launch renamed the same inputs into place first: its layer is this one. */
202
- if (!(await exists(path.join(root, FINGERPRINT_FILE)))) throw error;
203
- }
204
- await pointCurrentAt(dir, fingerprint);
205
- await pruneStaleLayers(dir, fingerprint);
206
- return { audience: request.audience, root, rendered: true };
207
- }
@@ -1,52 +0,0 @@
1
- // oxlint-disable no-await-in-loop
2
- import { getActiveDistribution } from "../../distribution";
3
- import type { FrameworkConfig } from "../../lib/orchestrator/types";
4
- import { validateDeclaration } from "./dynamic-plugins/declarations";
5
- import { loadDynamicPlugin, type DynamicPluginLoadDeps } from "./dynamic-plugins/loader";
6
- import type { PluginRegistry, NormalizedRegistration } from "./registry";
7
-
8
- export interface LoadedAudience {
9
- registrations: NormalizedRegistration[];
10
- warnings: string[];
11
- }
12
-
13
- /**
14
- * The registrations of one named audience, independent of `MATE_AUDIENCE`: a
15
- * plugin this process already hydrated is reused, any other is loaded for its
16
- * declarations only — never activated, no lifecycle hook run. Used where the
17
- * audience comes from a checkout's wrap rather than the environment.
18
- */
19
- export async function loadAudienceRegistrations(
20
- companionPath: string,
21
- config: FrameworkConfig,
22
- audience: string,
23
- registry: PluginRegistry = getActiveDistribution().registry,
24
- deps: DynamicPluginLoadDeps = {},
25
- ): Promise<LoadedAudience> {
26
- const loaded: LoadedAudience = { registrations: [], warnings: [] };
27
- const hydrated = registry.getEntries();
28
- for (const entry of config.audiences?.[audience]?.plugins ?? []) {
29
- const { declaration, error } = validateDeclaration(entry);
30
- if (!declaration) {
31
- if (error) loaded.warnings.push(error);
32
- continue;
33
- }
34
- const known = hydrated.find((candidate) => candidate.packageName === declaration.package);
35
- if (known) {
36
- loaded.registrations.push({ ...known, audience });
37
- continue;
38
- }
39
- const result = await loadDynamicPlugin(companionPath, declaration, deps);
40
- if (!result.ok) {
41
- loaded.warnings.push(`audience "${audience}": ${result.warning}`);
42
- continue;
43
- }
44
- loaded.registrations.push({
45
- plugin: result.plugin,
46
- policy: "required",
47
- audience,
48
- packageName: declaration.package,
49
- });
50
- }
51
- return loaded;
52
- }