@uniqbit/mate-core 0.15.3 → 0.15.4-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 (52) hide show
  1. package/package.json +3 -2
  2. package/src/cli/commands/companion/hub.ts +99 -0
  3. package/src/cli/commands/companion/tui.ts +3 -1
  4. package/src/cli/commands/doctor.ts +3 -6
  5. package/src/cli/commands/install.ts +10 -10
  6. package/src/cli/commands/launch/shared.ts +4 -4
  7. package/src/cli/commands/plugin/install.ts +94 -0
  8. package/src/cli/commands/plugin/plugin.ts +17 -0
  9. package/src/cli/commands/setup.ts +2 -2
  10. package/src/cli/commands/update.ts +9 -11
  11. package/src/cli/main.ts +25 -9
  12. package/src/cli/plugin-commands.ts +4 -4
  13. package/src/cli/usage.ts +7 -3
  14. package/src/distribution.ts +3 -5
  15. package/src/framework.ts +4 -36
  16. package/src/index.ts +4 -0
  17. package/src/lib/install.ts +1 -5
  18. package/src/lib/orchestrator/adapters/base.ts +3 -4
  19. package/src/lib/orchestrator/companion-hub.ts +374 -0
  20. package/src/lib/orchestrator/config-store.ts +72 -0
  21. package/src/lib/orchestrator/editor.ts +4 -20
  22. package/src/lib/orchestrator/framework-context.ts +119 -23
  23. package/src/lib/orchestrator/global-config-store.ts +1 -5
  24. package/src/lib/orchestrator/migration.ts +0 -23
  25. package/src/lib/orchestrator/setup-preflight.ts +4 -17
  26. package/src/lib/orchestrator/types.ts +26 -4
  27. package/src/lib/orchestrator/working-repo-store.ts +2 -2
  28. package/src/lib/update-checker.ts +5 -5
  29. package/src/playbooks/companion-guidance.ts +6 -6
  30. package/src/runtime/env.ts +0 -1
  31. package/src/templates/capabilities/openspec-cap/claude/hooks/mate-artifact-finish.sh +79 -3
  32. package/src/templates/capabilities/openspec-cap/mate-skills/{mate-artifact-finish → agents/mate-artifact-finish}/SKILL.md +8 -8
  33. package/src/templates/capabilities/openspec-cap/mate-skills/{mate-artifact-finish → agents/mate-artifact-finish}/references/openspec.md +3 -3
  34. package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/SKILL.md +58 -0
  35. package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/references/openspec.md +139 -0
  36. package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +5 -5
  37. package/src/tools/setup/capabilities/openspec.ts +1 -1
  38. package/src/tools/setup/capabilities/tokensave.ts +4 -4
  39. package/src/tools/setup/dynamic-plugins/host.ts +2 -1
  40. package/src/tools/setup/dynamic-plugins/hydrate.ts +3 -13
  41. package/src/tools/setup/dynamic-plugins/install.ts +124 -109
  42. package/src/tools/setup/dynamic-plugins/loader.ts +23 -58
  43. package/src/tools/setup/dynamic-plugins/paths.ts +8 -19
  44. package/src/tools/setup/dynamic-plugins/registry-hint.ts +14 -0
  45. package/src/tools/setup/mate.ts +28 -31
  46. package/src/tools/setup/plugins/gitignore.ts +15 -8
  47. package/src/tools/setup/plugins/guidance.ts +1 -5
  48. package/src/tools/setup/providers/claude.ts +20 -8
  49. package/src/tools/setup/providers/opencode.ts +3 -4
  50. package/src/tools/setup.ts +2 -3
  51. package/src/tui.ts +5 -0
  52. package/src/tools/setup/dynamic-plugins/pin-store.ts +0 -30
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniqbit/mate-core",
3
- "version": "0.15.3",
3
+ "version": "0.15.4-canary.3",
4
4
  "description": "Core framework and plugin APIs for Mate.",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -14,7 +14,8 @@
14
14
  ".": "./src/index.ts",
15
15
  "./plugins": "./src/plugins.ts",
16
16
  "./runtime": "./src/runtime/index.ts",
17
- "./opencode": "./src/opencode/index.ts"
17
+ "./opencode": "./src/opencode/index.ts",
18
+ "./tui": "./src/tui.ts"
18
19
  },
