@uniqbit/mate-core 0.17.0-canary.8 → 0.17.1

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 (103) hide show
  1. package/package.json +11 -5
  2. package/src/cli/commands/artifact/finish/index.ts +0 -9
  3. package/src/cli/commands/artifact/finish/openspec.ts +2 -2
  4. package/src/cli/commands/artifact/finish/registry.ts +1 -1
  5. package/src/cli/commands/artifact/pending/discovery.ts +1 -1
  6. package/src/cli/commands/artifact/pending/index.ts +0 -16
  7. package/src/cli/commands/cap/graphify.ts +1 -1
  8. package/src/cli/commands/cap/index-cmd.ts +1 -1
  9. package/src/cli/commands/cap/tokensave.ts +4 -3
  10. package/src/cli/commands/companion/companion.ts +4 -0
  11. package/src/cli/commands/companion/hub.ts +2 -1
  12. package/src/cli/commands/companion/list.ts +2 -1
  13. package/src/cli/commands/companion/prepare.ts +2 -2
  14. package/src/cli/commands/companion/update.ts +334 -0
  15. package/src/cli/commands/doctor.ts +2 -1
  16. package/src/cli/commands/install.ts +59 -5
  17. package/src/cli/commands/launch/shared.ts +2 -1
  18. package/src/cli/commands/plugin/plugin.ts +4 -0
  19. package/src/cli/commands/plugin/verify.ts +25 -0
  20. package/src/cli/commands/report/highlight.ts +1 -4
  21. package/src/cli/commands/report/renderer.ts +0 -2
  22. package/src/cli/commands/studio/access.ts +201 -0
  23. package/src/cli/commands/studio/index.ts +207 -26
  24. package/src/cli/commands/studio/openspec-cli.ts +1 -1
  25. package/src/cli/commands/studio/selection.ts +127 -11
  26. package/src/cli/commands/studio/server.ts +355 -64
  27. package/src/cli/commands/studio/terminal-assets.ts +50 -0
  28. package/src/cli/commands/studio/terminal-launch.ts +71 -0
  29. package/src/cli/commands/studio/terminal.ts +593 -0
  30. package/src/cli/commands/studio/vault.ts +320 -75
  31. package/src/cli/commands/studio/views/client.ts +90 -9
  32. package/src/cli/commands/studio/views/companion-picker.tsx +9 -5
  33. package/src/cli/commands/studio/views/companion-selector.tsx +11 -7
  34. package/src/cli/commands/studio/views/document.tsx +411 -107
  35. package/src/cli/commands/studio/views/model.ts +14 -1
  36. package/src/cli/commands/studio/views/selection-fields.tsx +14 -0
  37. package/src/cli/commands/studio/views/styles.ts +106 -13
  38. package/src/cli/commands/studio/views/terminal.tsx +426 -0
  39. package/src/cli/commands/studio/views/vault/state.ts +42 -0
  40. package/src/cli/commands/studio/views/workflow/steps.ts +10 -3
  41. package/src/cli/commands/update.ts +11 -8
  42. package/src/cli/companion-link-wizard.tsx +1 -1
  43. package/src/cli/companion-selector.tsx +1 -1
  44. package/src/cli/launch-selector.tsx +1 -1
  45. package/src/cli/main.ts +2 -1
  46. package/src/cli/usage.ts +7 -3
  47. package/src/cli/write-json-stdout.ts +2 -2
  48. package/src/lib/components/spinner.tsx +2 -2
  49. package/src/lib/components/startup-progress.tsx +1 -1
  50. package/src/lib/context-mode-package.ts +1 -1
  51. package/src/lib/opencode-plugin-package.ts +3 -0
  52. package/src/lib/orchestrator/adapters/opencode.ts +53 -13
  53. package/src/lib/orchestrator/companion-hub.ts +3 -3
  54. package/src/lib/orchestrator/companion-registry-store.ts +1 -1
  55. package/src/lib/orchestrator/config-store.ts +2 -2
  56. package/src/lib/orchestrator/editor.ts +4 -12
  57. package/src/lib/orchestrator/engine-guard.ts +9 -1
  58. package/src/lib/orchestrator/projection-entries.ts +1 -1
  59. package/src/lib/orchestrator/projection-record.ts +1 -1
  60. package/src/lib/orchestrator/projection-types.ts +0 -4
  61. package/src/lib/orchestrator/repo-local-registry.ts +0 -1
  62. package/src/lib/orchestrator/root-context.ts +1 -1
  63. package/src/lib/orchestrator/setup-compatibilities.ts +1 -1
  64. package/src/lib/orchestrator/setup-preflight.ts +1 -1
  65. package/src/lib/orchestrator/types.ts +1 -1
  66. package/src/lib/orchestrator/working-repo-projection.ts +1 -1
  67. package/src/lib/orchestrator/workspace-inventory.ts +2 -2
  68. package/src/lib/package-paths.ts +1 -11
  69. package/src/lib/package-root.ts +43 -0
  70. package/src/lib/prebuilt-workspace.ts +1 -1
  71. package/src/lib/public-npm.ts +1 -1
  72. package/src/opencode/companion-hooks.ts +131 -153
  73. package/src/opencode/index.ts +12 -6
  74. package/src/opencode/tui.tsx +46 -49
  75. package/src/playbooks/companion-guidance.ts +1 -28
  76. package/src/runtime/companion-sync.ts +0 -3
  77. package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +2 -1
  78. package/src/templates/providers/opencode/opencode.json +6 -4
  79. package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +500 -408
  80. package/src/tools/setup/capabilities/context-mode.ts +45 -25
  81. package/src/tools/setup/capabilities/graphify-shared.ts +3 -3
  82. package/src/tools/setup/capabilities/graphify.ts +2 -2
  83. package/src/tools/setup/capabilities/openspec.ts +2 -7
  84. package/src/tools/setup/capabilities/react-doctor.ts +1 -1
  85. package/src/tools/setup/capabilities/rtk.ts +2 -2
  86. package/src/tools/setup/capabilities/tokensave.ts +98 -12
  87. package/src/tools/setup/context-services.ts +3 -6
  88. package/src/tools/setup/dynamic-plugins/declarations.ts +47 -0
  89. package/src/tools/setup/dynamic-plugins/frozen.ts +215 -0
  90. package/src/tools/setup/dynamic-plugins/hydrate.ts +1 -40
  91. package/src/tools/setup/dynamic-plugins/loader.ts +16 -0
  92. package/src/tools/setup/dynamic-plugins/policy.ts +55 -0
  93. package/src/tools/setup/dynamic-plugins/staging.ts +53 -0
  94. package/src/tools/setup/dynamic-plugins/verify.ts +70 -0
  95. package/src/tools/setup/mate.ts +1 -1
  96. package/src/tools/setup/providers/claude-format.ts +1 -1
  97. package/src/tools/setup/providers/claude.ts +1 -1
  98. package/src/tools/setup/providers/opencode-format.ts +220 -25
  99. package/src/tools/setup/providers/opencode.ts +63 -21
  100. package/src/tools/setup/runtime-documents.ts +9 -11
  101. package/src/tools/setup.ts +0 -4
  102. package/src/cli/commands/studio/views/pairings.tsx +0 -38
  103. package/src/templates/providers/opencode/tui.json +0 -3
