@uniqbit/mate-core 0.15.4-canary.7 → 0.15.4-canary.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniqbit/mate-core",
3
- "version": "0.15.4-canary.7",
3
+ "version": "0.15.4-canary.9",
4
4
  "description": "Core framework and plugin APIs for Mate.",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -56,10 +56,14 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
56
56
  process.exitCode = 1;
57
57
  return false;
58
58
  }
59
- // Companion-declared plugins install first and hydrate into the registry
60
- // before the plan is built, so one run goes install → load → plan and their
61
- // own install requirements are part of the plan.
62
- if (context.kind === "companion" && context.companionPath && context.config.plugins?.length) {
59
+ // Root-declared plugins (companion or hub) install first and hydrate into
60
+ // the registry before the plan is built, so one run goes install → load →
61
+ // plan and their own install requirements are part of the plan.
62
+ if (
63
+ (context.kind === "companion" || context.kind === "hub") &&
64
+ context.companionPath &&
65
+ context.config.plugins?.length
66
+ ) {
63
67
  reportPluginInstallResults(
64
68
  await installDeclaredPlugins(context.companionPath, context.config.plugins),
65
69
  );
@@ -37,7 +37,7 @@ export interface PluginInstallCommandDeps {
37
37
 
38
38
  /**
39
39
  * @command mate plugin install <package>[@version]
40
- * @description Declares `<package>` in the companion's `framework.yaml`
40
+ * @description Declares `<package>` in the companion's (or hub's) `framework.yaml`
41
41
  * `plugins:` list (adding it, or updating its version if already declared)
42
42
  * and installs it immediately into the shared workspace, the same way
43
43
  * `mate install` would on its next run.
@@ -62,9 +62,9 @@ export async function runPluginInstallCommand(
62
62
  process.exitCode = 1;
63
63
  return false;
64
64
  }
65
- if (context.kind !== "companion" || !context.companionPath) {
65
+ if ((context.kind !== "companion" && context.kind !== "hub") || !context.companionPath) {
66
66
  process.stderr.write(
67
- `${FRAMEWORK_NAME}: \`plugin install\` requires a companion context; run it from inside a linked working repository.\n`,
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
68
  );
69
69
  process.exitCode = 1;
70
70
  return false;
package/src/cli/main.ts CHANGED
@@ -30,6 +30,8 @@ export interface GateNeeds {
30
30
  updateGuard?: boolean;
31
31
  /** Block the command when the current directory resolves to a hub root. */
32
32
  notHubRoot?: boolean;
33
+ /** Block the command when the current directory resolves to a companion root. */
34
+ notCompanionRoot?: boolean;
33
35
  /** Require an unambiguous companion (selection wizard on ambiguity) before dispatch. */
34
36
  companion?: boolean;
35
37
  /** Require a complete installation (install preflight) before dispatch. */
@@ -91,12 +93,22 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
91
93
  await showUpdateBannerIfAvailable(updateStore);
92
94
  }
93
95
 
94
- if (needs.notHubRoot && (await deps.resolveRootContext()).kind === "hub") {
95
- console.error(
96
- `${FRAMEWORK_NAME}: this directory resolves to a companion hub root; companion commands are not available here. Use \`${FRAMEWORK_NAME} hub ...\` to manage the hub.`,
97
- );
98
- process.exitCode = 1;
99
- return false;
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
+ }
100
112
  }
101
113
 
102
114
  if (needs.companion && !(await deps.ensureUnambiguousCompanion())) {
@@ -126,9 +138,10 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
126
138
  return;
127
139
  case "plugin":
128
140
  // Declares into framework.yaml and installs on the spot, so it needs
129
- // the same gating as `install` — an unambiguous companion, but must
130
- // stay runnable when installation is otherwise incomplete.
131
- if (!(await gate({ updateGuard: true }))) return;
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;
132
145
  await runPluginCommand(subcommand, rest);
133
146
  return;
134
147
  case "artifact":
@@ -144,9 +157,10 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
144
157
  if (!(await gate({ notHubRoot: true }))) return;
145
158
  break;
146
159
  // Hub commands establish and operate on a local hub root directly;
147
- // they must not require a linked working repository or installation.
160
+ // they must not require a linked working repository or installation —
161
+ // but companions are never hubs.
148
162
  case "hub":
149
- if (!(await gate({ updateGuard: true }))) return;
163
+ if (!(await gate({ updateGuard: true, notCompanionRoot: true }))) return;
150
164
  break;
151
165
  // open/tui consume a companion context.
152
166
  case "open":
@@ -160,7 +174,8 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
160
174
  await runCompanionCommand(subcommand, rest);
161
175
  return;
162
176
  case "hub":
163
- if (!(await gate({ updateGuard: true }))) return;
177
+ // Hubs are never companions: hub commands are blocked in companion roots.
178
+ if (!(await gate({ updateGuard: true, notCompanionRoot: true }))) return;
164
179
  await runHubCommand(subcommand ? [subcommand, ...rest] : []);
165
180
  return;
166
181
  case "claude":
@@ -37,7 +37,9 @@ export function isContextModeNodeVersionSupported(version: string): boolean {
37
37
  }
38
38
 
39
39
  export function validateContextModeNodeRuntime(): void {
40
- const result = spawnSync("node", ["--version"], { encoding: "utf8" });
40
+ // env passed explicitly: bun's spawnSync otherwise ignores in-process PATH
41
+ // changes when resolving the executable.
42
+ const result = spawnSync("node", ["--version"], { encoding: "utf8", env: process.env });
41
43
  const version = result.stdout?.trim() ?? "";
42
44
  if (result.status !== 0 || !isContextModeNodeVersionSupported(version)) {
43
45
  throw new Error(
@@ -83,7 +85,7 @@ export async function installContextModePackage(companionPath: string): Promise<
83
85
  const result = spawnSync(
84
86
  "npm",
85
87
  ["install", "--no-audit", "--no-fund", "--silent", "--registry", PUBLIC_NPM_REGISTRY],
86
- { cwd: installDir, encoding: "utf8" },
88
+ { cwd: installDir, encoding: "utf8", env: process.env },
87
89
  );
88
90
  if (result.error || result.status !== 0) {
89
91
  throw new Error(
@@ -9,61 +9,15 @@ import {
9
9
  getOpenCodePluginPackageReference,
10
10
  isMateOpenCodePluginReference,
11
11
  } from "../../opencode-plugin-package";
12
+ import {
13
+ getOpenCodePluginReferences,
14
+ mergeOpenCodeConfigContent,
15
+ readOpenCodeConfig,
16
+ } from "../../../tools/setup/providers/opencode-format";
12
17
  import { buildOpenCodeGuidance } from "../opencode-guidance";
13
18
  import { LaunchPreflightError } from "../types";
14
19
  import { type AdapterContext, LaunchAdapter } from "./base";
15
20
 
16
- function isRecord(value: unknown): value is Record<string, unknown> {
17
- return typeof value === "object" && value !== null && !Array.isArray(value);
18
- }
19
-
20
- function mergeConfig(target: Record<string, unknown>, source: Record<string, unknown>): void {
21
- for (const [key, sourceValue] of Object.entries(source)) {
22
- const targetValue = target[key];
23
- if (isRecord(targetValue) && isRecord(sourceValue)) {
24
- mergeConfig(targetValue, sourceValue);
25
- continue;
26
- }
27
-
28
- target[key] = sourceValue;
29
- }
30
- }
31
-
32
- function mergeOpenCodeConfigContent(
33
- overlay: Record<string, unknown>,
34
- env: NodeJS.ProcessEnv = process.env,
35
- options: { appendSkillPaths?: string[] } = {},
36
- ): string {
37
- let config: Record<string, unknown> = {};
38
- const existing = env.OPENCODE_CONFIG_CONTENT;
39
-
40
- if (existing) {
41
- try {
42
- const parsed = JSON.parse(existing) as unknown;
43
- if (isRecord(parsed)) {
44
- config = parsed;
45
- }
46
- } catch {
47
- // Ignore invalid inherited config content rather than breaking launch.
48
- }
49
- }
50
-
51
- mergeConfig(config, overlay);
52
-
53
- if (options.appendSkillPaths && options.appendSkillPaths.length > 0) {
54
- const skills = isRecord(config.skills) ? config.skills : {};
55
- const paths = Array.isArray(skills.paths) ? skills.paths : [];
56
- const existingPaths = new Set(paths);
57
- skills.paths = [
58
- ...paths,
59
- ...options.appendSkillPaths.filter((skillPath) => !existingPaths.has(skillPath)),
60
- ];
61
- config.skills = skills;
62
- }
63
-
64
- return JSON.stringify(config);
65
- }
66
-
67
21
  export class OpenCodeAdapter extends LaunchAdapter {
68
22
  readonly toolName = "opencode";
69
23
  readonly interactive = true;
@@ -190,18 +144,14 @@ export class OpenCodeAdapter extends LaunchAdapter {
190
144
  ): Promise<string[]> {
191
145
  const configPath = path.join(context.companionPath, configFile);
192
146
 
193
- let config: unknown;
194
- try {
195
- config = JSON.parse(await fs.readFile(configPath, "utf8"));
196
- } catch {
197
- return [`Unreadable OpenCode configuration: ${configPath}`];
198
- }
199
- if (!isRecord(config)) {
147
+ const { present, config } = await readOpenCodeConfig(configPath);
148
+ if (!present) {
200
149
  return [`Unreadable OpenCode configuration: ${configPath}`];
201
150
  }
202
151
 
203
- const plugins = Array.isArray(config.plugin) ? (config.plugin as unknown[]) : [];
204
- const mateReferences = plugins.filter(isMateOpenCodePluginReference) as string[];
152
+ const mateReferences = getOpenCodePluginReferences(config).filter(
153
+ isMateOpenCodePluginReference,
154
+ ) as string[];
205
155
 
206
156
  if (mateReferences.includes(expectedPluginReference)) {
207
157
  return [];