@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.
Files changed (46) hide show
  1. package/package.json +1 -1
  2. package/src/cli/commands/companion/link.ts +2 -2
  3. package/src/cli/commands/config.ts +2 -2
  4. package/src/cli/commands/doctor.ts +3 -3
  5. package/src/cli/commands/install.ts +41 -6
  6. package/src/cli/commands/launch/shared.ts +6 -3
  7. package/src/cli/commands/setup.ts +3 -3
  8. package/src/cli/commands/update.ts +9 -9
  9. package/src/cli/main.ts +14 -2
  10. package/src/cli/plugin-commands.ts +3 -2
  11. package/src/cli/usage.ts +6 -3
  12. package/src/create-mate.ts +4 -3
  13. package/src/framework.ts +20 -0
  14. package/src/index.ts +15 -1
  15. package/src/lib/install.ts +4 -9
  16. package/src/lib/orchestrator/adapters/base.ts +5 -4
  17. package/src/lib/orchestrator/config-store.ts +21 -3
  18. package/src/lib/orchestrator/framework-context.ts +4 -4
  19. package/src/lib/orchestrator/global-config-store.ts +2 -3
  20. package/src/lib/orchestrator/repo-local-registry.ts +2 -3
  21. package/src/lib/orchestrator/setup-preflight.ts +5 -5
  22. package/src/lib/orchestrator/types.ts +23 -0
  23. package/src/lib/orchestrator/working-repo-store.ts +3 -3
  24. package/src/lib/update-checker.ts +24 -9
  25. package/src/playbooks/companion-guidance.ts +8 -9
  26. package/src/runtime/env.ts +1 -0
  27. package/src/templates/capabilities/openspec-cap/claude/hooks/mate-artifact-finish.sh +2 -1
  28. package/src/templates/capabilities/openspec-cap/mate-skills/mate-artifact-finish/SKILL.md +8 -8
  29. package/src/templates/capabilities/openspec-cap/mate-skills/mate-artifact-finish/references/openspec.md +3 -3
  30. package/src/templates/providers/claude/.claude/hooks/validate-artifact-path +25 -2
  31. package/src/tools/setup/capabilities/graphify.ts +4 -4
  32. package/src/tools/setup/capabilities/openspec.ts +29 -1
  33. package/src/tools/setup/capabilities/tokensave.ts +18 -44
  34. package/src/tools/setup/dynamic-plugins/config-resolution.ts +101 -0
  35. package/src/tools/setup/dynamic-plugins/host.ts +51 -0
  36. package/src/tools/setup/dynamic-plugins/hydrate.ts +156 -0
  37. package/src/tools/setup/dynamic-plugins/install.ts +171 -0
  38. package/src/tools/setup/dynamic-plugins/loader.ts +171 -0
  39. package/src/tools/setup/dynamic-plugins/paths.ts +42 -0
  40. package/src/tools/setup/dynamic-plugins/pin-store.ts +30 -0
  41. package/src/tools/setup/engine.ts +2 -1
  42. package/src/tools/setup/plugins/gitignore.ts +12 -1
  43. package/src/tools/setup/plugins/guidance.ts +3 -3
  44. package/src/tools/setup/providers/claude.ts +22 -5
  45. package/src/tools/setup/providers/opencode.ts +3 -2
  46. package/src/tools/setup.ts +28 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniqbit/mate-core",
3
- "version": "0.15.1-canary.1",
3
+ "version": "0.15.1-canary.3",
4
4
  "description": "Core framework and plugin APIs for Mate.",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -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 { frameworkConfig } from "../../../framework";
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(), `.${frameworkConfig.name}`, "companions");
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 { frameworkConfig } from "../../framework";
4
+ import { FRAMEWORK_NAME } from "../../framework";
5
5
 
