@uniqbit/mate-core 0.15.3 → 0.15.4-canary.10
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/.claude-plugin/plugin.json +4 -0
- package/claude-plugin/hooks/artifact-finish-nudge.mjs +8 -0
- package/claude-plugin/hooks/hooks.json +37 -0
- package/claude-plugin/hooks/session-banner.mjs +8 -0
- package/claude-plugin/hooks/ts-loader.mjs +28 -0
- package/claude-plugin/hooks/validate-artifact-path.mjs +8 -0
- package/package.json +4 -2
- package/src/cli/commands/artifact/finish/openspec.ts +1 -5
- package/src/cli/commands/companion/hub.ts +99 -0
- package/src/cli/commands/companion/link.ts +0 -5
- package/src/cli/commands/companion/tui.ts +3 -2
- package/src/cli/commands/doctor.ts +254 -170
- package/src/cli/commands/install.ts +18 -14
- package/src/cli/commands/launch/shared.ts +4 -4
- package/src/cli/commands/plugin/install.ts +94 -0
- package/src/cli/commands/plugin/plugin.ts +17 -0
- package/src/cli/commands/setup.ts +4 -6
- package/src/cli/commands/update.ts +9 -11
- package/src/cli/companion-link-wizard.tsx +22 -78
- package/src/cli/main.ts +57 -13
- package/src/cli/plugin-commands.ts +4 -4
- package/src/cli/repo-list-table.tsx +2 -7
- package/src/cli/usage.ts +7 -3
- package/src/distribution.ts +3 -5
- package/src/framework.ts +4 -36
- package/src/hooks/artifact-finish-nudge.ts +212 -0
- package/src/hooks/session-banner.ts +25 -0
- package/src/hooks/validate-artifact-path.ts +236 -0
- package/src/index.ts +4 -0
- package/src/lib/context-mode-package.ts +5 -3
- package/src/lib/install.ts +31 -43
- package/src/lib/orchestrator/adapters/base.ts +7 -8
- package/src/lib/orchestrator/adapters/claude.ts +16 -0
- package/src/lib/orchestrator/adapters/opencode.ts +10 -60
- package/src/lib/orchestrator/companion-hub.ts +380 -0
- package/src/lib/orchestrator/companion-store.ts +1 -35
- package/src/lib/orchestrator/config-store.ts +94 -7
- package/src/lib/orchestrator/editor.ts +4 -20
- package/src/lib/orchestrator/framework-context.ts +119 -24
- package/src/lib/orchestrator/global-config-store.ts +1 -5
- package/src/lib/orchestrator/launcher.ts +3 -9
- package/src/lib/orchestrator/migration.ts +0 -23
- package/src/lib/orchestrator/opencode-guidance.ts +1 -2
- package/src/lib/orchestrator/repo-local-registry.ts +3 -1
- package/src/lib/orchestrator/root-context.ts +117 -0
- package/src/lib/orchestrator/setup-compatibilities.ts +1 -1
- package/src/lib/orchestrator/setup-preflight.ts +5 -18
- package/src/lib/orchestrator/types.ts +27 -19
- package/src/lib/orchestrator/working-repo-store.ts +9 -3
- package/src/lib/package-paths.ts +37 -0
- package/src/lib/update-checker.ts +5 -5
- package/src/playbooks/companion-guidance.ts +7 -7
- package/src/runtime/env.ts +0 -4
- package/src/templates/capabilities/openspec-cap/mate-skills/{mate-artifact-finish → agents/mate-artifact-finish}/SKILL.md +8 -8
- package/src/templates/capabilities/openspec-cap/mate-skills/{mate-artifact-finish → agents/mate-artifact-finish}/references/openspec.md +3 -3
- package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/SKILL.md +58 -0
- package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/references/openspec.md +139 -0
- package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +6 -5
- 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 +3541 -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 +27 -30
- 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 +50 -71
- package/src/tools/setup/context-services.ts +29 -0
- package/src/tools/setup/dynamic-plugins/host.ts +2 -1
- package/src/tools/setup/dynamic-plugins/hydrate.ts +3 -13
- package/src/tools/setup/dynamic-plugins/install.ts +124 -109
- package/src/tools/setup/dynamic-plugins/loader.ts +23 -58
- package/src/tools/setup/dynamic-plugins/paths.ts +8 -19
- package/src/tools/setup/dynamic-plugins/registry-hint.ts +14 -0
- package/src/tools/setup/engine.ts +65 -1
- package/src/tools/setup/mate.ts +28 -31
- package/src/tools/setup/plugin.ts +81 -0
- package/src/tools/setup/plugins/gitignore.ts +19 -9
- package/src/tools/setup/plugins/guidance.ts +2 -6
- package/src/tools/setup/policy.ts +4 -7
- 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 +283 -292
- package/src/tools/setup/providers/opencode-format.ts +146 -0
- package/src/tools/setup/providers/opencode.ts +171 -66
- package/src/tools/setup/providers/skill-tree.ts +38 -0
- package/src/tools/setup.ts +12 -7
- package/src/tui.ts +5 -0
- package/src/templates/capabilities/openspec-cap/claude/hooks/mate-artifact-finish.sh +0 -161
- package/src/templates/providers/claude/.claude/hooks/mate-session-banner +0 -28
- package/src/templates/providers/claude/.claude/hooks/validate-artifact-path +0 -242
- package/src/tools/setup/dynamic-plugins/pin-store.ts +0 -30
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
import { FRAMEWORK_NAME
|
|
4
|
+
import { FRAMEWORK_NAME } from "../../framework";
|
|
5
|
+
import { parse } from "yaml";
|
|
5
6
|
import { CompanionResolver } from "./companion-resolver";
|
|
6
7
|
import { ConfigStore } from "./config-store";
|
|
7
8
|
import { GlobalConfigStore } from "./global-config-store";
|
|
8
|
-
import { migrateConfigDir } from "./migration";
|
|
9
9
|
import { findRepoLocalLinkedRepository } from "./repo-local-registry";
|
|
10
10
|
import {
|
|
11
11
|
AmbiguousCompanionError,
|
|
12
|
+
ConfigError,
|
|
13
|
+
type FrameworkConfig,
|
|
14
|
+
type HubConfig,
|
|
12
15
|
type LinkedRepository,
|
|
13
16
|
RepositoryNotFoundError,
|
|
14
17
|
WorkingRepoRequiredError,
|
|
@@ -20,7 +23,8 @@ export interface FrameworkContext {
|
|
|
20
23
|
workingRepoStore: WorkingRepoStore;
|
|
21
24
|
companionPath: string;
|
|
22
25
|
repository?: LinkedRepository;
|
|
23
|
-
contextKind: "env" | "working-repo" | "companion-root";
|
|
26
|
+
contextKind: "env" | "working-repo" | "companion-root" | "hub";
|
|
27
|
+
hub?: HubConfig;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
export interface LaunchContext extends FrameworkContext {
|
|
@@ -34,7 +38,6 @@ function repositoryFromEnvironment(): LinkedRepository | undefined {
|
|
|
34
38
|
return {
|
|
35
39
|
id: process.env.MATE_REPO_ID ?? path.basename(path.resolve(repositoryPath)),
|
|
36
40
|
path: path.resolve(repositoryPath),
|
|
37
|
-
profile: process.env.MATE_REPO_PROFILE ?? "default",
|
|
38
41
|
};
|
|
39
42
|
}
|
|
40
43
|
|
|
@@ -44,6 +47,7 @@ function makeContext(
|
|
|
44
47
|
companionPath: string,
|
|
45
48
|
contextKind: FrameworkContext["contextKind"],
|
|
46
49
|
repository?: LinkedRepository,
|
|
50
|
+
hub?: HubConfig,
|
|
47
51
|
): FrameworkContext {
|
|
48
52
|
const configDir = path.join(companionPath, `.${FRAMEWORK_NAME}`, "config");
|
|
49
53
|
return {
|
|
@@ -52,9 +56,90 @@ function makeContext(
|
|
|
52
56
|
companionPath,
|
|
53
57
|
repository,
|
|
54
58
|
contextKind,
|
|
59
|
+
hub,
|
|
55
60
|
};
|
|
56
61
|
}
|
|
57
62
|
|
|
63
|
+
function isStrictChildPath(parentPath: string, candidatePath: string): boolean {
|
|
64
|
+
const relative = path.relative(parentPath, candidatePath);
|
|
65
|
+
return relative !== "" && !relative.startsWith(".." + path.sep) && !path.isAbsolute(relative);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function validateHubMember(
|
|
69
|
+
resolvedRoot: string,
|
|
70
|
+
realRoot: string,
|
|
71
|
+
member: HubConfig["companions"][number],
|
|
72
|
+
): Promise<ConfigError | null> {
|
|
73
|
+
const memberPath = path.resolve(resolvedRoot, member.path);
|
|
74
|
+
if (!isStrictChildPath(resolvedRoot, memberPath)) {
|
|
75
|
+
return new ConfigError(
|
|
76
|
+
`Hub member "${member.id}" path resolves outside the hub root: ${member.path}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const stats = await fs.stat(memberPath).catch(() => null);
|
|
81
|
+
if (!stats?.isDirectory()) {
|
|
82
|
+
return new ConfigError(`Hub member "${member.id}" directory is missing: ${memberPath}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const realMemberPath = await fs.realpath(memberPath).catch(() => memberPath);
|
|
86
|
+
if (!isStrictChildPath(realRoot, realMemberPath)) {
|
|
87
|
+
return new ConfigError(
|
|
88
|
+
`Hub member "${member.id}" path resolves outside the hub root: ${member.path}`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const childConfigPath = path.join(memberPath, `.${FRAMEWORK_NAME}`, "config", "framework.yaml");
|
|
93
|
+
let childConfig: Partial<FrameworkConfig> | null;
|
|
94
|
+
try {
|
|
95
|
+
childConfig = parse(
|
|
96
|
+
await fs.readFile(childConfigPath, "utf8"),
|
|
97
|
+
) as Partial<FrameworkConfig> | null;
|
|
98
|
+
} catch {
|
|
99
|
+
return new ConfigError(
|
|
100
|
+
`Hub member "${member.id}" must contain a framework.yaml declaring type "companion".`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (childConfig?.type !== "companion") {
|
|
105
|
+
return new ConfigError(
|
|
106
|
+
`Hub member "${member.id}" must declare type "companion" in ${childConfigPath}.`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function validateHubMembers(hubRoot: string, hub: HubConfig): Promise<void> {
|
|
114
|
+
const resolvedRoot = path.resolve(hubRoot);
|
|
115
|
+
const realRoot = await fs.realpath(resolvedRoot).catch(() => resolvedRoot);
|
|
116
|
+
const errors = await Promise.all(
|
|
117
|
+
hub.companions.map((member) => validateHubMember(resolvedRoot, realRoot, member)),
|
|
118
|
+
);
|
|
119
|
+
const firstError = errors.find((error): error is ConfigError => error !== null);
|
|
120
|
+
if (firstError) throw firstError;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function withResolvedHub(context: FrameworkContext): Promise<FrameworkContext> {
|
|
124
|
+
let rawConfig: Partial<FrameworkConfig> | null;
|
|
125
|
+
try {
|
|
126
|
+
rawConfig = parse(
|
|
127
|
+
await fs.readFile(context.configStore.configPath, "utf8"),
|
|
128
|
+
) as Partial<FrameworkConfig> | null;
|
|
129
|
+
} catch {
|
|
130
|
+
return context;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (rawConfig?.type !== "hub") return context;
|
|
134
|
+
|
|
135
|
+
const config = await context.configStore.load();
|
|
136
|
+
if (!config.hub) {
|
|
137
|
+
throw new ConfigError('A "hub" framework requires a hub.companions array.');
|
|
138
|
+
}
|
|
139
|
+
await validateHubMembers(context.companionPath, config.hub);
|
|
140
|
+
return { ...context, contextKind: "hub", hub: config.hub };
|
|
141
|
+
}
|
|
142
|
+
|
|
58
143
|
// Resolves the companion framework context without a repositoryId. Used by commands
|
|
59
144
|
// that only need companion config (e.g. `mate doctor`, `mate config`, `mate report`). Resolution order:
|
|
60
145
|
// 1. MATE_ARTIFACT_PATH env var (agent-launched sessions)
|
|
@@ -67,31 +152,32 @@ export async function resolveFrameworkContext(
|
|
|
67
152
|
const envCompanionPath = process.env.MATE_ARTIFACT_PATH;
|
|
68
153
|
if (envCompanionPath) {
|
|
69
154
|
const repository = repositoryFromEnvironment() ?? (await findRepoLocalLinkedRepository(cwd));
|
|
70
|
-
return
|
|
155
|
+
return withResolvedHub(
|
|
156
|
+
makeContext(path.resolve(envCompanionPath), "env", repository ?? undefined),
|
|
157
|
+
);
|
|
71
158
|
}
|
|
72
159
|
|
|
73
160
|
const match = await new CompanionResolver(globalConfigStore).resolve(cwd);
|
|
74
161
|
if (match) {
|
|
75
|
-
return
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
162
|
+
return withResolvedHub(
|
|
163
|
+
makeContext(
|
|
164
|
+
match.companionPath,
|
|
165
|
+
"working-repo",
|
|
166
|
+
(await findRepoLocalLinkedRepository(cwd)) ?? undefined,
|
|
167
|
+
),
|
|
79
168
|
);
|
|
80
169
|
}
|
|
81
170
|
|
|
82
171
|
const localDir = path.join(cwd, `.${FRAMEWORK_NAME}`);
|
|
83
|
-
const localLegacyDirs = frameworkConfig.legacyNames.map((n) => path.join(cwd, `.${n}`));
|
|
84
|
-
await migrateConfigDir(localDir, localLegacyDirs);
|
|
85
172
|
|
|
86
173
|
const localConfigPath = path.join(localDir, "config", "framework.yaml");
|
|
87
174
|
try {
|
|
88
175
|
await fs.access(localConfigPath);
|
|
89
|
-
return makeContext(cwd, "companion-root");
|
|
90
176
|
} catch {
|
|
91
177
|
// no local config
|
|
178
|
+
throw new RepositoryNotFoundError(`No companion found for current directory: ${cwd}`);
|
|
92
179
|
}
|
|
93
|
-
|
|
94
|
-
throw new RepositoryNotFoundError(`No companion found for current directory: ${cwd}`);
|
|
180
|
+
return withResolvedHub(makeContext(cwd, "companion-root"));
|
|
95
181
|
}
|
|
96
182
|
|
|
97
183
|
// Resolves context for agent-launch commands. Returns a LaunchContext with repositoryId.
|
|
@@ -108,8 +194,11 @@ export async function resolveForLaunch(
|
|
|
108
194
|
if (envCompanionPath) {
|
|
109
195
|
const repository = repositoryFromEnvironment() ?? (await findRepoLocalLinkedRepository(cwd));
|
|
110
196
|
const repositoryId = process.env.MATE_REPO_ID ?? repository?.id ?? "";
|
|
197
|
+
const context = await withResolvedHub(
|
|
198
|
+
makeContext(path.resolve(envCompanionPath), "env", repository ?? undefined),
|
|
199
|
+
);
|
|
111
200
|
return {
|
|
112
|
-
...
|
|
201
|
+
...context,
|
|
113
202
|
repositoryId,
|
|
114
203
|
};
|
|
115
204
|
}
|
|
@@ -123,8 +212,11 @@ export async function resolveForLaunch(
|
|
|
123
212
|
|
|
124
213
|
if (resolution.match) {
|
|
125
214
|
const repository = (await findRepoLocalLinkedRepository(cwd)) ?? undefined;
|
|
215
|
+
const context = await withResolvedHub(
|
|
216
|
+
makeContext(resolution.match.companionPath, "working-repo", repository),
|
|
217
|
+
);
|
|
126
218
|
return {
|
|
127
|
-
...
|
|
219
|
+
...context,
|
|
128
220
|
repositoryId: resolution.match.repositoryId,
|
|
129
221
|
};
|
|
130
222
|
}
|
|
@@ -143,7 +235,9 @@ export async function resolveForCapability(
|
|
|
143
235
|
const envCompanionPath = process.env.MATE_ARTIFACT_PATH;
|
|
144
236
|
if (envCompanionPath) {
|
|
145
237
|
const repository = repositoryFromEnvironment() ?? (await findRepoLocalLinkedRepository(cwd));
|
|
146
|
-
const ctx =
|
|
238
|
+
const ctx = await withResolvedHub(
|
|
239
|
+
makeContext(path.resolve(envCompanionPath), "env", repository ?? undefined),
|
|
240
|
+
);
|
|
147
241
|
const repositoryId = process.env.MATE_REPO_ID;
|
|
148
242
|
if (repositoryId) {
|
|
149
243
|
return { ...ctx, repositoryId };
|
|
@@ -158,12 +252,15 @@ export async function resolveForCapability(
|
|
|
158
252
|
|
|
159
253
|
const match = await new CompanionResolver(globalConfigStore).resolve(cwd);
|
|
160
254
|
if (match) {
|
|
161
|
-
|
|
162
|
-
|
|
255
|
+
const context = await withResolvedHub(
|
|
256
|
+
makeContext(
|
|
163
257
|
match.companionPath,
|
|
164
258
|
"working-repo",
|
|
165
259
|
(await findRepoLocalLinkedRepository(cwd)) ?? undefined,
|
|
166
260
|
),
|
|
261
|
+
);
|
|
262
|
+
return {
|
|
263
|
+
...context,
|
|
167
264
|
repositoryId: match.repositoryId,
|
|
168
265
|
};
|
|
169
266
|
}
|
|
@@ -171,16 +268,14 @@ export async function resolveForCapability(
|
|
|
171
268
|
// Fallback: cwd is the companion directory — resolve from its local config.
|
|
172
269
|
// This is specific to mate cap; resolveForLaunch does NOT get this fallback.
|
|
173
270
|
const localDir = path.join(cwd, `.${FRAMEWORK_NAME}`);
|
|
174
|
-
const localLegacyDirs = frameworkConfig.legacyNames.map((n) => path.join(cwd, `.${n}`));
|
|
175
|
-
await migrateConfigDir(localDir, localLegacyDirs);
|
|
176
271
|
|
|
177
272
|
const localConfigPath = path.join(localDir, "config", "framework.yaml");
|
|
178
273
|
try {
|
|
179
274
|
await fs.access(localConfigPath);
|
|
180
|
-
return { ...makeContext(cwd, "companion-root"), repositoryId: process.env.MATE_REPO_ID ?? "" };
|
|
181
275
|
} catch {
|
|
182
276
|
// no local config
|
|
277
|
+
throw new WorkingRepoRequiredError("cap");
|
|
183
278
|
}
|
|
184
|
-
|
|
185
|
-
|
|
279
|
+
const context = await withResolvedHub(makeContext(cwd, "companion-root"));
|
|
280
|
+
return { ...context, repositoryId: process.env.MATE_REPO_ID ?? "" };
|
|
186
281
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
import { FRAMEWORK_NAME
|
|
4
|
+
import { FRAMEWORK_NAME } from "../../framework";
|
|
5
5
|
import { YamlFileStore } from "./yaml-file-store";
|
|
6
|
-
import { migrateConfigDir } from "./migration";
|
|
7
6
|
|
|
8
7
|
interface CompanionEntry {
|
|
9
8
|
path: string;
|
|
@@ -35,9 +34,6 @@ export class GlobalConfigStore extends YamlFileStore<GlobalConfig> {
|
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
override async load(): Promise<GlobalConfig> {
|
|
38
|
-
const currentDir = path.dirname(this.configPath);
|
|
39
|
-
const legacyDirs = frameworkConfig.legacyNames.map((n) => path.join(os.homedir(), `.${n}`));
|
|
40
|
-
await migrateConfigDir(currentDir, legacyDirs);
|
|
41
37
|
return normalizeGlobalConfig(await super.load());
|
|
42
38
|
}
|
|
43
39
|
|
|
@@ -5,7 +5,7 @@ import type { CapabilityPlugin, LaunchPreflightContext } from "../../tools/setup
|
|
|
5
5
|
import type { LaunchAdapter, AdapterContext } from "./adapters/base";
|
|
6
6
|
import { ClaudeAdapter } from "./adapters/claude";
|
|
7
7
|
import { OpenCodeAdapter } from "./adapters/opencode";
|
|
8
|
-
import { CompanionStore
|
|
8
|
+
import { CompanionStore } from "./companion-store";
|
|
9
9
|
import { syncCompanionGit } from "./companion-git-sync";
|
|
10
10
|
import { resolveForLaunch, type LaunchContext } from "./framework-context";
|
|
11
11
|
import {
|
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
type LaunchRequest,
|
|
17
17
|
type LaunchResult,
|
|
18
18
|
type LinkedRepository,
|
|
19
|
-
type PolicySettings,
|
|
20
19
|
} from "./types";
|
|
21
20
|
|
|
22
21
|
export interface LaunchPreview {
|
|
@@ -34,7 +33,6 @@ interface ResolvedLaunchState {
|
|
|
34
33
|
adapter: LaunchAdapter;
|
|
35
34
|
companionPath: string;
|
|
36
35
|
config: FrameworkConfig;
|
|
37
|
-
policy: PolicySettings;
|
|
38
36
|
repository: LinkedRepository;
|
|
39
37
|
}
|
|
40
38
|
|
|
@@ -100,10 +98,7 @@ export class FrameworkLauncher {
|
|
|
100
98
|
}
|
|
101
99
|
|
|
102
100
|
const config = await configStore.load();
|
|
103
|
-
|
|
104
|
-
? resolvePolicyFromConfig(config, [repository], repository.id)
|
|
105
|
-
: await store.resolvePolicy(repository.id);
|
|
106
|
-
if (!policy.allowedAgents.includes(request.tool)) {
|
|
101
|
+
if (!config.allowedAgents.includes(request.tool)) {
|
|
107
102
|
throw new ToolNotAllowedError(`Tool is disallowed by policy: ${request.tool}`);
|
|
108
103
|
}
|
|
109
104
|
|
|
@@ -116,7 +111,6 @@ export class FrameworkLauncher {
|
|
|
116
111
|
adapter,
|
|
117
112
|
companionPath,
|
|
118
113
|
config,
|
|
119
|
-
policy,
|
|
120
114
|
repository,
|
|
121
115
|
};
|
|
122
116
|
}
|
|
@@ -124,7 +118,7 @@ export class FrameworkLauncher {
|
|
|
124
118
|
private makeAdapterContext(state: ResolvedLaunchState): AdapterContext {
|
|
125
119
|
return {
|
|
126
120
|
repository: state.repository,
|
|
127
|
-
|
|
121
|
+
allowedAgents: state.config.allowedAgents,
|
|
128
122
|
companionPath: state.companionPath,
|
|
129
123
|
capabilities: state.config.capabilities ?? [],
|
|
130
124
|
git: state.config.git,
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
// oxlint-disable no-await-in-loop
|
|
2
1
|
import fs from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
2
|
|
|
5
3
|
import { parse, stringify } from "yaml";
|
|
6
4
|
|
|
@@ -18,24 +16,3 @@ export async function migrateRegistryData(filePath: string): Promise<void> {
|
|
|
18
16
|
await fs.writeFile(filePath, stringify(rest), "utf8");
|
|
19
17
|
}
|
|
20
18
|
}
|
|
21
|
-
|
|
22
|
-
export async function migrateConfigDir(currentDir: string, legacyDirs: string[]): Promise<void> {
|
|
23
|
-
try {
|
|
24
|
-
await fs.access(currentDir);
|
|
25
|
-
return;
|
|
26
|
-
} catch {
|
|
27
|
-
// current dir missing — check legacy locations
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
for (const legacyDir of legacyDirs) {
|
|
31
|
-
try {
|
|
32
|
-
await fs.access(legacyDir);
|
|
33
|
-
await fs.mkdir(path.dirname(currentDir), { recursive: true });
|
|
34
|
-
await fs.rename(legacyDir, currentDir);
|
|
35
|
-
process.stderr.write(`[migration] ${legacyDir} → ${currentDir}\n`);
|
|
36
|
-
return;
|
|
37
|
-
} catch {
|
|
38
|
-
// this legacy dir also absent, try next
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
@@ -25,9 +25,8 @@ export function buildOpenCodeGuidance(capabilities: CapabilityConfig[]): MateGui
|
|
|
25
25
|
repository: {
|
|
26
26
|
id: "$MATE_REPO_ID",
|
|
27
27
|
path: "$MATE_REPO_PATH",
|
|
28
|
-
profile: "$MATE_REPO_PROFILE",
|
|
29
28
|
},
|
|
30
|
-
|
|
29
|
+
allowedAgents: [],
|
|
31
30
|
capabilities,
|
|
32
31
|
},
|
|
33
32
|
{ wrapperBinPath: "$MATE_WRAPPER_BIN_PATH" },
|
|
@@ -105,9 +105,11 @@ async function saveRepoLocalRegistry(repoPath: string, registry: RepoLocalRegist
|
|
|
105
105
|
await store.save(registry);
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
// Field-explicit on purpose: also strips legacy per-repo policy fields
|
|
109
|
+
// (profile/overrides) from entries read from or written to disk.
|
|
108
110
|
function normalizeLinkedRepository(repository: LinkedRepository): LinkedRepository {
|
|
109
111
|
return {
|
|
110
|
-
|
|
112
|
+
id: repository.id,
|
|
111
113
|
path: path.resolve(repository.path),
|
|
112
114
|
};
|
|
113
115
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { FRAMEWORK_NAME } from "../../framework";
|
|
5
|
+
import { CompanionResolver, type CompanionResolutionResult } from "./companion-resolver";
|
|
6
|
+
import { ConfigStore, mergeWithDefaults } from "./config-store";
|
|
7
|
+
import { GlobalConfigStore } from "./global-config-store";
|
|
8
|
+
import { findRepoLocalLinkedRepository } from "./repo-local-registry";
|
|
9
|
+
import type { FrameworkConfig, LinkedRepository } from "./types";
|
|
10
|
+
|
|
11
|
+
/** Root classification shared by install and doctor. `core` = no configured root. */
|
|
12
|
+
export type RootKind = "core" | "working" | "companion" | "hub";
|
|
13
|
+
|
|
14
|
+
/** How the configured root was found. */
|
|
15
|
+
export type RootOrigin = "env" | "registry" | "local" | "none";
|
|
16
|
+
|
|
17
|
+
export interface RootContext {
|
|
18
|
+
/** Kind of the resolved configured root (from `config.type`), or `core`. */
|
|
19
|
+
kind: RootKind;
|
|
20
|
+
origin: RootOrigin;
|
|
21
|
+
rootPath?: string;
|
|
22
|
+
/** Merged config of the resolved root; absent for `core`. */
|
|
23
|
+
config?: FrameworkConfig;
|
|
24
|
+
repositoryId?: string;
|
|
25
|
+
/** Repo-local `registry.yaml` repository entry for cwd, when present. */
|
|
26
|
+
linkedRepository: LinkedRepository | null;
|
|
27
|
+
resolution: CompanionResolutionResult;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface RootContextDeps {
|
|
31
|
+
globalConfigStore?: GlobalConfigStore;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const emptyResolution = (): CompanionResolutionResult => ({
|
|
35
|
+
match: null,
|
|
36
|
+
ambiguousMatches: [],
|
|
37
|
+
failures: [],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function kindForConfig(config: FrameworkConfig): Exclude<RootKind, "core"> {
|
|
41
|
+
return config.type === "hub" ? "hub" : config.type === "working" ? "working" : "companion";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function loadRootConfig(rootPath: string): Promise<FrameworkConfig> {
|
|
45
|
+
const configPath = path.join(rootPath, `.${FRAMEWORK_NAME}`, "config", "framework.yaml");
|
|
46
|
+
return mergeWithDefaults(await new ConfigStore(configPath).load());
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function resolveRootContext(
|
|
50
|
+
cwd = process.cwd(),
|
|
51
|
+
deps: RootContextDeps = {},
|
|
52
|
+
): Promise<RootContext> {
|
|
53
|
+
const resolvedCwd = path.resolve(cwd);
|
|
54
|
+
const linkedRepository = await findRepoLocalLinkedRepository(resolvedCwd);
|
|
55
|
+
|
|
56
|
+
const envRootPath = process.env.MATE_ARTIFACT_PATH;
|
|
57
|
+
if (envRootPath) {
|
|
58
|
+
const rootPath = path.resolve(envRootPath);
|
|
59
|
+
const config = await loadRootConfig(rootPath);
|
|
60
|
+
return {
|
|
61
|
+
kind: kindForConfig(config),
|
|
62
|
+
origin: "env",
|
|
63
|
+
rootPath,
|
|
64
|
+
config,
|
|
65
|
+
repositoryId: process.env.MATE_REPO_ID ?? linkedRepository?.id,
|
|
66
|
+
linkedRepository,
|
|
67
|
+
resolution: emptyResolution(),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const resolver = new CompanionResolver(deps.globalConfigStore ?? new GlobalConfigStore());
|
|
72
|
+
const resolution = await resolver.resolveWithDiagnostics(resolvedCwd);
|
|
73
|
+
if (resolution.match) {
|
|
74
|
+
const rootPath = resolution.match.companionPath;
|
|
75
|
+
const config = await loadRootConfig(rootPath);
|
|
76
|
+
return {
|
|
77
|
+
kind: kindForConfig(config),
|
|
78
|
+
origin: "registry",
|
|
79
|
+
rootPath,
|
|
80
|
+
config,
|
|
81
|
+
repositoryId: linkedRepository?.id ?? resolution.match.repositoryId,
|
|
82
|
+
linkedRepository,
|
|
83
|
+
resolution,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const localRoot = await findConfiguredRoot(resolvedCwd);
|
|
88
|
+
if (localRoot) {
|
|
89
|
+
const config = await loadRootConfig(localRoot);
|
|
90
|
+
return {
|
|
91
|
+
kind: kindForConfig(config),
|
|
92
|
+
origin: "local",
|
|
93
|
+
rootPath: localRoot,
|
|
94
|
+
config,
|
|
95
|
+
linkedRepository,
|
|
96
|
+
resolution,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { kind: "core", origin: "none", linkedRepository, resolution };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Nearest ancestor of `cwd` (inclusive) holding `.<framework>/config/framework.yaml`. */
|
|
104
|
+
export async function findConfiguredRoot(cwd: string): Promise<string | null> {
|
|
105
|
+
let dir = path.resolve(cwd);
|
|
106
|
+
for (;;) {
|
|
107
|
+
try {
|
|
108
|
+
await fs.access(path.join(dir, `.${FRAMEWORK_NAME}`, "config", "framework.yaml"));
|
|
109
|
+
return dir;
|
|
110
|
+
} catch {
|
|
111
|
+
// keep walking up
|
|
112
|
+
}
|
|
113
|
+
const parent = path.dirname(dir);
|
|
114
|
+
if (parent === dir) return null;
|
|
115
|
+
dir = parent;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -287,7 +287,7 @@ export function getDefaultSetupSelections(): SetupSelections {
|
|
|
287
287
|
|
|
288
288
|
export function getSetupSelectionsFromConfig(config: FrameworkConfig): SetupSelections {
|
|
289
289
|
return {
|
|
290
|
-
allowedAgents: [...new Set(config.
|
|
290
|
+
allowedAgents: [...new Set(config.allowedAgents ?? [])],
|
|
291
291
|
packageManagers: [
|
|
292
292
|
...new Set([...getLockedPackageManagers(), ...(config.packageManagers ?? [])]),
|
|
293
293
|
],
|
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
|
|
4
4
|
import { parse } from "yaml";
|
|
5
5
|
|
|
6
|
-
import { FRAMEWORK_NAME
|
|
6
|
+
import { FRAMEWORK_NAME } from "../../framework";
|
|
7
7
|
import { fileExists } from "../fs-utils";
|
|
8
8
|
import { GlobalConfigStore } from "./global-config-store";
|
|
9
9
|
import { CompanionResolver, type CompanionMatch } from "./companion-resolver";
|
|
@@ -126,20 +126,7 @@ export async function looksLikeWorkingRepo(cwd: string): Promise<boolean> {
|
|
|
126
126
|
|
|
127
127
|
export async function hasLocalCompanionConfig(cwd: string): Promise<boolean> {
|
|
128
128
|
const resolvedCwd = path.resolve(cwd);
|
|
129
|
-
|
|
130
|
-
path.join(resolvedCwd, `.${FRAMEWORK_NAME}`, "config", "framework.yaml"),
|
|
131
|
-
...frameworkConfig.legacyNames.map((name) =>
|
|
132
|
-
path.join(resolvedCwd, `.${name}`, "config", "framework.yaml"),
|
|
133
|
-
),
|
|
134
|
-
];
|
|
135
|
-
|
|
136
|
-
for (const candidatePath of candidatePaths) {
|
|
137
|
-
if (await fileExists(candidatePath)) {
|
|
138
|
-
return true;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return false;
|
|
129
|
+
return fileExists(path.join(resolvedCwd, `.${FRAMEWORK_NAME}`, "config", "framework.yaml"));
|
|
143
130
|
}
|
|
144
131
|
|
|
145
132
|
async function hasWorkingRepoConfig(cwd: string): Promise<boolean> {
|
|
@@ -167,7 +154,7 @@ export async function inspectSetupPreflight(
|
|
|
167
154
|
// linked working repo, even if it sits inside another repo's working tree.
|
|
168
155
|
// A stale registration of a working repo is the exception: its local type
|
|
169
156
|
// marker is authoritative and must not make setup load working-repo config
|
|
170
|
-
// as a companion
|
|
157
|
+
// as a companion framework config.
|
|
171
158
|
if (isRegisteredCompanion && !(await hasWorkingRepoConfig(resolvedCwd))) {
|
|
172
159
|
return { kind: "existing-companion" };
|
|
173
160
|
}
|
|
@@ -190,7 +177,7 @@ export async function inspectSetupPreflight(
|
|
|
190
177
|
|
|
191
178
|
export function formatSetupGuardrailError(cwd: string, match: CompanionMatch): string {
|
|
192
179
|
return [
|
|
193
|
-
`\`${
|
|
194
|
-
`Run \`${
|
|
180
|
+
`\`${FRAMEWORK_NAME} setup\` cannot run here: ${path.resolve(cwd)} is inside linked working repo \`${match.repositoryId}\`.`,
|
|
181
|
+
`Run \`${FRAMEWORK_NAME} setup\` from the companion repo instead: ${match.companionPath}`,
|
|
195
182
|
].join("\n");
|
|
196
183
|
}
|
|
@@ -42,23 +42,9 @@ export class WorkingRepoRequiredError extends MateError {
|
|
|
42
42
|
|
|
43
43
|
export type CompanionSource = "git" | "existing" | "local";
|
|
44
44
|
|
|
45
|
-
export interface PolicySettings {
|
|
46
|
-
allowedAgents: string[];
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface PolicyOverride {
|
|
50
|
-
allowedAgents?: string[];
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface PolicyProfile extends PolicySettings {
|
|
54
|
-
name: string;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
45
|
export interface LinkedRepository {
|
|
58
46
|
id: string;
|
|
59
47
|
path: string;
|
|
60
|
-
profile: string;
|
|
61
|
-
overrides?: PolicyOverride;
|
|
62
48
|
}
|
|
63
49
|
|
|
64
50
|
export interface CliToolConfig {
|
|
@@ -74,6 +60,26 @@ export interface CapabilityConfig {
|
|
|
74
60
|
schemaProfile?: OpenSpecSchemaProfile;
|
|
75
61
|
}
|
|
76
62
|
|
|
63
|
+
export type FrameworkType = "working" | "companion" | "hub";
|
|
64
|
+
|
|
65
|
+
export interface HubMemberSource {
|
|
66
|
+
kind: "git" | "local";
|
|
67
|
+
url?: string;
|
|
68
|
+
ref?: string;
|
|
69
|
+
path?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface HubMember {
|
|
73
|
+
id: string;
|
|
74
|
+
path: string;
|
|
75
|
+
source: HubMemberSource;
|
|
76
|
+
materializedCommit?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface HubConfig {
|
|
80
|
+
companions: HubMember[];
|
|
81
|
+
}
|
|
82
|
+
|
|
77
83
|
/**
|
|
78
84
|
* Distribution-name-keyed semver ranges (e.g. `{ mate: ">=0.15.0" }` or
|
|
79
85
|
* `{ "acme-mate": ">=1.2.0" }`). A running CLI only checks the key matching
|
|
@@ -88,9 +94,10 @@ export type EngineConstraints = Record<string, string>;
|
|
|
88
94
|
export type PluginDeclarationPolicy = "default" | "optional";
|
|
89
95
|
|
|
90
96
|
/**
|
|
91
|
-
* A companion-declared npm plugin: installed by setup/install into
|
|
92
|
-
* `.mate/
|
|
93
|
-
*
|
|
97
|
+
* A companion-declared npm plugin: installed by setup/install into the
|
|
98
|
+
* companion's own shared plugin workspace (`.mate/plugins/`) and loaded
|
|
99
|
+
* from there on every invocation. Declaration registers the plugin;
|
|
100
|
+
* enablement stays in the `capabilities` list.
|
|
94
101
|
*/
|
|
95
102
|
export interface PluginDeclaration {
|
|
96
103
|
/** npm package name (e.g. `@acme/custom-plugin`). */
|
|
@@ -104,9 +111,10 @@ export interface PluginDeclaration {
|
|
|
104
111
|
}
|
|
105
112
|
|
|
106
113
|
export interface FrameworkConfig {
|
|
107
|
-
type?:
|
|
114
|
+
type?: FrameworkType;
|
|
108
115
|
git?: GitModeProfile;
|
|
109
|
-
|
|
116
|
+
hub?: HubConfig;
|
|
117
|
+
allowedAgents: string[];
|
|
110
118
|
capabilities?: CapabilityConfig[];
|
|
111
119
|
plugins?: PluginDeclaration[];
|
|
112
120
|
migrations?: string[];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
|
-
import { FRAMEWORK_NAME
|
|
3
|
+
import { FRAMEWORK_NAME } from "../../framework";
|
|
4
4
|
import { migrateRegistryData } from "./migration";
|
|
5
5
|
import { YamlFileStore } from "./yaml-file-store";
|
|
6
6
|
import { ConfigError, type WorkingRepoConfig } from "./types";
|
|
@@ -16,12 +16,18 @@ export class WorkingRepoStore extends YamlFileStore<WorkingRepoConfig> {
|
|
|
16
16
|
|
|
17
17
|
override async load(): Promise<WorkingRepoConfig> {
|
|
18
18
|
await migrateRegistryData(this.configPath);
|
|
19
|
-
|
|
19
|
+
const config = await super.load();
|
|
20
|
+
// Silently drop legacy per-repo policy fields (profile/overrides); the
|
|
21
|
+
// clean shape persists on the next save.
|
|
22
|
+
return {
|
|
23
|
+
...config,
|
|
24
|
+
repos: (config.repos ?? []).map(({ id, path: repoPath }) => ({ id, path: repoPath })),
|
|
25
|
+
};
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
protected async onMissing(): Promise<WorkingRepoConfig> {
|
|
23
29
|
throw new ConfigError(
|
|
24
|
-
`Working repo config not found. Please run \`${
|
|
30
|
+
`Working repo config not found. Please run \`${FRAMEWORK_NAME} companion setup\` to initialize the framework.`,
|
|
25
31
|
);
|
|
26
32
|
}
|
|
27
33
|
|