@uniqbit/mate-core 0.15.4-canary.8 → 0.15.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/artifact-finish-nudge.mjs +5 -3
- package/claude-plugin/hooks/session-banner.mjs +5 -3
- package/claude-plugin/hooks/ts-loader.mjs +28 -0
- package/claude-plugin/hooks/validate-artifact-path.mjs +5 -3
- package/package.json +1 -1
- package/src/cli/commands/cap/openspec.ts +20 -1
- package/src/cli/commands/companion/hub.ts +1 -0
- package/src/cli/commands/plugin/install.ts +15 -3
- package/src/hooks/artifact-finish-nudge.ts +35 -3
- package/src/lib/context-mode-package.ts +5 -3
- package/src/lib/orchestrator/adapters/opencode.ts +10 -60
- package/src/lib/orchestrator/companion-git-sync.ts +67 -8
- package/src/lib/orchestrator/companion-hub.ts +28 -6
- package/src/lib/orchestrator/types.ts +2 -2
- package/src/lib/package-paths.ts +1 -0
- package/src/opencode/companion-hooks.ts +33 -12
- package/src/playbooks/companion-guidance.ts +1 -1
- package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-openspec-backfill/SKILL.md +65 -0
- package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +1 -0
- package/src/templates/root/TEMPLATE_AGENTS.md +2 -0
- package/src/templates/root/TEMPLATE_CLAUDE.md +2 -0
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +3612 -0
- package/src/tools/setup/capabilities/context-mode.ts +57 -83
- package/src/tools/setup/capabilities/graphify-shared.ts +27 -0
- package/src/tools/setup/capabilities/graphify.ts +86 -295
- package/src/tools/setup/capabilities/openspec.ts +34 -1
- package/src/tools/setup/capabilities/react-doctor.ts +37 -37
- package/src/tools/setup/capabilities/rtk.ts +4 -1
- package/src/tools/setup/capabilities/tokensave-shared.ts +6 -0
- package/src/tools/setup/capabilities/tokensave.ts +51 -72
- package/src/tools/setup/context-services.ts +29 -0
- package/src/tools/setup/dynamic-plugins/hydrate.ts +15 -1
- package/src/tools/setup/engine.ts +68 -1
- package/src/tools/setup/mate.ts +10 -2
- package/src/tools/setup/plugin.ts +105 -0
- package/src/tools/setup/plugins/gitignore.ts +7 -4
- package/src/tools/setup/plugins/guidance.ts +1 -1
- package/src/tools/setup/providers/agent-file-sections.ts +78 -0
- package/src/tools/setup/providers/claude-format.ts +159 -0
- package/src/tools/setup/providers/claude.ts +281 -227
- package/src/tools/setup/providers/opencode-format.ts +146 -0
- package/src/tools/setup/providers/opencode.ts +209 -60
- package/src/tools/setup/providers/skill-tree.ts +38 -0
- package/src/tools/setup.ts +7 -56
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Thin shim: hook logic lives in src/hooks/artifact-finish-nudge.ts
|
|
3
|
-
//
|
|
4
|
-
|
|
2
|
+
// Thin shim: hook logic lives in src/hooks/artifact-finish-nudge.ts, loaded
|
|
3
|
+
// through ts-loader.mjs because the installed plugin sits inside node_modules
|
|
4
|
+
// where node's native type stripping is disabled.
|
|
5
|
+
import "./ts-loader.mjs";
|
|
5
6
|
|
|
7
|
+
const { run } = await import("../../src/hooks/artifact-finish-nudge.ts");
|
|
6
8
|
process.exitCode = await run();
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Thin shim: hook logic lives in src/hooks/session-banner.ts
|
|
3
|
-
//
|
|
4
|
-
|
|
2
|
+
// Thin shim: hook logic lives in src/hooks/session-banner.ts, loaded through
|
|
3
|
+
// ts-loader.mjs because the installed plugin sits inside node_modules where
|
|
4
|
+
// node's native type stripping is disabled.
|
|
5
|
+
import "./ts-loader.mjs";
|
|
5
6
|
|
|
7
|
+
const { run } = await import("../../src/hooks/session-banner.ts");
|
|
6
8
|
process.exitCode = run();
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Synchronous module loader that strips TypeScript types itself. Node's
|
|
2
|
+
// built-in .ts loading refuses files under node_modules
|
|
3
|
+
// (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING), which is exactly where the
|
|
4
|
+
// published plugin lives — so the hook shims register this loader before
|
|
5
|
+
// importing their src/hooks/*.ts implementation.
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import { registerHooks, stripTypeScriptTypes } from "node:module";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
// stripTypeScriptTypes emits an ExperimentalWarning; hooks must keep stderr
|
|
11
|
+
// clean because Claude Code surfaces it to the user on non-zero exits.
|
|
12
|
+
const emitWarning = process.emitWarning;
|
|
13
|
+
process.emitWarning = (warning, ...args) => {
|
|
14
|
+
if (String(warning).includes("stripTypeScriptTypes")) return;
|
|
15
|
+
emitWarning.call(process, warning, ...args);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
registerHooks({
|
|
19
|
+
load(url, context, next) {
|
|
20
|
+
if (!url.endsWith(".ts")) return next(url, context);
|
|
21
|
+
const source = fs.readFileSync(fileURLToPath(url), "utf8");
|
|
22
|
+
return {
|
|
23
|
+
format: "module",
|
|
24
|
+
source: stripTypeScriptTypes(source),
|
|
25
|
+
shortCircuit: true,
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Thin shim: hook logic lives in src/hooks/validate-artifact-path.ts
|
|
3
|
-
//
|
|
4
|
-
|
|
2
|
+
// Thin shim: hook logic lives in src/hooks/validate-artifact-path.ts, loaded
|
|
3
|
+
// through ts-loader.mjs because the installed plugin sits inside node_modules
|
|
4
|
+
// where node's native type stripping is disabled.
|
|
5
|
+
import "./ts-loader.mjs";
|
|
5
6
|
|
|
7
|
+
const { run } = await import("../../src/hooks/validate-artifact-path.ts");
|
|
6
8
|
process.exitCode = await run();
|
package/package.json
CHANGED
|
@@ -79,7 +79,26 @@ export async function runOpenspecCapCommand(
|
|
|
79
79
|
readOpenSpecProjectSchema(companionPath),
|
|
80
80
|
);
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
if (forwardedArgs[0] === "init" || forwardedArgs[0] === "update") {
|
|
83
|
+
const reset = spawnSync("openspec", ["config", "reset", "--all", "-y"], {
|
|
84
|
+
cwd,
|
|
85
|
+
stdio: "inherit",
|
|
86
|
+
});
|
|
87
|
+
if (reset.error) {
|
|
88
|
+
process.stderr.write(`Failed to reset openspec config: ${reset.error.message}\n`);
|
|
89
|
+
process.exitCode = 1;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (reset.status !== 0) {
|
|
93
|
+
process.exitCode = reset.status ?? 1;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const result = spawnSync("openspec", forwardedArgs, {
|
|
99
|
+
cwd,
|
|
100
|
+
stdio: "inherit",
|
|
101
|
+
});
|
|
83
102
|
if (result.error) {
|
|
84
103
|
process.stderr.write(`Failed to run openspec: ${result.error.message}\n`);
|
|
85
104
|
process.exitCode = 1;
|
|
@@ -50,6 +50,7 @@ async function selectRegisteredCompanion(): Promise<string> {
|
|
|
50
50
|
async function runHubInit(argv: string[]): Promise<void> {
|
|
51
51
|
const folder = positionalArgs(argv)[0] ?? process.cwd();
|
|
52
52
|
const hubPath = await initializeCompanionHub(folder);
|
|
53
|
+
await updateHubPlugins(hubPath);
|
|
53
54
|
console.log(`Initialized companion hub: ${hubPath}`);
|
|
54
55
|
}
|
|
55
56
|
|
|
@@ -2,8 +2,10 @@ import path from "node:path";
|
|
|
2
2
|
|
|
3
3
|
import { FRAMEWORK_NAME } from "../../../framework";
|
|
4
4
|
import { resolveInstallContext } from "../../../lib/install";
|
|
5
|
+
import { backfillHubAllowedAgents } from "../../../lib/orchestrator/companion-hub";
|
|
5
6
|
import { ConfigStore } from "../../../lib/orchestrator/config-store";
|
|
6
|
-
import type { PluginDeclaration } from "../../../lib/orchestrator/types";
|
|
7
|
+
import type { FrameworkConfig, PluginDeclaration } from "../../../lib/orchestrator/types";
|
|
8
|
+
import { applySetupCompatibilities } from "../../../tools/setup";
|
|
7
9
|
import { hydrateDynamicPlugins } from "../../../tools/setup/dynamic-plugins/hydrate";
|
|
8
10
|
import {
|
|
9
11
|
installDeclaredPlugins,
|
|
@@ -33,6 +35,7 @@ function withDeclaration(
|
|
|
33
35
|
export interface PluginInstallCommandDeps {
|
|
34
36
|
cwd?: string;
|
|
35
37
|
installDeps?: PluginInstallDeps;
|
|
38
|
+
setupHub?: (companionPath: string, config: FrameworkConfig) => Promise<void>;
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
/**
|
|
@@ -78,8 +81,10 @@ export async function runPluginInstallCommand(
|
|
|
78
81
|
);
|
|
79
82
|
const store = new ConfigStore(configPath);
|
|
80
83
|
const nextPlugins = withDeclaration(context.config.plugins ?? [], packageName, version);
|
|
81
|
-
|
|
82
|
-
|
|
84
|
+
const nextConfig = { ...context.config, plugins: nextPlugins };
|
|
85
|
+
const agentsBackfilled = context.kind === "hub" && backfillHubAllowedAgents(nextConfig);
|
|
86
|
+
if (nextPlugins !== context.config.plugins || agentsBackfilled) {
|
|
87
|
+
await store.save(nextConfig);
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
const results = await installDeclaredPlugins(
|
|
@@ -89,6 +94,13 @@ export async function runPluginInstallCommand(
|
|
|
89
94
|
);
|
|
90
95
|
reportPluginInstallResults(results);
|
|
91
96
|
await hydrateDynamicPlugins({ companionPath: context.companionPath });
|
|
97
|
+
if (context.kind === "hub") {
|
|
98
|
+
await (
|
|
99
|
+
deps.setupHub ??
|
|
100
|
+
((companionPath, config) =>
|
|
101
|
+
applySetupCompatibilities(companionPath, config, "sync", undefined, undefined, "hub"))
|
|
102
|
+
)(context.companionPath, nextConfig);
|
|
103
|
+
}
|
|
92
104
|
|
|
93
105
|
return results.find((result) => result.package === packageName)?.status !== "failed";
|
|
94
106
|
}
|
|
@@ -79,6 +79,36 @@ function extractChangeFromPath(value: string): string | null {
|
|
|
79
79
|
return match ? match[1].replace(/^\d{4}-\d{2}-\d{2}-/, "") : null;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
// Best-effort shell variable substitution, NOT a shell interpreter: a change
|
|
83
|
+
// name is often built up through variables (DEST="...archive/$TARGET"; mv a
|
|
84
|
+
// "$DEST") rather than appearing as a literal path next to mv/archive. This
|
|
85
|
+
// resolves $NAME/${NAME} references against preceding literal NAME=value
|
|
86
|
+
// assignment tokens in the same command string; unresolved references are
|
|
87
|
+
// left as-is, which keeps prior (silent) behavior for anything it can't follow.
|
|
88
|
+
const ASSIGNMENT_PATTERN = /^([A-Za-z_][A-Za-z0-9_]*)=([\s\S]*)$/;
|
|
89
|
+
const VARIABLE_REFERENCE_PATTERN = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
90
|
+
|
|
91
|
+
function substituteVariables(value: string, vars: Map<string, string>): string {
|
|
92
|
+
return value.replace(VARIABLE_REFERENCE_PATTERN, (match, braced: string, bare: string) => {
|
|
93
|
+
const name = braced ?? bare;
|
|
94
|
+
return vars.has(name) ? vars.get(name)! : match;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Assignments are collected in left-to-right token order so a later
|
|
99
|
+
// assignment (e.g. DEST) can reference an earlier one (e.g. TARGET) the same
|
|
100
|
+
// way the shell would resolve it at that point in the script.
|
|
101
|
+
function collectVariableAssignments(parts: string[]): Map<string, string> {
|
|
102
|
+
const vars = new Map<string, string>();
|
|
103
|
+
for (const part of parts) {
|
|
104
|
+
const match = part.match(ASSIGNMENT_PATTERN);
|
|
105
|
+
if (!match) continue;
|
|
106
|
+
const [, name, rawValue] = match;
|
|
107
|
+
vars.set(name, substituteVariables(rawValue, vars));
|
|
108
|
+
}
|
|
109
|
+
return vars;
|
|
110
|
+
}
|
|
111
|
+
|
|
82
112
|
// Shell syntax tokens are never change-name positionals: separators end the
|
|
83
113
|
// archive invocation, redirections are skipped (a bare operator also consumes
|
|
84
114
|
// its target token).
|
|
@@ -95,6 +125,7 @@ export function extractArchiveCommand(command: string): string | null {
|
|
|
95
125
|
(parts[position - 1] === "openspec" || parts[position - 1].endsWith("/openspec")),
|
|
96
126
|
);
|
|
97
127
|
if (index < 0) return null;
|
|
128
|
+
const vars = collectVariableAssignments(parts);
|
|
98
129
|
for (let position = index + 1; position < parts.length; position += 1) {
|
|
99
130
|
const part = parts[position];
|
|
100
131
|
if (!part || part === "--") continue;
|
|
@@ -107,7 +138,7 @@ export function extractArchiveCommand(command: string): string | null {
|
|
|
107
138
|
if (part === "--store") position += 1;
|
|
108
139
|
continue;
|
|
109
140
|
}
|
|
110
|
-
return part;
|
|
141
|
+
return substituteVariables(part, vars);
|
|
111
142
|
}
|
|
112
143
|
return null;
|
|
113
144
|
}
|
|
@@ -130,15 +161,16 @@ function isPython(part: string): boolean {
|
|
|
130
161
|
|
|
131
162
|
export function extractMoveCommand(command: string): string | null {
|
|
132
163
|
const parts = shellSplit(command);
|
|
164
|
+
const vars = collectVariableAssignments(parts);
|
|
133
165
|
for (let index = 0; index < parts.length; index += 1) {
|
|
134
166
|
if (MOVE_COMMANDS.has(parts[index].toLowerCase())) {
|
|
135
167
|
for (const part of parts.slice(index + 1)) {
|
|
136
|
-
const change = extractChangeFromPath(part);
|
|
168
|
+
const change = extractChangeFromPath(substituteVariables(part, vars));
|
|
137
169
|
if (change) return change;
|
|
138
170
|
}
|
|
139
171
|
}
|
|
140
172
|
if (isPython(parts[index]) && /(?:shutil\.move|os\.rename|\.rename\s*\()/.test(command)) {
|
|
141
|
-
return extractChangeFromPath(command);
|
|
173
|
+
return extractChangeFromPath(substituteVariables(command, vars));
|
|
142
174
|
}
|
|
143
175
|
}
|
|
144
176
|
return null;
|
|
@@ -20,7 +20,7 @@ export function isContextModePackageReference(value: unknown): value is string {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export function getContextModeInstallDir(companionPath: string): string {
|
|
23
|
-
return path.join(companionPath, ".mate", "
|
|
23
|
+
return path.join(companionPath, ".mate", "plugins", ".local");
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export function getContextModePackageRoot(companionPath: string): string {
|
|
@@ -37,7 +37,9 @@ export function isContextModeNodeVersionSupported(version: string): boolean {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export function validateContextModeNodeRuntime(): void {
|
|
40
|
-
|
|
40
|
+
// env passed explicitly: bun's spawnSync otherwise ignores in-process PATH
|
|
41
|
+
// changes when resolving the executable.
|
|
42
|
+
const result = spawnSync("node", ["--version"], { encoding: "utf8", env: process.env });
|
|
41
43
|
const version = result.stdout?.trim() ?? "";
|
|
42
44
|
if (result.status !== 0 || !isContextModeNodeVersionSupported(version)) {
|
|
43
45
|
throw new Error(
|
|
@@ -83,7 +85,7 @@ export async function installContextModePackage(companionPath: string): Promise<
|
|
|
83
85
|
const result = spawnSync(
|
|
84
86
|
"npm",
|
|
85
87
|
["install", "--no-audit", "--no-fund", "--silent", "--registry", PUBLIC_NPM_REGISTRY],
|
|
86
|
-
{ cwd: installDir, encoding: "utf8" },
|
|
88
|
+
{ cwd: installDir, encoding: "utf8", env: process.env },
|
|
87
89
|
);
|
|
88
90
|
if (result.error || result.status !== 0) {
|
|
89
91
|
throw new Error(
|
|
@@ -9,61 +9,15 @@ import {
|
|
|
9
9
|
getOpenCodePluginPackageReference,
|
|
10
10
|
isMateOpenCodePluginReference,
|
|
11
11
|
} from "../../opencode-plugin-package";
|
|
12
|
+
import {
|
|
13
|
+
getOpenCodePluginReferences,
|
|
14
|
+
mergeOpenCodeConfigContent,
|
|
15
|
+
readOpenCodeConfig,
|
|
16
|
+
} from "../../../tools/setup/providers/opencode-format";
|
|
12
17
|
import { buildOpenCodeGuidance } from "../opencode-guidance";
|
|
13
18
|
import { LaunchPreflightError } from "../types";
|
|
14
19
|
import { type AdapterContext, LaunchAdapter } from "./base";
|
|
15
20
|
|
|
16
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
17
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function mergeConfig(target: Record<string, unknown>, source: Record<string, unknown>): void {
|
|
21
|
-
for (const [key, sourceValue] of Object.entries(source)) {
|
|
22
|
-
const targetValue = target[key];
|
|
23
|
-
if (isRecord(targetValue) && isRecord(sourceValue)) {
|
|
24
|
-
mergeConfig(targetValue, sourceValue);
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
target[key] = sourceValue;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function mergeOpenCodeConfigContent(
|
|
33
|
-
overlay: Record<string, unknown>,
|
|
34
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
35
|
-
options: { appendSkillPaths?: string[] } = {},
|
|
36
|
-
): string {
|
|
37
|
-
let config: Record<string, unknown> = {};
|
|
38
|
-
const existing = env.OPENCODE_CONFIG_CONTENT;
|
|
39
|
-
|
|
40
|
-
if (existing) {
|
|
41
|
-
try {
|
|
42
|
-
const parsed = JSON.parse(existing) as unknown;
|
|
43
|
-
if (isRecord(parsed)) {
|
|
44
|
-
config = parsed;
|
|
45
|
-
}
|
|
46
|
-
} catch {
|
|
47
|
-
// Ignore invalid inherited config content rather than breaking launch.
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
mergeConfig(config, overlay);
|
|
52
|
-
|
|
53
|
-
if (options.appendSkillPaths && options.appendSkillPaths.length > 0) {
|
|
54
|
-
const skills = isRecord(config.skills) ? config.skills : {};
|
|
55
|
-
const paths = Array.isArray(skills.paths) ? skills.paths : [];
|
|
56
|
-
const existingPaths = new Set(paths);
|
|
57
|
-
skills.paths = [
|
|
58
|
-
...paths,
|
|
59
|
-
...options.appendSkillPaths.filter((skillPath) => !existingPaths.has(skillPath)),
|
|
60
|
-
];
|
|
61
|
-
config.skills = skills;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return JSON.stringify(config);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
21
|
export class OpenCodeAdapter extends LaunchAdapter {
|
|
68
22
|
readonly toolName = "opencode";
|
|
69
23
|
readonly interactive = true;
|
|
@@ -190,18 +144,14 @@ export class OpenCodeAdapter extends LaunchAdapter {
|
|
|
190
144
|
): Promise<string[]> {
|
|
191
145
|
const configPath = path.join(context.companionPath, configFile);
|
|
192
146
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
config = JSON.parse(await fs.readFile(configPath, "utf8"));
|
|
196
|
-
} catch {
|
|
197
|
-
return [`Unreadable OpenCode configuration: ${configPath}`];
|
|
198
|
-
}
|
|
199
|
-
if (!isRecord(config)) {
|
|
147
|
+
const { present, config } = await readOpenCodeConfig(configPath);
|
|
148
|
+
if (!present) {
|
|
200
149
|
return [`Unreadable OpenCode configuration: ${configPath}`];
|
|
201
150
|
}
|
|
202
151
|
|
|
203
|
-
const
|
|
204
|
-
|
|
152
|
+
const mateReferences = getOpenCodePluginReferences(config).filter(
|
|
153
|
+
isMateOpenCodePluginReference,
|
|
154
|
+
) as string[];
|
|
205
155
|
|
|
206
156
|
if (mateReferences.includes(expectedPluginReference)) {
|
|
207
157
|
return [];
|
|
@@ -9,6 +9,9 @@ import { LaunchPreflightError } from "./types";
|
|
|
9
9
|
|
|
10
10
|
const execFile = promisify(execFileCallback);
|
|
11
11
|
|
|
12
|
+
/** Bounded but far above any diagnostic output; default 1 MiB kills git mid-merge on large trees. */
|
|
13
|
+
const GIT_MAX_BUFFER = 64 * 1024 * 1024;
|
|
14
|
+
|
|
12
15
|
export interface GitCommandResult {
|
|
13
16
|
status: number;
|
|
14
17
|
stdout: string;
|
|
@@ -26,22 +29,30 @@ export interface CompanionGitSyncResult {
|
|
|
26
29
|
export class CompanionGitSyncError extends LaunchPreflightError {
|
|
27
30
|
readonly companionPath: string;
|
|
28
31
|
readonly conflictingPaths: string[];
|
|
32
|
+
readonly reason: string;
|
|
33
|
+
readonly recovery: string;
|
|
34
|
+
readonly stashRef: string | undefined;
|
|
29
35
|
|
|
30
36
|
constructor(
|
|
31
37
|
companionPath: string,
|
|
32
38
|
reason: string,
|
|
33
39
|
conflictingPaths: string[] = [],
|
|
34
40
|
recovery = "Resolve or abort the Git operation, then retry the launch.",
|
|
41
|
+
stashRef?: string,
|
|
35
42
|
) {
|
|
36
43
|
const conflicts = conflictingPaths.length
|
|
37
44
|
? `\n Conflicting paths:\n${conflictingPaths.map((entry) => ` - ${entry}`).join("\n")}`
|
|
38
45
|
: "";
|
|
46
|
+
const stash = stashRef
|
|
47
|
+
? ` Local changes were stashed as ${stashRef}. Recover with \`git stash apply ${stashRef}\` in the companion.`
|
|
48
|
+
: "";
|
|
39
49
|
super(
|
|
40
50
|
[
|
|
41
51
|
"mate: companion Git synchronization failed.",
|
|
42
52
|
` Companion: ${companionPath}`,
|
|
43
53
|
` ${reason}`,
|
|
44
54
|
conflicts,
|
|
55
|
+
stash,
|
|
45
56
|
` ${recovery}`,
|
|
46
57
|
" Bypass: `mate claude -- --no-git` or `mate opencode -- --no-git`.",
|
|
47
58
|
]
|
|
@@ -51,6 +62,9 @@ export class CompanionGitSyncError extends LaunchPreflightError {
|
|
|
51
62
|
this.name = "CompanionGitSyncError";
|
|
52
63
|
this.companionPath = companionPath;
|
|
53
64
|
this.conflictingPaths = conflictingPaths;
|
|
65
|
+
this.reason = reason;
|
|
66
|
+
this.recovery = recovery;
|
|
67
|
+
this.stashRef = stashRef;
|
|
54
68
|
}
|
|
55
69
|
}
|
|
56
70
|
|
|
@@ -68,6 +82,7 @@ export const companionGitSyncDeps: { runGit: GitRunner } = {
|
|
|
68
82
|
cwd,
|
|
69
83
|
encoding: "utf8",
|
|
70
84
|
env: gitEnv,
|
|
85
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
71
86
|
});
|
|
72
87
|
return { status: 0, stdout: String(result.stdout), stderr: String(result.stderr) };
|
|
73
88
|
} catch (error) {
|
|
@@ -106,6 +121,21 @@ function outputLines(stdout: string): string[] {
|
|
|
106
121
|
.filter(Boolean);
|
|
107
122
|
}
|
|
108
123
|
|
|
124
|
+
/** Matches Git progress-meter lines such as `Updating files: 76% (11340/14790)` or `..., done.` */
|
|
125
|
+
const GIT_PROGRESS_LINE = /^\S.*?: +\d+% \(\d+\/\d+\)(?:, done\.)?$/;
|
|
126
|
+
|
|
127
|
+
function stripGitProgress(text: string): string {
|
|
128
|
+
return text
|
|
129
|
+
.split(/\r\n|\r|\n/)
|
|
130
|
+
.filter((line) => !GIT_PROGRESS_LINE.test(line.trim()))
|
|
131
|
+
.join("\n")
|
|
132
|
+
.trim();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function describeGitFailure(result: GitCommandResult): string {
|
|
136
|
+
return stripGitProgress(result.stderr) || stripGitProgress(result.stdout) || "unknown Git error";
|
|
137
|
+
}
|
|
138
|
+
|
|
109
139
|
export class CompanionGitSync {
|
|
110
140
|
constructor(private readonly runGit: GitRunner = companionGitSyncDeps.runGit) {}
|
|
111
141
|
|
|
@@ -160,7 +190,42 @@ export class CompanionGitSync {
|
|
|
160
190
|
stashRef = ref.stdout.trim();
|
|
161
191
|
}
|
|
162
192
|
|
|
163
|
-
const
|
|
193
|
+
const headBefore = await this.command(companionPath, ["rev-parse", "HEAD"]);
|
|
194
|
+
try {
|
|
195
|
+
await this.mergeAndRestore(companionPath, target, stashRef);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (stashRef && error instanceof CompanionGitSyncError && !error.stashRef) {
|
|
198
|
+
throw new CompanionGitSyncError(
|
|
199
|
+
error.companionPath,
|
|
200
|
+
error.reason,
|
|
201
|
+
error.conflictingPaths,
|
|
202
|
+
error.recovery,
|
|
203
|
+
stashRef,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const headAfter = await this.command(companionPath, ["rev-parse", "HEAD"]);
|
|
210
|
+
return {
|
|
211
|
+
skipped: false,
|
|
212
|
+
changed: headBefore.stdout.trim() !== headAfter.stdout.trim(),
|
|
213
|
+
companionPath,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private async mergeAndRestore(
|
|
218
|
+
companionPath: string,
|
|
219
|
+
target: SyncTarget,
|
|
220
|
+
stashRef: string | undefined,
|
|
221
|
+
): Promise<void> {
|
|
222
|
+
const merge = await this.command(companionPath, [
|
|
223
|
+
"merge",
|
|
224
|
+
target.ref,
|
|
225
|
+
"--no-stat",
|
|
226
|
+
"--no-progress",
|
|
227
|
+
"--no-edit",
|
|
228
|
+
]);
|
|
164
229
|
if (merge.status !== 0) {
|
|
165
230
|
await this.resolveManagedConflicts(companionPath, target.ref);
|
|
166
231
|
const conflicts = await this.unresolvedPaths(companionPath);
|
|
@@ -220,12 +285,6 @@ export class CompanionGitSync {
|
|
|
220
285
|
// Keep the stash if dropping it fails; it remains a recoverable backup.
|
|
221
286
|
await this.command(companionPath, ["stash", "drop", stashRef]);
|
|
222
287
|
}
|
|
223
|
-
|
|
224
|
-
return {
|
|
225
|
-
skipped: false,
|
|
226
|
-
changed: merge.status === 0 && merge.stdout.trim() !== "",
|
|
227
|
-
companionPath,
|
|
228
|
-
};
|
|
229
288
|
}
|
|
230
289
|
|
|
231
290
|
private async assertSafeCompanionRoot(
|
|
@@ -410,7 +469,7 @@ export class CompanionGitSync {
|
|
|
410
469
|
}
|
|
411
470
|
|
|
412
471
|
private detail(result: GitCommandResult): string {
|
|
413
|
-
return (result
|
|
472
|
+
return describeGitFailure(result);
|
|
414
473
|
}
|
|
415
474
|
|
|
416
475
|
private failure(
|
|
@@ -8,12 +8,14 @@ import { FRAMEWORK_NAME } from "../../framework";
|
|
|
8
8
|
import { ConfigStore, validateHubConfig } from "./config-store";
|
|
9
9
|
import { GlobalConfigStore } from "./global-config-store";
|
|
10
10
|
import type { FrameworkConfig, HubMember, HubMemberSource } from "./types";
|
|
11
|
+
import { listSetupProviderCompatibilities } from "./setup-compatibilities";
|
|
11
12
|
import {
|
|
12
13
|
installDeclaredPlugins,
|
|
13
14
|
type PluginInstallDeps,
|
|
14
15
|
type PluginInstallResult,
|
|
15
16
|
} from "../../tools/setup/dynamic-plugins/install";
|
|
16
17
|
import { hydrateDynamicPlugins } from "../../tools/setup/dynamic-plugins/hydrate";
|
|
18
|
+
import { applySetupCompatibilities } from "../../tools/setup";
|
|
17
19
|
import { findRepoLocalRegistryFile } from "./repo-local-registry";
|
|
18
20
|
|
|
19
21
|
export interface GitCommandResult {
|
|
@@ -41,6 +43,7 @@ export interface HubSyncResult {
|
|
|
41
43
|
export interface HubPluginSyncDeps {
|
|
42
44
|
installDeps?: PluginInstallDeps;
|
|
43
45
|
hydrate?: (options: { companionPath: string }) => Promise<void>;
|
|
46
|
+
setup?: (companionPath: string, config: FrameworkConfig, mode: "setup" | "sync") => Promise<void>;
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
export function defaultGitCommand(cwd: string, args: string[]): GitCommandResult {
|
|
@@ -123,7 +126,7 @@ export async function initializeCompanionHub(
|
|
|
123
126
|
} else {
|
|
124
127
|
const config: FrameworkConfig = {
|
|
125
128
|
type: "hub",
|
|
126
|
-
allowedAgents:
|
|
129
|
+
allowedAgents: listSetupProviderCompatibilities().map((entry) => entry.agent),
|
|
127
130
|
packageManagers: [],
|
|
128
131
|
capabilities: [],
|
|
129
132
|
hub: { companions: [] },
|
|
@@ -365,16 +368,35 @@ export async function syncHub(
|
|
|
365
368
|
return results;
|
|
366
369
|
}
|
|
367
370
|
|
|
371
|
+
/**
|
|
372
|
+
* Hubs persisted before provider bootstrapping carry `allowedAgents: []`;
|
|
373
|
+
* an empty list on a hub means "all built-in agents", not "none".
|
|
374
|
+
*/
|
|
375
|
+
export function backfillHubAllowedAgents(config: FrameworkConfig): boolean {
|
|
376
|
+
if (config.type !== "hub" || config.allowedAgents.length > 0) return false;
|
|
377
|
+
config.allowedAgents = listSetupProviderCompatibilities().map((entry) => entry.agent);
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
|
|
368
381
|
export async function updateHubPlugins(
|
|
369
382
|
hubPath: string,
|
|
370
383
|
deps: HubPluginSyncDeps = {},
|
|
371
384
|
): Promise<PluginInstallResult[]> {
|
|
372
|
-
const { config } = await assertHubRoot(hubPath);
|
|
385
|
+
const { config, store } = await assertHubRoot(hubPath);
|
|
386
|
+
if (backfillHubAllowedAgents(config)) await store.save(config);
|
|
373
387
|
const declarations = config.plugins ?? [];
|
|
374
|
-
if (declarations.length === 0) return [];
|
|
375
|
-
|
|
376
388
|
const resolvedHubPath = path.resolve(hubPath);
|
|
377
|
-
const results =
|
|
378
|
-
|
|
389
|
+
const results =
|
|
390
|
+
declarations.length > 0
|
|
391
|
+
? await installDeclaredPlugins(resolvedHubPath, declarations, deps.installDeps)
|
|
392
|
+
: [];
|
|
393
|
+
if (declarations.length > 0) {
|
|
394
|
+
await (deps.hydrate ?? hydrateDynamicPlugins)({ companionPath: resolvedHubPath });
|
|
395
|
+
}
|
|
396
|
+
await (
|
|
397
|
+
deps.setup ??
|
|
398
|
+
((companionPath, setupConfig, mode) =>
|
|
399
|
+
applySetupCompatibilities(companionPath, setupConfig, mode, undefined, undefined, "hub"))
|
|
400
|
+
)(resolvedHubPath, config, "sync");
|
|
379
401
|
return results;
|
|
380
402
|
}
|
|
@@ -96,8 +96,8 @@ export type PluginDeclarationPolicy = "default" | "optional";
|
|
|
96
96
|
/**
|
|
97
97
|
* A companion-declared npm plugin: installed by setup/install into the
|
|
98
98
|
* companion's own shared plugin workspace (`.mate/plugins/`) and loaded
|
|
99
|
-
* from there on every invocation. Declaration registers
|
|
100
|
-
*
|
|
99
|
+
* from there on every invocation. Declaration both registers and activates
|
|
100
|
+
* the plugin; dynamic plugins do not need a separate `capabilities` entry.
|
|
101
101
|
*/
|
|
102
102
|
export interface PluginDeclaration {
|
|
103
103
|
/** npm package name (e.g. `@acme/custom-plugin`). */
|
package/src/lib/package-paths.ts
CHANGED
|
@@ -46,6 +46,7 @@ export function validateClaudePluginAssets(pluginRoot = getClaudePluginRoot()):
|
|
|
46
46
|
const expected = [
|
|
47
47
|
path.join(".claude-plugin", "plugin.json"),
|
|
48
48
|
path.join("hooks", "hooks.json"),
|
|
49
|
+
path.join("hooks", "ts-loader.mjs"),
|
|
49
50
|
...CLAUDE_PLUGIN_HOOK_SHIMS.map((shim) => path.join("hooks", shim)),
|
|
50
51
|
];
|
|
51
52
|
const missing = expected.filter((asset) => !fs.existsSync(path.join(pluginRoot, asset)));
|