@uniqbit/mate-core 0.18.3-canary.0 → 0.18.3
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/companion.ts +47 -12
- package/opencode-plugin/src/server.ts +39 -4
- 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/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 +1 -1
- 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/unwrap.ts +28 -3
- package/src/cli/commands/wrap.ts +53 -59
- package/src/cli/companion-selector.tsx +1 -1
- package/src/cli/main.ts +17 -0
- package/src/cli/usage.ts +3 -1
- package/src/hooks/session-banner.ts +19 -10
- package/src/hooks/session-prepare.ts +101 -0
- package/src/lib/fs-utils.ts +13 -0
- package/src/lib/orchestrator/adapters/base.ts +43 -8
- package/src/lib/orchestrator/adapters/claude.ts +130 -29
- package/src/lib/orchestrator/adapters/opencode.ts +17 -19
- 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 +56 -2
- package/src/lib/orchestrator/framework-context.ts +5 -29
- package/src/lib/orchestrator/launcher.ts +36 -171
- package/src/lib/orchestrator/projection-claude-entry.ts +17 -6
- package/src/lib/orchestrator/projection-claude-skills.ts +54 -22
- package/src/lib/orchestrator/projection-entries.ts +78 -11
- package/src/lib/orchestrator/projection-opencode-copies.ts +230 -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 +38 -33
- 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 +425 -0
- package/src/lib/orchestrator/setup-compatibilities.ts +1 -1
- package/src/lib/orchestrator/setup-preflight.ts +3 -1
- package/src/lib/orchestrator/types.ts +4 -1
- package/src/lib/orchestrator/working-repo-projection.ts +71 -159
- package/src/lib/orchestrator/workspace-inventory.ts +2 -2
- package/src/lib/orchestrator/wrap-pass.ts +131 -0
- package/src/lib/runtime-directory-refresh.ts +186 -0
- package/src/lib/update-checker.ts +6 -0
- package/src/opencode/companion-hooks.ts +24 -132
- package/src/opencode/companion-policy.ts +26 -10
- package/src/opencode/index.ts +1 -7
- 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/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/runtime-directory.ts +43 -0
- package/src/runtime/runtime-document-manifest.ts +335 -0
- package/src/runtime/session-prepare-runner.ts +148 -0
- package/src/runtime/wrap-state.ts +90 -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/capabilities/react-doctor.ts +34 -50
- package/src/tools/setup/dynamic-plugins/hydrate.ts +1 -1
- package/src/tools/setup/engine.ts +15 -4
- package/src/tools/setup/mate.ts +1 -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 +23 -6
- package/src/tools/setup/runtime-documents.ts +152 -32
- package/src/tools/setup/working-repo-local-state.ts +82 -13
- package/src/tools/setup.ts +2 -2
- 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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "Mate hooks
|
|
2
|
+
"description": "Mate hooks \u2014 PreToolUse artifact-path guardrail, SessionStart banner, companion guidance and session prepare",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"PreToolUse": [
|
|
5
5
|
{
|
|
@@ -28,6 +28,15 @@
|
|
|
28
28
|
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session-guidance.mjs\""
|
|
29
29
|
}
|
|
30
30
|
]
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"hooks": [
|
|
34
|
+
{
|
|
35
|
+
"type": "command",
|
|
36
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session-prepare.mjs\"",
|
|
37
|
+
"timeout": 45
|
|
38
|
+
}
|
|
39
|
+
]
|
|
31
40
|
}
|
|
32
41
|
]
|
|
33
42
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Thin shim for src/hooks/session-banner.ts, loaded through ts-loader.mjs: the installed
|
|
4
|
+
* plugin sits inside node_modules, where node's native type stripping is off.
|
|
5
|
+
*/
|
|
5
6
|
import "./ts-loader.mjs";
|
|
6
7
|
|
|
7
8
|
const { run } = await import("../../src/hooks/session-banner.ts");
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Thin shim for src/hooks/session-prepare.ts, loaded through ts-loader.mjs: the installed
|
|
4
|
+
* plugin sits inside node_modules, where node's native type stripping is off.
|
|
5
|
+
*/
|
|
6
|
+
import "./ts-loader.mjs";
|
|
7
|
+
|
|
8
|
+
const { run } = await import("../../src/hooks/session-prepare.ts");
|
|
9
|
+
process.exitCode = await run();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
|
|
3
4
|
import type { Context } from "@opencode/plugin/promise/plugin";
|
|
@@ -6,14 +7,36 @@ import { COMPANION_POLICY_MARKER, type MateGuidanceFile } from "../../src/runtim
|
|
|
6
7
|
|
|
7
8
|
import type { CompanionContext } from "../../src/opencode/companion-policy";
|
|
8
9
|
import { resolveOpenCodeGuidance } from "../../src/opencode/projected-guidance";
|
|
10
|
+
import {
|
|
11
|
+
runtimeDirectoryPath,
|
|
12
|
+
runtimeMateBin,
|
|
13
|
+
runtimeWrapperBinPath,
|
|
14
|
+
} from "../../src/runtime/runtime-directory";
|
|
9
15
|
|
|
10
16
|
function prependPathEntry(pathValue: string | undefined, entry: string): string {
|
|
11
17
|
const entries = (pathValue ?? "").split(path.delimiter).filter(Boolean);
|
|
12
18
|
return [entry, ...entries.filter((value) => value !== entry)].join(path.delimiter);
|
|
13
19
|
}
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
|
|
21
|
+
/**
|
|
22
|
+
* The launch environment first, then the runtime directory every `mate` run
|
|
23
|
+
* keeps current. `null` when neither exists: guidance naming an unresolved
|
|
24
|
+
* placeholder would send the model to a path that is not there.
|
|
25
|
+
*/
|
|
26
|
+
function resolveWrapperBinPath(): string | null {
|
|
27
|
+
if (process.env.MATE_WRAPPER_BIN_PATH) return process.env.MATE_WRAPPER_BIN_PATH;
|
|
28
|
+
const runtime = runtimeWrapperBinPath();
|
|
29
|
+
return fs.existsSync(runtime) ? runtime : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The runtime-directory launcher for the Mate CLI; `null` until a `mate` run wrote it. */
|
|
33
|
+
export function resolveMateBin(): string | null {
|
|
34
|
+
const launcher = runtimeMateBin();
|
|
35
|
+
return fs.existsSync(launcher) ? launcher : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function missingRuntimeDirectoryMessage(): string {
|
|
39
|
+
return `Mate runtime directory ${runtimeDirectoryPath()} is missing; run \`mate\` once to repair it.`;
|
|
17
40
|
}
|
|
18
41
|
|
|
19
42
|
function buildStartupError(details: string[]): Error {
|
|
@@ -37,8 +60,11 @@ function loadGuidance(directory: string): MateGuidanceFile | null {
|
|
|
37
60
|
return guidance;
|
|
38
61
|
}
|
|
39
62
|
|
|
40
|
-
function materializeCompanionGuidance(
|
|
41
|
-
|
|
63
|
+
function materializeCompanionGuidance(
|
|
64
|
+
guidance: string,
|
|
65
|
+
context: CompanionContext,
|
|
66
|
+
wrapperBinPath: string,
|
|
67
|
+
): string {
|
|
42
68
|
return guidance
|
|
43
69
|
.replaceAll("$MATE_REPO_PATH", context.repositoryPath)
|
|
44
70
|
.replaceAll("$MATE_ARTIFACT_PATH", context.companionPath)
|
|
@@ -46,8 +72,12 @@ function materializeCompanionGuidance(guidance: string, context: CompanionContex
|
|
|
46
72
|
.replaceAll("$MATE_REPO_ID", context.repositoryId);
|
|
47
73
|
}
|
|
48
74
|
|
|
49
|
-
function buildSystemPrompt(
|
|
50
|
-
|
|
75
|
+
function buildSystemPrompt(
|
|
76
|
+
context: CompanionContext,
|
|
77
|
+
guidance: MateGuidanceFile,
|
|
78
|
+
wrapperBinPath: string,
|
|
79
|
+
): string[] {
|
|
80
|
+
const lines = [materializeCompanionGuidance(guidance.companionGuidance, context, wrapperBinPath)];
|
|
51
81
|
|
|
52
82
|
if (guidance.codebaseExplorationGuidance.trim()) {
|
|
53
83
|
lines.push("", guidance.codebaseExplorationGuidance);
|
|
@@ -70,8 +100,8 @@ function readSystemText(part: unknown): string {
|
|
|
70
100
|
function applyCompanionShellEnv(
|
|
71
101
|
context: CompanionContext,
|
|
72
102
|
env: Record<string, string | undefined>,
|
|
103
|
+
wrapperBinPath: string,
|
|
73
104
|
): void {
|
|
74
|
-
const wrapperBinPath = resolveWrapperBinPath();
|
|
75
105
|
env.MATE_NAME = context.frameworkName;
|
|
76
106
|
env.MATE_VERSION = process.env.MATE_VERSION ?? "unknown";
|
|
77
107
|
env.MATE_ARTIFACT_PATH = context.companionPath;
|
|
@@ -95,8 +125,7 @@ function applyCompanionShellEnv(
|
|
|
95
125
|
env.PATH = prependPathEntry(process.env.PATH, wrapperBinPath);
|
|
96
126
|
}
|
|
97
127
|
|
|
98
|
-
function companionPathsResult(context: CompanionContext) {
|
|
99
|
-
const wrapperBinPath = resolveWrapperBinPath();
|
|
128
|
+
function companionPathsResult(context: CompanionContext, wrapperBinPath: string) {
|
|
100
129
|
return {
|
|
101
130
|
content: JSON.stringify(
|
|
102
131
|
{
|
|
@@ -146,8 +175,14 @@ function mergeCompanionSystem(system: unknown[], companion: string): string[] {
|
|
|
146
175
|
export async function registerCompanion(api: Context, context: CompanionContext): Promise<void> {
|
|
147
176
|
const guidance = loadGuidance(api.location.directory);
|
|
148
177
|
if (!guidance) return;
|
|
178
|
+
/** Reported, not thrown: the guardrails registered before this must stay live. */
|
|
179
|
+
const wrapperBinPath = resolveWrapperBinPath();
|
|
180
|
+
if (!wrapperBinPath) {
|
|
181
|
+
console.error(`mate: ${missingRuntimeDirectoryMessage()}`);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
149
184
|
|
|
150
|
-
const companion = buildSystemPrompt(context, guidance).join("\n");
|
|
185
|
+
const companion = buildSystemPrompt(context, guidance, wrapperBinPath).join("\n");
|
|
151
186
|
|
|
152
187
|
await api.session.hook("context", (event) => {
|
|
153
188
|
const merged = mergeCompanionSystem(event.system, companion);
|
|
@@ -164,7 +199,7 @@ export async function registerCompanion(api: Context, context: CompanionContext)
|
|
|
164
199
|
});
|
|
165
200
|
|
|
166
201
|
await api.shell.hook("create.before", (event) => {
|
|
167
|
-
applyCompanionShellEnv(context, event.env);
|
|
202
|
+
applyCompanionShellEnv(context, event.env, wrapperBinPath);
|
|
168
203
|
});
|
|
169
204
|
|
|
170
205
|
await api.tool.transform((editor) => {
|
|
@@ -173,7 +208,7 @@ export async function registerCompanion(api: Context, context: CompanionContext)
|
|
|
173
208
|
description: `Return active ${context.frameworkName} companion framework and working repository paths.`,
|
|
174
209
|
input: { type: "object", properties: {}, additionalProperties: false },
|
|
175
210
|
async execute() {
|
|
176
|
-
return companionPathsResult(context);
|
|
211
|
+
return companionPathsResult(context, wrapperBinPath);
|
|
177
212
|
},
|
|
178
213
|
});
|
|
179
214
|
});
|
|
@@ -1,9 +1,40 @@
|
|
|
1
1
|
import { Plugin } from "@opencode/plugin";
|
|
2
|
-
import {
|
|
2
|
+
import type { Context } from "@opencode/plugin/promise/plugin";
|
|
3
|
+
import {
|
|
4
|
+
registerCompanionHooks,
|
|
5
|
+
repositoryWrappedForOpenCode,
|
|
6
|
+
} from "../../src/opencode/companion-hooks";
|
|
3
7
|
import { readContext } from "../../src/opencode/companion-policy";
|
|
8
|
+
import { registerSessionPrepare } from "../../src/opencode/session-prepare";
|
|
9
|
+
import { hasLaunchEnvironment } from "../../src/runtime/env";
|
|
10
|
+
import { spawnPrepareRunner } from "../../src/runtime/session-prepare-runner";
|
|
11
|
+
import { readWrapStateSync } from "../../src/runtime/wrap-state";
|
|
4
12
|
|
|
5
13
|
import { registerCompanionAccess } from "./add-dir";
|
|
6
|
-
import { registerCompanion } from "./companion";
|
|
14
|
+
import { missingRuntimeDirectoryMessage, registerCompanion, resolveMateBin } from "./companion";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Only a location in a repository wrapped for OpenCode prepares, and only
|
|
18
|
+
* without a launch environment: `mate opencode` has already prepared the
|
|
19
|
+
* session it starts.
|
|
20
|
+
*/
|
|
21
|
+
async function registerPrepare(api: Context, companionPath: string) {
|
|
22
|
+
if (hasLaunchEnvironment(process.env)) return undefined;
|
|
23
|
+
const repoRoot = repositoryWrappedForOpenCode(api.location.directory);
|
|
24
|
+
if (!repoRoot) return undefined;
|
|
25
|
+
const mateBin = resolveMateBin();
|
|
26
|
+
if (!mateBin) {
|
|
27
|
+
console.error(`mate: ${missingRuntimeDirectoryMessage()}`);
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
return registerSessionPrepare(api, {
|
|
31
|
+
directory: api.location.directory,
|
|
32
|
+
repoPath: repoRoot,
|
|
33
|
+
companionPath,
|
|
34
|
+
mateServers: readWrapStateSync(repoRoot).openCodeMcpServers,
|
|
35
|
+
run: spawnPrepareRunner(mateBin, repoRoot),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
7
38
|
|
|
8
39
|
/**
|
|
9
40
|
* Mate OpenCode server plugin (OpenCode 2.x). Loaded through the package's
|
|
@@ -17,9 +48,13 @@ export const MateOpenCodePlugin = Plugin.define({
|
|
|
17
48
|
if (!context.companionPath) return;
|
|
18
49
|
|
|
19
50
|
await registerCompanionAccess(api, context.companionPath);
|
|
20
|
-
|
|
51
|
+
await registerCompanionHooks(api, context);
|
|
21
52
|
await registerCompanion(api, context);
|
|
22
|
-
|
|
53
|
+
/** Last: the guardrails above are live while the local phase holds `setup`. */
|
|
54
|
+
const prepareCleanup = await registerPrepare(api, context.companionPath);
|
|
55
|
+
return async () => {
|
|
56
|
+
await prepareCleanup?.();
|
|
57
|
+
};
|
|
23
58
|
},
|
|
24
59
|
});
|
|
25
60
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniqbit/mate-core",
|
|
3
|
-
"version": "0.18.3
|
|
3
|
+
"version": "0.18.3",
|
|
4
4
|
"description": "Core framework and plugin APIs for Mate.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"typecheck": "bunx tsc --noEmit -p ../../tsconfig.json",
|
|
36
36
|
"lint": "oxlint src/ opencode-plugin/",
|
|
37
37
|
"lint:fix": "oxlint src/ opencode-plugin/ --fix",
|
|
38
|
-
"format": "oxfmt src/ opencode-plugin/ \"!**/*.generated.*\"
|
|
39
|
-
"format:check": "oxfmt src/ opencode-plugin/ \"!**/*.generated.*\"
|
|
38
|
+
"format": "oxfmt src/ opencode-plugin/ \"!**/*.generated.*\"",
|
|
39
|
+
"format:check": "oxfmt src/ opencode-plugin/ \"!**/*.generated.*\" --check"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@opencode/plugin": "^2.0.23",
|
|
@@ -356,7 +356,7 @@ export async function resolveDriftedSpecs(
|
|
|
356
356
|
message: `mate: not a canonical spec: ${outside.join(", ")}. Only specs under ${SPECS_RELATIVE_DIR}/ are spec publication targets.`,
|
|
357
357
|
};
|
|
358
358
|
}
|
|
359
|
-
const driftedPaths = new Set(drifted);
|
|
359
|
+
const driftedPaths = new Set(drifted.map(withoutTrailingSlash));
|
|
360
360
|
const notDrifted = normalized.filter((candidate) => !driftedPaths.has(candidate));
|
|
361
361
|
if (notDrifted.length > 0) {
|
|
362
362
|
return {
|
|
@@ -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
|
),
|
|
@@ -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 {
|
|
@@ -6,7 +6,7 @@ import { gitEnvironment } from "../../runtime/companion-git";
|
|
|
6
6
|
import { getActiveDistribution } from "../../distribution";
|
|
7
7
|
import { checkEngineRequirement } from "../../lib/orchestrator/engine-guard";
|
|
8
8
|
import type { GlobalConfigStore } from "../../lib/orchestrator/global-config-store";
|
|
9
|
-
import { pathIsDirectory } from "../../lib/
|
|
9
|
+
import { pathIsDirectory } from "../../lib/fs-utils";
|
|
10
10
|
import { resolveRootContext, type RootContext } from "../../lib/orchestrator/root-context";
|
|
11
11
|
import type { CapabilityConfig, FrameworkConfig, HubMember } from "../../lib/orchestrator/types";
|
|
12
12
|
import {
|
|
@@ -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>
|