@uniqbit/mate-core 0.15.3 → 0.15.4-canary.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/claude-plugin/.claude-plugin/plugin.json +4 -0
  2. package/claude-plugin/hooks/artifact-finish-nudge.mjs +8 -0
  3. package/claude-plugin/hooks/hooks.json +37 -0
  4. package/claude-plugin/hooks/session-banner.mjs +8 -0
  5. package/claude-plugin/hooks/ts-loader.mjs +28 -0
  6. package/claude-plugin/hooks/validate-artifact-path.mjs +8 -0
  7. package/package.json +4 -2
  8. package/src/cli/commands/artifact/finish/openspec.ts +1 -5
  9. package/src/cli/commands/companion/hub.ts +99 -0
  10. package/src/cli/commands/companion/link.ts +0 -5
  11. package/src/cli/commands/companion/tui.ts +3 -2
  12. package/src/cli/commands/doctor.ts +254 -170
  13. package/src/cli/commands/install.ts +18 -14
  14. package/src/cli/commands/launch/shared.ts +4 -4
  15. package/src/cli/commands/plugin/install.ts +94 -0
  16. package/src/cli/commands/plugin/plugin.ts +17 -0
  17. package/src/cli/commands/setup.ts +4 -6
  18. package/src/cli/commands/update.ts +9 -11
  19. package/src/cli/companion-link-wizard.tsx +22 -78
  20. package/src/cli/main.ts +57 -13
  21. package/src/cli/plugin-commands.ts +4 -4
  22. package/src/cli/repo-list-table.tsx +2 -7
  23. package/src/cli/usage.ts +7 -3
  24. package/src/distribution.ts +3 -5
  25. package/src/framework.ts +4 -36
  26. package/src/hooks/artifact-finish-nudge.ts +212 -0
  27. package/src/hooks/session-banner.ts +25 -0
  28. package/src/hooks/validate-artifact-path.ts +236 -0
  29. package/src/index.ts +4 -0
  30. package/src/lib/context-mode-package.ts +5 -3
  31. package/src/lib/install.ts +31 -43
  32. package/src/lib/orchestrator/adapters/base.ts +7 -8
  33. package/src/lib/orchestrator/adapters/claude.ts +16 -0
  34. package/src/lib/orchestrator/adapters/opencode.ts +10 -60
  35. package/src/lib/orchestrator/companion-hub.ts +380 -0
  36. package/src/lib/orchestrator/companion-store.ts +1 -35
  37. package/src/lib/orchestrator/config-store.ts +94 -7
  38. package/src/lib/orchestrator/editor.ts +4 -20
  39. package/src/lib/orchestrator/framework-context.ts +119 -24
  40. package/src/lib/orchestrator/global-config-store.ts +1 -5
  41. package/src/lib/orchestrator/launcher.ts +3 -9
  42. package/src/lib/orchestrator/migration.ts +0 -23
  43. package/src/lib/orchestrator/opencode-guidance.ts +1 -2
  44. package/src/lib/orchestrator/repo-local-registry.ts +3 -1
  45. package/src/lib/orchestrator/root-context.ts +117 -0
  46. package/src/lib/orchestrator/setup-compatibilities.ts +1 -1
  47. package/src/lib/orchestrator/setup-preflight.ts +5 -18
  48. package/src/lib/orchestrator/types.ts +27 -19
  49. package/src/lib/orchestrator/working-repo-store.ts +9 -3
  50. package/src/lib/package-paths.ts +37 -0
  51. package/src/lib/update-checker.ts +5 -5
  52. package/src/playbooks/companion-guidance.ts +7 -7
  53. package/src/runtime/env.ts +0 -4
  54. package/src/templates/capabilities/openspec-cap/mate-skills/{mate-artifact-finish → agents/mate-artifact-finish}/SKILL.md +8 -8
  55. package/src/templates/capabilities/openspec-cap/mate-skills/{mate-artifact-finish → agents/mate-artifact-finish}/references/openspec.md +3 -3
  56. package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/SKILL.md +58 -0
  57. package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/references/openspec.md +139 -0
  58. package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +6 -5
  59. package/src/templates/root/TEMPLATE_AGENTS.md +2 -0
  60. package/src/templates/root/TEMPLATE_CLAUDE.md +2 -0
  61. package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +3541 -0
  62. package/src/tools/setup/capabilities/context-mode.ts +57 -83
  63. package/src/tools/setup/capabilities/graphify-shared.ts +27 -0
  64. package/src/tools/setup/capabilities/graphify.ts +86 -295
  65. package/src/tools/setup/capabilities/openspec.ts +27 -30
  66. package/src/tools/setup/capabilities/react-doctor.ts +37 -37
  67. package/src/tools/setup/capabilities/rtk.ts +4 -1
  68. package/src/tools/setup/capabilities/tokensave-shared.ts +6 -0
  69. package/src/tools/setup/capabilities/tokensave.ts +50 -71
  70. package/src/tools/setup/context-services.ts +29 -0
  71. package/src/tools/setup/dynamic-plugins/host.ts +2 -1
  72. package/src/tools/setup/dynamic-plugins/hydrate.ts +3 -13
  73. package/src/tools/setup/dynamic-plugins/install.ts +124 -109
  74. package/src/tools/setup/dynamic-plugins/loader.ts +23 -58
  75. package/src/tools/setup/dynamic-plugins/paths.ts +8 -19
  76. package/src/tools/setup/dynamic-plugins/registry-hint.ts +14 -0
  77. package/src/tools/setup/engine.ts +65 -1
  78. package/src/tools/setup/mate.ts +28 -31
  79. package/src/tools/setup/plugin.ts +81 -0
  80. package/src/tools/setup/plugins/gitignore.ts +19 -9
  81. package/src/tools/setup/plugins/guidance.ts +2 -6
  82. package/src/tools/setup/policy.ts +4 -7
  83. package/src/tools/setup/providers/agent-file-sections.ts +78 -0
  84. package/src/tools/setup/providers/claude-format.ts +159 -0
  85. package/src/tools/setup/providers/claude.ts +283 -292
  86. package/src/tools/setup/providers/opencode-format.ts +146 -0
  87. package/src/tools/setup/providers/opencode.ts +171 -66
  88. package/src/tools/setup/providers/skill-tree.ts +38 -0
  89. package/src/tools/setup.ts +12 -7
  90. package/src/tui.ts +5 -0
  91. package/src/templates/capabilities/openspec-cap/claude/hooks/mate-artifact-finish.sh +0 -161
  92. package/src/templates/providers/claude/.claude/hooks/mate-session-banner +0 -28
  93. package/src/templates/providers/claude/.claude/hooks/validate-artifact-path +0 -242
  94. package/src/tools/setup/dynamic-plugins/pin-store.ts +0 -30