19
20
  "publishConfig": {
20
21
  "access": "public",
@@ -0,0 +1,99 @@
1
+ import readline from "node:readline/promises";
2
+
3
+ import {
4
+ addHubMember,
5
+ discoverHubSource,
6
+ initializeCompanionHub,
7
+ syncHub,
8
+ updateHubPlugins,
9
+ } from "../../../lib/orchestrator/companion-hub";
10
+ import { GlobalConfigStore } from "../../../lib/orchestrator/global-config-store";
11
+
12
+ function positionalArgs(argv: string[]): string[] {
13
+ const values: string[] = [];
14
+ const valueFlags = new Set(["id", "path", "url", "companion", "ref"]);
15
+ for (let index = 0; index < argv.length; index += 1) {
16
+ const value = argv[index]!;
17
+ if (!value.startsWith("--")) {
18
+ values.push(value);
19
+ continue;
20
+ }
21
+ if (valueFlags.has(value.slice(2))) index += 1;
22
+ }
23
+ return values;
24
+ }
25
+
26
+ function flagValue(argv: string[], name: string): string | undefined {
27
+ const index = argv.indexOf(`--${name}`);
28
+ return index >= 0 ? argv[index + 1] : undefined;
29
+ }
30
+
31
+ async function selectRegisteredCompanion(): Promise<string> {
32
+ const companions = await new GlobalConfigStore().list();
33
+ if (companions.length === 0) throw new Error("No registered companions are available.");
34
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
35
+ throw new Error("hub add requires a source path or Git URL outside a TTY.");
36
+ }
37
+ const prompt = readline.createInterface({ input: process.stdin, output: process.stdout });
38
+ try {
39
+ console.log("Registered companions:");
40
+ companions.forEach((candidate, index) => console.log(` ${index + 1}. ${candidate}`));
41
+ const answer = await prompt.question("Select a companion: ");
42
+ const selected = companions[Number.parseInt(answer, 10) - 1];
43
+ if (!selected) throw new Error("Invalid companion selection.");
44
+ return selected;
45
+ } finally {
46
+ prompt.close();
47
+ }
48
+ }
49
+
50
+ async function runHubInit(argv: string[]): Promise<void> {
51
+ const folder = positionalArgs(argv)[0] ?? process.cwd();
52
+ const hubPath = await initializeCompanionHub(folder);
53
+ console.log(`Initialized companion hub: ${hubPath}`);
54
+ }
55
+
56
+ async function runHubAdd(argv: string[]): Promise<void> {
57
+ const sourceArg =
58
+ positionalArgs(argv)[0] ?? flagValue(argv, "url") ?? flagValue(argv, "companion");
59
+ const source = discoverHubSource(sourceArg ?? (await selectRegisteredCompanion()));
60
+ const ref = flagValue(argv, "ref");
61
+ if (ref && source.kind === "git") source.ref = ref;
62
+ const member = await addHubMember(process.cwd(), source, {
63
+ id: flagValue(argv, "id"),
64
+ memberPath: flagValue(argv, "path"),
65
+ });
66
+ console.log(`Added hub member ${member.id}: ${member.path}`);
67
+ }
68
+
69
+ async function runHubSync(argv: string[]): Promise<void> {
70
+ const results = await syncHub(process.cwd());
71
+ const plugins = await updateHubPlugins(process.cwd());
72
+ if (argv.includes("--json")) {
73
+ console.log(JSON.stringify({ companions: results, plugins }));
74
+ return;
75
+ }
76
+ for (const result of results) console.log(`${result.id}: ${result.status} (${result.message})`);
77
+ for (const plugin of plugins) {
78
+ const detail = plugin.status === "failed" ? `: ${plugin.error ?? "failed"}` : "";
79
+ console.log(`hub plugin ${plugin.package}: ${plugin.status}${detail}`);
80
+ }
81
+ if (plugins.some((plugin) => plugin.status === "failed")) process.exitCode = 1;
82
+ }
83
+
84
+ export async function runHubCommand(argv: string[]): Promise<void> {
85
+ const [subcommand, ...rest] = argv;
86
+ switch (subcommand) {
87
+ case "init":
88
+ await runHubInit(rest);
89
+ return;
90
+ case "add":
91
+ await runHubAdd(rest);
92
+ return;
93
+ case "sync":
94
+ await runHubSync(rest);
95
+ return;
96
+ default:
97
+ throw new Error("Usage: mate hub <init|add|sync>");
98
+ }
99
+ }
@@ -2,12 +2,14 @@ import { spawn } from "node:child_process";
2
2
  import { accessSync } from "node:fs";
3
3
 
4
4
  import { getActiveDistribution } from "../../../distribution";
5
+ import { FRAMEWORK_NAME } from "../../../framework";
5
6
  import { resolveFrameworkContext } from "../../../lib/orchestrator/framework-context";