6
- const getGlobalConfigDir = () => path.join(os.homedir(), `.${frameworkConfig.name}`);
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, `.${frameworkConfig.name}`, "config");
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, `.${frameworkConfig.name}`);
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("Installation declined. Run `mate install --yes` to continue.\n");
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
- "mate: installation requires confirmation in a TTY. Re-run with `mate install --yes`.\n",
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(`mate: ${result.id} failed: ${result.error ?? "unknown error"}\n`);
105
+ process.stderr.write(
106
+ `${frameworkCommandName()}: ${result.id} failed: ${result.error ?? "unknown error"}\n`,
107
+ );
77
108
  }
78
- process.stderr.write("Installation is incomplete. Re-run `mate install` after remediation.\n");
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
- `mate: installation completed but companion reconciliation failed: ${error instanceof Error ? error.message : String(error)}\n`,
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(`mate: unknown launch option: ${arg}\n`);
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(`mate: \`${tool}\` is disallowed by active repository policy.\n`);
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("Run `mate companion link` first.\n");
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 { frameworkConfig } from "../../framework";
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 ${frameworkConfig.name} companion here may not be what you want.\n`,
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, `.${frameworkConfig.name}`, "config", "framework.yaml"),
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 { frameworkConfig } from "../../framework";
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
- `${frameworkConfig.name}: self-update is only supported for npm-installed Mate.\n`,
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
- `${frameworkConfig.name}: could not reach registry to check for updates\n`,
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(`${frameworkConfig.name}: update available (${current} → ${latest})`);
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
- `${frameworkConfig.name}: npm is required for self-update but was not found on PATH\n`,
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
- `${frameworkConfig.name}: could not verify the npm installation used for self-update\n`,
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
- `${frameworkConfig.name}: upgrade command exited with status ${result.status ?? 1}\n`,
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
- `${frameworkConfig.name}: could not pre-fetch ${OPENCODE_PLUGIN_PACKAGE_NAME}@${latest} for OpenCode.`,
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 \`mate install\`.\n`,
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(`mate: ${preflight.reason ?? "installation is incomplete"}`);
85
- console.error("Run `mate install` before continuing.");
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 = distribution.config.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 = getActiveDistribution().config.name;
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 { frameworkConfig } from "../framework";
1
+ import { getActiveDistribution } from "../distribution";
2
+ import { FRAMEWORK_NAME, frameworkCommandName } from "../framework";
2
3
 
3
4
  export function usage(): string {
4
- const n = frameworkConfig.name;
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 (@uniqbit/${n})`,
9
+ `${n.charAt(0).toUpperCase() + n.slice(1)} CLI (${packageName})`,
7
10
  "",
8
11
  "Commands:",
9
12
  ` ${n} install [--yes]`,
@@ -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 `config.name` (built last since it reads the full plugin set).
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(config.name),
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 { FrameworkConfig } from "./lib/orchestrator/types";
49
+ export type {
50
+ FrameworkConfig,
51
+ PluginDeclaration,
52
+ PluginDeclarationPolicy,
53
+ } from "./lib/orchestrator/types";
@@ -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 { frameworkConfig } from "../framework";
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(), `.${frameworkConfig.name}`, "install-state");
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, `.${frameworkConfig.name}`, "config", "framework.yaml"));
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 { frameworkConfig } from "../../../framework";
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: frameworkConfig.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: `${frameworkConfig.name}: headroom capability enabled but \`headroom\` was not found on PATH; install with \`uv tool install "headroom-ai[all]"\`; launching ${this.toolName} directly\n`,
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: `${frameworkConfig.name}: headroom proxy failed to become ready on port ${port} after ${maxProxyStartAttempts} attempts; launching ${this.toolName} directly${errorDetail}\n`,
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 { frameworkConfig } from "../../framework";
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 `.${frameworkConfig.name}/config/framework.yaml`;
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, `.${frameworkConfig.name}`, "config");
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, `.${frameworkConfig.name}`);
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, `.${frameworkConfig.name}`);
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(), `.${frameworkConfig.name}`, "config.yaml");
25
+ return path.join(os.homedir(), `.${FRAMEWORK_NAME}`, "config.yaml");
27
26
  }
28
27
 
29
28
  export class GlobalConfigStore extends YamlFileStore<GlobalConfig> {