@textcortex/zenocode 0.1.11 → 0.1.13

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/README.md CHANGED
@@ -21,13 +21,14 @@ npm install -g @textcortex/zenocode
21
21
 
22
22
  ```bash
23
23
  zenocode login --email you@company.com
24
- zenocode
25
24
  ```
26
25
 
27
26
  Use your work email when logging in so Zenocode can route you to the correct onboarding and SSO flow for your workspace domain, for example `companyA.textcortex.com`.
28
27
 
29
28
  If you skip `--email`, Zenocode will ask for it interactively during login.
30
29
 
30
+ The first `zenocode login` launches Zenocode automatically after browser authentication succeeds. In later terminal sessions, start it again with `zenocode`.
31
+
31
32
  If you already have an API key, you can also start Zenocode by setting `TEXTCORTEX_API_KEY` or `TEXTCORTEX_API_TOKEN`.
32
33
 
33
34
  ## Built For Security And Compliance
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@textcortex/zenocode",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Secure, EU-hosted coding agent for TextCortex customers that runs in your terminal, edits files, runs scripts, and more.",
5
5
  "keywords": [
6
6
  "ai",
@@ -139,21 +139,55 @@ export function patchOpenCodeVersionFooterText(text) {
139
139
  return { patched, text: next };
140
140
  }
141
141
 
142
+ export function patchOpenCodeDisplayNameText(text) {
143
+ const replacements = [
144
+ ['TD("Uninstall OpenCode")', 'TD("Uninstall Zenocode")'],
145
+ ['"name":"OpenCode"', '"name":"Zenocode"'],
146
+ ['"short_name":"OpenCode"', '"short_name":"Zenocode"'],
147
+ ];
148
+
149
+ let patched = false;
150
+ let nextText = text;
151
+ for (const [target, replacement] of replacements) {
152
+ if (!nextText.includes(target)) {
153
+ continue;
154
+ }
155
+ nextText = nextText.replaceAll(target, replacement);
156
+ patched = true;
157
+ }
158
+
159
+ return { patched, text: nextText };
160
+ }
161
+
142
162
  export function patchZenocodeBinaryText(text) {
143
163
  let patched = false;
144
164
  let nextText = text;
165
+ const patches = {
166
+ logo: false,
167
+ footer: false,
168
+ displayName: false,
169
+ };
145
170
 
146
171
  const logoPatch = patchLogoSnippetText(nextText);
147
172
  if (logoPatch.patched) {
148
173
  nextText = logoPatch.text;
149
174
  patched = true;
175
+ patches.logo = true;
150
176
  }
151
177
 
152
178
  const footerPatch = patchOpenCodeVersionFooterText(nextText);
153
179
  if (footerPatch.patched) {
154
180
  nextText = footerPatch.text;
155
181
  patched = true;
182
+ patches.footer = true;
156
183
  }
157
184
 
158
- return { patched, text: nextText };
185
+ const displayNamePatch = patchOpenCodeDisplayNameText(nextText);
186
+ if (displayNamePatch.patched) {
187
+ nextText = displayNamePatch.text;
188
+ patched = true;
189
+ patches.displayName = true;
190
+ }
191
+
192
+ return { patched, patches, text: nextText };
159
193
  }
@@ -1,6 +1,11 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
- import { padBinaryReplacement, patchOpenCodeVersionFooterText } from "./branding-patch.mjs";
3
+ import {
4
+ padBinaryReplacement,
5
+ patchOpenCodeDisplayNameText,
6
+ patchOpenCodeVersionFooterText,
7
+ patchZenocodeBinaryText,
8
+ } from "./branding-patch.mjs";
4
9
 
5
10
  test("padBinaryReplacement keeps binary length stable", () => {
6
11
  const padded = padBinaryReplacement("abcdef", "abc");
@@ -54,3 +59,31 @@ test("patchOpenCodeVersionFooterText is a no-op when already branded", () => {
54
59
  assert.equal(result.patched, false);
55
60
  assert.equal(result.text, branded);
56
61
  });
62
+
63
+ test("patchOpenCodeDisplayNameText rewrites current compiled display names", () => {
64
+ const text = [
65
+ 'TD("Uninstall OpenCode");"name":"OpenCode","short_name":"OpenCode"',
66
+ '"Powered by OpenCode"',
67
+ ].join(";");
68
+
69
+ const result = patchOpenCodeDisplayNameText(text);
70
+
71
+ assert.equal(result.patched, true);
72
+ assert.equal(result.text.length, text.length);
73
+ assert.equal(
74
+ result.text,
75
+ 'TD("Uninstall Zenocode");"name":"Zenocode","short_name":"Zenocode";"Powered by OpenCode"',
76
+ );
77
+ });
78
+
79
+ test("patchZenocodeBinaryText reports display-name-only branding separately", () => {
80
+ const result = patchZenocodeBinaryText('"name":"OpenCode"');
81
+
82
+ assert.equal(result.patched, true);
83
+ assert.deepEqual(result.patches, {
84
+ logo: false,
85
+ footer: false,
86
+ displayName: true,
87
+ });
88
+ assert.equal(result.text, '"name":"Zenocode"');
89
+ });
@@ -7,17 +7,27 @@ import path from "node:path";
7
7
  import process from "node:process";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { patchZenocodeBinaryText } from "./branding-patch.mjs";
10
+ import { openCodeRef, openCodeVersion } from "./opencode-version.mjs";
10
11
 
11
12
  const currentFilePath = realpathSync(fileURLToPath(import.meta.url));
12
13
  const __dirname = path.dirname(currentFilePath);
13
14
  const appRoot = path.resolve(__dirname, "..");
14
15
  const defaultOutputDir = path.join(appRoot, ".zenocode", "brand-build");
16
+ export const defaultOpenCodeForkUrl = "https://github.com/sst/opencode";
17
+ export const defaultOpenCodeRef = openCodeRef;
18
+ export const defaultOpenCodeVersion = openCodeVersion;
19
+ export const defaultOpenCodeBuildArgs = ["--skip-install"];
20
+ const displayNameOnlyBrandingRefs = new Set([defaultOpenCodeRef]);
15
21
 
16
22
  const forkUrl =
17
23
  process.env.ZENOCODE_OPENCODE_FORK_URL ||
18
24
  process.env.CODECORTEX_OPENCODE_FORK_URL ||
19
- "https://github.com/anomalyco/opencode";
20
- const forkRef = (process.env.ZENOCODE_OPENCODE_REF || process.env.CODECORTEX_OPENCODE_REF || "").trim();
25
+ defaultOpenCodeForkUrl;
26
+ const forkRef = (
27
+ process.env.ZENOCODE_OPENCODE_REF ||
28
+ process.env.CODECORTEX_OPENCODE_REF ||
29
+ defaultOpenCodeRef
30
+ ).trim();
21
31
  const outputDir =
22
32
  process.env.ZENOCODE_BRANDED_OUTPUT_DIR ||
23
33
  process.env.CODECORTEX_BRANDED_OUTPUT_DIR ||
@@ -36,16 +46,25 @@ const publishTag =
36
46
  process.env.ZENOCODE_PUBLISH_TAG ||
37
47
  process.env.CODECORTEX_PUBLISH_TAG ||
38
48
  "latest";
39
- const buildArgs = (
49
+ const configuredBuildArgs = (
40
50
  process.env.ZENOCODE_OPENCODE_BUILD_ARGS ||
41
51
  process.env.CODECORTEX_OPENCODE_BUILD_ARGS ||
42
52
  ""
43
- )
44
- .trim()
45
- .split(/\s+/)
46
- .filter(Boolean);
53
+ ).trim();
54
+ const buildArgs = configuredBuildArgs
55
+ ? configuredBuildArgs.split(/\s+/).filter(Boolean)
56
+ : defaultOpenCodeBuildArgs;
47
57
  const cliArgs = process.argv.slice(2);
48
58
 
59
+ export function deriveOpenCodeVersionFromRef(ref, fallbackVersion = defaultOpenCodeVersion) {
60
+ const match = ref.match(/^v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)(?:\+.*)?$/);
61
+ return match?.[1] || fallbackVersion;
62
+ }
63
+
64
+ export function allowsDisplayNameOnlyBranding(ref) {
65
+ return displayNameOnlyBrandingRefs.has(ref);
66
+ }
67
+
49
68
  function _command(name) {
50
69
  if (process.platform === "win32" && ["npm", "pnpm", "npx"].includes(name)) {
51
70
  return `${name}.cmd`;
@@ -270,6 +289,13 @@ function _binaryFilename() {
270
289
  return process.platform === "win32" ? "opencode.exe" : "opencode";
271
290
  }
272
291
 
292
+ async function adHocSignBinary(binaryPath) {
293
+ if (process.platform !== "darwin") return;
294
+ await run("codesign", ["--force", "--sign", "-", binaryPath], {
295
+ stdio: "ignore",
296
+ });
297
+ }
298
+
273
299
  async function patchBinaryAtPath(binaryPath) {
274
300
  const buffer = await fs.readFile(binaryPath);
275
301
  const originalLength = buffer.length;
@@ -277,6 +303,12 @@ async function patchBinaryAtPath(binaryPath) {
277
303
  if (!patch.patched) {
278
304
  throw new Error(`Branding patch did not match binary ${binaryPath}`);
279
305
  }
306
+ const patchedMandatoryBranding = patch.patches.logo || patch.patches.footer;
307
+ if (!patchedMandatoryBranding && !allowsDisplayNameOnlyBranding(forkRef)) {
308
+ throw new Error(
309
+ `Branding patch only matched display names for ${binaryPath}; update the logo/footer patch signatures before building ${forkRef}.`,
310
+ );
311
+ }
280
312
  const nextBuffer = Buffer.from(patch.text, "latin1");
281
313
  if (nextBuffer.length !== originalLength) {
282
314
  throw new Error(`Branding patch changed binary length for ${binaryPath}`);
@@ -285,6 +317,7 @@ async function patchBinaryAtPath(binaryPath) {
285
317
  if (process.platform !== "win32") {
286
318
  await fs.chmod(binaryPath, 0o755);
287
319
  }
320
+ await adHocSignBinary(binaryPath);
288
321
  }
289
322
 
290
323
  function _buildWrapperExecutable({ runtimePackageName, binName }) {
@@ -636,7 +669,16 @@ async function main() {
636
669
  await run(
637
670
  bunCommand,
638
671
  ["./packages/opencode/script/build.ts", "--single", ...buildArgs],
639
- { cwd: checkoutDir },
672
+ {
673
+ cwd: checkoutDir,
674
+ env: {
675
+ ...process.env,
676
+ OPENCODE_CHANNEL: process.env.OPENCODE_CHANNEL || "latest",
677
+ OPENCODE_VERSION:
678
+ process.env.OPENCODE_VERSION ||
679
+ deriveOpenCodeVersionFromRef(forkRef, defaultOpenCodeVersion),
680
+ },
681
+ },
640
682
  );
641
683
 
642
684
  await fs.mkdir(artifactDir, { recursive: true });
@@ -3,9 +3,33 @@ import test from "node:test";
3
3
  import {
4
4
  buildPublishCommandArgs,
5
5
  buildWrapperBinMap,
6
+ allowsDisplayNameOnlyBranding,
7
+ defaultOpenCodeBuildArgs,
8
+ defaultOpenCodeForkUrl,
9
+ defaultOpenCodeRef,
10
+ defaultOpenCodeVersion,
11
+ deriveOpenCodeVersionFromRef,
6
12
  mapBrandedBinaryPackageName,
7
13
  } from "./build-branded-opencode.mjs";
8
14
 
15
+ test("branded OpenCode build defaults to the latest stable upstream release", () => {
16
+ assert.equal(defaultOpenCodeForkUrl, "https://github.com/sst/opencode");
17
+ assert.equal(defaultOpenCodeRef, "v1.17.6");
18
+ assert.equal(defaultOpenCodeVersion, "1.17.6");
19
+ assert.deepEqual(defaultOpenCodeBuildArgs, ["--skip-install"]);
20
+ });
21
+
22
+ test("deriveOpenCodeVersionFromRef follows explicit tag overrides", () => {
23
+ assert.equal(deriveOpenCodeVersionFromRef("v1.17.5"), "1.17.5");
24
+ assert.equal(deriveOpenCodeVersionFromRef("1.18.0-beta.1"), "1.18.0-beta.1");
25
+ assert.equal(deriveOpenCodeVersionFromRef("main", "1.17.4"), "1.17.4");
26
+ });
27
+
28
+ test("display-name-only branding is limited to reviewed upstream refs", () => {
29
+ assert.equal(allowsDisplayNameOnlyBranding(defaultOpenCodeRef), true);
30
+ assert.equal(allowsDisplayNameOnlyBranding("v1.17.7"), false);
31
+ });
32
+
9
33
  test("mapBrandedBinaryPackageName scopes runtime binaries under zenocode", () => {
10
34
  assert.equal(
11
35
  mapBrandedBinaryPackageName("opencode-darwin-arm64", "@textcortex/zenocode-ai"),
@@ -0,0 +1,4 @@
1
+ export const openCodePackageName = "opencode-ai";
2
+ export const openCodeVersion = "1.17.6";
3
+ export const openCodeRef = `v${openCodeVersion}`;
4
+ export const openCodePackageSpec = `${openCodePackageName}@${openCodeVersion}`;
@@ -11,6 +11,10 @@ import {
11
11
  patchZenocodeBinaryText,
12
12
  zenocodeLogo,
13
13
  } from "./branding-patch.mjs";
14
+ import {
15
+ openCodePackageName,
16
+ openCodePackageSpec,
17
+ } from "./opencode-version.mjs";
14
18
 
15
19
  const currentFilePath = realpathSync(fileURLToPath(import.meta.url));
16
20
  const __dirname = path.dirname(currentFilePath);
@@ -29,6 +33,7 @@ const legacyRuntimeCredentialsPath = path.join(
29
33
  const logoutMarkerPath = path.join(runtimeDir, "logout-marker.json");
30
34
  const modelsPath = path.join(runtimeDir, "models.json");
31
35
  const configPath = path.join(runtimeDir, "opencode.jsonc");
36
+ const tuiConfigPath = path.join(runtimeDir, "opencode.tui.json");
32
37
  const localBaseUrlDefault = "http://127.0.0.1:8080";
33
38
  const cloudBaseUrlDefault = "https://api.textcortex.com";
34
39
  const localBaseUrlFlags = new Set(["--local", "--localhost"]);
@@ -39,19 +44,21 @@ const configuredOpencodePackage =
39
44
  process.env.CODECORTEX_OPENCODE_PACKAGE ||
40
45
  process.env.OPENCODE_PACKAGE ||
41
46
  null;
42
- const defaultBrandedOpencodePackage = "@textcortex/zenocode-ai";
47
+ const defaultBrandedOpencodePackage = openCodePackageSpec;
43
48
  const legacyBrandedOpencodePackage = "@textcortex/opencode-ai";
44
- const fallbackOpencodePackage = "opencode-ai";
49
+ const fallbackOpencodePackage = openCodePackageName;
45
50
  const opencodePackage = configuredOpencodePackage || defaultBrandedOpencodePackage;
46
51
  const opencodeBinaryPath =
47
52
  process.env.ZENOCODE_OPENCODE_BIN_PATH ||
48
53
  process.env.CODECORTEX_OPENCODE_BIN_PATH ||
49
54
  "";
50
- const oauthInitiatePath = "/internal/v1/fastapi/zenocode/oauth2/initiate";
51
- const oauthTokenPath = "/internal/v1/fastapi/zenocode/oauth2/token";
55
+ const oauthInitiatePath = "/internal/v2/fastapi/zenocode/oauth2/initiate";
56
+ const oauthTokenPath = "/internal/v2/fastapi/zenocode/oauth2/token";
52
57
  const defaultOrder = [
58
+ "minimax-m3-thinking",
59
+ "kimi-k2-6",
53
60
  "kimi-k2-5-thinking",
54
- "glm-5",
61
+ "glm-5-1",
55
62
  "gpt-5-2",
56
63
  "gpt-5-1",
57
64
  "gpt-5",
@@ -394,13 +401,15 @@ export function buildOpenCodeConfig({ baseUrl, providerID, model, smallModel })
394
401
  return {
395
402
  $schema: "https://opencode.ai/config.json",
396
403
  enabled_providers: [providerID],
404
+ // Keep the legacy theme key populated for older OpenCode builds.
405
+ theme: "system",
397
406
  model: `${providerID}/${model}`,
398
407
  small_model: `${providerID}/${smallModel}`,
399
408
  provider: {
400
409
  [providerID]: {
401
410
  name: "Zenocode",
402
411
  options: {
403
- baseURL: new URL("/internal/v1/fastapi/zenocode/v1", baseUrl).toString(),
412
+ baseURL: new URL("/internal/v2/fastapi/zenocode/v1", baseUrl).toString(),
404
413
  },
405
414
  },
406
415
  // Older fallback opencode-ai builds can load the Codex auth plugin when
@@ -415,6 +424,13 @@ export function buildOpenCodeConfig({ baseUrl, providerID, model, smallModel })
415
424
  };
416
425
  }
417
426
 
427
+ export function buildOpenCodeTuiConfig() {
428
+ return {
429
+ $schema: "https://opencode.ai/tui.json",
430
+ theme: "system",
431
+ };
432
+ }
433
+
418
434
  function unwrapData(payload) {
419
435
  if (payload && typeof payload === "object" && payload.data && typeof payload.data === "object") {
420
436
  return payload.data;
@@ -454,7 +470,7 @@ async function requestJson(url, init) {
454
470
  }
455
471
 
456
472
  async function prepareRuntime(baseUrl, token) {
457
- const modelsUrl = new URL("/internal/v1/fastapi/zenocode/models/api.json", baseUrl).toString();
473
+ const modelsUrl = new URL("/internal/v2/fastapi/zenocode/models/api.json", baseUrl).toString();
458
474
  const { response, payload, text } = await requestJson(modelsUrl, {
459
475
  headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
460
476
  });
@@ -482,10 +498,12 @@ async function prepareRuntime(baseUrl, token) {
482
498
  model,
483
499
  smallModel,
484
500
  });
501
+ const tuiConfig = buildOpenCodeTuiConfig();
485
502
 
486
503
  await fs.mkdir(runtimeDir, { recursive: true });
487
504
  await fs.writeFile(modelsPath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
488
505
  await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
506
+ await fs.writeFile(tuiConfigPath, `${JSON.stringify(tuiConfig, null, 2)}\n`, "utf-8");
489
507
  return model;
490
508
  }
491
509
 
@@ -942,11 +960,26 @@ function _runtimeBrandedBinaryPath() {
942
960
  return path.join(runtimeDir, "bin", binaryName);
943
961
  }
944
962
 
963
+ export function packageNameFromSpecifier(packageSpecifier) {
964
+ if (!packageSpecifier.includes("@")) {
965
+ return packageSpecifier;
966
+ }
967
+ if (!packageSpecifier.startsWith("@")) {
968
+ return packageSpecifier.split("@")[0];
969
+ }
970
+
971
+ const versionSeparator = packageSpecifier.lastIndexOf("@");
972
+ return versionSeparator > packageSpecifier.indexOf("/")
973
+ ? packageSpecifier.slice(0, versionSeparator)
974
+ : packageSpecifier;
975
+ }
976
+
945
977
  function shouldPatchOpencodeRuntimePackage(packageName) {
978
+ const runtimePackageName = packageNameFromSpecifier(packageName);
946
979
  return (
947
- packageName.endsWith("/opencode-ai") ||
948
- packageName.endsWith("/zenocode-ai") ||
949
- packageName === "opencode-ai"
980
+ runtimePackageName.endsWith("/opencode-ai") ||
981
+ runtimePackageName.endsWith("/zenocode-ai") ||
982
+ runtimePackageName === openCodePackageName
950
983
  );
951
984
  }
952
985
 
@@ -987,6 +1020,88 @@ async function _collectPnpmDlxPnpmDirs(rootDir) {
987
1020
  return pnpmDirs;
988
1021
  }
989
1022
 
1023
+ async function _collectNodeModulesDirs(rootDir) {
1024
+ const queue = [{ dir: rootDir, depth: 0 }];
1025
+ const nodeModulesDirs = [];
1026
+
1027
+ while (queue.length) {
1028
+ const current = queue.shift();
1029
+ const nodeModulesPath = path.join(current.dir, "node_modules");
1030
+ if (await _pathExists(nodeModulesPath)) {
1031
+ nodeModulesDirs.push(nodeModulesPath);
1032
+ }
1033
+
1034
+ if (current.depth >= 3) continue;
1035
+ let entries = [];
1036
+ try {
1037
+ entries = await fs.readdir(current.dir, { withFileTypes: true });
1038
+ } catch {
1039
+ continue;
1040
+ }
1041
+ for (const entry of entries) {
1042
+ if (!entry.isDirectory()) continue;
1043
+ if (entry.name === "node_modules") continue;
1044
+ queue.push({ dir: path.join(current.dir, entry.name), depth: current.depth + 1 });
1045
+ }
1046
+ }
1047
+
1048
+ return nodeModulesDirs;
1049
+ }
1050
+
1051
+ async function _collectOpencodeBinariesFromNodeModules(nodeModulesPath) {
1052
+ const binaryName = process.platform === "win32" ? "opencode.exe" : "opencode";
1053
+ const hiddenCachedBinaryName = ".opencode";
1054
+ const candidates = [];
1055
+
1056
+ let topLevel = [];
1057
+ try {
1058
+ topLevel = await fs.readdir(nodeModulesPath, { withFileTypes: true });
1059
+ } catch {
1060
+ return candidates;
1061
+ }
1062
+
1063
+ for (const moduleEntry of topLevel) {
1064
+ if (!moduleEntry.isDirectory()) continue;
1065
+
1066
+ if (moduleEntry.name === openCodePackageName) {
1067
+ const cachedPath = path.join(nodeModulesPath, openCodePackageName, "bin", hiddenCachedBinaryName);
1068
+ if (await _pathExists(cachedPath)) candidates.push(cachedPath);
1069
+ continue;
1070
+ }
1071
+
1072
+ if (moduleEntry.name.startsWith("opencode-")) {
1073
+ const binaryPath = path.join(nodeModulesPath, moduleEntry.name, "bin", binaryName);
1074
+ if (await _pathExists(binaryPath)) candidates.push(binaryPath);
1075
+ continue;
1076
+ }
1077
+
1078
+ if (!moduleEntry.name.startsWith("@")) continue;
1079
+ const scopePath = path.join(nodeModulesPath, moduleEntry.name);
1080
+ let scopedPackages = [];
1081
+ try {
1082
+ scopedPackages = await fs.readdir(scopePath, { withFileTypes: true });
1083
+ } catch {
1084
+ continue;
1085
+ }
1086
+ for (const scopedPackage of scopedPackages) {
1087
+ if (!scopedPackage.isDirectory()) continue;
1088
+ const scopedNodeModulesPath = path.join(scopePath, scopedPackage.name);
1089
+
1090
+ if (scopedPackage.name === "opencode-ai") {
1091
+ const cachedPath = path.join(scopedNodeModulesPath, "bin", hiddenCachedBinaryName);
1092
+ if (await _pathExists(cachedPath)) candidates.push(cachedPath);
1093
+ continue;
1094
+ }
1095
+
1096
+ if (!scopedPackage.name.startsWith("opencode-")) continue;
1097
+ const binaryPath = path.join(scopedNodeModulesPath, "bin", binaryName);
1098
+ if (await _pathExists(binaryPath)) candidates.push(binaryPath);
1099
+ }
1100
+ }
1101
+
1102
+ return candidates;
1103
+ }
1104
+
990
1105
  async function _collectPnpmDlxOpencodeBinaries() {
991
1106
  const dlxRoots = [
992
1107
  path.join(os.homedir(), "Library", "Caches", "pnpm", "dlx"),
@@ -994,8 +1109,6 @@ async function _collectPnpmDlxOpencodeBinaries() {
994
1109
  path.join(process.env.LOCALAPPDATA || "", "pnpm", "dlx"),
995
1110
  ].filter(Boolean);
996
1111
 
997
- const binaryName = process.platform === "win32" ? "opencode.exe" : "opencode";
998
- const hiddenCachedBinaryName = ".opencode";
999
1112
  const candidates = [];
1000
1113
 
1001
1114
  for (const root of dlxRoots) {
@@ -1014,51 +1127,7 @@ async function _collectPnpmDlxOpencodeBinaries() {
1014
1127
  const nodeModulesPath = path.join(pnpmDir, entry.name, "node_modules");
1015
1128
  if (!(await _pathExists(nodeModulesPath))) continue;
1016
1129
 
1017
- let topLevel = [];
1018
- try {
1019
- topLevel = await fs.readdir(nodeModulesPath, { withFileTypes: true });
1020
- } catch {
1021
- continue;
1022
- }
1023
-
1024
- for (const moduleEntry of topLevel) {
1025
- if (!moduleEntry.isDirectory()) continue;
1026
-
1027
- if (moduleEntry.name === "opencode-ai") {
1028
- const cachedPath = path.join(nodeModulesPath, "opencode-ai", "bin", hiddenCachedBinaryName);
1029
- if (await _pathExists(cachedPath)) candidates.push(cachedPath);
1030
- continue;
1031
- }
1032
-
1033
- if (moduleEntry.name.startsWith("opencode-")) {
1034
- const binaryPath = path.join(nodeModulesPath, moduleEntry.name, "bin", binaryName);
1035
- if (await _pathExists(binaryPath)) candidates.push(binaryPath);
1036
- continue;
1037
- }
1038
-
1039
- if (!moduleEntry.name.startsWith("@")) continue;
1040
- const scopePath = path.join(nodeModulesPath, moduleEntry.name);
1041
- let scopedPackages = [];
1042
- try {
1043
- scopedPackages = await fs.readdir(scopePath, { withFileTypes: true });
1044
- } catch {
1045
- continue;
1046
- }
1047
- for (const scopedPackage of scopedPackages) {
1048
- if (!scopedPackage.isDirectory()) continue;
1049
- const scopedNodeModulesPath = path.join(scopePath, scopedPackage.name);
1050
-
1051
- if (scopedPackage.name === "opencode-ai") {
1052
- const cachedPath = path.join(scopedNodeModulesPath, "bin", hiddenCachedBinaryName);
1053
- if (await _pathExists(cachedPath)) candidates.push(cachedPath);
1054
- continue;
1055
- }
1056
-
1057
- if (!scopedPackage.name.startsWith("opencode-")) continue;
1058
- const binaryPath = path.join(scopedNodeModulesPath, "bin", binaryName);
1059
- if (await _pathExists(binaryPath)) candidates.push(binaryPath);
1060
- }
1061
- }
1130
+ candidates.push(...await _collectOpencodeBinariesFromNodeModules(nodeModulesPath));
1062
1131
  }
1063
1132
  }
1064
1133
  }
@@ -1066,6 +1135,44 @@ async function _collectPnpmDlxOpencodeBinaries() {
1066
1135
  return [...new Set(candidates)];
1067
1136
  }
1068
1137
 
1138
+ async function _collectNpxOpencodeBinaries() {
1139
+ const npxRoots = [
1140
+ path.join(os.homedir(), ".npm", "_npx"),
1141
+ path.join(process.env.LOCALAPPDATA || "", "npm-cache", "_npx"),
1142
+ ].filter(Boolean);
1143
+ const candidates = [];
1144
+
1145
+ for (const root of npxRoots) {
1146
+ if (!(await _pathExists(root))) continue;
1147
+ const nodeModulesDirs = await _collectNodeModulesDirs(root);
1148
+ for (const nodeModulesDir of nodeModulesDirs) {
1149
+ candidates.push(...await _collectOpencodeBinariesFromNodeModules(nodeModulesDir));
1150
+ }
1151
+ }
1152
+
1153
+ return [...new Set(candidates)];
1154
+ }
1155
+
1156
+ function _buildPackageWarmupArgs(packageName, runner) {
1157
+ if (runner.command === _runnerCommand("pnpm")) {
1158
+ return ["dlx", packageName, "--version"];
1159
+ }
1160
+ if (runner.command === _runnerCommand("npx")) {
1161
+ return ["--yes", packageName, "--version"];
1162
+ }
1163
+ return null;
1164
+ }
1165
+
1166
+ async function _collectRunnerOpencodeBinaries(runner) {
1167
+ if (runner.command === _runnerCommand("pnpm")) {
1168
+ return _collectPnpmDlxOpencodeBinaries();
1169
+ }
1170
+ if (runner.command === _runnerCommand("npx")) {
1171
+ return _collectNpxOpencodeBinaries();
1172
+ }
1173
+ return [];
1174
+ }
1175
+
1069
1176
  async function _adHocSignBinary(binaryPath) {
1070
1177
  if (process.platform !== "darwin") return;
1071
1178
  try {
@@ -1131,9 +1238,6 @@ async function _ensurePatchedOpencodeDlxBinaries(packageName, runner, options) {
1131
1238
  if (!shouldPatchOpencodeRuntimePackage(packageName)) {
1132
1239
  return null;
1133
1240
  }
1134
- if (runner.command !== _runnerCommand("pnpm")) {
1135
- return null;
1136
- }
1137
1241
  if (
1138
1242
  process.env.ZENOCODE_DISABLE_OPENCODE_LOGO_PATCH === "1" ||
1139
1243
  process.env.CODECORTEX_DISABLE_OPENCODE_LOGO_PATCH === "1"
@@ -1141,19 +1245,18 @@ async function _ensurePatchedOpencodeDlxBinaries(packageName, runner, options) {
1141
1245
  return null;
1142
1246
  }
1143
1247
 
1144
- let binaryCandidates = await _collectPnpmDlxOpencodeBinaries();
1145
- if (!binaryCandidates.length) {
1146
- // Warm cache so we have a concrete binary to patch.
1147
- try {
1148
- await runChild(runner.command, ["dlx", packageName, "--version"], {
1248
+ const warmupArgs = _buildPackageWarmupArgs(packageName, runner);
1249
+ try {
1250
+ if (warmupArgs) {
1251
+ await runChild(runner.command, warmupArgs, {
1149
1252
  ...options,
1150
1253
  stdio: "ignore",
1151
1254
  });
1152
- } catch {
1153
- // ignore warm-up failures and continue with best effort patching
1154
1255
  }
1155
- binaryCandidates = await _collectPnpmDlxOpencodeBinaries();
1256
+ } catch {
1257
+ // ignore warm-up failures and continue with best effort patching
1156
1258
  }
1259
+ const binaryCandidates = await _collectRunnerOpencodeBinaries(runner);
1157
1260
 
1158
1261
  if (!binaryCandidates.length) {
1159
1262
  return null;
@@ -1186,6 +1289,22 @@ export function buildPackageLauncherChildOptions(options, pinnedRuntimePath) {
1186
1289
  };
1187
1290
  }
1188
1291
 
1292
+ export function buildPackageLauncherInvocation({ runner, args, pinnedRuntimePath }) {
1293
+ if (pinnedRuntimePath) {
1294
+ return {
1295
+ command: pinnedRuntimePath,
1296
+ args,
1297
+ usePinnedRuntime: true,
1298
+ };
1299
+ }
1300
+
1301
+ return {
1302
+ command: runner.command,
1303
+ args: runner.args,
1304
+ usePinnedRuntime: false,
1305
+ };
1306
+ }
1307
+
1189
1308
  async function runPackageLauncher(packageName, args, options) {
1190
1309
  const runners = [
1191
1310
  { command: _runnerCommand("pnpm"), args: ["dlx", packageName, ...args] },
@@ -1203,9 +1322,17 @@ async function runPackageLauncher(packageName, args, options) {
1203
1322
  }
1204
1323
  }
1205
1324
 
1206
- const childOptions = buildPackageLauncherChildOptions(options, pinnedRuntimePath);
1325
+ const invocation = buildPackageLauncherInvocation({
1326
+ runner,
1327
+ args,
1328
+ pinnedRuntimePath,
1329
+ });
1330
+ const childOptions = buildPackageLauncherChildOptions(
1331
+ options,
1332
+ invocation.usePinnedRuntime ? null : pinnedRuntimePath,
1333
+ );
1207
1334
 
1208
- const result = await runChild(runner.command, runner.args, childOptions);
1335
+ const result = await runChild(invocation.command, invocation.args, childOptions);
1209
1336
  if (result.signal) {
1210
1337
  process.kill(process.pid, result.signal);
1211
1338
  return;
@@ -1351,10 +1478,11 @@ export async function runRuntimeWithSessionRecovery({
1351
1478
  }
1352
1479
 
1353
1480
  async function packageExistsOnNpm(packageName) {
1481
+ const packageSpecName = packageNameFromSpecifier(packageName);
1354
1482
  const controller = new AbortController();
1355
1483
  const timeout = setTimeout(() => controller.abort(), 4_000);
1356
1484
  try {
1357
- const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}`, {
1485
+ const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageSpecName)}`, {
1358
1486
  method: "GET",
1359
1487
  headers: { Accept: "application/json" },
1360
1488
  signal: controller.signal,
@@ -1412,6 +1540,11 @@ function shouldRenderBanner(args) {
1412
1540
  return !args.some((arg) => suppressFlags.has(arg));
1413
1541
  }
1414
1542
 
1543
+ export function shouldBypassZenocodePreparation(args) {
1544
+ const metadataFlags = new Set(["--help", "-h", "--version", "-v", "completion"]);
1545
+ return metadataFlags.has(args[0]);
1546
+ }
1547
+
1415
1548
  function maybeRenderBanner(args) {
1416
1549
  if (!shouldRenderBanner(args)) {
1417
1550
  return;
@@ -1578,7 +1711,7 @@ async function main() {
1578
1711
  }
1579
1712
 
1580
1713
  const preferLocalhost = hasLocalBaseUrlFlag(passthrough);
1581
- const runtimeArgs = stripLocalBaseUrlFlags(passthrough);
1714
+ let runtimeArgs = stripLocalBaseUrlFlags(passthrough);
1582
1715
  const storedBaseUrl = await resolveStoredBaseUrl();
1583
1716
  const baseUrl = resolveTextCortexBaseUrl({
1584
1717
  envBaseUrl: process.env.TEXTCORTEX_BASE_URL,
@@ -1596,7 +1729,7 @@ async function main() {
1596
1729
  runtimeArgs.slice(1),
1597
1730
  { preferLocalhost },
1598
1731
  );
1599
- return;
1732
+ runtimeArgs = [];
1600
1733
  }
1601
1734
 
1602
1735
  if (subcommand === "logout") {
@@ -1605,6 +1738,15 @@ async function main() {
1605
1738
  }
1606
1739
 
1607
1740
  maybeRenderBanner(runtimeArgs);
1741
+ if (shouldBypassZenocodePreparation(runtimeArgs)) {
1742
+ const launchPackage = await resolveLaunchPackage();
1743
+ await runPackageLauncher(launchPackage, runtimeArgs, {
1744
+ cwd: process.cwd(),
1745
+ env: { ...process.env },
1746
+ });
1747
+ return;
1748
+ }
1749
+
1608
1750
  const tokenResolution = await resolveTokenWithAutoLogin(baseUrl, runtimeArgs, {
1609
1751
  preferLocalhost,
1610
1752
  });
@@ -1626,6 +1768,7 @@ async function main() {
1626
1768
  ...process.env,
1627
1769
  OPENCODE_MODELS_PATH: modelsPath,
1628
1770
  OPENCODE_CONFIG: configPath,
1771
+ OPENCODE_TUI_CONFIG: tuiConfigPath,
1629
1772
  TEXTCORTEX_API_KEY: token,
1630
1773
  },
1631
1774
  };
@@ -11,32 +11,56 @@ import {
11
11
  } from "./branding-patch.mjs";
12
12
  import {
13
13
  buildOpenCodeConfig,
14
+ buildPackageLauncherInvocation,
14
15
  buildPackageLauncherChildOptions,
15
16
  canRecoverRuntimeSessionFromTranscript,
16
17
  buildZenocodeBanner,
17
18
  chooseDefaults,
18
19
  hasLocalBaseUrlFlag,
20
+ packageNameFromSpecifier,
19
21
  resolveLoginBaseUrl,
20
22
  resolveLoginSuccessIdentifier,
21
23
  resolveTextCortexBaseUrl,
22
24
  runRuntimeWithSessionRecovery,
25
+ shouldBypassZenocodePreparation,
23
26
  shouldFallbackLoginToCloud,
24
27
  writePrivateJsonFile,
25
28
  } from "./run-zenocode.mjs";
26
29
 
27
- test("chooseDefaults prefers kimi k2.5 thinking for Zenocode", () => {
30
+ test("chooseDefaults prefers MiniMax M3 thinking for Zenocode", () => {
28
31
  const defaults = chooseDefaults({
29
- "glm-5": {},
30
- "kimi-k2-5-thinking": {},
32
+ "glm-5-1": {},
33
+ "kimi-k2-6": {},
34
+ "minimax-m3-thinking": {},
31
35
  "gpt-5-2": {},
32
36
  });
33
37
 
34
38
  assert.deepEqual(defaults, {
35
- model: "kimi-k2-5-thinking",
36
- smallModel: "kimi-k2-5-thinking",
39
+ model: "minimax-m3-thinking",
40
+ smallModel: "minimax-m3-thinking",
37
41
  });
38
42
  });
39
43
 
44
+ test("packageNameFromSpecifier strips npm versions without breaking scopes", () => {
45
+ assert.equal(packageNameFromSpecifier("opencode-ai@1.17.6"), "opencode-ai");
46
+ assert.equal(
47
+ packageNameFromSpecifier("@textcortex/zenocode-ai@1.17.6"),
48
+ "@textcortex/zenocode-ai",
49
+ );
50
+ assert.equal(
51
+ packageNameFromSpecifier("@textcortex/zenocode-ai"),
52
+ "@textcortex/zenocode-ai",
53
+ );
54
+ });
55
+
56
+ test("shouldBypassZenocodePreparation skips auth for runtime metadata commands", () => {
57
+ assert.equal(shouldBypassZenocodePreparation(["--version"]), true);
58
+ assert.equal(shouldBypassZenocodePreparation(["completion", "zsh"]), true);
59
+ assert.equal(shouldBypassZenocodePreparation(["run"]), false);
60
+ assert.equal(shouldBypassZenocodePreparation(["run", "implement", "completion"]), false);
61
+ assert.equal(shouldBypassZenocodePreparation(["run", "--help"]), false);
62
+ });
63
+
40
64
  test("resolveTextCortexBaseUrl defaults to the cloud API for packaged usage", () => {
41
65
  assert.equal(
42
66
  resolveTextCortexBaseUrl({
@@ -128,12 +152,14 @@ test("buildOpenCodeConfig includes an openai stub for fallback runtime auth plug
128
152
  const config = buildOpenCodeConfig({
129
153
  baseUrl: "http://127.0.0.1:8080",
130
154
  providerID: "textcortex",
131
- model: "kimi-k2-5-thinking",
132
- smallModel: "kimi-k2-5-thinking",
155
+ model: "minimax-m3-thinking",
156
+ smallModel: "gpt-5-2",
133
157
  });
134
158
 
135
159
  assert.deepEqual(config.enabled_providers, ["textcortex"]);
136
- assert.equal(config.model, "textcortex/kimi-k2-5-thinking");
160
+ assert.equal(config.model, "textcortex/minimax-m3-thinking");
161
+ assert.equal(config.small_model, "textcortex/gpt-5-2");
162
+ assert.equal(config.theme, "system");
137
163
  assert.ok(config.provider.openai);
138
164
  assert.deepEqual(config.provider.openai.models, {});
139
165
  });
@@ -155,6 +181,40 @@ test("buildPackageLauncherChildOptions keeps fallback package launchers attached
155
181
  });
156
182
  });
157
183
 
184
+ test("buildPackageLauncherInvocation runs pinned runtimes directly", () => {
185
+ const invocation = buildPackageLauncherInvocation({
186
+ runner: {
187
+ command: "npx",
188
+ args: ["--yes", "opencode-ai@1.17.6", "--version"],
189
+ },
190
+ args: ["--version"],
191
+ pinnedRuntimePath: "/tmp/zenocode-runtime",
192
+ });
193
+
194
+ assert.deepEqual(invocation, {
195
+ command: "/tmp/zenocode-runtime",
196
+ args: ["--version"],
197
+ usePinnedRuntime: true,
198
+ });
199
+ });
200
+
201
+ test("buildPackageLauncherInvocation uses package runners without a pinned runtime", () => {
202
+ const invocation = buildPackageLauncherInvocation({
203
+ runner: {
204
+ command: "pnpm",
205
+ args: ["dlx", "opencode-ai@1.17.6", "--version"],
206
+ },
207
+ args: ["--version"],
208
+ pinnedRuntimePath: null,
209
+ });
210
+
211
+ assert.deepEqual(invocation, {
212
+ command: "pnpm",
213
+ args: ["dlx", "opencode-ai@1.17.6", "--version"],
214
+ usePinnedRuntime: false,
215
+ });
216
+ });
217
+
158
218
  test("buildZenocodeBanner renders block logo art instead of plain text", () => {
159
219
  const banner = buildZenocodeBanner();
160
220
 
@@ -509,7 +569,7 @@ test("prepare-only refreshes stored Zenocode credentials when the access token h
509
569
 
510
570
  if (
511
571
  req.method === "GET" &&
512
- req.url === "/internal/v1/fastapi/zenocode/models/api.json"
572
+ req.url === "/internal/v2/fastapi/zenocode/models/api.json"
513
573
  ) {
514
574
  modelAuthHeader = req.headers.authorization || null;
515
575
  if (modelAuthHeader === "Bearer expired-access") {
@@ -523,8 +583,10 @@ test("prepare-only refreshes stored Zenocode credentials when the access token h
523
583
  JSON.stringify({
524
584
  textcortex: {
525
585
  models: {
586
+ "minimax-m3-thinking": {},
587
+ "kimi-k2-6": {},
526
588
  "kimi-k2-5-thinking": {},
527
- "glm-5": {},
589
+ "glm-5-1": {},
528
590
  },
529
591
  },
530
592
  }),
@@ -579,6 +641,175 @@ test("prepare-only refreshes stored Zenocode credentials when the access token h
579
641
  const savedCredentials = JSON.parse(await fs.readFile(credentialsPath, "utf-8"));
580
642
  assert.equal(savedCredentials.access_token, "fresh-access");
581
643
  assert.equal(savedCredentials.refresh_token, "fresh-refresh");
644
+
645
+ const savedConfig = JSON.parse(
646
+ await fs.readFile(path.join(zenocodeHome, "opencode.jsonc"), "utf-8"),
647
+ );
648
+ assert.equal(savedConfig.model, "textcortex/minimax-m3-thinking");
649
+ });
650
+
651
+ test("login launches the runtime immediately with system TUI theming", async (t) => {
652
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "zenocode-login-"));
653
+ const zenocodeHome = path.join(tempDir, ".zenocode");
654
+ const runtimeLogPath = path.join(tempDir, "runtime-log.json");
655
+ const fakeRuntimePath = path.join(tempDir, "fake-opencode");
656
+ const scriptPath = new URL("./run-zenocode.mjs", import.meta.url);
657
+
658
+ t.after(async () => {
659
+ await fs.rm(tempDir, { recursive: true, force: true });
660
+ });
661
+
662
+ await fs.mkdir(zenocodeHome, { recursive: true });
663
+ await fs.writeFile(
664
+ fakeRuntimePath,
665
+ `#!/usr/bin/env node
666
+ const fs = require("node:fs/promises");
667
+
668
+ async function main() {
669
+ const payload = {
670
+ args: process.argv.slice(2),
671
+ env: {
672
+ OPENCODE_CONFIG: process.env.OPENCODE_CONFIG,
673
+ OPENCODE_TUI_CONFIG: process.env.OPENCODE_TUI_CONFIG,
674
+ TEXTCORTEX_API_KEY: process.env.TEXTCORTEX_API_KEY,
675
+ },
676
+ };
677
+ await fs.writeFile(process.env.RUNTIME_LOG_PATH, JSON.stringify(payload), "utf-8");
678
+ }
679
+
680
+ main().catch((error) => {
681
+ console.error(error instanceof Error ? error.message : String(error));
682
+ process.exit(1);
683
+ });
684
+ `,
685
+ "utf-8",
686
+ );
687
+ await fs.chmod(fakeRuntimePath, 0o755);
688
+
689
+ const server = http.createServer((req, res) => {
690
+ if (
691
+ req.method === "POST" &&
692
+ req.url === "/internal/v2/fastapi/zenocode/oauth2/initiate"
693
+ ) {
694
+ res.writeHead(200, { "Content-Type": "application/json" });
695
+ res.end(
696
+ JSON.stringify({
697
+ data: {
698
+ device_code: "device-code",
699
+ user_code: "ABCD-1234",
700
+ verification_url_complete: "https://textcortex.example/verify",
701
+ interval: 0,
702
+ expires_in: 30,
703
+ },
704
+ }),
705
+ );
706
+ return;
707
+ }
708
+
709
+ if (
710
+ req.method === "POST" &&
711
+ req.url === "/internal/v2/fastapi/zenocode/oauth2/token"
712
+ ) {
713
+ res.writeHead(200, { "Content-Type": "application/json" });
714
+ res.end(
715
+ JSON.stringify({
716
+ data: {
717
+ access_token: "fresh-access",
718
+ refresh_token: "fresh-refresh",
719
+ auth_id: "auth_123",
720
+ },
721
+ }),
722
+ );
723
+ return;
724
+ }
725
+
726
+ if (
727
+ req.method === "GET" &&
728
+ req.url === "/internal/v2/fastapi/zenocode/models/api.json"
729
+ ) {
730
+ res.writeHead(200, { "Content-Type": "application/json" });
731
+ res.end(
732
+ JSON.stringify({
733
+ textcortex: {
734
+ models: {
735
+ "minimax-m3-thinking": {},
736
+ "kimi-k2-6": {},
737
+ "kimi-k2-5-thinking": {},
738
+ "glm-5-1": {},
739
+ },
740
+ },
741
+ }),
742
+ );
743
+ return;
744
+ }
745
+
746
+ res.writeHead(404, { "Content-Type": "application/json" });
747
+ res.end(JSON.stringify({ detail: "not found" }));
748
+ });
749
+
750
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
751
+ const address = server.address();
752
+ const baseUrl = `http://127.0.0.1:${address.port}`;
753
+
754
+ t.after(async () => {
755
+ await new Promise((resolve, reject) =>
756
+ server.close((error) => (error ? reject(error) : resolve())),
757
+ );
758
+ });
759
+
760
+ const result = await new Promise((resolve, reject) => {
761
+ const child = spawn(
762
+ process.execPath,
763
+ [
764
+ scriptPath.pathname,
765
+ "login",
766
+ "--email",
767
+ "person@example.com",
768
+ "--no-launch-browser",
769
+ ],
770
+ {
771
+ cwd: tempDir,
772
+ env: {
773
+ ...process.env,
774
+ ZENOCODE_HOME: zenocodeHome,
775
+ ZENOCODE_NO_BANNER: "1",
776
+ ZENOCODE_OPENCODE_BIN_PATH: fakeRuntimePath,
777
+ TEXTCORTEX_BASE_URL: baseUrl,
778
+ RUNTIME_LOG_PATH: runtimeLogPath,
779
+ },
780
+ stdio: ["ignore", "pipe", "pipe"],
781
+ },
782
+ );
783
+ let stdout = "";
784
+ let stderr = "";
785
+ child.stdout.on("data", (chunk) => {
786
+ stdout += String(chunk);
787
+ });
788
+ child.stderr.on("data", (chunk) => {
789
+ stderr += String(chunk);
790
+ });
791
+ child.on("error", reject);
792
+ child.on("exit", (code, signal) => resolve({ code, signal, stdout, stderr }));
793
+ });
794
+
795
+ assert.equal(result.code, 0);
796
+ assert.match(result.stdout, /Login successful for auth_123/);
797
+ assert.match(result.stdout, /Zenocode config ready at/);
798
+
799
+ const runtimeInvocation = JSON.parse(await fs.readFile(runtimeLogPath, "utf-8"));
800
+ assert.deepEqual(runtimeInvocation.args, []);
801
+ assert.equal(runtimeInvocation.env.TEXTCORTEX_API_KEY, "fresh-access");
802
+
803
+ const opencodeConfig = JSON.parse(
804
+ await fs.readFile(runtimeInvocation.env.OPENCODE_CONFIG, "utf-8"),
805
+ );
806
+ assert.equal(opencodeConfig.theme, "system");
807
+ assert.equal(opencodeConfig.model, "textcortex/minimax-m3-thinking");
808
+
809
+ const tuiConfig = JSON.parse(
810
+ await fs.readFile(runtimeInvocation.env.OPENCODE_TUI_CONFIG, "utf-8"),
811
+ );
812
+ assert.equal(tuiConfig.theme, "system");
582
813
  });
583
814
 
584
815
  test("logout removes runtime credentials and blocks shared fallback credentials", async (t) => {