@uniqbit/mate-core 0.18.4 → 1.0.0-canary.0
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/claude-plugin/hooks/companion-instructions.mjs +9 -0
- package/claude-plugin/hooks/hooks.json +12 -2
- package/opencode-plugin/src/add-dir.ts +76 -10
- package/opencode-plugin/src/companion.ts +1 -8
- package/opencode-plugin/src/server.ts +2 -15
- package/package.json +10 -2
- package/src/brand/art.generated.ts +69 -0
- package/src/brand/index.ts +12 -0
- package/src/brand/palette.ts +19 -0
- package/src/brand/svg.generated.ts +8 -0
- package/src/brand/terminal.ts +83 -0
- package/src/brand/types.ts +33 -0
- package/src/cli/commands/artifact/artifact.ts +8 -0
- package/src/cli/commands/artifact/delivery/archive.ts +117 -0
- package/src/cli/commands/artifact/delivery/branch-flags.ts +55 -0
- package/src/cli/commands/artifact/delivery/environment.ts +104 -0
- package/src/cli/commands/artifact/delivery/link.ts +154 -0
- package/src/cli/commands/artifact/delivery/output.ts +58 -0
- package/src/cli/commands/artifact/finish/command.ts +33 -3
- package/src/cli/commands/artifact/finish/engine.ts +47 -3
- package/src/cli/commands/artifact/finish/finisher.ts +10 -0
- package/src/cli/commands/artifact/finish/openspec.ts +200 -129
- package/src/cli/commands/artifact/finish/secret-scan.ts +43 -0
- package/src/cli/commands/artifact/pending/command.ts +31 -3
- package/src/cli/commands/artifact/pending/discovery.ts +21 -57
- package/src/cli/commands/cap/graphify.ts +52 -3
- package/src/cli/commands/cap/index-cmd.ts +8 -1
- package/src/cli/commands/cap/openspec.ts +13 -25
- package/src/cli/commands/companion/open.ts +56 -39
- package/src/cli/commands/companion/update.ts +75 -42
- package/src/cli/commands/doctor.ts +257 -78
- package/src/cli/commands/install.ts +0 -8
- package/src/cli/commands/plugin/install.ts +9 -4
- package/src/cli/commands/plugin/plugin.ts +3 -2
- package/src/cli/commands/report/adapter.ts +11 -2
- package/src/cli/commands/report/contract.ts +21 -0
- package/src/cli/commands/report/highlight.ts +34 -4
- package/src/cli/commands/report/markdown.ts +110 -0
- package/src/cli/commands/report/palette.ts +118 -0
- package/src/cli/commands/report/renderer.ts +456 -153
- package/src/cli/commands/report/types.ts +16 -0
- package/src/cli/commands/setup.ts +7 -15
- package/src/cli/commands/studio/client-asset.ts +16 -0
- package/src/cli/commands/studio/openspec-cli.ts +10 -58
- package/src/cli/commands/studio/routes.ts +2 -0
- package/src/cli/commands/studio/selection.ts +19 -1
- package/src/cli/commands/studio/server.ts +42 -1
- package/src/cli/commands/studio/terminal-launch.ts +7 -16
- package/src/cli/commands/studio/views/client.ts +6 -616
- package/src/cli/commands/studio/views/dashboard/changes.tsx +5 -10
- package/src/cli/commands/studio/views/document.tsx +137 -69
- package/src/cli/commands/studio/views/markdown.tsx +250 -0
- package/src/cli/commands/studio/views/palette.ts +134 -0
- package/src/cli/commands/studio/views/reports/index.tsx +2 -7
- package/src/cli/commands/studio/views/skills/index.tsx +43 -62
- package/src/cli/commands/studio/views/specs/index.tsx +7 -15
- package/src/cli/commands/studio/views/studio-client/actions.ts +51 -0
- package/src/cli/commands/studio/views/studio-client/companion.ts +13 -0
- package/src/cli/commands/studio/views/studio-client/index.ts +32 -0
- package/src/cli/commands/studio/views/studio-client/keys.ts +3 -0
- package/src/cli/commands/studio/views/studio-client/main.ts +3 -0
- package/src/cli/commands/studio/views/studio-client/page.ts +146 -0
- package/src/cli/commands/studio/views/studio-client/panels.ts +57 -0
- package/src/cli/commands/studio/views/studio-client/reports.ts +122 -0
- package/src/cli/commands/studio/views/studio-client/theme.ts +63 -0
- package/src/cli/commands/studio/views/studio-client/vault-editor.ts +166 -0
- package/src/cli/commands/studio/views/studio-client/vault-pane.ts +202 -0
- package/src/cli/commands/studio/views/studio-client/vault-tree.ts +258 -0
- package/src/cli/commands/studio/views/studio-client.generated.ts +2 -0
- package/src/cli/commands/studio/views/styles.ts +217 -206
- package/src/cli/commands/studio/views/vault/state.ts +3 -5
- package/src/cli/commands/studio/views/workflow/delivery-path.tsx +370 -0
- package/src/cli/commands/studio/views/workflow/graph.ts +220 -0
- package/src/cli/commands/studio/views/workflow/index.tsx +2 -3
- package/src/cli/commands/studio/views/workflow/steps.ts +228 -287
- package/src/cli/commands/wrap.ts +9 -42
- package/src/cli/main.ts +4 -1
- package/src/cli/parse-flags.ts +25 -7
- package/src/cli/setup-selector.tsx +38 -162
- package/src/cli/usage.ts +5 -2
- package/src/hooks/companion-instructions.ts +190 -0
- package/src/hooks/hook-io.ts +63 -0
- package/src/hooks/session-banner.ts +21 -29
- package/src/hooks/session-guidance.ts +10 -36
- package/src/hooks/session-prepare.ts +115 -28
- package/src/hooks/validate-artifact-path.ts +82 -24
- package/src/index.ts +4 -0
- package/src/lib/change-layout.ts +154 -0
- package/src/lib/components/wizard-header.tsx +3 -1
- package/src/lib/context-mode-package.ts +24 -11
- package/src/lib/delivery/archive-change.ts +154 -0
- package/src/lib/delivery/branches.ts +108 -0
- package/src/lib/delivery/environment.ts +29 -0
- package/src/lib/delivery/forge.ts +279 -0
- package/src/lib/delivery/overlap.ts +202 -0
- package/src/lib/delivery/record-delivery.ts +196 -0
- package/src/lib/delivery/record.ts +343 -0
- package/src/lib/delivery/refresh.ts +174 -0
- package/src/lib/delivery/repository-access.ts +97 -0
- package/src/lib/delivery/stages.ts +111 -0
- package/src/lib/delivery/token.ts +58 -0
- package/src/lib/frontmatter.ts +18 -0
- package/src/lib/opencode-plugin-package.ts +5 -5
- package/src/lib/openspec-cli.ts +116 -0
- package/src/lib/openspec-scopes.ts +86 -0
- package/src/lib/orchestrator/adapters/claude-args.ts +50 -0
- package/src/lib/orchestrator/adapters/claude.ts +60 -162
- package/src/lib/orchestrator/adapters/opencode.ts +9 -24
- package/src/lib/orchestrator/config-store.ts +2 -70
- package/src/lib/orchestrator/editor.ts +9 -9
- package/src/lib/orchestrator/openspec-store.ts +14 -54
- package/src/lib/orchestrator/projection-claude-commands.ts +36 -0
- package/src/lib/orchestrator/projection-claude-links.ts +159 -0
- package/src/lib/orchestrator/projection-claude-skills.ts +19 -190
- package/src/lib/orchestrator/projection-entries.ts +45 -15
- package/src/lib/orchestrator/projection-runtime-documents.ts +15 -6
- package/src/lib/orchestrator/projection-types.ts +3 -2
- package/src/lib/orchestrator/refresh-working-claude-links.ts +87 -0
- package/src/lib/orchestrator/session-prepare.ts +49 -87
- package/src/lib/orchestrator/setup-compatibilities.ts +18 -75
- package/src/lib/orchestrator/types.ts +2 -2
- package/src/lib/orchestrator/wrap-pass.ts +22 -88
- package/src/lib/package-paths.ts +8 -0
- package/src/opencode/companion-hooks.ts +64 -5
- package/src/opencode/companion-policy.ts +37 -18
- package/src/opencode/projected-guidance.ts +4 -1
- package/src/opencode/session-prepare.ts +2 -1
- package/src/opencode/tui.tsx +4 -2
- package/src/runtime/artifact-refusal.ts +63 -0
- package/src/runtime/claude-delivery-plan.ts +89 -7
- package/src/runtime/commit-attribution-guard.ts +181 -0
- package/src/runtime/companion-guidance.ts +15 -3
- package/src/runtime/companion-instructions.ts +340 -0
- package/src/runtime/error-message.ts +4 -0
- package/src/runtime/guidance.ts +0 -17
- package/src/runtime/index.ts +5 -0
- package/src/runtime/managed-block.ts +117 -0
- package/src/runtime/markdown-scan.ts +266 -0
- package/src/runtime/policy.ts +16 -15
- package/src/runtime/projected-flag.ts +7 -0
- package/src/runtime/projected-guidance.ts +1 -0
- package/src/runtime/runtime-document-manifest.ts +62 -28
- package/src/runtime/secret-guard.ts +148 -0
- package/src/runtime/secret-patterns.ts +164 -0
- package/src/runtime/secret-scan.ts +90 -0
- package/src/runtime/session-kind.ts +13 -0
- package/src/runtime/shell-heredoc.ts +81 -0
- package/src/runtime/wrap-state.ts +36 -6
- package/src/runtime/write-atomic.ts +64 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +8 -6
- package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +3 -5
- package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +7 -9
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
- package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +10 -12
- package/src/templates/mate-skills/THIRD-PARTY-NOTICES.md +40 -0
- package/src/templates/mate-skills/agents/mate-apply-change/SKILL.md +76 -0
- package/src/templates/mate-skills/agents/mate-apply-change/references/openspec-apply-change.md +198 -0
- package/src/templates/mate-skills/agents/mate-archive-change/SKILL.md +59 -0
- package/src/templates/mate-skills/{claude/mate-openspec-backfill → agents/mate-backfill-spec}/SKILL.md +4 -4
- package/src/templates/mate-skills/agents/mate-clarify-intent/SKILL.md +95 -0
- package/src/templates/mate-skills/agents/mate-explore-change/SKILL.md +50 -0
- package/src/templates/mate-skills/agents/mate-explore-change/references/openspec-explore.md +348 -0
- package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +3 -3
- package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +5 -5
- package/src/templates/mate-skills/agents/mate-propose-change/SKILL.md +103 -0
- package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-propose.md +167 -0
- package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-update-change.md +101 -0
- package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/SKILL.md +91 -23
- package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
- package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +74 -16
- package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +74 -68
- package/src/templates/mate-skills/claude/mate-apply-change/SKILL.md +76 -0
- package/src/templates/mate-skills/claude/mate-apply-change/references/openspec-apply-change.md +198 -0
- package/src/templates/mate-skills/claude/mate-archive-change/SKILL.md +59 -0
- package/src/templates/mate-skills/{agents/mate-openspec-backfill → claude/mate-backfill-spec}/SKILL.md +4 -4
- package/src/templates/mate-skills/claude/mate-clarify-intent/SKILL.md +95 -0
- package/src/templates/mate-skills/claude/mate-explore-change/SKILL.md +50 -0
- package/src/templates/mate-skills/claude/mate-explore-change/references/openspec-explore.md +348 -0
- package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +3 -3
- package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +5 -5
- package/src/templates/mate-skills/claude/mate-propose-change/SKILL.md +103 -0
- package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-propose.md +167 -0
- package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-update-change.md +101 -0
- package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/SKILL.md +91 -23
- package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
- package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +74 -16
- package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +74 -68
- package/src/templates/mate-skills/openspec-references.ts +140 -0
- package/src/templates/root/TEMPLATE_AGENTS.md +2 -2
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +3727 -320
- package/src/tools/setup/capabilities/context-mode.ts +25 -5
- package/src/tools/setup/capabilities/graphify-gate.ts +31 -0
- package/src/tools/setup/capabilities/graphify-secrets.ts +255 -0
- package/src/tools/setup/capabilities/graphify.ts +12 -0
- package/src/tools/setup/capabilities/openspec.ts +273 -270
- package/src/tools/setup/capabilities/tokensave.ts +36 -3
- package/src/tools/setup/context-services.ts +9 -17
- package/src/tools/setup/dynamic-plugins/audiences.ts +7 -39
- package/src/tools/setup/dynamic-plugins/declarations.ts +1 -1
- package/src/tools/setup/dynamic-plugins/hydrate.ts +4 -7
- package/src/tools/setup/dynamic-plugins/install.ts +24 -28
- package/src/tools/setup/engine.ts +93 -148
- package/src/tools/setup/managed-output.ts +51 -0
- package/src/tools/setup/mate.ts +86 -11
- package/src/tools/setup/plugin.ts +37 -17
- package/src/tools/setup/plugins/gitignore.ts +1 -7
- package/src/tools/setup/plugins/guidance.ts +7 -4
- package/src/tools/setup/providers/agent-file-sections.ts +4 -2
- package/src/tools/setup/providers/claude-attribution.ts +65 -0
- package/src/tools/setup/providers/claude-plugin-folders.ts +625 -0
- package/src/tools/setup/providers/claude-plugin-hooks.ts +39 -19
- package/src/tools/setup/providers/claude.ts +285 -34
- package/src/tools/setup/providers/opencode.ts +180 -11
- package/src/tools/setup/registry.ts +2 -24
- package/src/tools/setup/surface-target.ts +4 -13
- package/src/tools/setup/utils.ts +14 -4
- package/src/tools/setup.ts +131 -18
- package/wrappers/bin/graphify +52 -4
- package/wrappers/bin/openspec +33 -9
- package/wrappers/lib/graphify-gate.mjs +8 -0
- package/opencode-plugin/src/audience-layer.ts +0 -91
- package/src/cli/commands/studio/views/workflow/transcript.tsx +0 -190
- package/src/runtime/audience-layer.ts +0 -84
- package/src/runtime/opencode-audience-layer.ts +0 -176
- package/src/tools/setup/audience-isolation.fixture.ts +0 -325
- package/src/tools/setup/audience-layer.ts +0 -207
- package/src/tools/setup/audience-registrations.ts +0 -52
- package/src/tools/setup/contribution-ledger.ts +0 -207
- package/src/tools/setup/isolation-diagnostics.ts +0 -139
- package/src/tools/setup/runtime-documents.ts +0 -291
- package/src/tools/setup/upgrade-sweep.ts +0 -153
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
import { FRAMEWORK_NAME } from "../../framework";
|
|
5
|
-
import { writeAtomic } from "../../runtime/write-atomic";
|
|
6
|
-
import { instructionBlockKey } from "./context-services";
|
|
7
|
-
import type { CapabilityContributionInput, RuntimeContributions } from "./plugin";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Contribution Ledger: which plugin owns each contribution a persisted pass
|
|
11
|
-
* applied to the companion, per Agent Runtime. Committed with the companion,
|
|
12
|
-
* because it describes committed files: a clone that never loaded a plugin
|
|
13
|
-
* must still be able to remove what another clone committed. It records
|
|
14
|
-
* identities only (names, block keys, markers, entries), never a value, so it
|
|
15
|
-
* carries no secret.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
export const CONTRIBUTION_LEDGER_VERSION = 1;
|
|
19
|
-
|
|
20
|
-
/** The identities one owner applied on one runtime. Absent kinds are none. */
|
|
21
|
-
export interface LedgerIdentities {
|
|
22
|
-
mcpServers?: string[];
|
|
23
|
-
guidanceKeys?: string[];
|
|
24
|
-
skillTrees?: string[];
|
|
25
|
-
agentDefinitions?: string[];
|
|
26
|
-
hookMarkers?: string[];
|
|
27
|
-
permissionEntries?: string[];
|
|
28
|
-
pluginReferences?: string[];
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface ContributionLedger {
|
|
32
|
-
version: number;
|
|
33
|
-
/** Runtime id → owning plugin id → identities. */
|
|
34
|
-
runtimes: Record<string, Record<string, LedgerIdentities>>;
|
|
35
|
-
/** Audience plugin packages the upgrade sweep could not load; retried by every pass. */
|
|
36
|
-
unswept?: string[];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function contributionLedgerPath(companionPath: string): string {
|
|
40
|
-
return path.join(companionPath, `.${FRAMEWORK_NAME}`, "config", "contribution-ledger.json");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
44
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function stringList(value: unknown): string[] {
|
|
48
|
-
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const IDENTITY_KINDS = [
|
|
52
|
-
"agentDefinitions",
|
|
53
|
-
"guidanceKeys",
|
|
54
|
-
"hookMarkers",
|
|
55
|
-
"mcpServers",
|
|
56
|
-
"permissionEntries",
|
|
57
|
-
"pluginReferences",
|
|
58
|
-
"skillTrees",
|
|
59
|
-
] as const satisfies readonly (keyof LedgerIdentities)[];
|
|
60
|
-
|
|
61
|
-
function sortedUnique(values: Iterable<string>): string[] {
|
|
62
|
-
return [...new Set(values)].toSorted();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Sorted kinds and values, empty kinds dropped; `null` when nothing is left. */
|
|
66
|
-
function normalizeIdentities(identities: LedgerIdentities): LedgerIdentities | null {
|
|
67
|
-
const normalized: LedgerIdentities = {};
|
|
68
|
-
for (const kind of IDENTITY_KINDS) {
|
|
69
|
-
const values = sortedUnique(identities[kind] ?? []);
|
|
70
|
-
if (values.length > 0) normalized[kind] = values;
|
|
71
|
-
}
|
|
72
|
-
return Object.keys(normalized).length > 0 ? normalized : null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** Sorted runtimes and owners; empty owners and runtimes dropped. */
|
|
76
|
-
function normalizeLedger(ledger: ContributionLedger): ContributionLedger {
|
|
77
|
-
const runtimes: ContributionLedger["runtimes"] = {};
|
|
78
|
-
for (const runtimeId of Object.keys(ledger.runtimes).toSorted()) {
|
|
79
|
-
const owners: Record<string, LedgerIdentities> = {};
|
|
80
|
-
for (const owner of Object.keys(ledger.runtimes[runtimeId] ?? {}).toSorted()) {
|
|
81
|
-
const identities = normalizeIdentities(ledger.runtimes[runtimeId]![owner]!);
|
|
82
|
-
if (identities) owners[owner] = identities;
|
|
83
|
-
}
|
|
84
|
-
if (Object.keys(owners).length > 0) runtimes[runtimeId] = owners;
|
|
85
|
-
}
|
|
86
|
-
const unswept = sortedUnique(ledger.unswept ?? []);
|
|
87
|
-
return {
|
|
88
|
-
version: CONTRIBUTION_LEDGER_VERSION,
|
|
89
|
-
runtimes,
|
|
90
|
-
...(unswept.length > 0 ? { unswept } : {}),
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** The companion's ledger; `null` when absent or unreadable, which a pass treats as never recorded. */
|
|
95
|
-
export async function readContributionLedger(
|
|
96
|
-
companionPath: string,
|
|
97
|
-
): Promise<ContributionLedger | null> {
|
|
98
|
-
let parsed: unknown;
|
|
99
|
-
try {
|
|
100
|
-
parsed = JSON.parse(await fs.readFile(contributionLedgerPath(companionPath), "utf8"));
|
|
101
|
-
} catch {
|
|
102
|
-
return null;
|
|
103
|
-
}
|
|
104
|
-
if (!isRecord(parsed) || !isRecord(parsed.runtimes)) return null;
|
|
105
|
-
const runtimes: ContributionLedger["runtimes"] = {};
|
|
106
|
-
for (const [runtimeId, owners] of Object.entries(parsed.runtimes)) {
|
|
107
|
-
if (!isRecord(owners)) continue;
|
|
108
|
-
runtimes[runtimeId] = {};
|
|
109
|
-
for (const [owner, identities] of Object.entries(owners)) {
|
|
110
|
-
if (!isRecord(identities)) continue;
|
|
111
|
-
const read: LedgerIdentities = {};
|
|
112
|
-
for (const kind of IDENTITY_KINDS) read[kind] = stringList(identities[kind]);
|
|
113
|
-
runtimes[runtimeId][owner] = read;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
return normalizeLedger({
|
|
117
|
-
version: CONTRIBUTION_LEDGER_VERSION,
|
|
118
|
-
runtimes,
|
|
119
|
-
unswept: stringList(parsed.unswept),
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** Writes the ledger in its stable form; an unchanged ledger leaves the file untouched. */
|
|
124
|
-
export async function writeContributionLedger(
|
|
125
|
-
companionPath: string,
|
|
126
|
-
ledger: ContributionLedger,
|
|
127
|
-
): Promise<void> {
|
|
128
|
-
const target = contributionLedgerPath(companionPath);
|
|
129
|
-
const text = `${JSON.stringify(normalizeLedger(ledger), null, 2)}\n`;
|
|
130
|
-
const existing = await fs.readFile(target, "utf8").catch(() => null);
|
|
131
|
-
if (existing === text) return;
|
|
132
|
-
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
133
|
-
await writeAtomic(target, text);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/** The identities a contribution set applies for `pluginId`. */
|
|
137
|
-
export function identitiesOf(
|
|
138
|
-
pluginId: string,
|
|
139
|
-
contributions: RuntimeContributions,
|
|
140
|
-
): LedgerIdentities {
|
|
141
|
-
return {
|
|
142
|
-
mcpServers: (contributions.mcpServers ?? []).map((server) => server.name),
|
|
143
|
-
guidanceKeys: (contributions.guidanceSections ?? []).map((section) =>
|
|
144
|
-
instructionBlockKey(pluginId, section.content),
|
|
145
|
-
),
|
|
146
|
-
skillTrees: (contributions.skillTrees ?? []).map((tree) => tree.name),
|
|
147
|
-
agentDefinitions: (contributions.agentDefinitions ?? []).map((agent) => agent.name),
|
|
148
|
-
hookMarkers: (contributions.hookGroups ?? []).map((hook) => hook.marker),
|
|
149
|
-
permissionEntries: [...(contributions.permissionEntries ?? [])],
|
|
150
|
-
pluginReferences: (contributions.pluginReferences ?? []).map((entry) => entry.reference),
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/** The ledger a pass leaves: what its enabled inputs applied, per runtime and owner. */
|
|
155
|
-
export function ledgerFromInputs(
|
|
156
|
-
inputsByRuntime: ReadonlyMap<string, readonly CapabilityContributionInput[]>,
|
|
157
|
-
unswept: readonly string[] = [],
|
|
158
|
-
): ContributionLedger {
|
|
159
|
-
const runtimes: ContributionLedger["runtimes"] = {};
|
|
160
|
-
for (const [runtimeId, inputs] of inputsByRuntime) {
|
|
161
|
-
const owners: Record<string, LedgerIdentities> = {};
|
|
162
|
-
for (const input of inputs) {
|
|
163
|
-
if (!input.enabled) continue;
|
|
164
|
-
const identities = identitiesOf(input.pluginId, input.contributions);
|
|
165
|
-
const merged = owners[input.pluginId] ?? {};
|
|
166
|
-
for (const kind of IDENTITY_KINDS) {
|
|
167
|
-
merged[kind] = [...(merged[kind] ?? []), ...(identities[kind] ?? [])];
|
|
168
|
-
}
|
|
169
|
-
owners[input.pluginId] = merged;
|
|
170
|
-
}
|
|
171
|
-
runtimes[runtimeId] = owners;
|
|
172
|
-
}
|
|
173
|
-
return normalizeLedger({ version: CONTRIBUTION_LEDGER_VERSION, runtimes, unswept: [...unswept] });
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* A disabled input naming every recorded identity of `owner`, so each Runtime
|
|
178
|
-
* Surface removes them through its existing disabled-input path. Removal keys
|
|
179
|
-
* on identity only, so no value is needed: guidance is stripped by owner, a
|
|
180
|
-
* hook group by its marker, a plugin reference by its exact entry.
|
|
181
|
-
*/
|
|
182
|
-
export function ledgerTeardownInput(
|
|
183
|
-
owner: string,
|
|
184
|
-
identities: LedgerIdentities,
|
|
185
|
-
): CapabilityContributionInput {
|
|
186
|
-
return {
|
|
187
|
-
pluginId: owner,
|
|
188
|
-
enabled: false,
|
|
189
|
-
capabilityEnabled: false,
|
|
190
|
-
contributions: {
|
|
191
|
-
mcpServers: (identities.mcpServers ?? []).map((name) => ({ name })),
|
|
192
|
-
guidanceSections: (identities.guidanceKeys ?? []).length > 0 ? [{ content: "" }] : [],
|
|
193
|
-
skillTrees: (identities.skillTrees ?? []).map((name) => ({ name, sourceDir: "" })),
|
|
194
|
-
agentDefinitions: (identities.agentDefinitions ?? []).map((name) => ({ name, content: "" })),
|
|
195
|
-
hookGroups: (identities.hookMarkers ?? []).map((marker) => ({
|
|
196
|
-
event: "",
|
|
197
|
-
marker,
|
|
198
|
-
group: {},
|
|
199
|
-
})),
|
|
200
|
-
permissionEntries: [...(identities.permissionEntries ?? [])],
|
|
201
|
-
pluginReferences: (identities.pluginReferences ?? []).map((reference) => ({
|
|
202
|
-
reference,
|
|
203
|
-
isManagedReference: (entry: unknown) => entry === reference,
|
|
204
|
-
})),
|
|
205
|
-
},
|
|
206
|
-
};
|
|
207
|
-
}
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
// oxlint-disable no-await-in-loop
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
import type { FrameworkConfig } from "../../lib/orchestrator/types";
|
|
5
|
-
import {
|
|
6
|
-
readContributionLedger,
|
|
7
|
-
type ContributionLedger,
|
|
8
|
-
type LedgerIdentities,
|
|
9
|
-
} from "./contribution-ledger";
|
|
10
|
-
import type { CapabilityPlugin, SetupContext } from "./plugin";
|
|
11
|
-
import type { PluginRegistry } from "./registry";
|
|
12
|
-
import { managedGuidanceOwners } from "./upgrade-sweep";
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* What `mate doctor` reports about audience isolation: contributions on the
|
|
16
|
-
* companion's persisted surfaces that no base plugin owns, audience plugins
|
|
17
|
-
* the upgrade sweep could not load, and the active audience's plugins whose
|
|
18
|
-
* imperative parts a launch layer cannot deliver.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
export interface LeakedContribution {
|
|
22
|
-
/** Companion-relative file or directory. */
|
|
23
|
-
surface: string;
|
|
24
|
-
kind: string;
|
|
25
|
-
/** The entry's name; absent where the surface entry is the owner's block. */
|
|
26
|
-
identity?: string;
|
|
27
|
-
owner: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface UndeliveredAudiencePlugin {
|
|
31
|
-
pluginId: string;
|
|
32
|
-
/** `apply`, `teardown`, `forProvider` or `pluginReferences`. */
|
|
33
|
-
parts: string[];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface AudienceIsolationReport {
|
|
37
|
-
leaked: LeakedContribution[];
|
|
38
|
-
unswept: string[];
|
|
39
|
-
undelivered: UndeliveredAudiencePlugin[];
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Ledger kinds by where each runtime keeps them; guidance is read from the files themselves. */
|
|
43
|
-
const LEDGER_SURFACES: Record<string, Partial<Record<keyof LedgerIdentities, string>>> = {
|
|
44
|
-
claude: {
|
|
45
|
-
mcpServers: ".mcp.json",
|
|
46
|
-
skillTrees: path.join(".claude", "skills"),
|
|
47
|
-
agentDefinitions: path.join(".claude", "agents"),
|
|
48
|
-
hookMarkers: path.join(".claude", "settings.local.json"),
|
|
49
|
-
permissionEntries: path.join(".claude", "settings.local.json"),
|
|
50
|
-
},
|
|
51
|
-
opencode: {
|
|
52
|
-
mcpServers: path.join(".opencode", "opencode.json"),
|
|
53
|
-
pluginReferences: path.join(".opencode", "opencode.json"),
|
|
54
|
-
skillTrees: path.join(".agents", "skills"),
|
|
55
|
-
agentDefinitions: path.join(".opencode", "agents"),
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
function leakedLedgerContributions(
|
|
60
|
-
ledger: ContributionLedger | null,
|
|
61
|
-
baseIds: ReadonlySet<string>,
|
|
62
|
-
): LeakedContribution[] {
|
|
63
|
-
const leaked: LeakedContribution[] = [];
|
|
64
|
-
for (const [runtimeId, owners] of Object.entries(ledger?.runtimes ?? {})) {
|
|
65
|
-
for (const [owner, identities] of Object.entries(owners)) {
|
|
66
|
-
if (baseIds.has(owner)) continue;
|
|
67
|
-
for (const [kind, surface] of Object.entries(LEDGER_SURFACES[runtimeId] ?? {})) {
|
|
68
|
-
for (const identity of identities[kind as keyof LedgerIdentities] ?? []) {
|
|
69
|
-
leaked.push({ surface: surface!, kind, identity, owner });
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return leaked;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Whether a lifecycle hook does anything. `apply` and `teardown` are required
|
|
79
|
-
* members, so a declarative plugin carries empty ones; only a body counts.
|
|
80
|
-
*/
|
|
81
|
-
function hasBody(hook: unknown): boolean {
|
|
82
|
-
if (typeof hook !== "function") return false;
|
|
83
|
-
const source = Function.prototype.toString.call(hook);
|
|
84
|
-
const open = source.indexOf("{");
|
|
85
|
-
const close = source.lastIndexOf("}");
|
|
86
|
-
if (open === -1 || close <= open) return true;
|
|
87
|
-
const body = source
|
|
88
|
-
.slice(open + 1, close)
|
|
89
|
-
.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, "")
|
|
90
|
-
.trim();
|
|
91
|
-
return body.length > 0;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async function undeliveredParts(plugin: CapabilityPlugin, ctx: SetupContext): Promise<string[]> {
|
|
95
|
-
const parts: string[] = [];
|
|
96
|
-
if (hasBody(plugin.apply)) parts.push("apply");
|
|
97
|
-
if (hasBody(plugin.teardown)) parts.push("teardown");
|
|
98
|
-
if (plugin.forProvider && Object.keys(plugin.forProvider).length > 0) parts.push("forProvider");
|
|
99
|
-
try {
|
|
100
|
-
const byRuntime = (await plugin.getRuntimeContributions?.(ctx)) ?? {};
|
|
101
|
-
if (Object.values(byRuntime).some((runtime) => (runtime?.pluginReferences ?? []).length > 0)) {
|
|
102
|
-
parts.push("pluginReferences");
|
|
103
|
-
}
|
|
104
|
-
} catch {
|
|
105
|
-
/** A plugin whose declarations fail is the plugin checks' finding, not this one. */
|
|
106
|
-
}
|
|
107
|
-
return parts;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export async function inspectAudienceIsolation(
|
|
111
|
-
companionPath: string,
|
|
112
|
-
config: FrameworkConfig,
|
|
113
|
-
registry: PluginRegistry,
|
|
114
|
-
activeAudience: string | null,
|
|
115
|
-
): Promise<AudienceIsolationReport> {
|
|
116
|
-
const baseIds = new Set(registry.baseRegistrations().map((entry) => entry.plugin.id));
|
|
117
|
-
const leaked: LeakedContribution[] = [];
|
|
118
|
-
|
|
119
|
-
for (const [surface, owners] of await managedGuidanceOwners(companionPath)) {
|
|
120
|
-
for (const owner of owners) {
|
|
121
|
-
if (!baseIds.has(owner)) leaked.push({ surface, kind: "guidance", owner });
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const ledger = await readContributionLedger(companionPath);
|
|
126
|
-
leaked.push(...leakedLedgerContributions(ledger, baseIds));
|
|
127
|
-
|
|
128
|
-
const undelivered: UndeliveredAudiencePlugin[] = [];
|
|
129
|
-
if (activeAudience) {
|
|
130
|
-
const ctx: SetupContext = { companionPath, config, mode: "sync", activeProviders: [] };
|
|
131
|
-
for (const { plugin } of registry.audienceRegistrations(activeAudience)) {
|
|
132
|
-
if (plugin.kind !== "capability") continue;
|
|
133
|
-
const parts = await undeliveredParts(plugin as CapabilityPlugin, ctx);
|
|
134
|
-
if (parts.length > 0) undelivered.push({ pluginId: plugin.id, parts });
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return { leaked, unswept: ledger?.unswept ?? [], undelivered };
|
|
139
|
-
}
|
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
|
|
3
|
-
import { runtimeOpenCodePluginRoot } from "../../runtime/runtime-directory";
|
|
4
|
-
import {
|
|
5
|
-
CLAUDE_LOCAL_CONFIG_DOCUMENT,
|
|
6
|
-
CLAUDE_SETTINGS_DOCUMENT,
|
|
7
|
-
OPENCODE_CONFIG_DOCUMENT,
|
|
8
|
-
OPENCODE_MCP_SERVERS_AT,
|
|
9
|
-
type ManagedRegion,
|
|
10
|
-
type RenderedRuntimeDocument,
|
|
11
|
-
} from "../../runtime/runtime-document-manifest";
|
|
12
|
-
import type { CapabilityContributionInput, SetupContext } from "./plugin";
|
|
13
|
-
import {
|
|
14
|
-
mateBannerHookGroups,
|
|
15
|
-
mateGuardHookGroups,
|
|
16
|
-
mateGuidanceHookGroups,
|
|
17
|
-
mateSessionPrepareHookGroups,
|
|
18
|
-
} from "./providers/claude-plugin-hooks";
|
|
19
|
-
import {
|
|
20
|
-
renderManagedClaudeHookGroups,
|
|
21
|
-
renderManagedClaudeMcpServers,
|
|
22
|
-
renderManagedClaudePermissionEntries,
|
|
23
|
-
} from "./providers/claude";
|
|
24
|
-
import {
|
|
25
|
-
renderCompanionExternalDirectoryPermissions,
|
|
26
|
-
renderManagedOpenCodeMcpServers,
|
|
27
|
-
} from "./providers/opencode";
|
|
28
|
-
import { isRecord, readOpenCodeConfig } from "./providers/opencode-format";
|
|
29
|
-
import { projectionRepoRoot } from "./surface-target";
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* The working target's documents, rendered as values. Each is a document the
|
|
33
|
-
* runtime finds by walking up from the current directory, and every path inside
|
|
34
|
-
* one still names the Companion Repository — only where the document is placed
|
|
35
|
-
* differs from the companion target.
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
export { CLAUDE_LOCAL_CONFIG_DOCUMENT };
|
|
39
|
-
|
|
40
|
-
function nonEmpty(region: ManagedRegion): boolean {
|
|
41
|
-
if (region.kind === "list") return region.values.length > 0;
|
|
42
|
-
if (region.kind === "map") return Object.keys(region.entries).length > 0;
|
|
43
|
-
return true;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function document(documentPath: string, regions: ManagedRegion[]): RenderedRuntimeDocument[] {
|
|
47
|
-
const kept = regions.filter(nonEmpty);
|
|
48
|
-
return kept.length > 0 ? [{ path: documentPath, regions: kept }] : [];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* The Mate plugin root is what makes an Unmanaged OpenCode session load the
|
|
53
|
-
* companion guidance at all: OpenCode has no config channel for a generated
|
|
54
|
-
* system prompt, so the plugin is the only injection point. It is named through
|
|
55
|
-
* the runtime directory, so a reinstall does not strand a wrapped repository.
|
|
56
|
-
* The managed launch carries no overlay of its own, so this document is the one
|
|
57
|
-
* place every OpenCode session in the repository loads it from.
|
|
58
|
-
*/
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Loaded by a managed session beside the launch's plugin. Mate hooks here run
|
|
62
|
-
* twice there, inertly: guard is input-pure, guidance/prepare yield under a
|
|
63
|
-
* launch env, banner is `--projected`. Capability groups run once: the launch
|
|
64
|
-
* omits them from `--settings`. A launch passes no `--add-dir`; it relies on
|
|
65
|
-
* `CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD` for companion `CLAUDE.md`.
|
|
66
|
-
*/
|
|
67
|
-
function claudeSettingsRegions(
|
|
68
|
-
ctx: SetupContext,
|
|
69
|
-
inputs: CapabilityContributionInput[],
|
|
70
|
-
): ManagedRegion[] {
|
|
71
|
-
const capabilityHooks = renderManagedClaudeHookGroups(inputs);
|
|
72
|
-
const mateHooks: Record<string, unknown[]> = {
|
|
73
|
-
PreToolUse: mateGuardHookGroups(),
|
|
74
|
-
SessionStart: [
|
|
75
|
-
...mateBannerHookGroups(),
|
|
76
|
-
...mateGuidanceHookGroups(),
|
|
77
|
-
...mateSessionPrepareHookGroups(),
|
|
78
|
-
],
|
|
79
|
-
};
|
|
80
|
-
const events = [...new Set([...Object.keys(mateHooks), ...Object.keys(capabilityHooks)])];
|
|
81
|
-
return [
|
|
82
|
-
...events.map((event): ManagedRegion => ({
|
|
83
|
-
at: ["hooks", event],
|
|
84
|
-
kind: "list",
|
|
85
|
-
values: [...(mateHooks[event] ?? []), ...(capabilityHooks[event] ?? [])],
|
|
86
|
-
})),
|
|
87
|
-
/**
|
|
88
|
-
* `stripPrefix` heals a repository an older Mate wrapped. That version wrote
|
|
89
|
-
* the whole launch environment here, and Claude Code applies `env` to the
|
|
90
|
-
* session, so every hook in a bare session inherited `MATE_*` and read the
|
|
91
|
-
* repository as a Managed Session — silently, since looking managed is
|
|
92
|
-
* exactly what makes the guidance hook emit nothing. Guidance must be the
|
|
93
|
-
* projection's to give in an Unmanaged Session, so no `MATE_*` key may
|
|
94
|
-
* survive here.
|
|
95
|
-
*/
|
|
96
|
-
{
|
|
97
|
-
at: ["env"],
|
|
98
|
-
kind: "map",
|
|
99
|
-
entries: { CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD: "1" },
|
|
100
|
-
stripPrefix: "MATE_",
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
at: ["permissions", "allow"],
|
|
104
|
-
kind: "list",
|
|
105
|
-
values: renderManagedClaudePermissionEntries(ctx.companionPath, inputs),
|
|
106
|
-
},
|
|
107
|
-
];
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function renderDocumentsForTarget(
|
|
111
|
-
ctx: SetupContext,
|
|
112
|
-
inputsByRuntime: Map<string, CapabilityContributionInput[]>,
|
|
113
|
-
): RenderedRuntimeDocument[] {
|
|
114
|
-
const repoRoot = projectionRepoRoot(ctx);
|
|
115
|
-
|
|
116
|
-
const documents: RenderedRuntimeDocument[] = [];
|
|
117
|
-
if (ctx.activeProviders.includes("claude")) {
|
|
118
|
-
const inputs = inputsByRuntime.get("claude") ?? [];
|
|
119
|
-
documents.push(...document(CLAUDE_SETTINGS_DOCUMENT, claudeSettingsRegions(ctx, inputs)));
|
|
120
|
-
/**
|
|
121
|
-
* Local scope, not `.mcp.json`: a project-scoped server is inert until a
|
|
122
|
-
* human approves it in a session, which an Unmanaged Session is exactly the
|
|
123
|
-
* case that cannot rely on. The repo path is the key Claude Code files a
|
|
124
|
-
* project under, resolved so a symlinked working directory still matches.
|
|
125
|
-
*
|
|
126
|
-
* Confirmed against a real `~/.claude.json`: the keys there are the
|
|
127
|
-
* directories sessions started in, not repository roots — sibling
|
|
128
|
-
* directories of one checkout each hold their own entry, as do temporary
|
|
129
|
-
* directories. So this reaches exactly one session: the one started at the
|
|
130
|
-
* repository root. A session started in a subdirectory of a wrapped
|
|
131
|
-
* repository has its own key, finds no `mcpServers` under it, and sees none
|
|
132
|
-
* of these servers — the Unmanaged Session case this document exists for,
|
|
133
|
-
* missed. That limit is deliberate. The directories a session might start
|
|
134
|
-
* in are unbounded, and every one of them would be a fresh entry in the
|
|
135
|
-
* user's global configuration that nothing later reclaims; keying the
|
|
136
|
-
* repository root only keeps the projection's footprint there to a single
|
|
137
|
-
* key. A subdirectory session still gets the rest of the projection — the
|
|
138
|
-
* documents inside the repository are found by the runtime's own upward
|
|
139
|
-
* walk — it loses only the MCP servers.
|
|
140
|
-
*/
|
|
141
|
-
documents.push(
|
|
142
|
-
...document(CLAUDE_LOCAL_CONFIG_DOCUMENT, [
|
|
143
|
-
{
|
|
144
|
-
at: ["projects", repoRoot, "mcpServers"],
|
|
145
|
-
kind: "map",
|
|
146
|
-
entries: renderManagedClaudeMcpServers(inputs),
|
|
147
|
-
},
|
|
148
|
-
]),
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
if (ctx.activeProviders.includes("opencode")) {
|
|
152
|
-
const inputs = inputsByRuntime.get("opencode") ?? [];
|
|
153
|
-
documents.push(
|
|
154
|
-
...document(OPENCODE_CONFIG_DOCUMENT, [
|
|
155
|
-
{
|
|
156
|
-
at: ["plugins"],
|
|
157
|
-
kind: "list",
|
|
158
|
-
values: [runtimeOpenCodePluginRoot()],
|
|
159
|
-
},
|
|
160
|
-
{
|
|
161
|
-
at: [...OPENCODE_MCP_SERVERS_AT],
|
|
162
|
-
kind: "map",
|
|
163
|
-
entries: renderManagedOpenCodeMcpServers(inputs),
|
|
164
|
-
},
|
|
165
|
-
{
|
|
166
|
-
at: ["permissions"],
|
|
167
|
-
kind: "list",
|
|
168
|
-
values: renderCompanionExternalDirectoryPermissions(ctx.companionPath),
|
|
169
|
-
},
|
|
170
|
-
{
|
|
171
|
-
at: ["skills"],
|
|
172
|
-
kind: "list",
|
|
173
|
-
values: [path.join(ctx.companionPath, ".agents", "skills")],
|
|
174
|
-
},
|
|
175
|
-
{ at: ["references"], kind: "map", entries: { mate: ctx.companionPath } },
|
|
176
|
-
]),
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
return documents;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/** Keys Mate renders its own regions for; the companion's values join those regions. */
|
|
183
|
-
const MATE_OPENCODE_KEYS = new Set(["plugins", "permissions", "skills", "mcp", "references"]);
|
|
184
|
-
|
|
185
|
-
function relativePathValues(value: unknown, at: string[]): string[] {
|
|
186
|
-
if (typeof value === "string") {
|
|
187
|
-
return value.startsWith("./") || value.startsWith("../") ? [at.join(".")] : [];
|
|
188
|
-
}
|
|
189
|
-
if (Array.isArray(value)) {
|
|
190
|
-
return value.flatMap((entry, index) => relativePathValues(entry, [...at, String(index)]));
|
|
191
|
-
}
|
|
192
|
-
if (isRecord(value)) {
|
|
193
|
-
return Object.entries(value).flatMap(([key, entry]) => relativePathValues(entry, [...at, key]));
|
|
194
|
-
}
|
|
195
|
-
return [];
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/** Replaces the region at `at` with what `merge` builds from it, or appends one. */
|
|
199
|
-
function upsertRegion(
|
|
200
|
-
regions: ManagedRegion[],
|
|
201
|
-
at: string[],
|
|
202
|
-
merge: (found: ManagedRegion | undefined) => ManagedRegion,
|
|
203
|
-
): void {
|
|
204
|
-
const key = JSON.stringify(at);
|
|
205
|
-
const index = regions.findIndex((region) => JSON.stringify(region.at) === key);
|
|
206
|
-
if (index === -1) regions.push(merge(undefined));
|
|
207
|
-
else regions[index] = merge(regions[index]);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/** Mate's own values stay; companion values it does not already list go first. */
|
|
211
|
-
function mergeListRegion(regions: ManagedRegion[], at: string[], extra: unknown[]): void {
|
|
212
|
-
upsertRegion(regions, at, (found) => {
|
|
213
|
-
const own = found?.kind === "list" ? found.values : [];
|
|
214
|
-
const seen = new Set(own.map((value) => JSON.stringify(value)));
|
|
215
|
-
const values = [...extra.filter((value) => !seen.has(JSON.stringify(value))), ...own];
|
|
216
|
-
return { at, kind: "list", values };
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/** Mate's own entries win over companion entries of the same name. */
|
|
221
|
-
function mergeMapRegion(
|
|
222
|
-
regions: ManagedRegion[],
|
|
223
|
-
at: string[],
|
|
224
|
-
extra: Record<string, unknown>,
|
|
225
|
-
): void {
|
|
226
|
-
upsertRegion(regions, at, (found) => {
|
|
227
|
-
const own = found?.kind === "map" ? found.entries : {};
|
|
228
|
-
return { at, kind: "map", entries: { ...extra, ...own } };
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/** Relative reference paths resolved against the companion's `.opencode/`. */
|
|
233
|
-
function resolveReferences(
|
|
234
|
-
references: Record<string, unknown>,
|
|
235
|
-
companionDir: string,
|
|
236
|
-
): Record<string, unknown> {
|
|
237
|
-
return Object.fromEntries(
|
|
238
|
-
Object.entries(references).map(([name, reference]) => [
|
|
239
|
-
name,
|
|
240
|
-
typeof reference === "string" && !path.isAbsolute(reference)
|
|
241
|
-
? path.resolve(companionDir, reference)
|
|
242
|
-
: reference,
|
|
243
|
-
]),
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Folds the companion's `.opencode/opencode.json` into the wrapped document.
|
|
249
|
-
* Mate's regions beat companion values; other top-level keys yield to the
|
|
250
|
-
* repository's. `references` resolve against the companion's `.opencode/`;
|
|
251
|
-
* other relative paths are reported, not rewritten.
|
|
252
|
-
*/
|
|
253
|
-
export async function mergeCompanionOpenCodeConfig(
|
|
254
|
-
companionPath: string,
|
|
255
|
-
documents: RenderedRuntimeDocument[],
|
|
256
|
-
): Promise<{ documents: RenderedRuntimeDocument[]; warnings: string[] }> {
|
|
257
|
-
const index = documents.findIndex((document) => document.path === OPENCODE_CONFIG_DOCUMENT);
|
|
258
|
-
const companionConfigPath = path.join(companionPath, ".opencode", "opencode.json");
|
|
259
|
-
const companion = index === -1 ? null : await readOpenCodeConfig(companionConfigPath);
|
|
260
|
-
if (!companion?.present) return { documents, warnings: [] };
|
|
261
|
-
|
|
262
|
-
const regions = [...documents[index]!.regions] as ManagedRegion[];
|
|
263
|
-
const warnings: string[] = [];
|
|
264
|
-
|
|
265
|
-
const companionDir = path.dirname(companionConfigPath);
|
|
266
|
-
for (const [key, value] of Object.entries(companion.config)) {
|
|
267
|
-
if (key === "$schema") continue;
|
|
268
|
-
if (key === "references" && isRecord(value)) {
|
|
269
|
-
mergeMapRegion(regions, ["references"], resolveReferences(value, companionDir));
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
for (const at of relativePathValues(value, [key])) {
|
|
273
|
-
warnings.push(
|
|
274
|
-
`${companionConfigPath}: ${at} is a relative path, which resolves against the working repository once merged into ${OPENCODE_CONFIG_DOCUMENT}; use an absolute path.`,
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
if (MATE_OPENCODE_KEYS.has(key)) {
|
|
278
|
-
if (key === "mcp" && isRecord(value) && isRecord(value.servers)) {
|
|
279
|
-
mergeMapRegion(regions, [...OPENCODE_MCP_SERVERS_AT], value.servers);
|
|
280
|
-
} else if (Array.isArray(value)) {
|
|
281
|
-
mergeListRegion(regions, [key], value);
|
|
282
|
-
}
|
|
283
|
-
continue;
|
|
284
|
-
}
|
|
285
|
-
regions.push({ at: [key], kind: "value", value, yields: true });
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const next = [...documents];
|
|
289
|
-
next[index] = { path: OPENCODE_CONFIG_DOCUMENT, regions };
|
|
290
|
-
return { documents: next, warnings };
|
|
291
|
-
}
|