@uniqbit/mate-core 0.17.0-canary.3 → 0.17.0-canary.6

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.17.0-canary.3",
3
+ "version": "0.17.0-canary.6",
4
4
  "description": "Core framework and plugin APIs for Mate.",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -8,7 +8,9 @@ import { validateGuidanceData, type MateGuidanceFile } from "../../../runtime/gu
8
8
  import {
9
9
  getOpenCodePluginPackageReference,
10
10
  isMateOpenCodePluginReference,
11
+ OPENCODE_PLUGIN_PACKAGE_NAME,
11
12
  } from "../../opencode-plugin-package";
13
+ import { installedVersionAt, isPreinstalledPluginPath } from "../../preinstalled-plugins";
12
14
  import {
13
15
  getOpenCodePluginReferences,
14
16
  mergeOpenCodeConfigContent,
@@ -129,6 +131,27 @@ export class OpenCodeAdapter extends LaunchAdapter {
129
131
  return [];
130
132
  }
131
133
 
134
+ // Setup writes one of two spellings, and both are current: the published
135
+ // spec on an ordinary workstation, and an absolute path into the
136
+ // machine-local workspace where the distribution supplied an installed
137
+ // copy. Comparing only against the spec would refuse the launch on exactly
138
+ // the deployments preinstalled binding exists for.
139
+ //
140
+ // A bound reference carries its version in the package it points at rather
141
+ // than in the string, so staleness is read off disk. It is still caught,
142
+ // and now names the version actually installed.
143
+ const expectedVersion = expectedPluginReference.slice(OPENCODE_PLUGIN_PACKAGE_NAME.length + 1);
144
+ const bound = mateReferences.find((reference) =>
145
+ isPreinstalledPluginPath(reference, OPENCODE_PLUGIN_PACKAGE_NAME),
146
+ );
147
+ if (bound !== undefined) {
148
+ const version = await installedVersionAt(bound);
149
+ if (version === expectedVersion) return [];
150
+ return [
151
+ `Stale Mate plugin package reference in ${configFile}: it points at ${bound}, which is ${version ?? "not installed"} rather than ${expectedVersion}.`,
152
+ ];
153
+ }
154
+
132
155
  if (mateReferences.length > 0) {
133
156
  return [
134
157
  `Stale Mate plugin package reference in ${configFile}: found ${mateReferences.join(", ")}.`,
@@ -36,6 +36,25 @@ export function isPreinstalledPluginPath(entry: unknown, packageName: string): b
36
36
  return normalized.endsWith(`/${packageName}`);
37
37
  }
38
38
 
39
+ /**
40
+ * The version behind a reference bound to an installed copy.
41
+ *
42
+ * A published reference carries its version in the string; a bound one carries
43
+ * it in the package it points at, so staleness has to be read off disk. Shared
44
+ * by every site that validates a bound reference, so they cannot disagree about
45
+ * what "stale" means.
46
+ */
47
+ export async function installedVersionAt(packageRoot: string): Promise<string | undefined> {
48
+ try {
49
+ const manifest = JSON.parse(
50
+ await fs.readFile(path.join(packageRoot, "package.json"), "utf8"),
51
+ ) as { version?: string };
52
+ return manifest.version;
53
+ } catch {
54
+ return undefined;
55
+ }
56
+ }
57
+
39
58
  export class PreinstalledPluginMismatchError extends Error {}
40
59
 
41
60
  /**
@@ -11,6 +11,7 @@ import {
11
11
  validateContextModePackage,
12
12
  } from "../../../lib/context-mode-package";
13
13
  import { warmOpenCodePackageCache } from "../../../lib/opencode-plugin-package";
14
+ import { installedVersionAt, isPreinstalledPluginPath } from "../../../lib/preinstalled-plugins";
14
15
  import type {
15
16
  CapabilityPlugin,
16
17
  LaunchPreflightContext,
@@ -70,17 +71,43 @@ async function assertNoMcpConflict(ctx: SetupContext, provider: "claude" | "open
70
71
  }
71
72
  }
72
73
 
74
+ /**
75
+ * Both spellings of a current reference are accepted, because the Runtime
76
+ * Surface writes either one: the published spec on an ordinary workstation, and
77
+ * an absolute path into the machine-local workspace where the distribution
78
+ * supplied an installed copy. Accepting only the first would refuse the launch
79
+ * on exactly the deployments preinstalled binding exists for.
80
+ *
81
+ * Staleness is still caught in both cases — a published reference by its
82
+ * version, a bound one by the version of the copy it points at — so this is not
83
+ * the looser check it might look like.
84
+ */
73
85
  async function validateOpenCodeReferences(ctx: LaunchPreflightContext): Promise<string[]> {
74
86
  const expected = getContextModePackageReference();
75
87
  const diagnostics: string[] = [];
76
88
  for (const name of ["opencode.json", "tui.json"]) {
77
89
  const configPath = path.join(ctx.companionPath, ".opencode", name);
78
90
  const { config } = await readOpenCodeConfig(configPath);
79
- if (!getOpenCodePluginReferences(config).includes(expected)) {
91
+ const references = getOpenCodePluginReferences(config);
92
+ if (references.includes(expected)) continue;
93
+
94
+ const bound = references.find(
95
+ (reference): reference is string =>
96
+ typeof reference === "string" &&
97
+ isPreinstalledPluginPath(reference, CONTEXT_MODE_PACKAGE_NAME),
98
+ );
99
+ if (bound !== undefined) {
100
+ const version = await installedVersionAt(bound);
101
+ if (version === CONTEXT_MODE_VERSION) continue;
80
102
  diagnostics.push(
81
- `Missing or stale context-mode package reference in ${configPath}; expected ${expected}.`,
103
+ `Stale context-mode package reference in ${configPath}; it points at ${bound}, which is ${version ?? "not installed"} rather than ${CONTEXT_MODE_VERSION}.`,
82
104
  );
105
+ continue;
83
106
  }
107
+
108
+ diagnostics.push(
109
+ `Missing or stale context-mode package reference in ${configPath}; expected ${expected}.`,
110
+ );
84
111
  }
85
112
  return diagnostics;
86
113
  }
@@ -6,7 +6,7 @@ import { promisify } from "node:util";
6
6
  import { getOpenSpecSchemaSelection } from "../../../lib/orchestrator/setup-compatibilities";
7
7
  import { getWrapperBinPath } from "../../../lib/package-paths";
8
8
  import { fetchPublicPackageVersion } from "../../../lib/public-npm";
9
- import { isNewer } from "../../../lib/update-checker";
9
+ import { isNewer, isPinnedDeployment } from "../../../lib/update-checker";
10
10
  import { FRAMEWORK_NAME } from "../../../framework";
11
11
  import { confirm } from "../../../cli/confirm";
12
12
  import type { CapabilityPlugin, RuntimeContributionsByRuntime, SetupContext } from "../plugin";
@@ -63,6 +63,7 @@ export interface OpenSpecPluginDeps {
63
63
  wrapperBinPath?: () => string;
64
64
  getInstalledVersion?: (pathValue: string) => Promise<string | undefined>;
65
65
  fetchLatestVersion?: () => Promise<string | undefined>;
66
+ isPinnedDeployment?: () => boolean;
66
67
  }
67
68
 
68
69
  const OPEN_SPEC_SUPPORTED_TOOLS = Object.keys(OPENSPEC_TOOL_DIRS) as OpenSpecTool[];
@@ -170,10 +171,17 @@ async function updateOpenSpecIfOutdated(
170
171
  deps: Required<
171
172
  Pick<
172
173
  OpenSpecPluginDeps,
173
- "installCommand" | "pathValue" | "getInstalledVersion" | "fetchLatestVersion"
174
+ | "installCommand"
175
+ | "pathValue"
176
+ | "getInstalledVersion"
177
+ | "fetchLatestVersion"
178
+ | "isPinnedDeployment"
174
179
  >
175
180
  >,
176
181
  ): Promise<void> {
182
+ /** Pinned artifacts change tool versions by replacement, never reconciliation. */
183
+ if (deps.isPinnedDeployment()) return;
184
+
177
185
  const installed = await deps.getInstalledVersion(deps.pathValue());
178
186
  if (!installed) return;
179
187
 
@@ -204,6 +212,7 @@ async function ensureOpenSpecInstalled(
204
212
  | "pathValue"
205
213
  | "getInstalledVersion"
206
214
  | "fetchLatestVersion"
215
+ | "isPinnedDeployment"
207
216
  >
208
217
  >,
209
218
  ): Promise<boolean> {
@@ -326,6 +335,7 @@ export function createOpenspecPlugin(deps: OpenSpecPluginDeps = {}): CapabilityP
326
335
  wrapperBinPath: deps.wrapperBinPath ?? getWrapperBinPath,
327
336
  getInstalledVersion: deps.getInstalledVersion ?? defaultGetInstalledVersion,
328
337
  fetchLatestVersion: deps.fetchLatestVersion ?? defaultFetchLatestVersion,
338
+ isPinnedDeployment: deps.isPinnedDeployment ?? (() => isPinnedDeployment()),
329
339
  };
330
340
 
331
341
  return {