6
7
  import { findRepoLocalLinkedRepository } from "../../../lib/orchestrator/repo-local-registry";
7
8
  import { ensureUnambiguousCompanion } from "../launch/shared";
8
9
 
9
10
  export function renderBanner(repoPath: string, companionPath: string): string {
10
- const { name, version } = getActiveDistribution().config;
11
+ const { version } = getActiveDistribution().config;
12
+ const name = FRAMEWORK_NAME;
11
13
  const lines = [`${name} v${version}`, "", `repo: ${repoPath}`, `${name}: ${companionPath}`];
12
14
  const contentWidth = Math.max(...lines.map((l) => l.length));
13
15
  const pad = (text: string) => `│ ${text.padEnd(contentWidth)} │`;
@@ -1,14 +1,13 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
- import { FRAMEWORK_NAME, frameworkConfig } from "../../framework";
4
+ import { FRAMEWORK_NAME } 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";
8
8
  import { ConfigStore } from "../../lib/orchestrator/config-store";
9
9
  import { checkEngineRequirement } from "../../lib/orchestrator/engine-guard";
10
10
  import { GlobalConfigStore } from "../../lib/orchestrator/global-config-store";
11
- import { migrateConfigDir } from "../../lib/orchestrator/migration";
12
11
  import { findRepoLocalLinkedRepository } from "../../lib/orchestrator/repo-local-registry";
13
12
  import type { CapabilityConfig, FrameworkConfig } from "../../lib/orchestrator/types";
14
13
  import { WorkingRepoStore } from "../../lib/orchestrator/working-repo-store";
@@ -43,8 +42,6 @@ function storesForCompanion(companionPath: string): CompanionStores {
43
42
 
44
43
  async function hasLocalCompanionConfig(cwd: string): Promise<boolean> {
45
44
  const localDir = path.join(cwd, `.${FRAMEWORK_NAME}`);
46
- const localLegacyDirs = frameworkConfig.legacyNames.map((name) => path.join(cwd, `.${name}`));
47
- await migrateConfigDir(localDir, localLegacyDirs);
48
45
 
49
46
  try {
50
47
  await fs.access(path.join(localDir, "config", "framework.yaml"));
@@ -74,10 +71,10 @@ function renderEngineRequirement(
74
71
  // diagnostic; foreign distribution keys are ignored entirely.
75
72
  function printEngineRequirement(config: FrameworkConfig): void {
76
73
  const distribution = getActiveDistribution().config;
77
- if (!config.engines?.[distribution.name]) return;
74
+ if (!config.engines?.[FRAMEWORK_NAME]) return;
78
75
  printSection(
79
76
  "Version Requirement",
80
- renderEngineRequirement(config, distribution.name, distribution.version),
77
+ renderEngineRequirement(config, FRAMEWORK_NAME, distribution.version),
81
78
  );
82
79
  }
83
80
 
@@ -1,5 +1,5 @@
1
1
  import { confirm } from "../confirm";
2
- import { frameworkCommandName } from "../../framework";
2
+ import { FRAMEWORK_NAME } from "../../framework";
3
3
  import {
4
4
  buildInstallPlan,
5
5
  inspectInstallPlan,
@@ -14,13 +14,15 @@ import {
14
14
  installDeclaredPlugins,
15
15
  type PluginInstallResult,
16
16
  } from "../../tools/setup/dynamic-plugins/install";
17
+ import { registryConfigHint } from "../../tools/setup/dynamic-plugins/registry-hint";
17
18
 
18
19
  export function reportPluginInstallResults(results: PluginInstallResult[]): void {
19
20
  for (const result of results) {
20
21
  if (result.status === "failed") {
21
22
  process.stderr.write(
22
- `${frameworkCommandName()}: plugin "${result.package}" failed to install: ${result.error ?? "unknown error"}\n`,
23
+ `${FRAMEWORK_NAME}: plugin "${result.package}" failed to install: ${result.error ?? "unknown error"}\n`,
23
24
  );
25
+ process.stderr.write(`${registryConfigHint(result.package)}\n`);
24
26
  } else if (result.status === "installed") {
25
27
  console.log(` installed plugin ${result.package}@${result.resolvedVersion}`);
26
28
  }
@@ -72,7 +74,7 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
72
74
  const ok = await confirm("Install the missing requirements? [y/N] ");
73
75
  if (!ok) {
74
76
  process.stderr.write(
75
- `Installation declined. Run \`${frameworkCommandName()} install --yes\` to continue.\n`,
77
+ `Installation declined. Run \`${FRAMEWORK_NAME} install --yes\` to continue.\n`,
76
78
  );
77
79
  process.exitCode = 1;
78
80
  return false;
@@ -83,7 +85,7 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
83
85
  printPlanText(plan);
84
86
  if (!skipConfirm) {
85
87
  process.stderr.write(
86
- `${frameworkCommandName()}: installation requires confirmation in a TTY. Re-run with \`${frameworkCommandName()} install --yes\`.\n`,
88
+ `${FRAMEWORK_NAME}: installation requires confirmation in a TTY. Re-run with \`${FRAMEWORK_NAME} install --yes\`.\n`,
87
89
  );
88
90
  process.exitCode = 1;
89
91
  return false;
@@ -103,11 +105,11 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
103
105
  if (!execution.ok) {
104
106
  for (const result of execution.results.filter((item) => item.status === "failed")) {
105
107
  process.stderr.write(
106
- `${frameworkCommandName()}: ${result.id} failed: ${result.error ?? "unknown error"}\n`,
108
+ `${FRAMEWORK_NAME}: ${result.id} failed: ${result.error ?? "unknown error"}\n`,
107
109
  );
108
110
  }
109
111
  process.stderr.write(
110
- `Installation is incomplete. Re-run \`${frameworkCommandName()} install\` after remediation.\n`,
112
+ `Installation is incomplete. Re-run \`${FRAMEWORK_NAME} install\` after remediation.\n`,
111
113
  );
112
114
  process.exitCode = 1;
113
115
  return false;
@@ -118,11 +120,9 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
118
120
  await saveCompleteInstallState(plan, execution.results);
119
121
  } catch (error) {
120
122
  process.stderr.write(
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`,
123
+ `${FRAMEWORK_NAME}: installation completed but companion reconciliation failed: ${error instanceof Error ? error.message : String(error)}\n`,
125
124
  );
125
+ process.stderr.write(`Installation is incomplete. Re-run \`${FRAMEWORK_NAME} install\`.\n`);
126
126
  process.exitCode = 1;
127
127
  return false;
128
128
  }
@@ -1,4 +1,4 @@
1
- import { frameworkCommandName } from "../../../framework";
1
+ import { FRAMEWORK_NAME } from "../../../framework";
2
2
  import { FrameworkLauncher } from "../../../lib/orchestrator/launcher";
3
3
  import {
4
4
  LaunchPreflightError,
@@ -43,7 +43,7 @@ export function parseLaunchArgs(argv: string[]): ParsedLaunchArgs | null {
43
43
  const agentArgs = separatorIndex >= 0 ? argv.slice(separatorIndex + 1) : [];
44
44
 
45
45
  for (const arg of optionArgs) {
46
- process.stderr.write(`${frameworkCommandName()}: unknown launch option: ${arg}\n`);
46
+ process.stderr.write(`${FRAMEWORK_NAME}: unknown launch option: ${arg}\n`);
47
47
  process.exitCode = 1;
48
48
  return null;
49
49
  }
@@ -109,7 +109,7 @@ export async function runLaunchToolCommand(
109
109
 
110
110
  if (error instanceof ToolNotAllowedError) {
111
111
  process.stderr.write(
112
- `${frameworkCommandName()}: \`${tool}\` is disallowed by active repository policy.\n`,
112
+ `${FRAMEWORK_NAME}: \`${tool}\` is disallowed by active repository policy.\n`,
113
113
  );
114
114
  process.exitCode = 1;
115
115
  return;
@@ -117,7 +117,7 @@ export async function runLaunchToolCommand(
117
117
 
118
118
  if (error instanceof RepositoryNotSelectedError) {
119
119
  process.stderr.write(`${error.message}\n`);
120
- process.stderr.write(`Run \`${frameworkCommandName()} companion link\` first.\n`);
120
+ process.stderr.write(`Run \`${FRAMEWORK_NAME} companion link\` first.\n`);
121
121
  process.exitCode = 1;
122
122
  return;
123
123
  }
@@ -0,0 +1,94 @@
1
+ import path from "node:path";
2
+
3
+ import { FRAMEWORK_NAME } from "../../../framework";
4
+ import { resolveInstallContext } from "../../../lib/install";
5
+ import { ConfigStore } from "../../../lib/orchestrator/config-store";
6
+ import type { PluginDeclaration } from "../../../lib/orchestrator/types";
7
+ import { hydrateDynamicPlugins } from "../../../tools/setup/dynamic-plugins/hydrate";
8
+ import {
9
+ installDeclaredPlugins,
10
+ type PluginInstallDeps,
11
+ } from "../../../tools/setup/dynamic-plugins/install";
12
+ import { reportPluginInstallResults } from "../install";
13
+
14
+ export function parsePackageSpec(spec: string): { package: string; version: string } {
15
+ const atIndex = spec.startsWith("@") ? spec.indexOf("@", 1) : spec.indexOf("@");
16
+ if (atIndex === -1) return { package: spec, version: "latest" };
17
+ return { package: spec.slice(0, atIndex), version: spec.slice(atIndex + 1) };
18
+ }
19
+
20
+ function withDeclaration(
21
+ declarations: PluginDeclaration[],
22
+ packageName: string,
23
+ version: string,
24
+ ): PluginDeclaration[] {
25
+ const existing = declarations.find((declaration) => declaration.package === packageName);
26
+ if (existing?.version === version) return declarations;
27
+ if (!existing) return [...declarations, { package: packageName, version }];
28
+ return declarations.map((declaration) =>
29
+ declaration.package === packageName ? { ...declaration, version } : declaration,
30
+ );
31
+ }
32
+
33
+ export interface PluginInstallCommandDeps {
34
+ cwd?: string;
35
+ installDeps?: PluginInstallDeps;
36
+ }
37
+
38
+ /**
39
+ * @command mate plugin install <package>[@version]
40
+ * @description Declares `<package>` in the companion's `framework.yaml`
41
+ * `plugins:` list (adding it, or updating its version if already declared)
42
+ * and installs it immediately into the shared workspace, the same way
43
+ * `mate install` would on its next run.
44
+ */
45
+ export async function runPluginInstallCommand(
46
+ argv: string[],
47
+ deps: PluginInstallCommandDeps = {},
48
+ ): Promise<boolean> {
49
+ const spec = argv[0];
50
+ if (!spec) {
51
+ process.stderr.write(
52
+ `${FRAMEWORK_NAME}: usage: ${FRAMEWORK_NAME} plugin install <package>[@version]\n`,
53
+ );
54
+ process.exitCode = 1;
55
+ return false;
56
+ }
57
+ const { package: packageName, version } = parsePackageSpec(spec);
58
+
59
+ const context = await resolveInstallContext(deps.cwd ?? process.cwd());
60
+ if (context.kind === "ambiguous") {
61
+ process.stderr.write(`${context.message}\n`);
62
+ process.exitCode = 1;
63
+ return false;
64
+ }
65
+ if (context.kind !== "companion" || !context.companionPath) {
66
+ process.stderr.write(
67
+ `${FRAMEWORK_NAME}: \`plugin install\` requires a companion context; run it from inside a linked working repository.\n`,
68
+ );
69
+ process.exitCode = 1;
70
+ return false;
71
+ }
72
+
73
+ const configPath = path.join(
74
+ context.companionPath,
75
+ `.${FRAMEWORK_NAME}`,
76
+ "config",
77
+ "framework.yaml",
78
+ );
79
+ const store = new ConfigStore(configPath);
80
+ const nextPlugins = withDeclaration(context.config.plugins ?? [], packageName, version);
81
+ if (nextPlugins !== context.config.plugins) {
82
+ await store.save({ ...context.config, plugins: nextPlugins });
83
+ }
84
+
85
+ const results = await installDeclaredPlugins(
86
+ context.companionPath,
87
+ nextPlugins,
88
+ deps.installDeps,
89
+ );
90
+ reportPluginInstallResults(results);
91
+ await hydrateDynamicPlugins({ companionPath: context.companionPath });
92
+
93
+ return results.find((result) => result.package === packageName)?.status !== "failed";
94
+ }
@@ -0,0 +1,17 @@
1
+ import { usage } from "../../usage";
2
+ import { runPluginInstallCommand } from "./install";
3
+
4
+ export async function runPluginCommand(
5
+ subcommand: string | undefined,
6
+ argv: string[],
7
+ ): Promise<void> {
8
+ switch (subcommand) {
9
+ case "install":
10
+ await runPluginInstallCommand(argv);
11
+ return;
12
+ default:
13
+ console.error(`Unknown plugin command: ${subcommand ?? ""}`);
14
+ console.error(usage());
15
+ process.exitCode = 1;
16
+ }
17
+ }
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
 
3
- import { FRAMEWORK_NAME, frameworkCommandName } from "../../framework";
3
+ import { FRAMEWORK_NAME } 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 ${frameworkCommandName()} companion here may not be what you want.\n`,
101
+ `Initializing it as a ${FRAMEWORK_NAME} 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] ");
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { spawnSync } from "node:child_process";
3
3
 
4
- import { frameworkCommandName } from "../../framework";
4
+ import { FRAMEWORK_NAME } 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
- `${frameworkCommandName()}: self-update is only supported for npm-installed Mate.\n`,
23
+ `${FRAMEWORK_NAME}: self-update is only supported for npm-installed Mate.\n`,
24
24
  );
25
25
  if (packageName.startsWith("@uniqbit/")) {
26
26
  process.stderr.write(
@@ -87,16 +87,14 @@ export async function runUpdateCommand(argv: string[]): Promise<void> {
87
87
  try {
88
88
  latest = await updateCommandDeps.fetchLatestVersion();
89
89
  } catch {
90
- process.stderr.write(
91
- `${frameworkCommandName()}: could not reach registry to check for updates\n`,
92
- );
90
+ process.stderr.write(`${FRAMEWORK_NAME}: could not reach registry to check for updates\n`);
93
91
  process.exitCode = 1;
94
92
  return;
95
93
  }
96
94
 
97
95
  if (checkOnly) {
98
96
  if (updateCommandDeps.isNewer(latest, current)) {
99
- console.log(`${frameworkCommandName()}: update available (${current} → ${latest})`);
97
+ console.log(`${FRAMEWORK_NAME}: update available (${current} → ${latest})`);
100
98
  process.exitCode = 1;
101
99
  } else {
102
100
  console.log("Up to date.");
@@ -127,11 +125,11 @@ export async function runUpdateCommand(argv: string[]): Promise<void> {
127
125
  } catch (error) {
128
126
  if ((error as NodeJS.ErrnoException).code === "ENOENT") {
129
127
  process.stderr.write(
130
- `${frameworkCommandName()}: npm is required for self-update but was not found on PATH\n`,
128
+ `${FRAMEWORK_NAME}: npm is required for self-update but was not found on PATH\n`,
131
129
  );
132
130
  } else {
133
131
  process.stderr.write(
134
- `${frameworkCommandName()}: could not verify the npm installation used for self-update\n`,
132
+ `${FRAMEWORK_NAME}: could not verify the npm installation used for self-update\n`,
135
133
  );
136
134
  }
137
135
  writeNpmOnlyRecoveryMessage();
@@ -148,7 +146,7 @@ export async function runUpdateCommand(argv: string[]): Promise<void> {
148
146
  const result = updateCommandDeps.installLatest(latest);
149
147
  if (result.status !== 0 || result.error) {
150
148
  process.stderr.write(
151
- `${frameworkCommandName()}: upgrade command exited with status ${result.status ?? 1}\n`,
149
+ `${FRAMEWORK_NAME}: upgrade command exited with status ${result.status ?? 1}\n`,
152
150
  );
153
151
  process.exitCode = 1;
154
152
  return;
@@ -163,7 +161,7 @@ export async function runUpdateCommand(argv: string[]): Promise<void> {
163
161
  if (!warmed.ok) {
164
162
  process.stderr.write(
165
163
  [
166
- `${frameworkCommandName()}: could not pre-fetch ${OPENCODE_PLUGIN_PACKAGE_NAME}@${latest} for OpenCode.`,
164
+ `${FRAMEWORK_NAME}: could not pre-fetch ${OPENCODE_PLUGIN_PACKAGE_NAME}@${latest} for OpenCode.`,
167
165
  "The next managed OpenCode launch will download it (requires registry access).",
168
166
  ...(warmed.detail ? [`Details: ${warmed.detail}`] : []),
169
167
  ].join("\n") + "\n",
@@ -173,7 +171,7 @@ export async function runUpdateCommand(argv: string[]): Promise<void> {
173
171
  const postInstall = updateCommandDeps.runPostInstall(skipConfirm);
174
172
  if (postInstall.status !== 0 || postInstall.error) {
175
173
  process.stderr.write(
176
- `\nUpgraded to ${latest}, but installation is incomplete. Run \`${frameworkCommandName()} install\`.\n`,
174
+ `\nUpgraded to ${latest}, but installation is incomplete. Run \`${FRAMEWORK_NAME} install\`.\n`,
177
175
  );
178
176
  process.exitCode = 1;
179
177
  return;
package/src/cli/main.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { getActiveDistribution } from "../distribution";
2
- import { frameworkCommandName } from "../framework";
2
+ import { FRAMEWORK_NAME } from "../framework";
3
3
  import {
4
4
  enforceUpdateIfRequired,
5
5
  scheduleBackgroundCheck,
@@ -9,10 +9,12 @@ import {
9
9
  import { runArtifactCommand } from "./commands/artifact/artifact";
10
10
  import { runCapCommand } from "./commands/cap";
11
11
  import { runCompanionCommand } from "./commands/companion/companion";
12
+ import { runHubCommand } from "./commands/companion/hub";
12
13
  import { runConfigCommand } from "./commands/config";
13
14
  import { runDoctorCommand } from "./commands/doctor";
14
15
  import { runLaunchClaudeCommand } from "./commands/launch/claude";
15
16
  import { runLaunchOpenCodeCommand } from "./commands/launch/opencode";
17
+ import { runPluginCommand } from "./commands/plugin/plugin";
16
18
  import { runReportCommand } from "./commands/report";
17
19
  import { runUpdateCommand } from "./commands/update";
18
20
  import { runInstallCommand } from "./commands/install";
@@ -51,10 +53,10 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
51
53
  return;
52
54
  }
53
55
 
54
- // Companion-declared plugins register before cap-command detection and
55
- // before help text is printed, so their commands route like compiled-in
56
- // ones (including MCP servers whose command is their own cap subcommand)
57
- // and show up in `mate help`. Diagnostics stay on stderr; a missing or
56
+ // Dynamic plugins register before cap-command detection and before help
57
+ // text is printed, so their commands route like compiled-in ones
58
+ // (including MCP servers whose command is their own cap subcommand) and
59
+ // show up in `mate help`. Diagnostics stay on stderr; a missing or
58
60
  // ambiguous companion makes this a no-op.
59
61
  await deps.hydrateDynamicPlugins();
60
62
 
@@ -92,10 +94,8 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
92
94
  if (needs.install) {
93
95
  const preflight = await deps.inspectInstallPreflight();
94
96
  if (!preflight.ok) {
95
- console.error(
96
- `${frameworkCommandName()}: ${preflight.reason ?? "installation is incomplete"}`,
97
- );
98
- console.error(`Run \`${frameworkCommandName()} install\` before continuing.`);
97
+ console.error(`${FRAMEWORK_NAME}: ${preflight.reason ?? "installation is incomplete"}`);
98
+ console.error(`Run \`${FRAMEWORK_NAME} install\` before continuing.`);
99
99
  process.exitCode = 1;
100
100
  return false;
101
101
  }
@@ -111,6 +111,13 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
111
111
  if (!(await gate({ companion: true }))) return;
112
112
  await runInstallCommand(argv.slice(3));
113
113
  return;
114
+ case "plugin":
115
+ // Declares into framework.yaml and installs on the spot, so it needs
116
+ // the same gating as `install` — an unambiguous companion, but must
117
+ // stay runnable when installation is otherwise incomplete.
118
+ if (!(await gate({ companion: true }))) return;
119
+ await runPluginCommand(subcommand, rest);
120
+ return;
114
121
  case "artifact":
115
122
  if (!(await gate({ updateGuard: true, companion: true, install: true }))) return;
116
123
  await runArtifactCommand(subcommand, rest);
@@ -123,6 +130,11 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
123
130
  case "link":
124
131
  if (!(await gate({}))) return;
125
132
  break;
133
+ // Hub commands establish and operate on a local hub root directly;
134
+ // they must not require a linked working repository or installation.
135
+ case "hub":
136
+ if (!(await gate({ updateGuard: true }))) return;
137
+ break;
126
138
  // open/tui consume a companion context.
127
139
  case "open":
128
140
  case "tui":
@@ -134,6 +146,10 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
134
146
  }
135
147
  await runCompanionCommand(subcommand, rest);
136
148
  return;
149
+ case "hub":
150
+ if (!(await gate({ updateGuard: true }))) return;
151
+ await runHubCommand(subcommand ? [subcommand, ...rest] : []);
152
+ return;
137
153
  case "claude":
138
154
  if (!(await gate({ updateGuard: true, companion: true, install: true }))) return;
139
155
  await runLaunchClaudeCommand(argv.slice(3), { directPassthrough: true });
@@ -1,5 +1,5 @@
1
1
  import { getActiveDistribution } from "../distribution";
2
- import { frameworkCommandName } from "../framework";
2
+ import { FRAMEWORK_NAME } from "../framework";
3
3
  import { resolveInstallContext } from "../lib/install";
4
4
  import type { FrameworkConfig } from "../lib/orchestrator/types";
5
5
  import type { Plugin, PluginCliCommand } from "../tools/setup/plugin";
@@ -86,7 +86,7 @@ export async function ensureCapabilityEnabled(
86
86
  const loadConfig = deps.loadConfig ?? (async () => (await resolveInstallContext()).config);
87
87
  if (plugin?.isEnabled(await loadConfig())) return true;
88
88
 
89
- const name = frameworkCommandName();
89
+ const name = FRAMEWORK_NAME;
90
90
  console.error(
91
91
  `${name}: the "${pluginId}" capability is not enabled for this companion. ` +
92
92
  `Enable it via \`${name} companion setup\`.`,
@@ -104,7 +104,7 @@ function formatCommand(
104
104
  }
105
105
 
106
106
  function usageFor(plugin: Plugin): string {
107
- const distributionName = frameworkCommandName();
107
+ const distributionName = FRAMEWORK_NAME;
108
108
  const namespace = namespaceOf(plugin);
109
109
  const lines = (plugin.cliCommands ?? []).map((command) =>
110
110
  formatCommand(distributionName, namespace, command),
@@ -119,7 +119,7 @@ function usageFor(plugin: Plugin): string {
119
119
  * commands show up in `mate help` alongside framework builtins.
120
120
  */
121
121
  export function pluginCliCommandLines(): string[] {
122
- const distributionName = frameworkCommandName();
122
+ const distributionName = FRAMEWORK_NAME;
123
123
  return getActiveDistribution()
124
124
  .registry.getAll()
125
125
  .flatMap((plugin) =>
package/src/cli/usage.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import { getActiveDistribution } from "../distribution";
2
- import { FRAMEWORK_NAME, frameworkCommandName } from "../framework";
2
+ import { FRAMEWORK_NAME } from "../framework";
3
3
  import { pluginCliCommandLines } from "./plugin-commands";
4
4
 
5
5
  export function usage(): string {
6
- const n = frameworkCommandName();
6
+ const n = FRAMEWORK_NAME;
7
7
  const packageName =
8
8
  getActiveDistribution().config.update?.packageName ?? `@uniqbit/${FRAMEWORK_NAME}`;
9
9
  return [
10
- `${n.charAt(0).toUpperCase() + n.slice(1)} CLI (${packageName})`,
10
+ `Mate CLI (${packageName})`,
11
11
  "",
12
12
  "Commands:",
13
13
  ` ${n} install [--yes]`,
@@ -16,6 +16,9 @@ export function usage(): string {
16
16
  ` ${n} companion list`,
17
17
  ` ${n} companion open`,
18
18
  ` ${n} companion tui`,
19
+ ` ${n} hub init [folder]`,
20
+ ` ${n} hub add [source] [--id ID] [--path PATH]`,
21
+ ` ${n} hub sync [--json] (companions + hub plugins)`,
19
22
  ` ${n} artifact finish <change-name> [--type openspec] [--force] [--no-push] [--json]`,
20
23
  ` ${n} doctor`,
21
24
  ` ${n} report [--days N] [--input FILE|-] [--json]`,
@@ -25,6 +28,7 @@ export function usage(): string {
25
28
  ` ${n} cap openspec <subcommand> [args...]`,
26
29
  ` ${n} cap graphify <subcommand> [args...]`,
27
30
  ` ${n} cap index [--graphify] [--tokensave]`,
31
+ ` ${n} plugin install <package>[@version]`,
28
32
  ...pluginCliCommandLines(),
29
33
  ` ${n} update`,
30
34
  ` ${n} update --check`,
@@ -1,7 +1,7 @@
1
1
  import type { PluginRegistry } from "./tools/setup/registry";
2
2
 
3
3
  export interface DistributionUpdateConfig {
4
- /** npm package whose versions are checked and installed by `mate update`. Defaults to `@uniqbit/<name>`. */
4
+ /** npm package whose versions are checked and installed by `mate update`. Defaults to `@uniqbit/mate`. */
5
5
  packageName?: string;
6
6
  /** npm registry used for update checks and downloads. Defaults to public npm. */
7
7
  registry?: string;
@@ -14,15 +14,13 @@ export interface DistributionUpdateConfig {
14
14
  enforce?: boolean;
15
15
  }
16
16
 
17
- /** Identity and version of the distribution assembled through `createMate`. */
17
+ /** Version and plugin registry of the distribution assembled through `createMate`. */
18
18
  export interface DistributionConfig {
19
- name: string;
20
- legacyNames?: string[];
21
19
  runtime: "bun";
22
20
  version: string;
23
21
  /** Asset directories that override the framework defaults, highest precedence first. */
24
22
  assetRoots?: string[];
25
- /** Optional update source; defaults to `@uniqbit/<name>` on public npm. */
23
+ /** Optional update source; defaults to `@uniqbit/mate` on public npm. */
26
24
  update?: DistributionUpdateConfig;
27
25
  }
28
26