@uniqbit/mate-core 0.15.1-canary.1 → 0.15.1-canary.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/package.json +1 -1
- package/src/cli/commands/companion/link.ts +2 -2
- package/src/cli/commands/config.ts +2 -2
- package/src/cli/commands/doctor.ts +3 -3
- package/src/cli/commands/install.ts +41 -6
- package/src/cli/commands/launch/shared.ts +6 -3
- package/src/cli/commands/setup.ts +3 -3
- package/src/cli/commands/update.ts +9 -9
- package/src/cli/main.ts +14 -2
- package/src/cli/plugin-commands.ts +3 -2
- package/src/cli/usage.ts +6 -3
- package/src/create-mate.ts +4 -3
- package/src/framework.ts +20 -0
- package/src/index.ts +15 -1
- package/src/lib/install.ts +4 -9
- package/src/lib/orchestrator/adapters/base.ts +5 -4
- package/src/lib/orchestrator/config-store.ts +21 -3
- package/src/lib/orchestrator/framework-context.ts +4 -4
- package/src/lib/orchestrator/global-config-store.ts +2 -3
- package/src/lib/orchestrator/repo-local-registry.ts +2 -3
- package/src/lib/orchestrator/setup-preflight.ts +5 -5
- package/src/lib/orchestrator/types.ts +23 -0
- package/src/lib/orchestrator/working-repo-store.ts +3 -3
- package/src/lib/update-checker.ts +24 -9
- package/src/playbooks/companion-guidance.ts +8 -9
- package/src/runtime/env.ts +1 -0
- package/src/templates/capabilities/openspec-cap/claude/hooks/mate-artifact-finish.sh +2 -1
- package/src/templates/capabilities/openspec-cap/mate-skills/mate-artifact-finish/SKILL.md +8 -8
- package/src/templates/capabilities/openspec-cap/mate-skills/mate-artifact-finish/references/openspec.md +3 -3
- package/src/templates/providers/claude/.claude/hooks/validate-artifact-path +25 -2
- package/src/tools/setup/capabilities/graphify.ts +4 -4
- package/src/tools/setup/capabilities/openspec.ts +29 -1
- package/src/tools/setup/capabilities/tokensave.ts +18 -44
- package/src/tools/setup/dynamic-plugins/config-resolution.ts +101 -0
- package/src/tools/setup/dynamic-plugins/host.ts +51 -0
- package/src/tools/setup/dynamic-plugins/hydrate.ts +156 -0
- package/src/tools/setup/dynamic-plugins/install.ts +171 -0
- package/src/tools/setup/dynamic-plugins/loader.ts +171 -0
- package/src/tools/setup/dynamic-plugins/paths.ts +42 -0
- package/src/tools/setup/dynamic-plugins/pin-store.ts +30 -0
- package/src/tools/setup/engine.ts +2 -1
- package/src/tools/setup/plugins/gitignore.ts +12 -1
- package/src/tools/setup/plugins/guidance.ts +3 -3
- package/src/tools/setup/providers/claude.ts +22 -5
- package/src/tools/setup/providers/opencode.ts +3 -2
- package/src/tools/setup.ts +28 -14
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { FRAMEWORK_NAME } from "../../../framework";
|
|
7
7
|
import {
|
|
8
8
|
selectCompanionLinkInputs,
|
|
9
9
|
type CompanionLinkInputs,
|
|
@@ -48,7 +48,7 @@ interface CompanionLinkCommandDeps {
|
|
|
48
48
|
|
|
49
49
|
/** All companions surfaced by the "existing companion" picker live under this directory. */
|
|
50
50
|
function companionsHomeDir(): string {
|
|
51
|
-
return path.join(os.homedir(), `.${
|
|
51
|
+
return path.join(os.homedir(), `.${FRAMEWORK_NAME}`, "companions");
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
function isInsideDir(parentDir: string, candidatePath: string): boolean {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { execSync } from "node:child_process";
|
|
4
|
-
import {
|
|
4
|
+
import { FRAMEWORK_NAME } from "../../framework";
|
|
5
5
|
|
|
6
|
-
const getGlobalConfigDir = () => path.join(os.homedir(), `.${
|
|
6
|
+
const getGlobalConfigDir = () => path.join(os.homedir(), `.${FRAMEWORK_NAME}`);
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* @command mate config
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
import { frameworkConfig } from "../../framework";
|
|
4
|
+
import { FRAMEWORK_NAME, frameworkConfig } from "../../framework";
|
|
5
5
|
import { getActiveDistribution } from "../../distribution";
|
|
6
6
|
import { CompanionResolver } from "../../lib/orchestrator/companion-resolver";
|
|
7
7
|
import { CompanionStore, resolvePolicyFromConfig } from "../../lib/orchestrator/companion-store";
|
|
@@ -34,7 +34,7 @@ interface ToolCheck {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
function storesForCompanion(companionPath: string): CompanionStores {
|
|
37
|
-
const configDir = path.join(companionPath, `.${
|
|
37
|
+
const configDir = path.join(companionPath, `.${FRAMEWORK_NAME}`, "config");
|
|
38
38
|
return {
|
|
39
39
|
configStore: new ConfigStore(path.join(configDir, "framework.yaml")),
|
|
40
40
|
workingRepoStore: new WorkingRepoStore(path.join(configDir, "registry.yaml")),
|
|
@@ -42,7 +42,7 @@ function storesForCompanion(companionPath: string): CompanionStores {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
async function hasLocalCompanionConfig(cwd: string): Promise<boolean> {
|
|
45
|
-
const localDir = path.join(cwd, `.${
|
|
45
|
+
const localDir = path.join(cwd, `.${FRAMEWORK_NAME}`);
|
|
46
46
|
const localLegacyDirs = frameworkConfig.legacyNames.map((name) => path.join(cwd, `.${name}`));
|
|
47
47
|
await migrateConfigDir(localDir, localLegacyDirs);
|
|
48
48
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { confirm } from "../confirm";
|
|
2
|
+
import { frameworkCommandName } from "../../framework";
|
|
2
3
|
import {
|
|
3
4
|
buildInstallPlan,
|
|
4
5
|
inspectInstallPlan,
|
|
@@ -8,6 +9,23 @@ import {
|
|
|
8
9
|
resolveInstallContext,
|
|
9
10
|
} from "../../lib/install";
|
|
10
11
|
import { renderInstallExecution } from "../install-plan";
|
|
12
|
+
import { hydrateDynamicPlugins } from "../../tools/setup/dynamic-plugins/hydrate";
|
|
13
|
+
import {
|
|
14
|
+
installDeclaredPlugins,
|
|
15
|
+
type PluginInstallResult,
|
|
16
|
+
} from "../../tools/setup/dynamic-plugins/install";
|
|
17
|
+
|
|
18
|
+
export function reportPluginInstallResults(results: PluginInstallResult[]): void {
|
|
19
|
+
for (const result of results) {
|
|
20
|
+
if (result.status === "failed") {
|
|
21
|
+
process.stderr.write(
|
|
22
|
+
`${frameworkCommandName()}: plugin "${result.package}" failed to install: ${result.error ?? "unknown error"}\n`,
|
|
23
|
+
);
|
|
24
|
+
} else if (result.status === "installed") {
|
|
25
|
+
console.log(` installed plugin ${result.package}@${result.resolvedVersion}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
11
29
|
|
|
12
30
|
function printPlanText(plan: Awaited<ReturnType<typeof inspectInstallPlan>>): void {
|
|
13
31
|
if (plan.context.message) process.stderr.write(`${plan.context.message}\n\n`);
|
|
@@ -36,6 +54,15 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
|
|
|
36
54
|
process.exitCode = 1;
|
|
37
55
|
return false;
|
|
38
56
|
}
|
|
57
|
+
// Companion-declared plugins install first and hydrate into the registry
|
|
58
|
+
// before the plan is built, so one run goes install → load → plan and their
|
|
59
|
+
// own install requirements are part of the plan.
|
|
60
|
+
if (context.kind === "companion" && context.companionPath && context.config.plugins?.length) {
|
|
61
|
+
reportPluginInstallResults(
|
|
62
|
+
await installDeclaredPlugins(context.companionPath, context.config.plugins),
|
|
63
|
+
);
|
|
64
|
+
await hydrateDynamicPlugins({ companionPath: context.companionPath });
|
|
65
|
+
}
|
|
39
66
|
const plan = await inspectInstallPlan(buildInstallPlan(context));
|
|
40
67
|
const missing = plan.requirements.filter((item) => !item.satisfied);
|
|
41
68
|
let execution: Awaited<ReturnType<typeof renderInstallExecution>>;
|
|
@@ -44,7 +71,9 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
|
|
|
44
71
|
if (missing.length > 0 && !skipConfirm) {
|
|
45
72
|
const ok = await confirm("Install the missing requirements? [y/N] ");
|
|
46
73
|
if (!ok) {
|
|
47
|
-
process.stderr.write(
|
|
74
|
+
process.stderr.write(
|
|
75
|
+
`Installation declined. Run \`${frameworkCommandName()} install --yes\` to continue.\n`,
|
|
76
|
+
);
|
|
48
77
|
process.exitCode = 1;
|
|
49
78
|
return false;
|
|
50
79
|
}
|
|
@@ -54,7 +83,7 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
|
|
|
54
83
|
printPlanText(plan);
|
|
55
84
|
if (!skipConfirm) {
|
|
56
85
|
process.stderr.write(
|
|
57
|
-
|
|
86
|
+
`${frameworkCommandName()}: installation requires confirmation in a TTY. Re-run with \`${frameworkCommandName()} install --yes\`.\n`,
|
|
58
87
|
);
|
|
59
88
|
process.exitCode = 1;
|
|
60
89
|
return false;
|
|
@@ -73,9 +102,13 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
|
|
|
73
102
|
}
|
|
74
103
|
if (!execution.ok) {
|
|
75
104
|
for (const result of execution.results.filter((item) => item.status === "failed")) {
|
|
76
|
-
process.stderr.write(
|
|
105
|
+
process.stderr.write(
|
|
106
|
+
`${frameworkCommandName()}: ${result.id} failed: ${result.error ?? "unknown error"}\n`,
|
|
107
|
+
);
|
|
77
108
|
}
|
|
78
|
-
process.stderr.write(
|
|
109
|
+
process.stderr.write(
|
|
110
|
+
`Installation is incomplete. Re-run \`${frameworkCommandName()} install\` after remediation.\n`,
|
|
111
|
+
);
|
|
79
112
|
process.exitCode = 1;
|
|
80
113
|
return false;
|
|
81
114
|
}
|
|
@@ -85,9 +118,11 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
|
|
|
85
118
|
await saveCompleteInstallState(plan, execution.results);
|
|
86
119
|
} catch (error) {
|
|
87
120
|
process.stderr.write(
|
|
88
|
-
|
|
121
|
+
`${frameworkCommandName()}: installation completed but companion reconciliation failed: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
122
|
+
);
|
|
123
|
+
process.stderr.write(
|
|
124
|
+
`Installation is incomplete. Re-run \`${frameworkCommandName()} install\`.\n`,
|
|
89
125
|
);
|
|
90
|
-
process.stderr.write("Installation is incomplete. Re-run `mate install`.\n");
|
|
91
126
|
process.exitCode = 1;
|
|
92
127
|
return false;
|
|
93
128
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { frameworkCommandName } from "../../../framework";
|
|
1
2
|
import { FrameworkLauncher } from "../../../lib/orchestrator/launcher";
|
|
2
3
|
import {
|
|
3
4
|
LaunchPreflightError,
|
|
@@ -42,7 +43,7 @@ export function parseLaunchArgs(argv: string[]): ParsedLaunchArgs | null {
|
|
|
42
43
|
const agentArgs = separatorIndex >= 0 ? argv.slice(separatorIndex + 1) : [];
|
|
43
44
|
|
|
44
45
|
for (const arg of optionArgs) {
|
|
45
|
-
process.stderr.write(
|
|
46
|
+
process.stderr.write(`${frameworkCommandName()}: unknown launch option: ${arg}\n`);
|
|
46
47
|
process.exitCode = 1;
|
|
47
48
|
return null;
|
|
48
49
|
}
|
|
@@ -107,14 +108,16 @@ export async function runLaunchToolCommand(
|
|
|
107
108
|
progress?.stop();
|
|
108
109
|
|
|
109
110
|
if (error instanceof ToolNotAllowedError) {
|
|
110
|
-
process.stderr.write(
|
|
111
|
+
process.stderr.write(
|
|
112
|
+
`${frameworkCommandName()}: \`${tool}\` is disallowed by active repository policy.\n`,
|
|
113
|
+
);
|
|
111
114
|
process.exitCode = 1;
|
|
112
115
|
return;
|
|
113
116
|
}
|
|
114
117
|
|
|
115
118
|
if (error instanceof RepositoryNotSelectedError) {
|
|
116
119
|
process.stderr.write(`${error.message}\n`);
|
|
117
|
-
process.stderr.write(
|
|
120
|
+
process.stderr.write(`Run \`${frameworkCommandName()} companion link\` first.\n`);
|
|
118
121
|
process.exitCode = 1;
|
|
119
122
|
return;
|
|
120
123
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { FRAMEWORK_NAME, frameworkCommandName } from "../../framework";
|
|
4
4
|
import { ConfigStore, defaultConfig } from "../../lib/orchestrator/config-store";
|
|
5
5
|
import { executeSetup } from "../../tools/setup";
|
|
6
6
|
import {
|
|
@@ -98,7 +98,7 @@ async function runSetupFlow(
|
|
|
98
98
|
if (await checkWorkingRepo(cwd)) {
|
|
99
99
|
process.stderr.write(
|
|
100
100
|
`Warning: this directory looks like a working repository (project files detected).\n` +
|
|
101
|
-
`Initializing it as a ${
|
|
101
|
+
`Initializing it as a ${frameworkCommandName()} companion here may not be what you want.\n`,
|
|
102
102
|
);
|
|
103
103
|
}
|
|
104
104
|
const ok = await askConfirm("Initialize this directory as a Mate companion repository? [y/N] ");
|
|
@@ -156,7 +156,7 @@ async function runSetupFlow(
|
|
|
156
156
|
? getSetupSelectionsFromConfig(defaultConfig())
|
|
157
157
|
: getSetupSelectionsFromConfig(
|
|
158
158
|
await new ConfigStore(
|
|
159
|
-
path.join(targetCwd, `.${
|
|
159
|
+
path.join(targetCwd, `.${FRAMEWORK_NAME}`, "config", "framework.yaml"),
|
|
160
160
|
).load(),
|
|
161
161
|
);
|
|
162
162
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { frameworkCommandName } from "../../framework";
|
|
5
5
|
import {
|
|
6
6
|
OPENCODE_PLUGIN_PACKAGE_NAME,
|
|
7
7
|
warmOpenCodePluginCache,
|
|
@@ -20,7 +20,7 @@ type InstallResult = ReturnType<typeof installPublicPackageSync>;
|
|
|
20
20
|
function writeNpmOnlyRecoveryMessage(): void {
|
|
21
21
|
const { packageName, registry } = getUpdateConfig();
|
|
22
22
|
process.stderr.write(
|
|
23
|
-
`${
|
|
23
|
+
`${frameworkCommandName()}: self-update is only supported for npm-installed Mate.\n`,
|
|
24
24
|
);
|
|
25
25
|
if (packageName.startsWith("@uniqbit/")) {
|
|
26
26
|
process.stderr.write(
|
|
@@ -88,7 +88,7 @@ export async function runUpdateCommand(argv: string[]): Promise<void> {
|
|
|
88
88
|
latest = await updateCommandDeps.fetchLatestVersion();
|
|
89
89
|
} catch {
|
|
90
90
|
process.stderr.write(
|
|
91
|
-
`${
|
|
91
|
+
`${frameworkCommandName()}: could not reach registry to check for updates\n`,
|
|
92
92
|
);
|
|
93
93
|
process.exitCode = 1;
|
|
94
94
|
return;
|
|
@@ -96,7 +96,7 @@ export async function runUpdateCommand(argv: string[]): Promise<void> {
|
|
|
96
96
|
|
|
97
97
|
if (checkOnly) {
|
|
98
98
|
if (updateCommandDeps.isNewer(latest, current)) {
|
|
99
|
-
console.log(`${
|
|
99
|
+
console.log(`${frameworkCommandName()}: update available (${current} → ${latest})`);
|
|
100
100
|
process.exitCode = 1;
|
|
101
101
|
} else {
|
|
102
102
|
console.log("Up to date.");
|
|
@@ -127,11 +127,11 @@ export async function runUpdateCommand(argv: string[]): Promise<void> {
|
|
|
127
127
|
} catch (error) {
|
|
128
128
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
129
129
|
process.stderr.write(
|
|
130
|
-
`${
|
|
130
|
+
`${frameworkCommandName()}: npm is required for self-update but was not found on PATH\n`,
|
|
131
131
|
);
|
|
132
132
|
} else {
|
|
133
133
|
process.stderr.write(
|
|
134
|
-
`${
|
|
134
|
+
`${frameworkCommandName()}: could not verify the npm installation used for self-update\n`,
|
|
135
135
|
);
|
|
136
136
|
}
|
|
137
137
|
writeNpmOnlyRecoveryMessage();
|
|
@@ -148,7 +148,7 @@ export async function runUpdateCommand(argv: string[]): Promise<void> {
|
|
|
148
148
|
const result = updateCommandDeps.installLatest(latest);
|
|
149
149
|
if (result.status !== 0 || result.error) {
|
|
150
150
|
process.stderr.write(
|
|
151
|
-
`${
|
|
151
|
+
`${frameworkCommandName()}: upgrade command exited with status ${result.status ?? 1}\n`,
|
|
152
152
|
);
|
|
153
153
|
process.exitCode = 1;
|
|
154
154
|
return;
|
|
@@ -163,7 +163,7 @@ export async function runUpdateCommand(argv: string[]): Promise<void> {
|
|
|
163
163
|
if (!warmed.ok) {
|
|
164
164
|
process.stderr.write(
|
|
165
165
|
[
|
|
166
|
-
`${
|
|
166
|
+
`${frameworkCommandName()}: could not pre-fetch ${OPENCODE_PLUGIN_PACKAGE_NAME}@${latest} for OpenCode.`,
|
|
167
167
|
"The next managed OpenCode launch will download it (requires registry access).",
|
|
168
168
|
...(warmed.detail ? [`Details: ${warmed.detail}`] : []),
|
|
169
169
|
].join("\n") + "\n",
|
|
@@ -173,7 +173,7 @@ export async function runUpdateCommand(argv: string[]): Promise<void> {
|
|
|
173
173
|
const postInstall = updateCommandDeps.runPostInstall(skipConfirm);
|
|
174
174
|
if (postInstall.status !== 0 || postInstall.error) {
|
|
175
175
|
process.stderr.write(
|
|
176
|
-
`\nUpgraded to ${latest}, but installation is incomplete. Run
|
|
176
|
+
`\nUpgraded to ${latest}, but installation is incomplete. Run \`${frameworkCommandName()} install\`.\n`,
|
|
177
177
|
);
|
|
178
178
|
process.exitCode = 1;
|
|
179
179
|
return;
|
package/src/cli/main.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getActiveDistribution } from "../distribution";
|
|
2
|
+
import { frameworkCommandName } from "../framework";
|
|
2
3
|
import {
|
|
3
4
|
enforceUpdateIfRequired,
|
|
4
5
|
scheduleBackgroundCheck,
|
|
@@ -17,6 +18,7 @@ import { runUpdateCommand } from "./commands/update";
|
|
|
17
18
|
import { runInstallCommand } from "./commands/install";
|
|
18
19
|
import { inspectInstallPreflight } from "../lib/install";
|
|
19
20
|
import { ensureUnambiguousCompanion } from "./commands/shared/companion-selection";
|
|
21
|
+
import { hydrateDynamicPlugins } from "../tools/setup/dynamic-plugins/hydrate";
|
|
20
22
|
import { findPluginCliCommand } from "./plugin-commands";
|
|
21
23
|
import { usage } from "./usage";
|
|
22
24
|
|
|
@@ -35,11 +37,13 @@ export function isInstallRecoveryCommand(command?: string, subcommand?: string):
|
|
|
35
37
|
export interface MainDeps {
|
|
36
38
|
ensureUnambiguousCompanion: typeof ensureUnambiguousCompanion;
|
|
37
39
|
inspectInstallPreflight: typeof inspectInstallPreflight;
|
|
40
|
+
hydrateDynamicPlugins: typeof hydrateDynamicPlugins;
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
const mainDeps: MainDeps = {
|
|
41
44
|
ensureUnambiguousCompanion,
|
|
42
45
|
inspectInstallPreflight,
|
|
46
|
+
hydrateDynamicPlugins,
|
|
43
47
|
};
|
|
44
48
|
|
|
45
49
|
export async function main(argv = process.argv, deps: MainDeps = mainDeps): Promise<void> {
|
|
@@ -50,6 +54,12 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
|
|
|
50
54
|
return;
|
|
51
55
|
}
|
|
52
56
|
|
|
57
|
+
// Companion-declared plugins register before cap-command detection so
|
|
58
|
+
// their commands route like compiled-in ones (including MCP servers whose
|
|
59
|
+
// command is their own cap subcommand). Diagnostics stay on stderr; a
|
|
60
|
+
// missing or ambiguous companion makes this a no-op.
|
|
61
|
+
await deps.hydrateDynamicPlugins();
|
|
62
|
+
|
|
53
63
|
// Plugin commands (`mate cap <namespace> <command>`) own their stdout (an
|
|
54
64
|
// MCP server speaks JSON-RPC over it), so banners and background chatter
|
|
55
65
|
// are suppressed for them.
|
|
@@ -81,8 +91,10 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
|
|
|
81
91
|
if (!isInstallRecoveryCommand(command, subcommand)) {
|
|
82
92
|
const preflight = await deps.inspectInstallPreflight();
|
|
83
93
|
if (!preflight.ok) {
|
|
84
|
-
console.error(
|
|
85
|
-
|
|
94
|
+
console.error(
|
|
95
|
+
`${frameworkCommandName()}: ${preflight.reason ?? "installation is incomplete"}`,
|
|
96
|
+
);
|
|
97
|
+
console.error(`Run \`${frameworkCommandName()} install\` before continuing.`);
|
|
86
98
|
process.exitCode = 1;
|
|
87
99
|
return;
|
|
88
100
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getActiveDistribution } from "../distribution";
|
|
2
|
+
import { frameworkCommandName } from "../framework";
|
|
2
3
|
import { resolveInstallContext } from "../lib/install";
|
|
3
4
|
import type { FrameworkConfig } from "../lib/orchestrator/types";
|
|
4
5
|
import type { Plugin, PluginCliCommand } from "../tools/setup/plugin";
|
|
@@ -85,7 +86,7 @@ export async function ensureCapabilityEnabled(
|
|
|
85
86
|
const loadConfig = deps.loadConfig ?? (async () => (await resolveInstallContext()).config);
|
|
86
87
|
if (plugin?.isEnabled(await loadConfig())) return true;
|
|
87
88
|
|
|
88
|
-
const name =
|
|
89
|
+
const name = frameworkCommandName();
|
|
89
90
|
console.error(
|
|
90
91
|
`${name}: the "${pluginId}" capability is not enabled for this companion. ` +
|
|
91
92
|
`Enable it via \`${name} companion setup\`.`,
|
|
@@ -95,7 +96,7 @@ export async function ensureCapabilityEnabled(
|
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
function usageFor(plugin: Plugin): string {
|
|
98
|
-
const distributionName =
|
|
99
|
+
const distributionName = frameworkCommandName();
|
|
99
100
|
const namespace = namespaceOf(plugin);
|
|
100
101
|
const lines = (plugin.cliCommands ?? []).map(
|
|
101
102
|
(command) => ` ${distributionName} cap ${namespace} ${command.name} ${command.description}`,
|
package/src/cli/usage.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getActiveDistribution } from "../distribution";
|
|
2
|
+
import { FRAMEWORK_NAME, frameworkCommandName } from "../framework";
|
|
2
3
|
|
|
3
4
|
export function usage(): string {
|
|
4
|
-
const n =
|
|
5
|
+
const n = frameworkCommandName();
|
|
6
|
+
const packageName =
|
|
7
|
+
getActiveDistribution().config.update?.packageName ?? `@uniqbit/${FRAMEWORK_NAME}`;
|
|
5
8
|
return [
|
|
6
|
-
`${n.charAt(0).toUpperCase() + n.slice(1)} CLI (
|
|
9
|
+
`${n.charAt(0).toUpperCase() + n.slice(1)} CLI (${packageName})`,
|
|
7
10
|
"",
|
|
8
11
|
"Commands:",
|
|
9
12
|
` ${n} install [--yes]`,
|
package/src/create-mate.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { main } from "./cli/main";
|
|
2
2
|
import { setActiveDistribution, type DistributionConfig } from "./distribution";
|
|
3
|
+
import { FRAMEWORK_NAME } from "./framework";
|
|
3
4
|
import { createBunPlugin } from "./tools/setup/package-managers/bun";
|
|
4
5
|
import { createUvPlugin } from "./tools/setup/package-managers/uv";
|
|
5
6
|
import type { PluginRegistration } from "./tools/setup/plugin";
|
|
@@ -23,15 +24,15 @@ export interface MateCli {
|
|
|
23
24
|
* Assemble a distribution from an identity config and plugin registration
|
|
24
25
|
* entries. Distributions choose their plugin set; the framework always adds
|
|
25
26
|
* bun and uv as required runtime substrate, plus gitignore management named
|
|
26
|
-
* after
|
|
27
|
-
* Returns the runnable CLI.
|
|
27
|
+
* after the framework identity (built last since it reads the full plugin
|
|
28
|
+
* set). Returns the runnable CLI.
|
|
28
29
|
*/
|
|
29
30
|
export function createMate({ config, plugins, main: runMain = main }: CreateMateOptions): MateCli {
|
|
30
31
|
const registry = new PluginRegistry([
|
|
31
32
|
...plugins,
|
|
32
33
|
{ plugin: createBunPlugin(), policy: "required" },
|
|
33
34
|
{ plugin: createUvPlugin(), policy: "required" },
|
|
34
|
-
createGitignorePlugin(
|
|
35
|
+
createGitignorePlugin(FRAMEWORK_NAME),
|
|
35
36
|
]);
|
|
36
37
|
setActiveDistribution({ config, registry });
|
|
37
38
|
return {
|
package/src/framework.ts
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
import { getActiveDistribution } from "./distribution";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Framework identity: names everything identity-shaped regardless of how the
|
|
5
|
+
* CLI is invoked — state directories (`~/.mate`, `.mate/`), managed-block
|
|
6
|
+
* markers, `MATE_*` env values, and the `companion-policy framework`
|
|
7
|
+
* attribute. A whitelabel distribution changes the invocation name only;
|
|
8
|
+
* identity stays `mate`. Use `frameworkCommandName()` for anything the user
|
|
9
|
+
* or an agent types.
|
|
10
|
+
*/
|
|
11
|
+
export const FRAMEWORK_NAME = "mate";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Invocation name: how this distribution's CLI is invoked, derived from the
|
|
15
|
+
* package's `bin` key (`config.name`). Drives usage output, command hints,
|
|
16
|
+
* error prefixes, agent guidance, and permission entries — never paths,
|
|
17
|
+
* markers, or env values (use `FRAMEWORK_NAME` for those).
|
|
18
|
+
*/
|
|
19
|
+
export function frameworkCommandName(): string {
|
|
20
|
+
return getActiveDistribution().config.name;
|
|
21
|
+
}
|
|
22
|
+
|
|
3
23
|
/**
|
|
4
24
|
* Live view of the active distribution's identity. Framework modules read
|
|
5
25
|
* identity through this object; the values always come from the config the
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,16 @@ export {
|
|
|
21
21
|
type NormalizedRegistration,
|
|
22
22
|
} from "./tools/setup/registry";
|
|
23
23
|
export { ensureCapabilityEnabled, type EnsureCapabilityEnabledDeps } from "./cli/plugin-commands";
|
|
24
|
+
// Dynamic plugin authors: import from this package as TYPES ONLY. Runtime
|
|
25
|
+
// imports resolve a second, uninitialized copy of mate-core inside the
|
|
26
|
+
// plugin's own node_modules; all runtime behavior flows through PluginHost.
|
|
27
|
+
export {
|
|
28
|
+
createPluginHost,
|
|
29
|
+
PLUGIN_API_VERSION,
|
|
30
|
+
SUPPORTED_PLUGIN_API_VERSIONS,
|
|
31
|
+
type CreatePlugin,
|
|
32
|
+
type PluginHost,
|
|
33
|
+
} from "./tools/setup/dynamic-plugins/host";
|
|
24
34
|
export type {
|
|
25
35
|
CapabilityPlugin,
|
|
26
36
|
InstructionsService,
|
|
@@ -36,4 +46,8 @@ export type {
|
|
|
36
46
|
SetupContext,
|
|
37
47
|
TemplatesService,
|
|
38
48
|
} from "./tools/setup/plugin";
|
|
39
|
-
export type {
|
|
49
|
+
export type {
|
|
50
|
+
FrameworkConfig,
|
|
51
|
+
PluginDeclaration,
|
|
52
|
+
PluginDeclarationPolicy,
|
|
53
|
+
} from "./lib/orchestrator/types";
|
package/src/lib/install.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { CompanionResolver } from "./orchestrator/companion-resolver";
|
|
|
8
8
|
import { checkEngineRequirement } from "./orchestrator/engine-guard";
|
|
9
9
|
import { GlobalConfigStore } from "./orchestrator/global-config-store";
|
|
10
10
|
import { YamlFileStore } from "./orchestrator/yaml-file-store";
|
|
11
|
-
import {
|
|
11
|
+
import { FRAMEWORK_NAME } from "../framework";
|
|
12
12
|
import type { FrameworkConfig } from "./orchestrator/types";
|
|
13
13
|
import { getActiveDistribution } from "../distribution";
|
|
14
14
|
import { createBunPlugin } from "../tools/setup/package-managers/bun";
|
|
@@ -55,7 +55,7 @@ export interface InstallState {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
function defaultInstallStateRoot(): string {
|
|
58
|
-
return path.join(os.homedir(), `.${
|
|
58
|
+
return path.join(os.homedir(), `.${FRAMEWORK_NAME}`, "install-state");
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
export function getInstallStatePath(
|
|
@@ -107,12 +107,7 @@ function coreContext(): InstallContext {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
async function contextForCompanion(companionPath: string): Promise<InstallContext> {
|
|
110
|
-
const configPath = path.join(
|
|
111
|
-
companionPath,
|
|
112
|
-
`.${frameworkConfig.name}`,
|
|
113
|
-
"config",
|
|
114
|
-
"framework.yaml",
|
|
115
|
-
);
|
|
110
|
+
const configPath = path.join(companionPath, `.${FRAMEWORK_NAME}`, "config", "framework.yaml");
|
|
116
111
|
const config = mergeWithDefaults(await new ConfigStore(configPath).load());
|
|
117
112
|
return {
|
|
118
113
|
kind: "companion",
|
|
@@ -124,7 +119,7 @@ async function contextForCompanion(companionPath: string): Promise<InstallContex
|
|
|
124
119
|
|
|
125
120
|
async function hasLocalConfig(cwd: string): Promise<boolean> {
|
|
126
121
|
try {
|
|
127
|
-
await fs.access(path.join(cwd, `.${
|
|
122
|
+
await fs.access(path.join(cwd, `.${FRAMEWORK_NAME}`, "config", "framework.yaml"));
|
|
128
123
|
return true;
|
|
129
124
|
} catch {
|
|
130
125
|
return false;
|
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
|
|
4
4
|
import { version } from "../../../../package.json";
|
|
5
5
|
import { isCommandOnPath } from "../../fs-utils";
|
|
6
|
-
import {
|
|
6
|
+
import { FRAMEWORK_NAME, frameworkCommandName } from "../../../framework";
|
|
7
7
|
import { getReactDoctorBinPath, getWrapperBinPath } from "../../package-paths";
|
|
8
8
|
import {
|
|
9
9
|
GRAPHIFY_OUTPUT_SUBDIR,
|
|
@@ -77,7 +77,8 @@ export abstract class LaunchAdapter {
|
|
|
77
77
|
const wrapperBinPath = getWrapperBinPath();
|
|
78
78
|
const env: NodeJS.ProcessEnv = {
|
|
79
79
|
...process.env,
|
|
80
|
-
MATE_NAME:
|
|
80
|
+
MATE_NAME: FRAMEWORK_NAME,
|
|
81
|
+
MATE_COMMAND: frameworkCommandName(),
|
|
81
82
|
MATE_VERSION: version,
|
|
82
83
|
MATE_ARTIFACT_PATH: context.companionPath,
|
|
83
84
|
MATE_WRAPPER_BIN_PATH: wrapperBinPath,
|
|
@@ -148,7 +149,7 @@ export abstract class LaunchAdapter {
|
|
|
148
149
|
command: this.toolName,
|
|
149
150
|
args: builtArgs,
|
|
150
151
|
env,
|
|
151
|
-
warning: `${
|
|
152
|
+
warning: `${frameworkCommandName()}: headroom capability enabled but \`headroom\` was not found on PATH; install with \`uv tool install "headroom-ai[all]"\`; launching ${this.toolName} directly\n`,
|
|
152
153
|
};
|
|
153
154
|
}
|
|
154
155
|
|
|
@@ -182,7 +183,7 @@ export abstract class LaunchAdapter {
|
|
|
182
183
|
command: this.toolName,
|
|
183
184
|
args: builtArgs,
|
|
184
185
|
env,
|
|
185
|
-
warning: `${
|
|
186
|
+
warning: `${frameworkCommandName()}: headroom proxy failed to become ready on port ${port} after ${maxProxyStartAttempts} attempts; launching ${this.toolName} directly${errorDetail}\n`,
|
|
186
187
|
};
|
|
187
188
|
}
|
|
188
189
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { FRAMEWORK_NAME } from "../../framework";
|
|
4
4
|
import { getDefaultSetupSelections } from "./setup-compatibilities";
|
|
5
5
|
import { YamlFileStore } from "./yaml-file-store";
|
|
6
|
-
import { type FrameworkConfig } from "./types";
|
|
6
|
+
import { ConfigError, type FrameworkConfig } from "./types";
|
|
7
7
|
|
|
8
8
|
export const RTK_CAPABILITY_SPLIT_MIGRATION = "rtk-capability-split-v1";
|
|
9
9
|
|
|
10
10
|
function defaultConfigPath(): string {
|
|
11
|
-
return `.${
|
|
11
|
+
return `.${FRAMEWORK_NAME}/config/framework.yaml`;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export function defaultConfig(): FrameworkConfig {
|
|
@@ -54,6 +54,23 @@ export function migrateRtkCapabilitySplit(config: FrameworkConfig): FrameworkCon
|
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
export const PLUGIN_DECLARATION_POLICIES = ["default", "optional"] as const;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Validates the `plugins` list of a loaded config. Declared plugins may not
|
|
61
|
+
* claim `required` — required-ness is a distribution prerogative.
|
|
62
|
+
*/
|
|
63
|
+
export function validatePluginDeclarations(config: FrameworkConfig): void {
|
|
64
|
+
for (const declaration of config.plugins ?? []) {
|
|
65
|
+
const policy = declaration.policy as string | undefined;
|
|
66
|
+
if (policy !== undefined && !PLUGIN_DECLARATION_POLICIES.includes(policy as never)) {
|
|
67
|
+
throw new ConfigError(
|
|
68
|
+
`plugins entry "${declaration.package}": policy "${policy}" is not allowed for declared plugins (allowed: ${PLUGIN_DECLARATION_POLICIES.join(", ")}).`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
57
74
|
export class ConfigStore extends YamlFileStore<FrameworkConfig> {
|
|
58
75
|
constructor(configPath = process.env.MATE_CONFIG ?? defaultConfigPath()) {
|
|
59
76
|
super(path.resolve(configPath));
|
|
@@ -61,6 +78,7 @@ export class ConfigStore extends YamlFileStore<FrameworkConfig> {
|
|
|
61
78
|
|
|
62
79
|
override async load(): Promise<FrameworkConfig> {
|
|
63
80
|
const merged = mergeWithDefaults(await super.load());
|
|
81
|
+
validatePluginDeclarations(merged);
|
|
64
82
|
const needsMigration = !(merged.migrations ?? []).includes(RTK_CAPABILITY_SPLIT_MIGRATION);
|
|
65
83
|
const config = migrateRtkCapabilitySplit(merged);
|
|
66
84
|
if (needsMigration) await this.save(config);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
import { frameworkConfig } from "../../framework";
|
|
4
|
+
import { FRAMEWORK_NAME, frameworkConfig } from "../../framework";
|
|
5
5
|
import { CompanionResolver } from "./companion-resolver";
|
|
6
6
|
import { ConfigStore } from "./config-store";
|
|
7
7
|
import { GlobalConfigStore } from "./global-config-store";
|
|
@@ -45,7 +45,7 @@ function makeContext(
|
|
|
45
45
|
contextKind: FrameworkContext["contextKind"],
|
|
46
46
|
repository?: LinkedRepository,
|
|
47
47
|
): FrameworkContext {
|
|
48
|
-
const configDir = path.join(companionPath, `.${
|
|
48
|
+
const configDir = path.join(companionPath, `.${FRAMEWORK_NAME}`, "config");
|
|
49
49
|
return {
|
|
50
50
|
configStore: new ConfigStore(path.join(configDir, "framework.yaml")),
|
|
51
51
|
workingRepoStore: new WorkingRepoStore(path.join(configDir, "registry.yaml")),
|
|
@@ -79,7 +79,7 @@ export async function resolveFrameworkContext(
|
|
|
79
79
|
);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
const localDir = path.join(cwd, `.${
|
|
82
|
+
const localDir = path.join(cwd, `.${FRAMEWORK_NAME}`);
|
|
83
83
|
const localLegacyDirs = frameworkConfig.legacyNames.map((n) => path.join(cwd, `.${n}`));
|
|
84
84
|
await migrateConfigDir(localDir, localLegacyDirs);
|
|
85
85
|
|
|
@@ -170,7 +170,7 @@ export async function resolveForCapability(
|
|
|
170
170
|
|
|
171
171
|
// Fallback: cwd is the companion directory — resolve from its local config.
|
|
172
172
|
// This is specific to mate cap; resolveForLaunch does NOT get this fallback.
|
|
173
|
-
const localDir = path.join(cwd, `.${
|
|
173
|
+
const localDir = path.join(cwd, `.${FRAMEWORK_NAME}`);
|
|
174
174
|
const localLegacyDirs = frameworkConfig.legacyNames.map((n) => path.join(cwd, `.${n}`));
|
|
175
175
|
await migrateConfigDir(localDir, localLegacyDirs);
|
|
176
176
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
import { frameworkConfig } from "../../framework";
|
|
4
|
+
import { FRAMEWORK_NAME, frameworkConfig } from "../../framework";
|
|
5
5
|
import { YamlFileStore } from "./yaml-file-store";
|
|
6
6
|
import { migrateConfigDir } from "./migration";
|
|
7
7
|
|
|
@@ -21,9 +21,8 @@ function normalizeGlobalConfig(config: GlobalConfig | null): GlobalConfig {
|
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
// Lazy: the distribution name is only known once createMate has run.
|
|
25
24
|
function defaultGlobalConfigPath(): string {
|
|
26
|
-
return path.join(os.homedir(), `.${
|
|
25
|
+
return path.join(os.homedir(), `.${FRAMEWORK_NAME}`, "config.yaml");
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
export class GlobalConfigStore extends YamlFileStore<GlobalConfig> {
|