@uniqbit/mate-core 0.18.3-canary.1 → 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 +9 -0
- 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/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
|
@@ -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";
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type StudioAccess,
|
|
8
8
|
type StudioInvocation,
|
|
9
9
|
} from "./access";
|
|
10
|
+
import { MERMAID_RUNTIME_URL } from "../report/renderer";
|
|
10
11
|
import { collectStudioInventory, type StudioInventory } from "./inventory";
|
|
11
12
|
import { assembleCompanionPayload, type StudioCompanionResponse } from "./payload";
|
|
12
13
|
import { createReportStore, isReportId, REPORT_LIMITS, type ReportStore } from "./reports";
|
|
@@ -189,11 +190,14 @@ function refused(reason: string, status: number): Response {
|
|
|
189
190
|
});
|
|
190
191
|
}
|
|
191
192
|
|
|
192
|
-
/**
|
|
193
|
+
/**
|
|
194
|
+
* Sent with every hosted report: opaque origin, no Studio cookie or storage, and
|
|
195
|
+
* no connections beyond the one pinned, integrity-checked diagram runtime.
|
|
196
|
+
*/
|
|
193
197
|
const REPORT_CSP = [
|
|
194
198
|
"sandbox allow-scripts allow-modals",
|
|
195
199
|
"default-src 'none'",
|
|
196
|
-
|
|
200
|
+
`script-src 'unsafe-inline' ${MERMAID_RUNTIME_URL}`,
|
|
197
201
|
"style-src 'unsafe-inline'",
|
|
198
202
|
"img-src data:",
|
|
199
203
|
"font-src data:",
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { FRAMEWORK_NAME } from "../../framework";
|
|
2
|
-
import {
|
|
2
|
+
import { attendedLockOptions } from "../../lib/orchestrator/companion-git-sync";
|
|
3
|
+
import { findRepoLocalLinkedRepository } from "../../lib/orchestrator/repository-link";
|
|
3
4
|
import { unwrapWorkingRuntimeDocuments } from "../../lib/orchestrator/working-repo-projection";
|
|
5
|
+
import { withCompanionLock } from "../../runtime/companion-lock";
|
|
6
|
+
import { readProjectionFile } from "../../runtime/projection";
|
|
4
7
|
|
|
5
8
|
export interface UnwrapCommandDependencies {
|
|
6
9
|
findRepoLocalLinkedRepository?: typeof findRepoLocalLinkedRepository;
|
|
@@ -48,7 +51,24 @@ export async function runUnwrapCommand(
|
|
|
48
51
|
return;
|
|
49
52
|
}
|
|
50
53
|
|
|
51
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Under the lock of the companion the projection names, so a prepare or sync
|
|
56
|
+
* for it cannot rewrite the manifest mid-withdrawal. No companion, no lock:
|
|
57
|
+
* unwrapping must still work when the companion is gone.
|
|
58
|
+
*/
|
|
59
|
+
const companionPath = readProjectionFile(repository.path)?.projection.companionPath;
|
|
60
|
+
const locked = companionPath
|
|
61
|
+
? await withCompanionLock(companionPath, attendedLockOptions(companionPath), () =>
|
|
62
|
+
unwrap(repository.path),
|
|
63
|
+
)
|
|
64
|
+
: { acquired: true as const, value: await unwrap(repository.path) };
|
|
65
|
+
if (!locked.acquired) {
|
|
66
|
+
console.error(`${FRAMEWORK_NAME}: another companion synchronization is still running.`);
|
|
67
|
+
process.exitCode = 1;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const result = locked.value;
|
|
52
72
|
if (result.kind === "failed") {
|
|
53
73
|
console.error(
|
|
54
74
|
`${FRAMEWORK_NAME}: failed to withdraw ${result.document} for ${repository.id}: ${result.error.message}`,
|
|
@@ -66,5 +86,10 @@ export async function runUnwrapCommand(
|
|
|
66
86
|
for (const document of result.documents) {
|
|
67
87
|
console.log(` Withdrawn: ${document}`);
|
|
68
88
|
}
|
|
69
|
-
|
|
89
|
+
if (result.copies.length > 0) {
|
|
90
|
+
console.log(` Withdrawn: ${result.copies.length} companion copies under .opencode/`);
|
|
91
|
+
}
|
|
92
|
+
console.log(
|
|
93
|
+
`The next \`${FRAMEWORK_NAME} claude\` or \`${FRAMEWORK_NAME} opencode\` wraps this repository again.`,
|
|
94
|
+
);
|
|
70
95
|
}
|
package/src/cli/commands/wrap.ts
CHANGED
|
@@ -1,38 +1,34 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
3
|
import { FRAMEWORK_NAME } from "../../framework";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from "../../
|
|
10
|
-
import {
|
|
4
|
+
import { findRepoLocalLinkedRepository } from "../../lib/orchestrator/repository-link";
|
|
5
|
+
import type { ProjectionChangedPaths } from "../../lib/orchestrator/projection-types";
|
|
6
|
+
import { attendedLockOptions } from "../../lib/orchestrator/companion-git-sync";
|
|
7
|
+
import { readCompanionConfig } from "../../lib/orchestrator/config-store";
|
|
8
|
+
import { runWrapPass, type WrapPassResult } from "../../lib/orchestrator/wrap-pass";
|
|
9
|
+
import { withCompanionLock } from "../../runtime/companion-lock";
|
|
10
|
+
import { WRAP_RUNTIMES } from "../../runtime/wrap-state";
|
|
11
11
|
import { launchAmbiguityDeps } from "./shared/companion-selection";
|
|
12
12
|
|
|
13
13
|
export interface WrapCommandDependencies {
|
|
14
14
|
findRepoLocalLinkedRepository?: typeof findRepoLocalLinkedRepository;
|
|
15
|
-
|
|
16
|
-
projectWorkingRuntimeDocuments?: typeof projectWorkingRuntimeDocuments;
|
|
17
|
-
renderWorkingRuntimeDocuments?: typeof renderWorkingRuntimeDocuments;
|
|
15
|
+
runWrapPass?: typeof runWrapPass;
|
|
18
16
|
resolveLinkedCompanions?: typeof launchAmbiguityDeps.resolveLinkedCompanions;
|
|
19
17
|
}
|
|
20
18
|
|
|
21
19
|
export const wrapCommandDeps: Required<WrapCommandDependencies> = {
|
|
22
20
|
findRepoLocalLinkedRepository,
|
|
23
|
-
|
|
24
|
-
projectWorkingRuntimeDocuments,
|
|
25
|
-
renderWorkingRuntimeDocuments,
|
|
21
|
+
runWrapPass,
|
|
26
22
|
resolveLinkedCompanions: (cwd: string) => launchAmbiguityDeps.resolveLinkedCompanions(cwd),
|
|
27
23
|
};
|
|
28
24
|
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
)
|
|
35
|
-
|
|
25
|
+
/** Counts rather than names: a companion folder can hold dozens of files. */
|
|
26
|
+
function reportChangedPaths(changed: ProjectionChangedPaths): void {
|
|
27
|
+
if (changed.written.length > 0 || changed.removed.length > 0) {
|
|
28
|
+
console.log(
|
|
29
|
+
` Copies: ${changed.written.length} written, ${changed.removed.length} withdrawn under .opencode/`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
36
32
|
}
|
|
37
33
|
|
|
38
34
|
/**
|
|
@@ -76,11 +72,7 @@ export async function runWrapCommand(
|
|
|
76
72
|
): Promise<void> {
|
|
77
73
|
const findRepository =
|
|
78
74
|
deps.findRepoLocalLinkedRepository ?? wrapCommandDeps.findRepoLocalLinkedRepository;
|
|
79
|
-
const
|
|
80
|
-
const projectDocuments =
|
|
81
|
-
deps.projectWorkingRuntimeDocuments ?? wrapCommandDeps.projectWorkingRuntimeDocuments;
|
|
82
|
-
const renderDocuments =
|
|
83
|
-
deps.renderWorkingRuntimeDocuments ?? wrapCommandDeps.renderWorkingRuntimeDocuments;
|
|
75
|
+
const wrap = deps.runWrapPass ?? wrapCommandDeps.runWrapPass;
|
|
84
76
|
const resolveLinked = deps.resolveLinkedCompanions ?? wrapCommandDeps.resolveLinkedCompanions;
|
|
85
77
|
|
|
86
78
|
const parsed = parseWrapArgs(argv);
|
|
@@ -105,8 +97,8 @@ export async function runWrapCommand(
|
|
|
105
97
|
* here — a single Linked Companion needs no answer to have been recorded.
|
|
106
98
|
*/
|
|
107
99
|
const companionPath =
|
|
108
|
-
(parsed.companion && path.resolve(parsed.companion))
|
|
109
|
-
process.env.MATE_ARTIFACT_PATH
|
|
100
|
+
(parsed.companion && path.resolve(parsed.companion)) ||
|
|
101
|
+
process.env.MATE_ARTIFACT_PATH ||
|
|
110
102
|
(await resolveLinked(cwd))[0]?.companionPath;
|
|
111
103
|
if (!companionPath) {
|
|
112
104
|
console.error(`${FRAMEWORK_NAME}: no registered companion links ${repository.id}.`);
|
|
@@ -115,50 +107,52 @@ export async function runWrapCommand(
|
|
|
115
107
|
return;
|
|
116
108
|
}
|
|
117
109
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
`${FRAMEWORK_NAME}: failed to write the projection for ${repository.id}: ${result.error.message}`,
|
|
122
|
-
);
|
|
123
|
-
process.exitCode = 1;
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* After the projection, never instead of it: a runtime document that cannot
|
|
129
|
-
* be written fails the command while the projection already written stays.
|
|
130
|
-
*/
|
|
131
|
-
const config = await readCompanionConfig(result.companionPath);
|
|
132
|
-
const documents = await projectDocuments(
|
|
133
|
-
result.companionPath,
|
|
134
|
-
repository,
|
|
135
|
-
config,
|
|
136
|
-
await renderDocuments(result.companionPath, config, repository.path),
|
|
110
|
+
/** Serialized with Session Prepare and every sync, which rewrite the same manifest. */
|
|
111
|
+
const locked = await withCompanionLock(companionPath, attendedLockOptions(companionPath), () =>
|
|
112
|
+
wrap(companionPath, repository),
|
|
137
113
|
);
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
114
|
+
const result: WrapPassResult = locked.acquired
|
|
115
|
+
? locked.value
|
|
116
|
+
: { kind: "failed", message: "another companion synchronization is still running." };
|
|
117
|
+
if (result.kind === "failed") {
|
|
118
|
+
console.error(`${FRAMEWORK_NAME}: ${result.message}`);
|
|
142
119
|
process.exitCode = 1;
|
|
143
120
|
return;
|
|
144
121
|
}
|
|
145
122
|
|
|
146
|
-
const changed = result.kind === "written" || documents.kind === "written";
|
|
147
123
|
console.log(
|
|
148
|
-
|
|
124
|
+
result.kind === "written"
|
|
149
125
|
? `Wrapped ${repository.id} at ${result.projectionRoot}`
|
|
150
126
|
: `Already wrapped: ${result.projectionRoot}`,
|
|
151
127
|
);
|
|
152
128
|
console.log(` Companion: ${result.companionPath}`);
|
|
153
|
-
for (const document of
|
|
129
|
+
for (const document of result.documents) {
|
|
154
130
|
console.log(` Runtime: ${document}`);
|
|
155
131
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
132
|
+
for (const warning of result.warnings) {
|
|
133
|
+
console.error(`${FRAMEWORK_NAME}: warning: ${warning}`);
|
|
134
|
+
}
|
|
135
|
+
reportChangedPaths(result.changed);
|
|
136
|
+
const { runtimes } = result;
|
|
137
|
+
const tools = WRAP_RUNTIMES.filter((runtime) => runtimes.has(runtime));
|
|
138
|
+
if (tools.length === 0) return;
|
|
139
|
+
const plain = tools.map((tool) => `\`${tool}\``).join(", ");
|
|
140
|
+
const managed = tools.map((tool) => `\`${FRAMEWORK_NAME} ${tool}\``).join(", ");
|
|
161
141
|
console.log(
|
|
162
|
-
`Start sessions yourself
|
|
142
|
+
`Start sessions yourself (${plain}) or through ${managed}; both work here. \`${FRAMEWORK_NAME} unwrap\` reverses this.`,
|
|
163
143
|
);
|
|
144
|
+
if (runtimes.has("claude") && (await contextModeEnabled(result.companionPath))) {
|
|
145
|
+
console.log(
|
|
146
|
+
`Note: context-mode loads only in sessions started with \`${FRAMEWORK_NAME} claude\`.`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function contextModeEnabled(companionPath: string): Promise<boolean> {
|
|
152
|
+
try {
|
|
153
|
+
const config = await readCompanionConfig(companionPath);
|
|
154
|
+
return (config.capabilities ?? []).some((capability) => capability.name === "context-mode");
|
|
155
|
+
} catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
164
158
|
}
|
|
@@ -3,7 +3,7 @@ import React, { useEffect } from "react";
|
|
|
3
3
|
import { SelectMenu, type SelectMenuItem } from "../lib/components/select-menu";
|
|
4
4
|
import { WizardFooter } from "../lib/components/wizard-footer";
|
|
5
5
|
import { WizardHeader } from "../lib/components/wizard-header";
|
|
6
|
-
import type { CompanionMatch } from "../lib/orchestrator/
|
|
6
|
+
import type { CompanionMatch } from "../lib/orchestrator/repository-link";
|
|
7
7
|
|
|
8
8
|
export interface CompanionSelectorOptions {
|
|
9
9
|
stdin?: NodeJS.ReadStream;
|
package/src/cli/main.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { runLaunchClaudeCommand } from "./commands/launch/claude";
|
|
|
16
16
|
import { runLaunchOpenCodeCommand } from "./commands/launch/opencode";
|
|
17
17
|
import { runPluginCommand } from "./commands/plugin/plugin";
|
|
18
18
|
import { runReportCommand } from "./commands/report";
|
|
19
|
+
import { runSessionCommand } from "./commands/session/prepare";
|
|
19
20
|
import { runStudioCommand } from "./commands/studio";
|
|
20
21
|
import { runUpdateCommand } from "./commands/update";
|
|
21
22
|
import { runUnwrapCommand } from "./commands/unwrap";
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
ensureUnambiguousCompanion,
|
|
34
35
|
type CompanionSelectionOptions,
|
|
35
36
|
} from "./commands/shared/companion-selection";
|
|
37
|
+
import { ensureRuntimeDirectory, refreshRuntimeDirectory } from "../lib/runtime-directory-refresh";
|
|
36
38
|
import { hydrateDynamicPlugins } from "../tools/setup/dynamic-plugins/hydrate";
|
|
37
39
|
import { findPluginCliCommand } from "./plugin-commands";
|
|
38
40
|
import { usage } from "./usage";
|
|
@@ -60,6 +62,7 @@ export interface MainDeps {
|
|
|
60
62
|
repairInstallState?: typeof repairInstallState;
|
|
61
63
|
hydrateDynamicPlugins: typeof hydrateDynamicPlugins;
|
|
62
64
|
resolveRootContext: typeof resolveRootContext;
|
|
65
|
+
refreshRuntimeDirectory?: typeof refreshRuntimeDirectory;
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
const mainDeps: MainDeps = {
|
|
@@ -68,11 +71,17 @@ const mainDeps: MainDeps = {
|
|
|
68
71
|
repairInstallState,
|
|
69
72
|
hydrateDynamicPlugins,
|
|
70
73
|
resolveRootContext,
|
|
74
|
+
refreshRuntimeDirectory,
|
|
71
75
|
};
|
|
72
76
|
|
|
73
77
|
export async function main(argv = process.argv, deps: MainDeps = mainDeps): Promise<void> {
|
|
74
78
|
const [, , command, subcommand, ...rest] = argv;
|
|
75
79
|
|
|
80
|
+
if (!(await ensureRuntimeDirectory(command, deps.refreshRuntimeDirectory))) {
|
|
81
|
+
process.exitCode = 1;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
76
85
|
if (command === "-v" || command === "--version") {
|
|
77
86
|
console.log(getActiveDistribution().config.version);
|
|
78
87
|
return;
|
|
@@ -292,6 +301,14 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
|
|
|
292
301
|
await runWrapCommand(wrapArgs);
|
|
293
302
|
return;
|
|
294
303
|
}
|
|
304
|
+
case "session":
|
|
305
|
+
/**
|
|
306
|
+
* Spawned by the OpenCode plugin for a bare session: no update prompt,
|
|
307
|
+
* no companion picker, no install repair may block it.
|
|
308
|
+
*/
|
|
309
|
+
if (!(await gate({}))) return;
|
|
310
|
+
await runSessionCommand(subcommand, rest);
|
|
311
|
+
return;
|
|
295
312
|
case "unwrap":
|
|
296
313
|
// A recovery path: it withdraws what a wrap placed, so it must never be
|
|
297
314
|
// gated on a companion or an installation it does not consult.
|
package/src/cli/usage.ts
CHANGED
|
@@ -23,6 +23,7 @@ export function usage(): string {
|
|
|
23
23
|
` ${n} working cleanup`,
|
|
24
24
|
` ${n} wrap [--companion PATH]`,
|
|
25
25
|
` ${n} unwrap`,
|
|
26
|
+
` ${n} session prepare --repo <dir> [--json] [--sync]`,
|
|
26
27
|
` ${n} hub init [folder]`,
|
|
27
28
|
` ${n} hub add [source] [--id ID] [--path PATH]`,
|
|
28
29
|
` ${n} hub sync [--json] (companions + hub plugins)`,
|
|
@@ -53,7 +54,8 @@ export function usage(): string {
|
|
|
53
54
|
"",
|
|
54
55
|
"Wrapped repositories:",
|
|
55
56
|
` ${n} wrap configures the repository so sessions you start yourself load the companion`,
|
|
56
|
-
` ${n} claude
|
|
57
|
+
` ${n} claude does not run while a repository is wrapped for Claude`,
|
|
58
|
+
` ${n} opencode wraps the repository for OpenCode itself, then starts plain \`opencode\``,
|
|
57
59
|
` ${n} unwrap withdraws the wrap and restores managed launches; the link is kept`,
|
|
58
60
|
"",
|
|
59
61
|
"Doctor states:",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Show the active Mate companion paths in Claude Code, and repair the
|
|
2
2
|
// companion's Git state before the session reads it.
|
|
3
|
+
import { unmanagedClaudeDelivery } from "../runtime/claude-delivery-plan";
|
|
3
4
|
import {
|
|
4
5
|
syncCompanionUnattended,
|
|
5
6
|
unattendedSyncStalenessLines,
|
|
@@ -44,17 +45,25 @@ export async function buildBanner(
|
|
|
44
45
|
if (!context.repositoryPath || !context.companionPath) return { exitCode: 0, stdout: "" };
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
48
|
+
* Repaired before the session reads the companion, so its first reads are
|
|
49
|
+
* current. The companion can still move later — a plain OpenCode session's
|
|
50
|
+
* background sync fast-forwards it — but a fast-forward never overwrites a
|
|
51
|
+
* local edit, so the worst case is a stale read of an upstream change. Only
|
|
52
|
+
* tries the companion lock: a held lock means another sync is already on it,
|
|
53
|
+
* and a session start must not stall behind it. The launch-environment check
|
|
54
|
+
* comes first, so a Managed Session pays nothing at all — not the freshness
|
|
55
|
+
* read, not the policy read; and the consent gate is the operation's own.
|
|
56
|
+
* Who owns it otherwise is the delivery plan's call, so a session start
|
|
57
|
+
* fetches once.
|
|
52
58
|
*/
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
const ownsGitSync =
|
|
60
|
+
!hasLaunchEnvironment(env) &&
|
|
61
|
+
unmanagedClaudeDelivery(projection?.repoRoot).gitSync === "banner";
|
|
62
|
+
const gitNotes = ownsGitSync
|
|
63
|
+
? unattendedSyncStalenessLines(
|
|
64
|
+
await syncCompanionUnattended(context.companionPath, { lock: "try", ...syncOptions }),
|
|
65
|
+
)
|
|
66
|
+
: [];
|
|
58
67
|
|
|
59
68
|
const lines = [
|
|
60
69
|
`mate v${env.MATE_VERSION || mateVersion()}`,
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/** Prepares a plain Claude session in a Claude-wrapped repository as `mate claude` would. */
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
|
|
4
|
+
import { unmanagedClaudeDelivery } from "../runtime/claude-delivery-plan";
|
|
5
|
+
import { hasLaunchEnvironment } from "../runtime/env";
|
|
6
|
+
import { resolveProjection } from "../runtime/projection";
|
|
7
|
+
import { runtimeDirectoryPath, runtimeMateBin } from "../runtime/runtime-directory";
|
|
8
|
+
import {
|
|
9
|
+
PREPARE_TIMEOUT_MS,
|
|
10
|
+
spawnPrepareRunner,
|
|
11
|
+
startDetachedPrepareSync,
|
|
12
|
+
type PrepareRun,
|
|
13
|
+
type PrepareRunner,
|
|
14
|
+
} from "../runtime/session-prepare-runner";
|
|
15
|
+
import type { HookEnv } from "./validate-artifact-path";
|
|
16
|
+
|
|
17
|
+
export interface SessionPrepareHookOutcome {
|
|
18
|
+
exitCode: number;
|
|
19
|
+
stdout: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SessionPrepareHookDeps {
|
|
23
|
+
/** The runtime-directory launcher; `null` until a `mate` run wrote it. */
|
|
24
|
+
mateBin: () => string | null;
|
|
25
|
+
runner: (mateBin: string, repoPath: string) => PrepareRunner;
|
|
26
|
+
startSync: (mateBin: string, repoPath: string) => void;
|
|
27
|
+
timeoutMs: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const sessionPrepareHookDeps: SessionPrepareHookDeps = {
|
|
31
|
+
mateBin: () => {
|
|
32
|
+
const launcher = runtimeMateBin();
|
|
33
|
+
return fs.existsSync(launcher) ? launcher : null;
|
|
34
|
+
},
|
|
35
|
+
runner: spawnPrepareRunner,
|
|
36
|
+
startSync: startDetachedPrepareSync,
|
|
37
|
+
timeoutMs: PREPARE_TIMEOUT_MS,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const INERT: SessionPrepareHookOutcome = { exitCode: 0, stdout: "" };
|
|
41
|
+
|
|
42
|
+
/** `systemMessage` only: the model must never be told to repair the companion. */
|
|
43
|
+
function operatorMessage(lines: string[]): SessionPrepareHookOutcome {
|
|
44
|
+
if (lines.length === 0) return INERT;
|
|
45
|
+
return { exitCode: 0, stdout: `${JSON.stringify({ systemMessage: lines.join("\n") })}\n` };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function runLocalPhase(
|
|
49
|
+
deps: SessionPrepareHookDeps,
|
|
50
|
+
mateBin: string,
|
|
51
|
+
projectDir: string,
|
|
52
|
+
): Promise<PrepareRun> {
|
|
53
|
+
const pending = deps.runner(mateBin, projectDir)({ sync: false, timeoutMs: deps.timeoutMs });
|
|
54
|
+
try {
|
|
55
|
+
return await pending;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
return { ok: false, notes: [error instanceof Error ? error.message : String(error)] };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Inert under a launch environment or where the plan gives it nothing to own.
|
|
63
|
+
* Awaits the local phase within a bound, then starts Git sync detached. Always
|
|
64
|
+
* exits zero: a failed prepare must not block the session.
|
|
65
|
+
*/
|
|
66
|
+
export async function runSessionPrepareHook(
|
|
67
|
+
env: HookEnv,
|
|
68
|
+
cwd: string = process.cwd(),
|
|
69
|
+
deps: SessionPrepareHookDeps = sessionPrepareHookDeps,
|
|
70
|
+
): Promise<SessionPrepareHookOutcome> {
|
|
71
|
+
if (hasLaunchEnvironment(env)) return INERT;
|
|
72
|
+
const projectDir = env.CLAUDE_PROJECT_DIR || cwd;
|
|
73
|
+
const projection = resolveProjection(projectDir);
|
|
74
|
+
if (!projection) return INERT;
|
|
75
|
+
const plan = unmanagedClaudeDelivery(projection.repoRoot);
|
|
76
|
+
const reconciles = plan.reconcile === "prepare-hook";
|
|
77
|
+
const syncs = plan.gitSync === "prepare-hook";
|
|
78
|
+
if (!reconciles && !syncs) return INERT;
|
|
79
|
+
|
|
80
|
+
const mateBin = deps.mateBin();
|
|
81
|
+
if (!mateBin) {
|
|
82
|
+
return operatorMessage([
|
|
83
|
+
`mate: session prepare skipped: ${runtimeDirectoryPath()} is missing; run \`mate\` once to repair it.`,
|
|
84
|
+
]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const local = reconciles
|
|
88
|
+
? await runLocalPhase(deps, mateBin, projectDir)
|
|
89
|
+
: { ok: true, notes: [] };
|
|
90
|
+
if (syncs) deps.startSync(mateBin, projectDir);
|
|
91
|
+
|
|
92
|
+
const verdict = local.ok ? "warning" : "failed";
|
|
93
|
+
return operatorMessage(local.notes.map((note) => `mate: session prepare ${verdict}: ${note}`));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Plugin-shim entry. */
|
|
97
|
+
export async function run(): Promise<number> {
|
|
98
|
+
const outcome = await runSessionPrepareHook(process.env);
|
|
99
|
+
if (outcome.stdout) process.stdout.write(outcome.stdout);
|
|
100
|
+
return outcome.exitCode;
|
|
101
|
+
}
|
package/src/lib/fs-utils.ts
CHANGED
|
@@ -33,3 +33,16 @@ export async function pathIsDirectory(candidatePath: string): Promise<boolean> {
|
|
|
33
33
|
return false;
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Real path, so a Windows junction (read back `\\?\`-prefixed) or an aliased
|
|
39
|
+
* spelling compares equal; resolved text when dangling.
|
|
40
|
+
*/
|
|
41
|
+
export async function realOrResolved(candidate: string): Promise<string> {
|
|
42
|
+
return fs.realpath(candidate).catch(() => path.resolve(candidate));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** {@link realOrResolved} for a link whose raw `readlink` text is `target`. */
|
|
46
|
+
export async function realLinkTarget(link: string, target: string): Promise<string> {
|
|
47
|
+
return fs.realpath(link).catch(() => path.resolve(path.dirname(link), target));
|
|
48
|
+
}
|