@@ -1,4 +1,3 @@
1
- import path from "node:path";
2
1
  import { spawnSync } from "node:child_process";
3
2
 
4
3
  import { FRAMEWORK_NAME } from "../../framework";
@@ -14,6 +13,7 @@ import {
14
13
  isNewer,
15
14
  } from "../../lib/update-checker";
16
15
  import { confirmPrompt } from "../../lib/components/confirm-prompt";
16
+ import { resolvePackageRoot } from "../../lib/package-root";
17
17
 
18
18
  type InstallResult = ReturnType<typeof installPublicPackageSync>;
19
19
 
@@ -38,11 +38,11 @@ export const updateCommandDeps = {
38
38
  getCurrentVersion,
39
39
  isNewer,
40
40
  confirmPrompt,
41
- isNpmManagedInstall: () =>
42
- isPackageInstalledAtNpmGlobalRoot(
43
- path.resolve(import.meta.dirname, "../../.."),
44
- getUpdateConfig().packageName,
45
- ),
41
+ isNpmManagedInstall: () => {
42
+ const packageName = getUpdateConfig().packageName;
43
+ const packageRoot = resolvePackageRoot(process.argv[1], packageName);
44
+ return packageRoot !== undefined && isPackageInstalledAtNpmGlobalRoot(packageRoot, packageName);
45
+ },
46
46
  installLatest: (latest: string): InstallResult => {
47
47
  const { packageName, registry } = getUpdateConfig();
48
48
  return installPublicPackageSync(`${packageName}@${latest}`, registry);
@@ -55,14 +55,17 @@ export const updateCommandDeps = {
55
55
  latestVersion: latest,
56
56
  });
57
57
  },