@@ -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 (or hub'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.kind !== "hub") || !context.companionPath) {
66
+ process.stderr.write(
67
+ `${FRAMEWORK_NAME}: \`plugin install\` requires a companion or hub context; run it from inside a linked working repository or a hub root.\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 {
@@ -56,7 +56,7 @@ export async function runSetupCommand(argv: string[]): Promise<void> {
56
56
  * Resolves agent/package-manager/capability selections either from CLI flags
57
57
  * (non-interactive) or from the interactive `selectSetupCompatibilities` picker,
58
58
  * seeded from the existing config when one exists. Writes the resulting config
59
- * via `setup.execute` and prints a summary table of profiles, package managers,
59
+ * via `setup.execute` and prints a summary table of allowed agents, package managers,
60
60
  * and capabilities.
61
61
  * @flags
62
62
  * - `--agents <list>` — parsed via `parseAllowedAgents`.
@@ -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] ");
@@ -192,9 +192,7 @@ async function runSetupFlow(
192
192
  const { config } = result;
193
193
  const rows: [string, string][] = [];
194
194
 
195
- for (const [profileName, profile] of Object.entries(config.profiles)) {
196
- rows.push([`profile:${profileName}`, (profile.allowedAgents ?? []).join(", ") || "(none)"]);
197
- }
195
+ rows.push(["allowedAgents", (config.allowedAgents ?? []).join(", ") || "(none)"]);
198
196
 
199
197
  const capabilities = config.capabilities ?? [];
200
198
  rows.push(["packageManagers", (config.packageManagers ?? ["bun"]).join(", ") || "(none)"]);
@@ -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;
@@ -17,7 +17,6 @@ export interface CompanionLinkInputs {
17
17
  gitUrl?: string;
18
18
  localPath?: string;
19
19
  companionPaths?: string[];
20
- profile: string;
21
20
  }
22
21
 
23
22
  interface SourceStep {
@@ -45,15 +44,7 @@ interface TextStep {
45
44
  error?: string;
46
45
  }
47
46
 
48
- interface ProfileStep {
49
- kind: "select";
50
- field: "profile";
51
- label: string;
52
- options: string[];
53
- activeIndex: number;
54
- }
55
-
56
- type WizardStep = SourceStep | CompanionMultiSelectStep | TextStep | ProfileStep;
47
+ type WizardStep = SourceStep | CompanionMultiSelectStep | TextStep;
57
48
 
58
49
  interface WizardAppProps {
59
50
  step: WizardStep;
@@ -99,10 +90,7 @@ function CompanionLinkWizardApp({ step, stepNumber, totalSteps, onMounted }: Wiz
99
90
  );
100
91
  }
101
92
 
102
- const items =
103
- step.field === "source"
104
- ? step.options.map((option) => ({ key: option.key, label: option.label }))
105
- : step.options.map((option) => ({ key: option, label: option }));
93
+ const items = step.options.map((option) => ({ key: option.key, label: option.label }));
106
94
 
107
95
  return (
108
96
  <Box flexDirection="column" width="100%">
@@ -116,7 +104,6 @@ function CompanionLinkWizardApp({ step, stepNumber, totalSteps, onMounted }: Wiz
116
104
  }
117
105
 
118
106
  export interface CompanionLinkWizardOptions {
119
- profiles: string[];
120
107
  existingCompanions?: string[];
121
108
  stdin?: NodeJS.ReadStream;
122
109
  stdout?: NodeJS.WriteStream;
@@ -125,7 +112,6 @@ export interface CompanionLinkWizardOptions {
125
112
 
126
113
  function buildSteps(
127
114
  source: CompanionSource,
128
- profiles: string[],
129
115
  values: {
130
116
  gitUrl: string;
131
117
  localPath: string;
@@ -176,21 +162,7 @@ function buildSteps(
176
162
  selected: values.companionPaths,
177
163
  };
178
164
 
179
- if (profiles.length === 1) {
180
- return [sourceStep, sourceSpecificStep];
181
- }
182
-
183
- return [
184
- sourceStep,
185
- sourceSpecificStep,
186
- {
187
- kind: "select",
188
- field: "profile",
189
- label: "Profile",
190
- options: profiles,
191
- activeIndex: 0,
192
- },
193
- ];
165
+ return [sourceStep, sourceSpecificStep];
194
166
  }
195
167
 
196
168
  export async function selectCompanionLinkInputs(
@@ -199,7 +171,6 @@ export async function selectCompanionLinkInputs(
199
171
  const stdin = options.stdin ?? process.stdin;
200
172
  const stdout = options.stdout ?? process.stdout;
201
173
  const stderr = options.stderr ?? process.stderr;
202
- const profiles = options.profiles;
203
174
  const existingCompanions = options.existingCompanions ?? [];
204
175
 
205
176
  return new Promise<CompanionLinkInputs | null>((resolve, reject) => {
@@ -214,7 +185,7 @@ export async function selectCompanionLinkInputs(
214
185
  let currentStepIndex = 0;
215
186
  const keepAlive = setInterval(() => {}, 1 << 30);
216
187
  let rendered: ReturnType<typeof inkRender> | undefined;
217
- let steps = buildSteps(source, profiles, values, existingCompanions);
188
+ let steps = buildSteps(source, values, existingCompanions);
218
189
 
219
190
  const cleanup = () => {
220
191
  clearInterval(keepAlive);
@@ -232,24 +203,12 @@ export async function selectCompanionLinkInputs(
232
203
  resolve(value);
233
204
  };
234
205
 
235
- const buildResult = (): CompanionLinkInputs => {
236
- const singleProfile = profiles.length === 1 ? profiles[0]! : null;
237
- const profile =
238
- singleProfile ??
239
- ((steps.find((step) => step.kind === "select" && step.field === "profile") as ProfileStep)
240
- .options[
241
- (steps.find((step) => step.kind === "select" && step.field === "profile") as ProfileStep)
242
- .activeIndex
243
- ] as string);
244
-
245
- return {
246
- source,
247
- gitUrl: source === "git" ? values.gitUrl : undefined,
248
- localPath: source === "local" ? values.localPath : undefined,
249
- companionPaths: source === "existing" ? [...values.companionPaths] : undefined,
250
- profile,
251
- };
252
- };
206
+ const buildResult = (): CompanionLinkInputs => ({
207
+ source,
208
+ gitUrl: source === "git" ? values.gitUrl : undefined,
209
+ localPath: source === "local" ? values.localPath : undefined,
210
+ companionPaths: source === "existing" ? [...values.companionPaths] : undefined,
211
+ });
253
212
 
254
213
  const rerender = () => {
255
214
  rendered?.rerender(
@@ -262,16 +221,7 @@ export async function selectCompanionLinkInputs(
262
221
  };
263
222
 
264
223
  const rebuildSteps = () => {
265
- const previousProfileStep = steps.find(
266
- (step): step is ProfileStep => step.kind === "select" && step.field === "profile",
267
- );
268
- steps = buildSteps(source, profiles, values, existingCompanions);
269
- const nextProfileStep = steps.find(
270
- (step): step is ProfileStep => step.kind === "select" && step.field === "profile",
271
- );
272
- if (previousProfileStep && nextProfileStep) {
273
- nextProfileStep.activeIndex = previousProfileStep.activeIndex;
274
- }
224
+ steps = buildSteps(source, values, existingCompanions);
275
225
  };
276
226
 
277
227
  const onData = (chunk: string) => {
@@ -352,7 +302,7 @@ export async function selectCompanionLinkInputs(
352
302
  return;
353
303
  }
354
304
 
355
- if (step.kind === "select" && step.field === "source") {
305
+ if (step.kind === "select") {
356
306
  source = step.options[step.activeIndex]!.key;
357
307
  rebuildSteps();
358
308
  if (currentStepIndex < steps.length - 1) {
@@ -362,23 +312,17 @@ export async function selectCompanionLinkInputs(
362
312
  return;
363
313
  }
364
314
 
365
- if (step.kind === "multiselect") {
366
- if (step.selected.size === 0) {
367
- const option = step.options[step.activeIndex];
368
- if (option) step.selected.add(option);
369
- }
370
- if (currentStepIndex < steps.length - 1) {
371
- currentStepIndex++;
372
- rerender();
373
- } else {
374
- doResolve(buildResult());
375
- rendered?.unmount();
376
- }
377
- return;
315
+ if (step.selected.size === 0) {
316
+ const option = step.options[step.activeIndex];
317
+ if (option) step.selected.add(option);
318
+ }
319
+ if (currentStepIndex < steps.length - 1) {
320
+ currentStepIndex++;
321
+ rerender();
322
+ } else {
323
+ doResolve(buildResult());
324
+ rendered?.unmount();
378
325
  }
379
-
380
- doResolve(buildResult());
381
- rendered?.unmount();
382
326
  };
383
327
 
384
328
  const onMounted = () => {
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,14 +9,17 @@ 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";
19
21
  import { inspectInstallPreflight } from "../lib/install";
22
+ import { resolveRootContext } from "../lib/orchestrator/root-context";
20
23
  import { ensureUnambiguousCompanion } from "./commands/shared/companion-selection";
21
24
  import { hydrateDynamicPlugins } from "../tools/setup/dynamic-plugins/hydrate";
22
25
  import { findPluginCliCommand } from "./plugin-commands";
@@ -25,6 +28,10 @@ import { usage } from "./usage";
25
28
  export interface GateNeeds {
26
29
  /** Block the command while an enforced update is pending. */
27
30
  updateGuard?: boolean;
31
+ /** Block the command when the current directory resolves to a hub root. */
32
+ notHubRoot?: boolean;
33
+ /** Block the command when the current directory resolves to a companion root. */
34
+ notCompanionRoot?: boolean;
28
35
  /** Require an unambiguous companion (selection wizard on ambiguity) before dispatch. */
29
36
  companion?: boolean;
30
37
  /** Require a complete installation (install preflight) before dispatch. */
@@ -35,12 +42,14 @@ export interface MainDeps {
35
42
  ensureUnambiguousCompanion: typeof ensureUnambiguousCompanion;
36
43
  inspectInstallPreflight: typeof inspectInstallPreflight;
37
44
  hydrateDynamicPlugins: typeof hydrateDynamicPlugins;
45
+ resolveRootContext: typeof resolveRootContext;
38
46
  }
39
47
 
40
48
  const mainDeps: MainDeps = {
41
49
  ensureUnambiguousCompanion,
42
50
  inspectInstallPreflight,
43
51
  hydrateDynamicPlugins,
52
+ resolveRootContext,
44
53
  };
45
54
 
46
55
  export async function main(argv = process.argv, deps: MainDeps = mainDeps): Promise<void> {
@@ -51,10 +60,10 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
51
60
  return;
52
61
  }
53
62
 
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
63
+ // Dynamic plugins register before cap-command detection and before help
64
+ // text is printed, so their commands route like compiled-in ones
65
+ // (including MCP servers whose command is their own cap subcommand) and
66
+ // show up in `mate help`. Diagnostics stay on stderr; a missing or
58
67
  // ambiguous companion makes this a no-op.
59
68
  await deps.hydrateDynamicPlugins();
60
69
 
@@ -84,6 +93,24 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
84
93
  await showUpdateBannerIfAvailable(updateStore);
85
94
  }
86
95
 
96
+ if (needs.notHubRoot || needs.notCompanionRoot) {
97
+ const kind = (await deps.resolveRootContext()).kind;
98
+ if (needs.notHubRoot && kind === "hub") {
99
+ console.error(
100
+ `${FRAMEWORK_NAME}: this directory resolves to a companion hub root; companion commands are not available here. Use \`${FRAMEWORK_NAME} hub ...\` to manage the hub.`,
101
+ );
102
+ process.exitCode = 1;
103
+ return false;
104
+ }
105
+ if (needs.notCompanionRoot && kind === "companion") {
106
+ console.error(
107
+ `${FRAMEWORK_NAME}: this directory resolves to a companion root; hub commands are not available here. Run them from a hub root.`,
108
+ );
109
+ process.exitCode = 1;
110
+ return false;
111
+ }
112
+ }
113
+
87
114
  if (needs.companion && !(await deps.ensureUnambiguousCompanion())) {
88
115
  process.exitCode = 1;
89
116
  return false;
@@ -92,10 +119,8 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
92
119
  if (needs.install) {
93
120
  const preflight = await deps.inspectInstallPreflight();
94
121
  if (!preflight.ok) {
95
- console.error(
96
- `${frameworkCommandName()}: ${preflight.reason ?? "installation is incomplete"}`,
97
- );
98
- console.error(`Run \`${frameworkCommandName()} install\` before continuing.`);
122
+ console.error(`${FRAMEWORK_NAME}: ${preflight.reason ?? "installation is incomplete"}`);
123
+ console.error(`Run \`${FRAMEWORK_NAME} install\` before continuing.`);
99
124
  process.exitCode = 1;
100
125
  return false;
101
126
  }
@@ -111,6 +136,14 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
111
136
  if (!(await gate({ companion: true }))) return;
112
137
  await runInstallCommand(argv.slice(3));
113
138
  return;
139
+ case "plugin":
140
+ // Declares into framework.yaml and installs on the spot, so it needs
141
+ // the same gating as `install` — an unambiguous companion (hubs pass
142
+ // trivially), but must stay runnable when installation is otherwise
143
+ // incomplete.
144
+ if (!(await gate({ companion: true }))) return;
145
+ await runPluginCommand(subcommand, rest);
146
+ return;
114
147
  case "artifact":
115
148
  if (!(await gate({ updateGuard: true, companion: true, install: true }))) return;
116
149
  await runArtifactCommand(subcommand, rest);
@@ -118,22 +151,33 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
118
151
  case "companion":
119
152
  switch (subcommand) {
120
153
  // setup/link are recovery paths and must never be gated on the
121
- // state they exist to repair.
154
+ // state they exist to repair — but hubs are never companions.
122
155
  case "setup":
123
156
  case "link":
124
- if (!(await gate({}))) return;
157
+ if (!(await gate({ notHubRoot: true }))) return;
158
+ break;
159
+ // Hub commands establish and operate on a local hub root directly;
160
+ // they must not require a linked working repository or installation —
161
+ // but companions are never hubs.
162
+ case "hub":
163
+ if (!(await gate({ updateGuard: true, notCompanionRoot: true }))) return;
125
164
  break;
126
165
  // open/tui consume a companion context.
127
166
  case "open":
128
167
  case "tui":
129
- if (!(await gate({ updateGuard: true, companion: true }))) return;
168
+ if (!(await gate({ updateGuard: true, notHubRoot: true, companion: true }))) return;
130
169
  break;
131
170
  // list and unknown subcommands (which fail inside the command).
132
171
  default:
133
- if (!(await gate({ updateGuard: true }))) return;
172
+ if (!(await gate({ updateGuard: true, notHubRoot: true }))) return;
134
173
  }
135
174
  await runCompanionCommand(subcommand, rest);
136
175
  return;
176
+ case "hub":
177
+ // Hubs are never companions: hub commands are blocked in companion roots.
178
+ if (!(await gate({ updateGuard: true, notCompanionRoot: true }))) return;
179
+ await runHubCommand(subcommand ? [subcommand, ...rest] : []);
180
+ return;
137
181
  case "claude":
138
182
  if (!(await gate({ updateGuard: true, companion: true, install: true }))) return;
139
183
  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) =>
@@ -39,10 +39,6 @@ function RepoListTable({
39
39
  "ID",
40
40
  repositories.map((repository) => repository.id),
41
41
  );
42
- const profileWidth = columnWidth(
43
- "Profile",
44
- repositories.map((repository) => repository.profile),
45
- );
46
42
  const pathWidth = columnWidth(
47
43
  "Path",
48
44
  repositories.map((repository) => repository.path),
@@ -53,8 +49,7 @@ function RepoListTable({
53
49
  <Text bold>Linked repositories</Text>
54
50
  <Box marginTop={1} flexDirection="column">
55
51
  <Text dimColor>
56
- {pad("Active", markerWidth)} {pad("ID", idWidth)} {pad("Profile", profileWidth)}{" "}
57
- {pad("Path", pathWidth)}
52
+ {pad("Active", markerWidth)} {pad("ID", idWidth)} {pad("Path", pathWidth)}
58
53
  </Text>
59
54
  {repositories.map((repository) => {
60
55
  const isActive = repository.id === activeRepositoryId;
@@ -63,7 +58,7 @@ function RepoListTable({
63
58
  return (
64
59
  <Text key={repository.id} color={isActive ? "green" : undefined}>
65
60
  {pad(marker, markerWidth)} {pad(repository.id, idWidth)}{" "}
66
- {pad(repository.profile, profileWidth)} {pad(repository.path, pathWidth)}
61
+ {pad(repository.path, pathWidth)}
67
62
  </Text>
68
63
  );
69
64
  })}
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`,