@uniqbit/mate-core 0.18.3-canary.1 → 0.18.4
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/hooks.json +10 -1
- package/claude-plugin/hooks/session-banner.mjs +4 -3
- package/claude-plugin/hooks/session-prepare.mjs +9 -0
- package/opencode-plugin/src/add-dir.ts +10 -76
- package/opencode-plugin/src/audience-layer.ts +91 -0
- package/opencode-plugin/src/companion.ts +70 -18
- package/opencode-plugin/src/launch-environment.ts +29 -0
- package/opencode-plugin/src/server.ts +60 -8
- package/package.json +3 -3
- package/src/cli/commands/artifact/finish/openspec.ts +1 -1
- package/src/cli/commands/artifact/pending/discovery.ts +60 -21
- package/src/cli/commands/cap/index-cmd.ts +2 -11
- package/src/cli/commands/companion/link.ts +16 -11
- package/src/cli/commands/companion/list.ts +4 -5
- package/src/cli/commands/companion/open.ts +4 -5
- package/src/cli/commands/companion/tui.ts +1 -1
- package/src/cli/commands/doctor.ts +87 -5
- package/src/cli/commands/install.ts +8 -0
- package/src/cli/commands/report/renderer.ts +9 -19
- package/src/cli/commands/session/prepare.ts +119 -0
- package/src/cli/commands/shared/companion-selection.ts +2 -2
- package/src/cli/commands/studio/reports.ts +0 -1
- package/src/cli/commands/studio/server.ts +6 -2
- package/src/cli/commands/studio/terminal-launch.ts +16 -7
- package/src/cli/commands/unwrap.ts +28 -3
- package/src/cli/commands/wrap.ts +79 -60
- package/src/cli/companion-selector.tsx +1 -1
- package/src/cli/main.ts +17 -0
- package/src/cli/usage.ts +5 -2
- package/src/hooks/session-banner.ts +19 -10
- package/src/hooks/session-guidance.ts +20 -2
- package/src/hooks/session-prepare.ts +101 -0
- package/src/lib/fs-utils.ts +13 -0
- package/src/lib/orchestrator/adapters/base.ts +45 -9
- package/src/lib/orchestrator/adapters/claude.ts +241 -30
- package/src/lib/orchestrator/adapters/opencode.ts +72 -34
- package/src/lib/orchestrator/companion-git-sync.ts +28 -1
- package/src/lib/orchestrator/companion-hub.ts +2 -1
- package/src/lib/orchestrator/config-store.ts +83 -1
- package/src/lib/orchestrator/framework-context.ts +5 -29
- package/src/lib/orchestrator/launcher.ts +36 -171
- package/src/lib/orchestrator/openspec-store.ts +202 -0
- package/src/lib/orchestrator/projection-claude-entry.ts +17 -6
- package/src/lib/orchestrator/projection-claude-skills.ts +126 -41
- package/src/lib/orchestrator/projection-entries.ts +124 -11
- package/src/lib/orchestrator/projection-opencode-copies.ts +230 -0
- package/src/lib/orchestrator/projection-openspec-store.ts +164 -0
- package/src/lib/orchestrator/projection-record.ts +7 -1
- package/src/lib/orchestrator/projection-runtime-documents.ts +83 -130
- package/src/lib/orchestrator/projection-types.ts +47 -34
- package/src/lib/orchestrator/repository-link.ts +473 -0
- package/src/lib/orchestrator/root-context.ts +6 -2
- package/src/lib/orchestrator/session-prepare.ts +503 -0
- package/src/lib/orchestrator/setup-compatibilities.ts +1 -1
- package/src/lib/orchestrator/setup-preflight.ts +3 -1
- package/src/lib/orchestrator/working-repo-projection.ts +73 -160
- package/src/lib/orchestrator/workspace-inventory.ts +2 -2
- package/src/lib/orchestrator/wrap-pass.ts +222 -0
- package/src/lib/runtime-directory-refresh.ts +186 -0
- package/src/lib/update-checker.ts +6 -0
- package/src/opencode/companion-hooks.ts +30 -135
- package/src/opencode/companion-policy.ts +26 -10
- package/src/opencode/index.ts +1 -7
- package/src/opencode/projected-guidance.ts +6 -13
- package/src/opencode/session-notes.ts +76 -0
- package/src/opencode/session-prepare.ts +139 -0
- package/src/opencode/tui.tsx +56 -9
- package/src/runtime/audience-layer.ts +84 -0
- package/src/runtime/claude-delivery-plan.ts +73 -0
- package/src/runtime/claude-hook-group.ts +21 -0
- package/src/runtime/companion-lock.ts +121 -0
- package/src/runtime/companion-sync.ts +66 -9
- package/src/runtime/guidance.ts +17 -0
- package/src/runtime/opencode-audience-layer.ts +176 -0
- package/src/runtime/policy.ts +14 -1
- package/src/runtime/projection.ts +1 -1
- package/src/runtime/runtime-directory.ts +43 -0
- package/src/runtime/runtime-document-manifest.ts +352 -0
- package/src/runtime/session-prepare-runner.ts +148 -0
- package/src/runtime/wrap-state.ts +94 -0
- package/src/runtime/write-atomic.ts +27 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +2 -1
- package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +2 -1
- package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +5 -0
- package/src/templates/capabilities/react-doctor/mate-react-doctor/SKILL.md +84 -0
- package/src/templates/capabilities/react-doctor/{skill → mate-react-doctor}/references/explain.md +18 -15
- package/src/templates/mate-skills/agents/mate-openspec-backfill/SKILL.md +45 -29
- package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +1 -1
- package/src/templates/mate-skills/claude/mate-openspec-backfill/SKILL.md +45 -29
- package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +1 -1
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +850 -499
- package/src/tools/setup/audience-isolation.fixture.ts +325 -0
- package/src/tools/setup/audience-layer.ts +207 -0
- package/src/tools/setup/audience-registrations.ts +52 -0
- package/src/tools/setup/capabilities/openspec.ts +9 -0
- package/src/tools/setup/capabilities/react-doctor.ts +34 -50
- package/src/tools/setup/capabilities/tokensave.ts +11 -0
- package/src/tools/setup/contribution-ledger.ts +207 -0
- package/src/tools/setup/dynamic-plugins/audiences.ts +89 -9
- package/src/tools/setup/dynamic-plugins/declarations.ts +3 -2
- package/src/tools/setup/dynamic-plugins/hydrate.ts +8 -5
- package/src/tools/setup/engine.ts +152 -10
- package/src/tools/setup/isolation-diagnostics.ts +139 -0
- package/src/tools/setup/mate.ts +1 -1
- package/src/tools/setup/plugin.ts +19 -4
- package/src/tools/setup/plugins/gitignore.ts +7 -1
- package/src/tools/setup/providers/claude-format.ts +11 -18
- package/src/tools/setup/providers/claude-plugin-hooks.ts +61 -24
- package/src/tools/setup/providers/claude.ts +50 -8
- package/src/tools/setup/providers/opencode.ts +10 -0
- package/src/tools/setup/registry.ts +24 -2
- package/src/tools/setup/runtime-documents.ts +152 -32
- package/src/tools/setup/surface-target.ts +13 -4
- package/src/tools/setup/upgrade-sweep.ts +153 -0
- package/src/tools/setup/working-repo-cleanup.ts +2 -0
- package/src/tools/setup/working-repo-local-state.ts +91 -14
- package/src/tools/setup.ts +11 -8
- package/wrappers/bin/openspec +42 -1
- package/src/lib/orchestrator/companion-registry-reader.ts +0 -37
- package/src/lib/orchestrator/companion-resolver.ts +0 -179
- package/src/lib/orchestrator/companion-store.ts +0 -94
- package/src/lib/orchestrator/repo-local-registry.ts +0 -143
- package/src/templates/capabilities/react-doctor/claude/hooks/react-doctor.sh +0 -144
- package/src/templates/capabilities/react-doctor/skill/SKILL.md +0 -51
- package/src/templates/report-assets/README.md +0 -32
- package/src/templates/report-assets/mermaid.LICENSE +0 -21
- package/src/templates/report-assets/mermaid.min.js +0 -4376
|
@@ -109,28 +109,32 @@ function changeKindFor(
|
|
|
109
109
|
);
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Every file beneath `directory`, as POSIX paths under `relativeRoot`. Reads directory
|
|
114
|
+
* entry names only, never file contents. Throws when `directory` cannot be read.
|
|
115
|
+
*/
|
|
116
|
+
async function filesUnder(directory: string, relativeRoot: string): Promise<string[]> {
|
|
117
|
+
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
118
|
+
const nested = await Promise.all(
|
|
119
|
+
entries.map(async (entry) => {
|
|
120
|
+
const entryRelative = path.posix.join(relativeRoot, entry.name);
|
|
121
|
+
if (entry.isDirectory()) return filesUnder(path.join(directory, entry.name), entryRelative);
|
|
122
|
+
return entry.isFile() ? [entryRelative] : [];
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
return nested.flat();
|
|
126
|
+
}
|
|
127
|
+
|
|
112
128
|
/**
|
|
113
129
|
* Canonical specs the change's delta specs applied to. Only directory entry names under
|
|
114
130
|
* the archive's `specs/` tree are read — never any file contents — so archived prose
|
|
115
131
|
* cannot influence discovery.
|
|
116
132
|
*/
|
|
117
133
|
async function canonicalSpecsFor(companionPath: string, anchor: string): Promise<string[]> {
|
|
118
|
-
const collectDeltaSpecFiles = async (directory: string, relative = ""): Promise<string[]> => {
|
|
119
|
-
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
120
|
-
const nested = await Promise.all(
|
|
121
|
-
entries.map(async (entry) => {
|
|
122
|
-
const entryRelative = path.posix.join(relative, entry.name);
|
|
123
|
-
if (entry.isDirectory()) {
|
|
124
|
-
return collectDeltaSpecFiles(path.join(directory, entry.name), entryRelative);
|
|
125
|
-
}
|
|
126
|
-
return entry.isFile() ? [path.posix.join(SPECS_RELATIVE_DIR, entryRelative)] : [];
|
|
127
|
-
}),
|
|
128
|
-
);
|
|
129
|
-
return nested.flat();
|
|
130
|
-
};
|
|
131
134
|
try {
|
|
132
|
-
const specs = await
|
|
135
|
+
const specs = await filesUnder(
|
|
133
136
|
path.join(companionPath, ARCHIVE_RELATIVE_DIR, anchor, "specs"),
|
|
137
|
+
SPECS_RELATIVE_DIR,
|
|
134
138
|
);
|
|
135
139
|
return specs.toSorted();
|
|
136
140
|
} catch {
|
|
@@ -216,6 +220,32 @@ async function attributionIndex(
|
|
|
216
220
|
);
|
|
217
221
|
}
|
|
218
222
|
|
|
223
|
+
/**
|
|
224
|
+
* The canonical spec files behind one uncommitted path. Porcelain collapses an untracked
|
|
225
|
+
* directory to `dir/`, so a collapsed path at or above a capability directory is expanded
|
|
226
|
+
* into the files beneath it, every one of them new; publish narrows by file, never by
|
|
227
|
+
* directory. Expansion reads directory entry names only, so spec prose cannot influence
|
|
228
|
+
* discovery.
|
|
229
|
+
*/
|
|
230
|
+
async function specFilesFor(
|
|
231
|
+
companionPath: string,
|
|
232
|
+
worktreePath: string,
|
|
233
|
+
changeKinds: Readonly<Record<string, WorkingTreeChange>>,
|
|
234
|
+
): Promise<Array<{ path: string; kind: WorkingTreeChange }>> {
|
|
235
|
+
if (!worktreePath.endsWith("/")) {
|
|
236
|
+
return [{ path: worktreePath, kind: changeKindFor(worktreePath, changeKinds) }];
|
|
237
|
+
}
|
|
238
|
+
const collapsed = withoutTrailingSlash(worktreePath);
|
|
239
|
+
const root = collapsed.startsWith(`${SPECS_RELATIVE_DIR}/`) ? collapsed : SPECS_RELATIVE_DIR;
|
|
240
|
+
try {
|
|
241
|
+
const files = await filesUnder(path.join(companionPath, root), root);
|
|
242
|
+
return files.map((file) => ({ path: file, kind: "new" as const }));
|
|
243
|
+
} catch {
|
|
244
|
+
// A collapsed ancestor such as `openspec/` need not contain a spec tree at all.
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
219
249
|
/**
|
|
220
250
|
* Uncommitted canonical specs that no pending change accounts for, each attributed to
|
|
221
251
|
* every archive whose delta specs name it. An attributed spec belongs to a change whose
|
|
@@ -230,18 +260,27 @@ export async function unattributedSpecs(
|
|
|
230
260
|
archives: ArchiveEntry[],
|
|
231
261
|
changeKinds: Readonly<Record<string, WorkingTreeChange>> = {},
|
|
232
262
|
): Promise<UnattributedSpec[]> {
|
|
233
|
-
const claimed =
|
|
234
|
-
const
|
|
235
|
-
.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
263
|
+
const claimed = pendingArchives(archives).flatMap((entry) => entry.uncommittedSpecs);
|
|
264
|
+
const reported = await Promise.all(
|
|
265
|
+
uncommittedPaths.flatMap((worktreePath) =>
|
|
266
|
+
touches(worktreePath, SPECS_RELATIVE_DIR)
|
|
267
|
+
? [specFilesFor(companionPath, worktreePath, changeKinds)]
|
|
268
|
+
: [],
|
|
269
|
+
),
|
|
270
|
+
);
|
|
271
|
+
const driftKinds = new Map<string, WorkingTreeChange>();
|
|
272
|
+
for (const spec of reported.flat()) {
|
|
273
|
+
if (!claimed.some((claimedPath) => touches(claimedPath, spec.path))) {
|
|
274
|
+
driftKinds.set(spec.path, spec.kind);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const specPaths = [...driftKinds.keys()].toSorted();
|
|
239
278
|
if (specPaths.length === 0) return [];
|
|
240
279
|
|
|
241
280
|
const index = await attributionIndex(companionPath, archives);
|
|
242
281
|
return specPaths.map((specPath) => ({
|
|
243
282
|
path: specPath,
|
|
244
|
-
kind:
|
|
283
|
+
kind: driftKinds.get(specPath) ?? "modified",
|
|
245
284
|
touchedByArchives: index.flatMap(({ anchor, state, specs }) =>
|
|
246
285
|
specs.some((spec) => touches(specPath, spec)) ? [{ anchor, state }] : [],
|
|
247
286
|
),
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
1
|
import {
|
|
5
2
|
hasGraphifyCapability,
|
|
6
3
|
hasTokensaveCapability,
|
|
@@ -10,7 +7,7 @@ import type { CapabilityConfig } from "../../../lib/orchestrator/types";
|
|
|
10
7
|
import {
|
|
11
8
|
ensureTokensaveBranchingPosture,
|
|
12
9
|
ensureTokensaveStoreExcluded,
|
|
13
|
-
|
|
10
|
+
isTokensaveIndexed,
|
|
14
11
|
} from "../../../tools/setup/capabilities/tokensave";
|
|
15
12
|
import { ensureUnambiguousCompanion } from "../shared/companion-selection";
|
|
16
13
|
import { runGraphifyCapCommand } from "./graphify";
|
|
@@ -43,13 +40,7 @@ async function defaultIsTokensaveInitialized(): Promise<boolean> {
|
|
|
43
40
|
const repo =
|
|
44
41
|
repository ?? (await workingRepoStore.load()).repos.find((entry) => entry.id === repositoryId);
|
|
45
42
|
if (!repo) return false;
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
const stat = await fs.stat(path.join(repo.path, TOKENSAVE_STORE_DIR));
|
|
49
|
-
return stat.isDirectory();
|
|
50
|
-
} catch {
|
|
51
|
-
return false;
|
|
52
|
-
}
|
|
43
|
+
return isTokensaveIndexed(repo.path);
|
|
53
44
|
}
|
|
54
45
|
|
|
55
46
|
// The reused cap commands (runGraphifyCapCommand / runTokensaveCapCommand) signal
|
|
@@ -10,10 +10,12 @@ import {
|
|
|
10
10
|
} from "../../companion-link-wizard";
|
|
11
11
|
import { extractRepoName } from "../artifact/extract-repo-name";
|
|
12
12
|
import { GlobalConfigStore } from "../../../lib/orchestrator/global-config-store";
|
|
13
|
-
import {
|
|
14
|
-
|
|
13
|
+
import {
|
|
14
|
+
CompanionResolver,
|
|
15
|
+
companionRootedStore,
|
|
16
|
+
findDescendantRepoLocalRegistries,
|
|
17
|
+
} from "../../../lib/orchestrator/repository-link";
|
|
15
18
|
import { inspectSetupPreflight } from "../../../lib/orchestrator/setup-preflight";
|
|
16
|
-
import { findDescendantRepoLocalRegistries } from "../../../lib/orchestrator/repo-local-registry";
|
|
17
19
|
import { runSetupFlowAtPath } from "../setup";
|
|
18
20
|
import { runInstallCommand } from "../install";
|
|
19
21
|
import type { CompanionSource, LinkedRepository } from "../../../lib/orchestrator/types";
|
|
@@ -164,14 +166,15 @@ export async function runCompanionLinkCommandWithDeps(
|
|
|
164
166
|
if (!inputs.companionPaths?.length) {
|
|
165
167
|
exitWithError("mate: no existing companion was selected.");
|
|
166
168
|
}
|
|
167
|
-
companionPaths =
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
169
|
+
companionPaths = inputs.companionPaths.map((rawCompanionPath) =>
|
|
170
|
+
path.resolve(rawCompanionPath),
|
|
171
|
+
);
|
|
172
|
+
const targets = await Promise.all(
|
|
173
|
+
companionPaths.map((companionPath) => stat(companionPath).catch(() => null)),
|
|
174
|
+
);
|
|
175
|
+
const notADirectory = companionPaths.find((_, index) => !targets[index]?.isDirectory());
|
|
176
|
+
if (notADirectory) {
|
|
177
|
+
exitWithError(`mate: existing companion must be a directory: ${notADirectory}`);
|
|
175
178
|
}
|
|
176
179
|
} else {
|
|
177
180
|
const rawCompanionPath = inputs.localPath?.trim();
|
|
@@ -205,6 +208,7 @@ export async function runCompanionLinkCommandWithDeps(
|
|
|
205
208
|
// Managed companions are git-backed; explicitly pasted paths retain local provenance.
|
|
206
209
|
const persistedSource: CompanionSource = companionSource === "local" ? "local" : "git";
|
|
207
210
|
|
|
211
|
+
/* oxlint-disable no-await-in-loop -- each companion is set up, linked and installed in turn; a declined install stops the rest */
|
|
208
212
|
for (const companionPath of companionPaths) {
|
|
209
213
|
const preflight = await inspectPreflight(companionPath);
|
|
210
214
|
if (companionSource === "git" && preflight.kind !== "existing-companion") {
|
|
@@ -216,6 +220,7 @@ export async function runCompanionLinkCommandWithDeps(
|
|
|
216
220
|
await invalidateInstallState({ kind: "companion", companionPath });
|
|
217
221
|
if ((await installCompanion(companionPath)) === false) return;
|
|
218
222
|
}
|
|
223
|
+
/* oxlint-enable no-await-in-loop */
|
|
219
224
|
|
|
220
225
|
let shadowedPaths: string[] = [];
|
|
221
226
|
try {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CompanionStore } from "../../../lib/orchestrator/
|
|
1
|
+
import { CompanionStore } from "../../../lib/orchestrator/repository-link";
|
|
2
2
|
import { resolveFrameworkContext } from "../../../lib/orchestrator/framework-context";
|
|
3
3
|
import { renderRepoListTable } from "../../repo-list-table";
|
|
4
4
|
import { writeJsonStdout } from "../../write-json-stdout";
|
|
@@ -13,14 +13,13 @@ import { writeJsonStdout } from "../../write-json-stdout";
|
|
|
13
13
|
* TTY (e.g. piped into another command).
|
|
14
14
|
*/
|
|
15
15
|
export async function runCompanionListCommand(argv: string[] = []): Promise<void> {
|
|
16
|
-
const {
|
|
17
|
-
process.cwd()
|
|
18
|
-
);
|
|
16
|
+
const { companionPath, workingRepoStore, repository, contextKind } =
|
|
17
|
+
await resolveFrameworkContext(process.cwd());
|
|
19
18
|
const repositories = repository
|
|
20
19
|
? [repository]
|
|
21
20
|
: contextKind === "companion-root"
|
|
22
21
|
? []
|
|
23
|
-
: await new CompanionStore(
|
|
22
|
+
: await new CompanionStore(companionPath, workingRepoStore).listRepositories();
|
|
24
23
|
const forceJson = argv.includes("--json");
|
|
25
24
|
|
|
26
25
|
if (forceJson || !process.stdout.isTTY) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CompanionStore } from "../../../lib/orchestrator/
|
|
1
|
+
import { CompanionStore } from "../../../lib/orchestrator/repository-link";
|
|
2
2
|
import { resolveForLaunch } from "../../../lib/orchestrator/framework-context";
|
|
3
3
|
import { injectEditorFolder, getPreferredEditorCli } from "../../../lib/orchestrator/editor";
|
|
4
4
|
import { ensureUnambiguousCompanion } from "../launch/shared";
|
|
@@ -7,9 +7,9 @@ export const workspaceOpenCommandDeps = {
|
|
|
7
7
|
ensureUnambiguousCompanion: () => ensureUnambiguousCompanion(),
|
|
8
8
|
resolveLaunchContext: () => resolveForLaunch(process.cwd()),
|
|
9
9
|
createStore: (
|
|
10
|
-
|
|
10
|
+
companionPath: ConstructorParameters<typeof CompanionStore>[0],
|
|
11
11
|
workingRepoStore: ConstructorParameters<typeof CompanionStore>[1],
|
|
12
|
-
) => new CompanionStore(
|
|
12
|
+
) => new CompanionStore(companionPath, workingRepoStore),
|
|
13
13
|
getPreferredEditorCli: () => getPreferredEditorCli(),
|
|
14
14
|
injectEditorFolder,
|
|
15
15
|
};
|
|
@@ -35,13 +35,12 @@ export async function runWorkspaceOpenCommand(): Promise<void> {
|
|
|
35
35
|
companionPath,
|
|
36
36
|
repositoryId,
|
|
37
37
|
repository: localRepository,
|
|
38
|
-
configStore,
|
|
39
38
|
workingRepoStore,
|
|
40
39
|
} = await workspaceOpenCommandDeps.resolveLaunchContext();
|
|
41
40
|
const repository =
|
|
42
41
|
localRepository ??
|
|
43
42
|
(await workspaceOpenCommandDeps
|
|
44
|
-
.createStore(
|
|
43
|
+
.createStore(companionPath, workingRepoStore)
|
|
45
44
|
.getRepository(repositoryId));
|
|
46
45
|
|
|
47
46
|
if (!repository) {
|
|
@@ -4,7 +4,7 @@ import { accessSync } from "node:fs";
|
|
|
4
4
|
import { getActiveDistribution } from "../../../distribution";
|
|
5
5
|
import { FRAMEWORK_NAME } from "../../../framework";
|
|
6
6
|
import { resolveFrameworkContext } from "../../../lib/orchestrator/framework-context";
|
|
7
|
-
import { findRepoLocalLinkedRepository } from "../../../lib/orchestrator/
|
|
7
|
+
import { findRepoLocalLinkedRepository } from "../../../lib/orchestrator/repository-link";
|
|
8
8
|
import { ensureUnambiguousCompanion } from "../launch/shared";
|
|
9
9
|
|
|
10
10
|
export function renderBanner(repoPath: string, companionPath: string): string {
|
|
@@ -3,10 +3,11 @@ import path from "node:path";
|
|
|
3
3
|
|
|
4
4
|
import { FRAMEWORK_NAME } from "../../framework";
|
|
5
5
|
import { gitEnvironment } from "../../runtime/companion-git";
|
|
6
|
+
import { readWrapStateSync } from "../../runtime/wrap-state";
|
|
6
7
|
import { getActiveDistribution } from "../../distribution";
|
|
7
8
|
import { checkEngineRequirement } from "../../lib/orchestrator/engine-guard";
|
|
8
9
|
import type { GlobalConfigStore } from "../../lib/orchestrator/global-config-store";
|
|
9
|
-
import { pathIsDirectory } from "../../lib/
|
|
10
|
+
import { pathIsDirectory } from "../../lib/fs-utils";
|
|
10
11
|
import { resolveRootContext, type RootContext } from "../../lib/orchestrator/root-context";
|
|
11
12
|
import type { CapabilityConfig, FrameworkConfig, HubMember } from "../../lib/orchestrator/types";
|
|
12
13
|
import {
|
|
@@ -19,7 +20,12 @@ import {
|
|
|
19
20
|
type PluginInspection,
|
|
20
21
|
type PluginVerificationFailure,
|
|
21
22
|
} from "../../tools/setup/dynamic-plugins/verify";
|
|
23
|
+
import {
|
|
24
|
+
inspectAudienceIsolation,
|
|
25
|
+
type AudienceIsolationReport,
|
|
26
|
+
} from "../../tools/setup/isolation-diagnostics";
|
|
22
27
|
import { getRequiredPluginDrift } from "../../tools/setup/policy";
|
|
28
|
+
import type { PluginRegistry } from "../../tools/setup/registry";
|
|
23
29
|
import { resolveCommandOnPath } from "../../tools/setup/utils";
|
|
24
30
|
import { writeJsonStdout } from "../write-json-stdout";
|
|
25
31
|
import { printSection, renderKeyValueTable, renderTable } from "./status";
|
|
@@ -33,6 +39,8 @@ interface DoctorDeps {
|
|
|
33
39
|
inspectPlugins?: (companionPath: string) => Promise<PluginInspection>;
|
|
34
40
|
/** Environment `MATE_AUDIENCE` is read from; defaults to the process environment. */
|
|
35
41
|
env?: Record<string, string | undefined>;
|
|
42
|
+
/** The hydrated registry; the active distribution's when absent. */
|
|
43
|
+
registry?: PluginRegistry;
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
export type DoctorKind = "core" | "working" | "companion" | "hub";
|
|
@@ -66,10 +74,15 @@ export interface DoctorReport {
|
|
|
66
74
|
toolInstallations?: DoctorToolInstallation[];
|
|
67
75
|
requiredPluginDrift?: Array<{ pluginId: string; kind: string; reason: string }>;
|
|
68
76
|
pluginFailures?: PluginVerificationFailure[];
|
|
69
|
-
/**
|
|
70
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Present for a companion that declares audiences or has one selected.
|
|
79
|
+
* `wrapped` is the audience the working repository is wrapped for, when any.
|
|
80
|
+
*/
|
|
81
|
+
audiences?: { declared: string[]; active: string | null; wrapped?: string };
|
|
71
82
|
/** Set when `MATE_AUDIENCE` names an audience the companion does not declare. */
|
|
72
83
|
audienceError?: string;
|
|
84
|
+
/** Present when an audience's contributions reached a persisted surface, or a launch layer cannot deliver part of one. */
|
|
85
|
+
audienceIsolation?: AudienceIsolationReport;
|
|
73
86
|
/** IDs of the capabilities the verified plugins provide. */
|
|
74
87
|
pluginCapabilities?: string[];
|
|
75
88
|
engineRequirement?: { range: string; ok: boolean; detail: string };
|
|
@@ -230,12 +243,18 @@ function collectAudiences(
|
|
|
230
243
|
report: DoctorReport,
|
|
231
244
|
config: FrameworkConfig,
|
|
232
245
|
env: Record<string, string | undefined>,
|
|
246
|
+
repositoryPath: string | null,
|
|
233
247
|
): void {
|
|
234
248
|
const declared = Object.keys(config.audiences ?? {});
|
|
235
249
|
const requested = readAudienceSelection(env);
|
|
236
250
|
if (declared.length === 0 && requested === null) return;
|
|
237
251
|
const resolution = resolveAudience(config, requested);
|
|
238
|
-
|
|
252
|
+
const wrapped = repositoryPath ? readWrapStateSync(repositoryPath).audience : null;
|
|
253
|
+
report.audiences = {
|
|
254
|
+
declared,
|
|
255
|
+
active: resolution.ok ? resolution.active : null,
|
|
256
|
+
...(wrapped ? { wrapped } : {}),
|
|
257
|
+
};
|
|
239
258
|
if (!resolution.ok) report.audienceError = describeUnknownAudience(resolution);
|
|
240
259
|
}
|
|
241
260
|
|
|
@@ -274,7 +293,16 @@ export async function collectDoctorReport(deps: DoctorDeps = {}): Promise<Doctor
|
|
|
274
293
|
|
|
275
294
|
const env = deps.env ?? process.env;
|
|
276
295
|
if (root.rootPath && root.config && kind !== "hub") {
|
|
277
|
-
collectAudiences(report, root.config, env);
|
|
296
|
+
collectAudiences(report, root.config, env, root.linkedRepository?.path ?? null);
|
|
297
|
+
const isolation = await inspectAudienceIsolation(
|
|
298
|
+
root.rootPath,
|
|
299
|
+
root.config,
|
|
300
|
+
deps.registry ?? getActiveDistribution().registry,
|
|
301
|
+
report.audiences?.active ?? null,
|
|
302
|
+
);
|
|
303
|
+
if (isolation.leaked.length + isolation.unswept.length + isolation.undelivered.length > 0) {
|
|
304
|
+
report.audienceIsolation = isolation;
|
|
305
|
+
}
|
|
278
306
|
}
|
|
279
307
|
|
|
280
308
|
if (root.rootPath && root.config) {
|
|
@@ -325,6 +353,49 @@ function renderHubMembers(members: DoctorHubMember[]): string {
|
|
|
325
353
|
);
|
|
326
354
|
}
|
|
327
355
|
|
|
356
|
+
function renderAudienceIsolation(isolation: AudienceIsolationReport): string {
|
|
357
|
+
const parts: string[] = [];
|
|
358
|
+
if (isolation.leaked.length > 0) {
|
|
359
|
+
parts.push(
|
|
360
|
+
"Contributions on persisted surfaces that no base plugin owns; the next sync removes them:",
|
|
361
|
+
renderTable(
|
|
362
|
+
["Surface", "Kind", "Entry", "Owner"],
|
|
363
|
+
isolation.leaked.map((entry) => [
|
|
364
|
+
entry.surface,
|
|
365
|
+
entry.kind,
|
|
366
|
+
entry.identity ?? "",
|
|
367
|
+
entry.owner,
|
|
368
|
+
]),
|
|
369
|
+
),
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
if (isolation.unswept.length > 0) {
|
|
373
|
+
parts.push(
|
|
374
|
+
`Audience plugins not swept (could not be loaded); run \`MATE_AUDIENCE=<audience> ${FRAMEWORK_NAME} install --yes\` with their credentials:`,
|
|
375
|
+
renderList(isolation.unswept, ""),
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
if (isolation.undelivered.length > 0) {
|
|
379
|
+
parts.push(
|
|
380
|
+
"Active audience plugins contribute declaratively only; these parts are not delivered:",
|
|
381
|
+
renderList(
|
|
382
|
+
isolation.undelivered.map(
|
|
383
|
+
(entry) =>
|
|
384
|
+
`${entry.pluginId}: ${entry.parts
|
|
385
|
+
.map((part) =>
|
|
386
|
+
part === "pluginReferences"
|
|
387
|
+
? "OpenCode plugin references are not delivered"
|
|
388
|
+
: `${part} is not run`,
|
|
389
|
+
)
|
|
390
|
+
.join("; ")}`,
|
|
391
|
+
),
|
|
392
|
+
"",
|
|
393
|
+
),
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
return parts.join("\n\n");
|
|
397
|
+
}
|
|
398
|
+
|
|
328
399
|
function renderHumanReport(report: DoctorReport): void {
|
|
329
400
|
console.log("Mate Doctor");
|
|
330
401
|
console.log("");
|
|
@@ -371,9 +442,20 @@ function renderHumanReport(report: DoctorReport): void {
|
|
|
371
442
|
renderKeyValueTable([
|
|
372
443
|
["Declared", report.audiences.declared.join(", ") || "none"],
|
|
373
444
|
["Active", report.audiences.active ?? "none"],
|
|
445
|
+
...(report.audiences.wrapped
|
|
446
|
+
? [
|
|
447
|
+
["Wrapped", `${report.audiences.wrapped} (bare sessions of this checkout)`] as [
|
|
448
|
+
string,
|
|
449
|
+
string,
|
|
450
|
+
],
|
|
451
|
+
]
|
|
452
|
+
: []),
|
|
374
453
|
]),
|
|
375
454
|
);
|
|
376
455
|
}
|
|
456
|
+
if (report.audienceIsolation) {
|
|
457
|
+
printSection("Audience Isolation", renderAudienceIsolation(report.audienceIsolation));
|
|
458
|
+
}
|
|
377
459
|
if (report.hub) printSection("Hub Members", renderHubMembers(report.hub.members));
|
|
378
460
|
// Companions with no drift produce no section at all: required plugins are
|
|
379
461
|
// silent when healthy.
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
1
3
|
import { confirm } from "../confirm";
|
|
2
4
|
import { FRAMEWORK_NAME } from "../../framework";
|
|
5
|
+
import { ConfigStore } from "../../lib/orchestrator/config-store";
|
|
3
6
|
import {
|
|
4
7
|
buildInstallPlan,
|
|
5
8
|
inspectInstallPlan,
|
|
@@ -176,6 +179,11 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
|
|
|
176
179
|
}
|
|
177
180
|
|
|
178
181
|
try {
|
|
182
|
+
if (!frozen && context.kind === "companion" && context.companionPath) {
|
|
183
|
+
await new ConfigStore(
|
|
184
|
+
path.join(context.companionPath, `.${FRAMEWORK_NAME}`, "config", "framework.yaml"),
|
|
185
|
+
).save(context.config);
|
|
186
|
+
}
|
|
179
187
|
await reconcileInstalledCompanion(plan);
|
|
180
188
|
await saveCompleteInstallState(plan, execution.results);
|
|
181
189
|
} catch (error) {
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
|
|
4
1
|
import { reportDataToDocument } from "./adapter";
|
|
5
2
|
import { validateReportDocument } from "./contract";
|
|
6
3
|
import { highlightInline, highlightStyleBlock } from "./highlight";
|
|
@@ -14,26 +11,19 @@ import type {
|
|
|
14
11
|
ReportValue,
|
|
15
12
|
} from "./types";
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Pinned and integrity-checked, so a CDN cannot serve other code. Offline, the
|
|
16
|
+
* script fails to load and each diagram stays readable as its escaped source.
|
|
17
|
+
*/
|
|
18
|
+
export const MERMAID_RUNTIME_URL =
|
|
19
|
+
"https://cdn.jsdelivr.net/npm/mermaid@12.0.0/dist/mermaid.min.js";
|
|
20
|
+
const MERMAID_RUNTIME_INTEGRITY =
|
|
21
|
+
"sha384-xzghz1GQ5u9HCpVskeDPqMsdogD1yvuMQbEK53+wi+G70+6J1AG0L2cfi9PHjDWI";
|
|
21
22
|
|
|
22
23
|
const MERMAID_RUNTIME_ID = "mermaid-runtime";
|
|
23
24
|
|
|
24
25
|
const CODE_BLOCK_SELECTOR = "pre.report-code";
|
|
25
26
|
|
|
26
|
-
let mermaidRuntime: string | undefined;
|
|
27
|
-
|
|
28
|
-
/** A literal `</script` inside the bundle would close the inline element, so it is neutralised. */
|
|
29
|
-
function readMermaidRuntime(): string {
|
|
30
|
-
mermaidRuntime ??= readFileSync(MERMAID_RUNTIME_PATH, "utf8").replaceAll(
|
|
31
|
-
"</script",
|
|
32
|
-
String.raw`<\/script`,
|
|
33
|
-
);
|
|
34
|
-
return mermaidRuntime;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
27
|
const carriesMermaid = (sections: ReportSection[]): boolean =>
|
|
38
28
|
sections.some((section) => section.type === "diagram" && section.mermaid !== undefined);
|
|
39
29
|
|
|
@@ -384,7 +374,7 @@ export function renderHTML(input: RenderInput): string {
|
|
|
384
374
|
? `\n <style>${highlightStyleBlock(CODE_BLOCK_SELECTOR)}${VISUAL_SECTION_CSS}</style>`
|
|
385
375
|
: "";
|
|
386
376
|
const diagramRuntime = carriesMermaid(data.sections)
|
|
387
|
-
? `\n <script id="${MERMAID_RUNTIME_ID}"
|
|
377
|
+
? `\n <script id="${MERMAID_RUNTIME_ID}" src="${MERMAID_RUNTIME_URL}" integrity="${MERMAID_RUNTIME_INTEGRITY}" crossorigin="anonymous"></script>\n <script>window.mermaid?.initialize({ startOnLoad: true, securityLevel: "strict" });</script>`
|
|
388
378
|
: "";
|
|
389
379
|
|
|
390
380
|
return `<!doctype html>
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { FRAMEWORK_NAME } from "../../../framework";
|
|
4
|
+
import { findRepoLocalLinkedRepository } from "../../../lib/orchestrator/repository-link";
|
|
5
|
+
import { prepareSession, type PrepareResult } from "../../../lib/orchestrator/session-prepare";
|
|
6
|
+
import { readProjectionFile } from "../../../runtime/projection";
|
|
7
|
+
import { readWrapState, WRAP_RUNTIMES } from "../../../runtime/wrap-state";
|
|
8
|
+
import { writeJsonStdout } from "../../write-json-stdout";
|
|
9
|
+
import { launchAmbiguityDeps } from "../shared/companion-selection";
|
|
10
|
+
|
|
11
|
+
export interface SessionPrepareArgs {
|
|
12
|
+
repo: string;
|
|
13
|
+
json: boolean;
|
|
14
|
+
/** Fast-forward the companion first, then the local phase when it moved. */
|
|
15
|
+
sync: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const USAGE = `Usage: ${FRAMEWORK_NAME} session prepare --repo <dir> [--json] [--sync]`;
|
|
19
|
+
|
|
20
|
+
function fail(message: string, usage = false): null {
|
|
21
|
+
console.error(`${FRAMEWORK_NAME}: ${message}`);
|
|
22
|
+
if (usage) console.error(USAGE);
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function parseSessionPrepareArgs(
|
|
28
|
+
argv: string[],
|
|
29
|
+
cwd: string = process.cwd(),
|
|
30
|
+
): SessionPrepareArgs | { error: string } {
|
|
31
|
+
const args: SessionPrepareArgs = { repo: cwd, json: false, sync: false };
|
|
32
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
33
|
+
const arg = argv[index]!;
|
|
34
|
+
const [flag, inline] = arg.startsWith("--") && arg.includes("=") ? arg.split(/=(.*)/s) : [arg];
|
|
35
|
+
if (flag === "--json") {
|
|
36
|
+
args.json = true;
|
|
37
|
+
} else if (flag === "--repo") {
|
|
38
|
+
const repo = inline ?? argv[++index];
|
|
39
|
+
if (!repo) return { error: "--repo requires a directory" };
|
|
40
|
+
args.repo = path.resolve(cwd, repo);
|
|
41
|
+
} else if (flag === "--sync") {
|
|
42
|
+
args.sync = true;
|
|
43
|
+
} else {
|
|
44
|
+
return { error: `unknown session prepare option: ${arg}` };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return args;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const sessionPrepareCommandDeps = {
|
|
51
|
+
findRepoLocalLinkedRepository,
|
|
52
|
+
prepareSession,
|
|
53
|
+
resolveLinkedCompanions: launchAmbiguityDeps.resolveLinkedCompanions,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
async function resolveCompanionPath(repoPath: string): Promise<string | undefined> {
|
|
57
|
+
const projected = readProjectionFile(repoPath)?.projection.companionPath;
|
|
58
|
+
if (projected) return projected;
|
|
59
|
+
return (await sessionPrepareCommandDeps.resolveLinkedCompanions(repoPath))[0]?.companionPath;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function printHuman(result: PrepareResult): void {
|
|
63
|
+
for (const step of result.steps) {
|
|
64
|
+
const detail = step.detail ? ` — ${step.detail}` : "";
|
|
65
|
+
const runtimes = step.runtimes ? ` (${step.runtimes.join(", ")})` : "";
|
|
66
|
+
console.log(` ${step.step}${runtimes}: ${step.outcome}${detail}`);
|
|
67
|
+
}
|
|
68
|
+
for (const warning of result.warnings) {
|
|
69
|
+
console.error(`${FRAMEWORK_NAME}: warning: ${warning}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* `mate session prepare`: a launch's preparation for each runtime the
|
|
75
|
+
* repository is wrapped for, without the launch. Without `--sync` it touches no remote; with it,
|
|
76
|
+
* it fast-forwards the companion and re-projects only when the companion moved.
|
|
77
|
+
* Non-zero when the repository is not wrapped or a step failed; a Git conflict
|
|
78
|
+
* or an offline remote is only a warning.
|
|
79
|
+
*/
|
|
80
|
+
export async function runSessionPrepareCommand(
|
|
81
|
+
argv: string[],
|
|
82
|
+
cwd: string = process.cwd(),
|
|
83
|
+
): Promise<PrepareResult | null> {
|
|
84
|
+
const parsed = parseSessionPrepareArgs(argv, cwd);
|
|
85
|
+
if ("error" in parsed) return fail(parsed.error, true);
|
|
86
|
+
|
|
87
|
+
const deps = sessionPrepareCommandDeps;
|
|
88
|
+
const repository = await deps.findRepoLocalLinkedRepository(parsed.repo);
|
|
89
|
+
const wrapped = repository ? (await readWrapState(repository.path)).runtimes : new Set();
|
|
90
|
+
const runtimes = WRAP_RUNTIMES.filter((runtime) => wrapped.has(runtime));
|
|
91
|
+
if (!repository || runtimes.length === 0) {
|
|
92
|
+
return fail(`${parsed.repo} is not wrapped. Run \`${FRAMEWORK_NAME} wrap\` there first.`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const companionPath = await resolveCompanionPath(repository.path);
|
|
96
|
+
if (!companionPath) return fail(`no companion is linked to ${repository.id}.`);
|
|
97
|
+
|
|
98
|
+
const result = await deps.prepareSession({
|
|
99
|
+
runtime: runtimes[0]!,
|
|
100
|
+
runtimes,
|
|
101
|
+
companionPath,
|
|
102
|
+
repository,
|
|
103
|
+
mode: parsed.sync ? "sync" : "local",
|
|
104
|
+
});
|
|
105
|
+
/** The failure's own error and the wrap state are for an in-process caller; the JSON names steps. */
|
|
106
|
+
const { failure: _failure, wrapState: _wrapState, ...reported } = result;
|
|
107
|
+
if (parsed.json) await writeJsonStdout(reported);
|
|
108
|
+
else printHuman(result);
|
|
109
|
+
if (!result.ok) process.exitCode = 1;
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function runSessionCommand(subcommand: string | undefined, rest: string[]) {
|
|
114
|
+
if (subcommand === "prepare") {
|
|
115
|
+
await runSessionPrepareCommand(rest);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
fail(`unknown session subcommand: ${subcommand ?? ""}`.trimEnd(), true);
|
|
119
|
+
}
|
|
@@ -4,9 +4,9 @@ import { FRAMEWORK_NAME } from "../../../framework";
|
|
|
4
4
|
import {
|
|
5
5
|
CompanionResolver,
|
|
6
6
|
type CompanionMatch,
|
|
7
|
-
|
|
7
|
+
findRepoLocalLinkedRepository,
|
|
8
|
+
} from "../../../lib/orchestrator/repository-link";
|
|
8
9
|
import { GlobalConfigStore } from "../../../lib/orchestrator/global-config-store";
|
|
9
|
-
import { findRepoLocalLinkedRepository } from "../../../lib/orchestrator/repo-local-registry";
|
|
10
10
|
import { projectWorkingRepositoryBestEffort } from "../../../lib/orchestrator/working-repo-projection";
|
|
11
11
|
import { resolveProjection } from "../../../runtime/projection";
|
|
12
12
|
import { selectCompanion } from "../../companion-selector";
|