58
+ /** Re-runs install from dev (`cli.ts`) or prod (`cli.mjs`/bootstrap) entrypoints so config reconciles during `mate update`. */
58
59
  runPostInstall: (skipConfirm: boolean): InstallResult => {
59
60
  const entrypoint = process.argv[1];
60
- if (!entrypoint?.endsWith("/cli.ts") && !entrypoint?.endsWith("\\cli.ts")) {
61
+ if (!entrypoint || !/(?:^|[\\/])(?:cli\.ts|cli\.mjs|cli-bootstrap\.cjs)$/.test(entrypoint)) {
61
62
  return { status: 0, error: undefined } as InstallResult;
62
63
  }
63
64
  const args = [entrypoint, "install", ...(skipConfirm ? ["--yes"] : [])];
64
- return spawnSync(process.execPath, args, { stdio: "inherit" }) as InstallResult;
65
+ return updateCommandDeps.spawnPostInstall(process.execPath, args);
65
66
  },
67
+ spawnPostInstall: (executable: string, args: string[]): InstallResult =>
68
+ spawnSync(executable, args, { stdio: "inherit" }) as InstallResult,
66
69
  warmOpenCodePluginCache: (latest: string) =>
67
70
  warmOpenCodePluginCache(latest, process.env, getUpdateConfig().registry),
68
71
  };
@@ -8,7 +8,7 @@ import { WizardHeader } from "../lib/components/wizard-header";
8
8
  import type { CompanionSource } from "../lib/orchestrator/types";
9
9
  import { parseRepoLinkInput } from "./wizard-key-input";
10
10
 
11
- export const companionLinkWizardDeps = {
11
+ const companionLinkWizardDeps = {
12
12
  render: inkRender,
13
13
  };
14
14
 
@@ -11,7 +11,7 @@ export interface CompanionSelectorOptions {
11
11
  stderr?: NodeJS.WriteStream;
12
12
  }
13
13
 
14
- export const companionSelectorDeps = {
14
+ const companionSelectorDeps = {
15
15
  render: inkRender,
16
16
  };
17
17
 
@@ -45,7 +45,7 @@ export interface LaunchWizardOptions {
45
45
  resolvePreview(target: LaunchTarget): Promise<LaunchPreviewData>;
46
46
  }
47
47
 
48
- export const launchSelectorDeps = {
48
+ const launchSelectorDeps = {
49
49
  render: inkRender,
50
50
  };
51
51
 
package/src/cli/main.ts CHANGED
@@ -206,10 +206,11 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
206
206
  case "hub":
207
207
  if (!(await gate({ updateGuard: true, notCompanionRoot: true }))) return;
208
208
  break;
209
- // open/tui/sync consume a companion context.
209
+ // open/tui/sync/update consume a companion context.
210
210
  case "open":
211
211
  case "tui":
212
212
  case "sync":
213
+ case "update":
213
214
  if (!(await gate({ updateGuard: true, notHubRoot: true, companion: true }))) return;
214
215
  break;
215
216
  // list and unknown subcommands (which fail inside the command).
package/src/cli/usage.ts CHANGED
@@ -10,13 +10,14 @@ export function usage(): string {
10
10
  `Mate CLI (${packageName})`,
11
11
  "",
12
12
  "Commands:",
13
- ` ${n} install [--yes]`,
13
+ ` ${n} install [--yes] [--frozen-plugins]`,
14
14
  ` ${n} companion link`,
15
15
  ` ${n} companion setup`,
16
16
  ` ${n} companion register [path]`,
17
17
  ` ${n} companion prepare --from <bundle> [path]`,
18
18
  ` ${n} companion list`,
19
19
  ` ${n} companion sync`,
20
+ ` ${n} companion update [--yes]`,
20
21
  ` ${n} companion open`,
21
22
  ` ${n} companion tui`,
22
23
  ` ${n} working cleanup`,
@@ -30,8 +31,10 @@ export function usage(): string {
30
31
  ` ${n} artifact pending [--json]`,
31
32
  ` ${n} doctor`,
32
33
  ` ${n} report [--days N] [--input FILE|-] [--json]`,
33
- ` ${n} studio [--writable]`,
34
- ` ${n} studio serve --port <port> [--host <host>] [--writable]`,
34
+ ` ${n} studio [--writable] [--terminal] [--detach-timeout <minutes>]`,
35
+ ` ${n} studio serve --port <port> [--host <host>] [--writable] [--terminal]`,
36
+ " [--detach-timeout <minutes>] [--token <value>] [--allowed-host <host>]",
37
+ " [--public-origin <origin>] [--companion <path>] [--no-git]",
35
38
  ` ${n} config`,
36
39
  ` ${n} claude [args...] (after --: --no-git, --companion, --yes no-op)`,
37
40
  ` ${n} opencode [args...] (after --: --no-git, --companion, --yes no-op)`,
@@ -39,6 +42,7 @@ export function usage(): string {
39
42
  ` ${n} cap graphify <subcommand> [args...]`,
40
43
  ` ${n} cap index [--graphify] [--tokensave]`,
41
44
  ` ${n} plugin install <package>[@version]`,
45
+ ` ${n} plugin verify`,
42
46
  ...pluginCliCommandLines(),
43
47
  ` ${n} update`,
44
48
  ` ${n} update --check`,
@@ -5,9 +5,9 @@
5
5
  * before the write flushes (observed at exactly the 64KB pipe boundary) —
6
6
  * awaiting the write callback is what actually guarantees delivery.
7
7
  */
8
- export function writeJsonStdout(data: unknown): Promise<void> {
8
+ export function writeJsonStdout(data: unknown, options: { space?: number } = {}): Promise<void> {
9
9
  return new Promise((resolve, reject) => {
10
- process.stdout.write(`${JSON.stringify(data, null, 2)}\n`, (error) =>
10
+ process.stdout.write(`${JSON.stringify(data, null, options.space ?? 2)}\n`, (error) =>
11
11
  error ? reject(error) : resolve(),
12
12
  );
13
13
  });
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useState } from "react";
2
2
 
3
- export const SPINNER_FRAMES = ["·", "✦", "✧", "✦"];
4
- export const SPINNER_INTERVAL_MS = 120;
3
+ const SPINNER_FRAMES = ["·", "✦", "✧", "✦"];
4
+ const SPINNER_INTERVAL_MS = 120;
5
5
 
6
6
  export function useSpinnerFrame(active: boolean, intervalMs: number = SPINNER_INTERVAL_MS): string {
7
7
  const [frame, setFrame] = useState(0);
@@ -38,7 +38,7 @@ export interface StartupProgressAppProps {
38
38
  steps: StartupStep[];
39
39
  }
40
40
 
41
- export function StartupProgressApp({ title, steps }: StartupProgressAppProps) {
41
+ function StartupProgressApp({ title, steps }: StartupProgressAppProps) {
42
42
  return (
43
43
  <Box flexDirection="column">
44
44
  <Text bold color={MIDNIGHT_PURPLE_BRIGHT}>
@@ -39,7 +39,7 @@ export function isContextModeNodeVersionSupported(version: string): boolean {
39
39
  return major > 22 || (major === 22 && minor >= 5);
40
40
  }
41
41
 
42
- export function validateContextModeNodeRuntime(): void {
42
+ function validateContextModeNodeRuntime(): void {
43
43
  // env passed explicitly: bun's spawnSync otherwise ignores in-process PATH
44
44
  // changes when resolving the executable.
45
45
  const result = spawnSync("node", ["--version"], { encoding: "utf8", env: process.env });
@@ -21,6 +21,9 @@ export function getOpenCodePluginPackageReference(version: string = getCurrentVe
21
21
 
22
22
  /** Matches the published reference and a reference bound to an installed copy alike. */
23
23
  export function isMateOpenCodePluginReference(value: unknown): boolean {
24
+ if (typeof value === "object" && value !== null && !Array.isArray(value) && "package" in value) {
25
+ return isMateOpenCodePluginReference(value.package);
26
+ }
24
27
  if (typeof value !== "string") return false;
25
28
  if (
26
29
  value === OPENCODE_PLUGIN_PACKAGE_NAME ||
@@ -1,4 +1,6 @@
1
1
  // oxlint-disable no-await-in-loop
2
+ import { spawnSync } from "node:child_process";
3
+
2
4
  const fs = await import("node:fs/promises");
3
5
  const path = await import("node:path");
4
6
 
@@ -21,18 +23,61 @@ import { buildOpenCodeGuidance } from "../opencode-guidance";
21
23
  import { LaunchPreflightError } from "../types";
22
24
  import { type AdapterContext, LaunchAdapter } from "./base";
23
25
 
26
+ /** The Mate plugin is a V2 plugin; an older host rejects it at load time. */
27
+ const MIN_OPENCODE_MAJOR = 2;
28
+
29
+ /**
30
+ * OpenCode 2.x attaches to a shared background service by default; plugins and
31
+ * config load in that server, so the per-launch Mate env never reaches it.
32
+ */
33
+ const STANDALONE_FLAG = "--standalone";
34
+
35
+ /** Raw `opencode --version` output, or `undefined` when the binary cannot run. */
36
+ export type OpenCodeVersionProbe = () => string | undefined;
37
+
38
+ const probeOpenCodeVersion: OpenCodeVersionProbe = () => {
39
+ const result = spawnSync("opencode", ["--version"], { encoding: "utf8", timeout: 10_000 });
40
+ return result.status === 0 ? result.stdout.trim() : undefined;
41
+ };
42
+
43
+ function parseOpenCodeMajor(output: string | undefined): number | undefined {
44
+ const match = output?.match(/(\d+)\.\d+\.\d+/);
45
+ return match ? Number(match[1]) : undefined;
46
+ }
47
+
24
48
  export class OpenCodeAdapter extends LaunchAdapter {
25
49
  readonly toolName = "opencode";
26
50
  readonly interactive = true;
51
+ private readonly requiredRuntimeAssets = [path.join(".opencode", "opencode.json")] as const;
52
+
53
+ constructor(private readonly probeVersion: OpenCodeVersionProbe = probeOpenCodeVersion) {
54
+ super();
55
+ }
27
56
 
28
- private readonly requiredRuntimeAssets = [
29
- path.join(".opencode", "opencode.json"),
30
- path.join(".opencode", "tui.json"),
31
- ] as const;
57
+ private validateOpenCodeVersion(): void {
58
+ const output = this.probeVersion();
59
+ const major = parseOpenCodeMajor(output);
60
+ if (major !== undefined && major >= MIN_OPENCODE_MAJOR) return;
61
+ throw new LaunchPreflightError(
62
+ [
63
+ output === undefined
64
+ ? "OpenCode is not installed or `opencode --version` failed."
65
+ : `OpenCode ${output} is not supported.`,
66
+ `Mate requires OpenCode ${MIN_OPENCODE_MAJOR}.x or newer. Upgrade OpenCode and retry.`,
67
+ ].join("\n"),
68
+ );
69
+ }
32
70
 
33
71
  buildArgs(context: AdapterContext, args: string[]): string[] {
34
72
  const codeDir = context.launchWorkingDirectory;
35
- return args.length === 0 || args[0]?.startsWith("-") ? [codeDir, ...args] : args;
73
+ const isTui = args.length === 0 || args[0]?.startsWith("-");
74
+ const [command, ...rest] = isTui ? [codeDir, ...args] : args;
75
+ if (!command) return args;
76
+ const hostsServer = isTui || command === "run";
77
+ const choosesServer = rest.some(
78
+ (arg) => arg === "--standalone" || arg === "--server" || arg.startsWith("--server="),
79
+ );
80
+ return hostsServer && !choosesServer ? [command, STANDALONE_FLAG, ...rest] : [command, ...rest];
36
81
  }
37
82
 
38
83
  extendEnvironment(context: AdapterContext): NodeJS.ProcessEnv {
@@ -40,9 +85,7 @@ export class OpenCodeAdapter extends LaunchAdapter {
40
85
  OPENCODE_CONFIG_DIR: path.join(context.companionPath, ".opencode"),
41
86
  OPENCODE_CONFIG_CONTENT: mergeOpenCodeConfigContent(
42
87
  {
43
- permission: {
44
- external_directory: renderCompanionExternalDirectoryPermissions(context.companionPath),
45
- },
88
+ permissions: renderCompanionExternalDirectoryPermissions(context.companionPath),
46
89
  references: {
47
90
  mate: context.companionPath,
48
91
  },
@@ -63,6 +106,8 @@ export class OpenCodeAdapter extends LaunchAdapter {
63
106
  }
64
107
 
65
108
  async validateLaunch(context: AdapterContext): Promise<void> {
109
+ this.validateOpenCodeVersion();
110
+
66
111
  const missingAssets = await Promise.all(
67
112
  this.requiredRuntimeAssets.map(async (asset) => {
68
113
  const assetPath = path.join(context.companionPath, asset);
@@ -92,11 +137,6 @@ export class OpenCodeAdapter extends LaunchAdapter {
92
137
  path.join(".opencode", "opencode.json"),
93
138
  expectedPluginReference,
94
139
  )),
95
- ...(await this.validatePluginReference(
96
- context,
97
- path.join(".opencode", "tui.json"),
98
- expectedPluginReference,
99
- )),
100
140
  ...this.validateGuidance(context),
101
141
  ];
102
142
  if (errors.length > 0) {
@@ -63,7 +63,7 @@ function isInsideDir(parent: string, candidate: string): boolean {
63
63
  return relative !== "" && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
64
64
  }
65
65
 
66
- export function normalizeHubMemberId(value: string): string {
66
+ function normalizeHubMemberId(value: string): string {
67
67
  const normalized = value
68
68
  .replace(/\.git$/i, "")
69
69
  .replace(/[^a-zA-Z0-9._-]+/g, "-")
@@ -72,7 +72,7 @@ export function normalizeHubMemberId(value: string): string {
72
72
  return normalized;
73
73
  }
74
74
 
75
- export function validateHubMemberPath(hubPath: string, memberPath: string): string {
75
+ function validateHubMemberPath(hubPath: string, memberPath: string): string {
76
76
  if (!memberPath || path.isAbsolute(memberPath)) {
77
77
  throw new Error("Hub member path must be relative to the hub root.");
78
78
  }
@@ -266,7 +266,7 @@ function syncTarget(memberPath: string, git: GitCommand): string | null {
266
266
  return upstream.stdout;
267
267
  }
268
268
 
269
- export async function syncHubMember(
269
+ async function syncHubMember(
270
270
  hubPath: string,
271
271
  member: HubMember,
272
272
  git: GitCommand = defaultGitCommand,
@@ -5,7 +5,7 @@ import { migrateRegistryData } from "./migration";
5
5
  import { YamlFileStore } from "./yaml-file-store";
6
6
  import { ConfigError, type CompanionRegistryConfig } from "./types";
7
7
 
8
- export function getDefaultCompanionRegistryPath(): string {
8
+ function getDefaultCompanionRegistryPath(): string {
9
9
  return `.${FRAMEWORK_NAME}/config/registry.yaml`;
10
10
  }
11
11
 
@@ -30,7 +30,7 @@ interface LegacyProfilesShape {
30
30
  * list (`profiles.default.allowedAgents` wins; other profiles are dropped).
31
31
  * The new shape persists on the next save.
32
32
  */
33
- export function migrateProfilesToAllowedAgents(config: FrameworkConfig): FrameworkConfig {
33
+ function migrateProfilesToAllowedAgents(config: FrameworkConfig): FrameworkConfig {
34
34
  const legacy = config as FrameworkConfig & LegacyProfilesShape;
35
35
  if (!legacy.profiles) return config;
36
36
  const { profiles, ...rest } = legacy;
@@ -130,7 +130,7 @@ export const PLUGIN_DECLARATION_POLICIES = ["default", "optional"] as const;
130
130
  * Validates the `plugins` list of a loaded config. Declared plugins may not
131
131
  * claim `required` — required-ness is a distribution prerogative.
132
132
  */
133
- export function validatePluginDeclarations(config: FrameworkConfig): void {
133
+ function validatePluginDeclarations(config: FrameworkConfig): void {
134
134
  for (const declaration of config.plugins ?? []) {
135
135
  const policy = declaration.policy as string | undefined;
136
136
  if (policy !== undefined && !PLUGIN_DECLARATION_POLICIES.includes(policy as never)) {
@@ -13,7 +13,7 @@ const CURSOR_FALLBACK_PATHS = [
13
13
  "/opt/homebrew/bin/cursor",
14
14
  ];
15
15
 
16
- export function isCursorSession(env: NodeJS.ProcessEnv = process.env): boolean {
16
+ function isCursorSession(env: NodeJS.ProcessEnv = process.env): boolean {
17
17
  const termProgram = env.TERM_PROGRAM?.toLowerCase() ?? "";
18
18
  const bundleId = env.__CFBundleIdentifier ?? "";
19
19
  return (
@@ -21,7 +21,7 @@ export function isCursorSession(env: NodeJS.ProcessEnv = process.env): boolean {
21
21
  );
22
22
  }
23
23
 
24
- export function isVsCodeSession(env: NodeJS.ProcessEnv = process.env): boolean {
24
+ function isVsCodeSession(env: NodeJS.ProcessEnv = process.env): boolean {
25
25
  const termProgram = env.TERM_PROGRAM?.toLowerCase() ?? "";
26
26
  const bundleId = env.__CFBundleIdentifier ?? "";
27
27
  return (
@@ -69,18 +69,10 @@ function resolveEditorCli(cli: string): { cli: string; binary: string } | null {
69
69
  return null;
70
70
  }
71
71
 
72
- export function writeMissingEditorCliGuidance(
73
- cli: string,
74
- write: (chunk: string) => boolean = process.stderr.write.bind(process.stderr),
75
- ): void {
76
- write(`mate: warning: ${cli} CLI not found on PATH.\n`);
77
- write(`mate: run "Install '${cli}' command in PATH" from the Command Palette to enable this.\n`);
78
- }
79
-
80
72
  /** The Projection Root owns the path; this is the editor's name for it. */
81
73
  export const editorWorkspacePath = workspaceDocumentPath;
82
74
 
83
- export interface WorkspaceDocument {
75
+ interface WorkspaceDocument {
84
76
  workspacePath: string;
85
77
  folders: [string, string];
86
78
  }
@@ -91,7 +83,7 @@ export interface WorkspaceDocument {
91
83
  * order. Called by editor-launching `injectEditorFolder`, which backs
92
84
  * `mate companion open`.
93
85
  */
94
- export async function writeWorkspaceDocument(
86
+ async function writeWorkspaceDocument(
95
87
  companionPath: string,
96
88
  repoPath: string,
97
89
  ): Promise<WorkspaceDocument> {
@@ -27,7 +27,15 @@ export function checkEngineRequirement(
27
27
  };
28
28
  }
29
29
 
30
- const satisfied = semver.satisfies(currentVersion, range, { includePrerelease: true });
30
+ const current = semver.parse(currentVersion);
31
+ const currentReleaseVersion =
32
+ current && current.prerelease.length > 0
33
+ ? `${current.major}.${current.minor}.${current.patch}`
34
+ : null;
35
+ const satisfied =
36
+ semver.satisfies(currentVersion, range, { includePrerelease: true }) ||
37
+ (currentReleaseVersion !== null &&
38
+ semver.satisfies(currentReleaseVersion, range, { includePrerelease: true }));
31
39
  if (!satisfied) {
32
40
  return {
33
41
  ok: false,
@@ -52,7 +52,7 @@ import {
52
52
  } from "./repo-local-store";
53
53
  import type { ProjectionEntry, ProjectionInput } from "./projection-types";
54
54
 
55
- export const WORKSPACE_DOCUMENT_FILE = "workspace.code-workspace";
55
+ const WORKSPACE_DOCUMENT_FILE = "workspace.code-workspace";
56
56
 
57
57
  /** The shared editor workspace document, inside the Projection Root. */
58
58
  export function workspaceDocumentPath(repoPath: string): string {
@@ -13,7 +13,7 @@ import {
13
13
  import { getReactDoctorBinPath, getWrapperBinPath } from "../package-paths";
14
14
  import type { LinkedRepository } from "./types";
15
15
 
16
- export const COMPANION_GRAPHIFY_REPOSITORY_ID = "__companion__";
16
+ const COMPANION_GRAPHIFY_REPOSITORY_ID = "__companion__";
17
17
 
18
18
  /**
19
19
  * The one derivation of the projected paths. Both the Projection Root and a
@@ -159,10 +159,6 @@ export interface ProjectionDescription {
159
159
  projection: ProjectionFile | null;
160
160
  }
161
161
 
162
- export function anyChanged(outcomes: ProjectionEntryOutcome[]): boolean {
163
- return outcomes.some((outcome) => outcome.state === "written" || outcome.state === "removed");
164
- }
165
-
166
162
  /** A degradable entry's failure is reported in the outcomes and nowhere else. */
167
163
  export function firstFailure(outcomes: ProjectionEntryOutcome[]): ProjectionEntryOutcome | null {
168
164
  return outcomes.find((outcome) => outcome.state === "failed" && !outcome.degradable) ?? null;
@@ -24,7 +24,6 @@ export { pathIsDirectory } from "../fs-utils";
24
24
  export {
25
25
  RepoLocalRegistryStore,
26
26
  upsertRepoLocalCompanionPointer,
27
- upsertRepoLocalLinkedRepository,
28
27
  writeRepoLocalFrameworkConfig,
29
28
  type RepoLocalCompanionPointer,
30
29
  type RepoLocalRegistry,
@@ -101,7 +101,7 @@ export async function resolveRootContext(
101
101
  }
102
102
 
103
103
  /** Nearest ancestor of `cwd` (inclusive) holding `.<framework>/config/framework.yaml`. */
104
- export async function findConfiguredRoot(cwd: string): Promise<string | null> {
104
+ async function findConfiguredRoot(cwd: string): Promise<string | null> {
105
105
  let dir = path.resolve(cwd);
106
106
  for (;;) {
107
107
  try {
@@ -8,7 +8,7 @@ import type {
8
8
 
9
9
  export type OpenSpecSchemaSelection = OpenSpecSchemaProfile | "default";
10
10
  export type GitModeSelection = GitModeProfile | "default";
11
- export const DEFAULT_OPENSPEC_SCHEMA_SELECTION: OpenSpecSchemaSelection = "default";
11
+ const DEFAULT_OPENSPEC_SCHEMA_SELECTION: OpenSpecSchemaSelection = "default";
12
12
  export const DEFAULT_GIT_MODE_SELECTION: GitModeSelection = "default";
13
13
 
14
14
  export interface OpenSpecSchemaCompatibility {
@@ -124,7 +124,7 @@ export async function looksLikeWorkingRepo(cwd: string): Promise<boolean> {
124
124
  return false;
125
125
  }
126
126
 
127
- export async function hasLocalCompanionConfig(cwd: string): Promise<boolean> {
127
+ async function hasLocalCompanionConfig(cwd: string): Promise<boolean> {
128
128
  const resolvedCwd = path.resolve(cwd);
129
129
  return fileExists(path.join(resolvedCwd, `.${FRAMEWORK_NAME}`, "config", "framework.yaml"));
130
130
  }
@@ -1,4 +1,4 @@
1
- export class MateError extends Error {}
1
+ class MateError extends Error {}
2
2
 
3
3
  export class ConfigError extends MateError {}
4
4
 
@@ -110,7 +110,7 @@ export async function unproject(input: ProjectionRemovalInput): Promise<Projecti
110
110
  }
111
111
 
112
112
  /** Removes entries that wrap owns but an unwrap must withdraw independently. */
113
- export async function removeWorkingRepositoryUnwrapEntries(
113
+ async function removeWorkingRepositoryUnwrapEntries(
114
114
  repoPath: string,
115
115
  ): Promise<ProjectionEntryOutcome[]> {
116
116
  const entries = projectionEntries().filter(
@@ -6,7 +6,7 @@ import { GlobalConfigStore } from "./global-config-store";
6
6
  import type { LinkedRepository } from "./types";
7
7
 
8
8
  /** Envelope version for the aggregate inventory shape; bump on any breaking shape change. */
9
- export const WORKSPACE_INVENTORY_SCHEMA_VERSION = 1;
9
+ const WORKSPACE_INVENTORY_SCHEMA_VERSION = 1;
10
10
 
11
11
  export type CompanionInventoryHealth = "ready" | "missing" | "unreadable";
12
12
  export type PairingInventoryHealth =
@@ -41,7 +41,7 @@ export interface WorkspaceInventoryDeps {
41
41
  readCompanionRegistry: typeof readCompanionRegistry;
42
42
  }
43
43
 
44
- export function defaultWorkspaceInventoryDeps(
44
+ function defaultWorkspaceInventoryDeps(
45
45
  globalConfigStore = new GlobalConfigStore(),
46
46
  ): WorkspaceInventoryDeps {
47
47
  return {
@@ -3,19 +3,9 @@ import { createRequire } from "node:module";
3
3
  import path from "node:path";
4
4
 
5
5
  import { getActiveDistribution } from "../distribution";
6
- import { mateInstallPath } from "../runtime/install";
7
6
 
8
7
  const require = createRequire(import.meta.url);
9
8
 
10
- /**
11
- * Root of the installed mate-core package. A stamp input for the durable
12
- * projection: a mate installed at a different path resolves different wrapper
13
- * and plugin assets.
14
- */
15
- export function getMateInstallPath(): string {
16
- return mateInstallPath();
17
- }
18
-
19
9
  /**
20
10
  * Resolved wrapper directory: a distribution asset root that ships
21
11
  * `wrappers/bin` wins over core's bundled default.
@@ -42,7 +32,7 @@ export function getClaudePluginRoot(): string {
42
32
  return path.resolve(import.meta.dirname, "../../claude-plugin");
43
33
  }
44
34
 
45
- export const CLAUDE_PLUGIN_HOOK_SHIMS = [
35
+ const CLAUDE_PLUGIN_HOOK_SHIMS = [
46
36
  "validate-artifact-path.mjs",
47
37
  "session-banner.mjs",
48
38
  "session-guidance.mjs",
@@ -0,0 +1,43 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ /**
5
+ * Resolve the distribution package containing a CLI entrypoint.
6
+ *
7
+ * Missing manifests are skipped while walking upward; malformed manifests and
8
+ * filesystem errors stop the search so callers cannot guess a package root.
9
+ */
10
+ export function resolvePackageRoot(
11
+ entrypoint: string | undefined,
12
+ packageName: string,
13
+ ): string | undefined {
14
+ if (!entrypoint || !packageName) return undefined;
15
+
16
+ let current: string;
17
+ try {
18
+ current = path.dirname(fs.realpathSync(entrypoint));
19
+ } catch {
20
+ return undefined;
21
+ }
22
+
23
+ while (true) {
24
+ try {
25
+ const manifest = JSON.parse(
26
+ fs.readFileSync(path.join(current, "package.json"), "utf8"),
27
+ ) as unknown;
28
+ if (
29
+ manifest &&
30
+ typeof manifest === "object" &&
31
+ (manifest as { name?: unknown }).name === packageName
32
+ ) {
33
+ return current;
34
+ }
35
+ } catch (error) {
36
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") return undefined;
37
+ }
38
+
39
+ const parent = path.dirname(current);
40
+ if (parent === current) return undefined;
41
+ current = parent;
42
+ }
43
+ }
@@ -17,7 +17,7 @@ import { getLocalWorkspaceDir } from "./preinstalled-plugins";
17
17
  * Packages the distribution selects and pins for the machine-local workspace.
18
18
  * A bundle may carry any subset; it may carry nothing else.
19
19
  */
20
- export function getDistributionLocalDependencies(): Record<string, string> {
20
+ function getDistributionLocalDependencies(): Record<string, string> {
21
21
  return {
22
22
  [CONTEXT_MODE_PACKAGE_NAME]: CONTEXT_MODE_VERSION,
23
23
  [OPENCODE_PLUGIN_PACKAGE_NAME]: getCurrentVersion(),
@@ -113,7 +113,7 @@ export function installPublicPackageSync(packageSpec: string, registry = PUBLIC_
113
113
  );
114
114
  }
115
115
 
116
- export function getNpmGlobalPackageRootSync(packageName: string): string {
116
+ function getNpmGlobalPackageRootSync(packageName: string): string {
117
117
  const npmRoot = publicNpmDeps
118
118
  .execFileSync("npm", ["root", "-g"], {
119
119
  encoding: "utf8",