@textcortex/zenocode 0.1.10 → 0.1.12

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.10",
3
+ "version": "0.1.12",
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",
@@ -30,6 +30,7 @@
30
30
  "scripts": {
31
31
  "build-branded-opencode": "node scripts/build-branded-opencode.mjs",
32
32
  "prepare-config": "node scripts/run-zenocode.mjs --prepare-only",
33
+ "prepare-release-version": "node scripts/prepare-release-version.mjs",
33
34
  "dev": "node scripts/run-zenocode.mjs",
34
35
  "login": "node scripts/run-zenocode.mjs login",
35
36
  "logout": "node scripts/run-zenocode.mjs logout"
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const defaultPackageJsonPath = path.resolve(__dirname, "..", "package.json");
9
+
10
+ export function parseSemver(value) {
11
+ const match = value.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+.*)?$/);
12
+ if (!match) {
13
+ throw new Error(`Invalid semver: ${value}`);
14
+ }
15
+ return {
16
+ major: Number(match[1]),
17
+ minor: Number(match[2]),
18
+ patch: Number(match[3]),
19
+ prerelease: match[4] ?? null,
20
+ };
21
+ }
22
+
23
+ export function compareSemver(left, right) {
24
+ const lhs = parseSemver(left);
25
+ const rhs = parseSemver(right);
26
+
27
+ for (const key of ["major", "minor", "patch"]) {
28
+ if (lhs[key] > rhs[key]) return 1;
29
+ if (lhs[key] < rhs[key]) return -1;
30
+ }
31
+
32
+ if (lhs.prerelease === rhs.prerelease) return 0;
33
+ if (lhs.prerelease === null) return 1;
34
+ if (rhs.prerelease === null) return -1;
35
+
36
+ const leftParts = lhs.prerelease.split(".");
37
+ const rightParts = rhs.prerelease.split(".");
38
+ const length = Math.max(leftParts.length, rightParts.length);
39
+ for (let index = 0; index < length; index += 1) {
40
+ const leftPart = leftParts[index];
41
+ const rightPart = rightParts[index];
42
+ if (leftPart === undefined) return -1;
43
+ if (rightPart === undefined) return 1;
44
+ const leftNumeric = /^\d+$/.test(leftPart);
45
+ const rightNumeric = /^\d+$/.test(rightPart);
46
+ if (leftNumeric && rightNumeric) {
47
+ const leftValue = Number(leftPart);
48
+ const rightValue = Number(rightPart);
49
+ if (leftValue > rightValue) return 1;
50
+ if (leftValue < rightValue) return -1;
51
+ continue;
52
+ }
53
+ if (leftNumeric && !rightNumeric) return -1;
54
+ if (!leftNumeric && rightNumeric) return 1;
55
+ if (leftPart > rightPart) return 1;
56
+ if (leftPart < rightPart) return -1;
57
+ }
58
+
59
+ return 0;
60
+ }
61
+
62
+ export function incrementPatchVersion(version) {
63
+ const parsed = parseSemver(version);
64
+ return `${parsed.major}.${parsed.minor}.${parsed.patch + 1}`;
65
+ }
66
+
67
+ export function resolveReleaseVersion(localVersion, publishedVersion) {
68
+ if (!publishedVersion) {
69
+ return localVersion;
70
+ }
71
+
72
+ const comparison = compareSemver(localVersion, publishedVersion);
73
+ if (comparison > 0) {
74
+ return localVersion;
75
+ }
76
+
77
+ return incrementPatchVersion(publishedVersion);
78
+ }
79
+
80
+ export async function prepareReleasePackageVersion({
81
+ packageJsonPath = defaultPackageJsonPath,
82
+ publishedVersion = null,
83
+ } = {}) {
84
+ const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf-8"));
85
+ const packageName = String(packageJson.name || "").trim();
86
+ const localVersion = String(packageJson.version || "").trim();
87
+
88
+ if (!packageName) {
89
+ throw new Error(`Missing package name in ${packageJsonPath}`);
90
+ }
91
+ if (!localVersion) {
92
+ throw new Error(`Missing package version in ${packageJsonPath}`);
93
+ }
94
+
95
+ parseSemver(localVersion);
96
+ if (publishedVersion) {
97
+ parseSemver(publishedVersion);
98
+ }
99
+
100
+ const nextVersion = resolveReleaseVersion(localVersion, publishedVersion);
101
+ const updated = nextVersion !== localVersion;
102
+
103
+ if (updated) {
104
+ packageJson.version = nextVersion;
105
+ await fs.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, "utf-8");
106
+ }
107
+
108
+ return {
109
+ packageName,
110
+ localVersion,
111
+ nextVersion,
112
+ publishedVersion,
113
+ updated,
114
+ };
115
+ }
116
+
117
+ export function parseArgs(argv) {
118
+ const options = {
119
+ packageJsonPath: defaultPackageJsonPath,
120
+ publishedVersion: null,
121
+ };
122
+
123
+ for (let index = 0; index < argv.length; index += 1) {
124
+ const arg = argv[index];
125
+ if (arg === "--package-json") {
126
+ if (!argv[index + 1]) {
127
+ throw new Error("--package-json requires a value");
128
+ }
129
+ options.packageJsonPath = path.resolve(argv[index + 1]);
130
+ index += 1;
131
+ continue;
132
+ }
133
+ if (arg === "--published-version") {
134
+ options.publishedVersion = (argv[index + 1] || "").trim() || null;
135
+ index += 1;
136
+ continue;
137
+ }
138
+ throw new Error(`Unknown argument: ${arg}`);
139
+ }
140
+
141
+ return options;
142
+ }
143
+
144
+ async function main() {
145
+ const result = await prepareReleasePackageVersion(parseArgs(process.argv.slice(2)));
146
+ console.log(
147
+ JSON.stringify(
148
+ {
149
+ package_name: result.packageName,
150
+ local_version: result.localVersion,
151
+ next_version: result.nextVersion,
152
+ published_version: result.publishedVersion,
153
+ updated: result.updated,
154
+ },
155
+ null,
156
+ 2,
157
+ ),
158
+ );
159
+ }
160
+
161
+ const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : "";
162
+ if (invokedPath === fileURLToPath(import.meta.url)) {
163
+ await main();
164
+ }
@@ -0,0 +1,96 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+
7
+ import {
8
+ parseArgs,
9
+ prepareReleasePackageVersion,
10
+ resolveReleaseVersion,
11
+ } from "./prepare-release-version.mjs";
12
+
13
+ test("resolveReleaseVersion keeps the local version when nothing is published yet", () => {
14
+ assert.equal(resolveReleaseVersion("0.1.10", null), "0.1.10");
15
+ });
16
+
17
+ test("resolveReleaseVersion bumps the patch version when the local version matches the published version", () => {
18
+ assert.equal(resolveReleaseVersion("0.1.10", "0.1.10"), "0.1.11");
19
+ });
20
+
21
+ test("resolveReleaseVersion bumps from the published version when the local version is behind", () => {
22
+ assert.equal(resolveReleaseVersion("0.1.9", "0.1.10"), "0.1.11");
23
+ });
24
+
25
+ test("resolveReleaseVersion keeps an already-ahead local version", () => {
26
+ assert.equal(resolveReleaseVersion("0.1.11", "0.1.10"), "0.1.11");
27
+ });
28
+
29
+ test("resolveReleaseVersion rejects invalid semver values", () => {
30
+ assert.throws(() => resolveReleaseVersion("invalid", "0.1.10"), /Invalid semver/);
31
+ });
32
+
33
+ test("prepareReleasePackageVersion rewrites package.json when an automatic bump is required", async () => {
34
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "zenocode-version-"));
35
+ const packageJsonPath = path.join(tempDir, "package.json");
36
+ await fs.writeFile(
37
+ packageJsonPath,
38
+ `${JSON.stringify(
39
+ {
40
+ name: "@textcortex/zenocode",
41
+ version: "0.1.10",
42
+ },
43
+ null,
44
+ 2,
45
+ )}\n`,
46
+ "utf-8",
47
+ );
48
+
49
+ const result = await prepareReleasePackageVersion({
50
+ packageJsonPath,
51
+ publishedVersion: "0.1.10",
52
+ });
53
+
54
+ const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf-8"));
55
+ assert.deepEqual(result, {
56
+ packageName: "@textcortex/zenocode",
57
+ localVersion: "0.1.10",
58
+ nextVersion: "0.1.11",
59
+ publishedVersion: "0.1.10",
60
+ updated: true,
61
+ });
62
+ assert.equal(packageJson.version, "0.1.11");
63
+ });
64
+
65
+ test("prepareReleasePackageVersion leaves package.json untouched when the local version is already ahead", async () => {
66
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "zenocode-version-"));
67
+ const packageJsonPath = path.join(tempDir, "package.json");
68
+ const originalContents = `${JSON.stringify(
69
+ {
70
+ name: "@textcortex/zenocode",
71
+ version: "0.1.11",
72
+ },
73
+ null,
74
+ 2,
75
+ )}\n`;
76
+ await fs.writeFile(packageJsonPath, originalContents, "utf-8");
77
+
78
+ const result = await prepareReleasePackageVersion({
79
+ packageJsonPath,
80
+ publishedVersion: "0.1.10",
81
+ });
82
+
83
+ const packageJson = await fs.readFile(packageJsonPath, "utf-8");
84
+ assert.deepEqual(result, {
85
+ packageName: "@textcortex/zenocode",
86
+ localVersion: "0.1.11",
87
+ nextVersion: "0.1.11",
88
+ publishedVersion: "0.1.10",
89
+ updated: false,
90
+ });
91
+ assert.equal(packageJson, originalContents);
92
+ });
93
+
94
+ test("parseArgs requires a value after --package-json", () => {
95
+ assert.throws(() => parseArgs(["--package-json"]), /--package-json requires a value/);
96
+ });
@@ -29,6 +29,7 @@ const legacyRuntimeCredentialsPath = path.join(
29
29
  const logoutMarkerPath = path.join(runtimeDir, "logout-marker.json");
30
30
  const modelsPath = path.join(runtimeDir, "models.json");
31
31
  const configPath = path.join(runtimeDir, "opencode.jsonc");
32
+ const tuiConfigPath = path.join(runtimeDir, "opencode.tui.json");
32
33
  const localBaseUrlDefault = "http://127.0.0.1:8080";
33
34
  const cloudBaseUrlDefault = "https://api.textcortex.com";
34
35
  const localBaseUrlFlags = new Set(["--local", "--localhost"]);
@@ -394,6 +395,8 @@ export function buildOpenCodeConfig({ baseUrl, providerID, model, smallModel })
394
395
  return {
395
396
  $schema: "https://opencode.ai/config.json",
396
397
  enabled_providers: [providerID],
398
+ // Keep the legacy theme key populated for older OpenCode builds.
399
+ theme: "system",
397
400
  model: `${providerID}/${model}`,
398
401
  small_model: `${providerID}/${smallModel}`,
399
402
  provider: {
@@ -415,6 +418,13 @@ export function buildOpenCodeConfig({ baseUrl, providerID, model, smallModel })
415
418
  };
416
419
  }
417
420
 
421
+ export function buildOpenCodeTuiConfig() {
422
+ return {
423
+ $schema: "https://opencode.ai/tui.json",
424
+ theme: "system",
425
+ };
426
+ }
427
+
418
428
  function unwrapData(payload) {
419
429
  if (payload && typeof payload === "object" && payload.data && typeof payload.data === "object") {
420
430
  return payload.data;
@@ -482,10 +492,12 @@ async function prepareRuntime(baseUrl, token) {
482
492
  model,
483
493
  smallModel,
484
494
  });
495
+ const tuiConfig = buildOpenCodeTuiConfig();
485
496
 
486
497
  await fs.mkdir(runtimeDir, { recursive: true });
487
498
  await fs.writeFile(modelsPath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
488
499
  await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
500
+ await fs.writeFile(tuiConfigPath, `${JSON.stringify(tuiConfig, null, 2)}\n`, "utf-8");
489
501
  return model;
490
502
  }
491
503
 
@@ -576,6 +588,16 @@ export function resolveTextCortexBaseUrl({
576
588
  return envBaseUrl || storedBaseUrl || cloudBaseUrlDefault;
577
589
  }
578
590
 
591
+ export function resolveLoginBaseUrl({
592
+ envBaseUrl,
593
+ preferLocalhost = false,
594
+ } = {}) {
595
+ if (preferLocalhost) {
596
+ return localBaseUrlDefault;
597
+ }
598
+ return envBaseUrl || cloudBaseUrlDefault;
599
+ }
600
+
579
601
  function _loginConnectivityHelp(baseUrl) {
580
602
  return [
581
603
  `Cannot reach Zenocode auth endpoint at ${baseUrl}.`,
@@ -1568,7 +1590,7 @@ async function main() {
1568
1590
  }
1569
1591
 
1570
1592
  const preferLocalhost = hasLocalBaseUrlFlag(passthrough);
1571
- const runtimeArgs = stripLocalBaseUrlFlags(passthrough);
1593
+ let runtimeArgs = stripLocalBaseUrlFlags(passthrough);
1572
1594
  const storedBaseUrl = await resolveStoredBaseUrl();
1573
1595
  const baseUrl = resolveTextCortexBaseUrl({
1574
1596
  envBaseUrl: process.env.TEXTCORTEX_BASE_URL,
@@ -1578,8 +1600,15 @@ async function main() {
1578
1600
  const subcommand = runtimeArgs[0];
1579
1601
 
1580
1602
  if (subcommand === "login") {
1581
- await runLoginCommand(baseUrl, runtimeArgs.slice(1), { preferLocalhost });
1582
- return;
1603
+ await runLoginCommand(
1604
+ resolveLoginBaseUrl({
1605
+ envBaseUrl: process.env.TEXTCORTEX_BASE_URL,
1606
+ preferLocalhost,
1607
+ }),
1608
+ runtimeArgs.slice(1),
1609
+ { preferLocalhost },
1610
+ );
1611
+ runtimeArgs = [];
1583
1612
  }
1584
1613
 
1585
1614
  if (subcommand === "logout") {
@@ -1609,6 +1638,7 @@ async function main() {
1609
1638
  ...process.env,
1610
1639
  OPENCODE_MODELS_PATH: modelsPath,
1611
1640
  OPENCODE_CONFIG: configPath,
1641
+ OPENCODE_TUI_CONFIG: tuiConfigPath,
1612
1642
  TEXTCORTEX_API_KEY: token,
1613
1643
  },
1614
1644
  };
@@ -16,6 +16,7 @@ import {
16
16
  buildZenocodeBanner,
17
17
  chooseDefaults,
18
18
  hasLocalBaseUrlFlag,
19
+ resolveLoginBaseUrl,
19
20
  resolveLoginSuccessIdentifier,
20
21
  resolveTextCortexBaseUrl,
21
22
  runRuntimeWithSessionRecovery,
@@ -67,6 +68,34 @@ test("resolveTextCortexBaseUrl prefers localhost when the local flag is enabled"
67
68
  );
68
69
  });
69
70
 
71
+ test("resolveLoginBaseUrl ignores stored credentials and defaults explicit login to cloud", () => {
72
+ assert.equal(
73
+ resolveLoginBaseUrl({
74
+ envBaseUrl: "",
75
+ }),
76
+ "https://api.textcortex.com",
77
+ );
78
+ });
79
+
80
+ test("resolveLoginBaseUrl still respects explicit env overrides", () => {
81
+ assert.equal(
82
+ resolveLoginBaseUrl({
83
+ envBaseUrl: "https://staging.textcortex.com",
84
+ }),
85
+ "https://staging.textcortex.com",
86
+ );
87
+ });
88
+
89
+ test("resolveLoginBaseUrl prefers localhost when the local flag is enabled", () => {
90
+ assert.equal(
91
+ resolveLoginBaseUrl({
92
+ envBaseUrl: "https://staging.textcortex.com",
93
+ preferLocalhost: true,
94
+ }),
95
+ "http://127.0.0.1:8080",
96
+ );
97
+ });
98
+
70
99
  test("hasLocalBaseUrlFlag detects localhost flags", () => {
71
100
  assert.equal(hasLocalBaseUrlFlag(["login", "--local"]), true);
72
101
  assert.equal(hasLocalBaseUrlFlag(["--localhost", "run"]), true);
@@ -105,6 +134,7 @@ test("buildOpenCodeConfig includes an openai stub for fallback runtime auth plug
105
134
 
106
135
  assert.deepEqual(config.enabled_providers, ["textcortex"]);
107
136
  assert.equal(config.model, "textcortex/kimi-k2-5-thinking");
137
+ assert.equal(config.theme, "system");
108
138
  assert.ok(config.provider.openai);
109
139
  assert.deepEqual(config.provider.openai.models, {});
110
140
  });
@@ -552,6 +582,167 @@ test("prepare-only refreshes stored Zenocode credentials when the access token h
552
582
  assert.equal(savedCredentials.refresh_token, "fresh-refresh");
553
583
  });
554
584
 
585
+ test("login launches the runtime immediately with system TUI theming", async (t) => {
586
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "zenocode-login-"));
587
+ const zenocodeHome = path.join(tempDir, ".zenocode");
588
+ const runtimeLogPath = path.join(tempDir, "runtime-log.json");
589
+ const fakeRuntimePath = path.join(tempDir, "fake-opencode");
590
+ const scriptPath = new URL("./run-zenocode.mjs", import.meta.url);
591
+
592
+ t.after(async () => {
593
+ await fs.rm(tempDir, { recursive: true, force: true });
594
+ });
595
+
596
+ await fs.mkdir(zenocodeHome, { recursive: true });
597
+ await fs.writeFile(
598
+ fakeRuntimePath,
599
+ `#!/usr/bin/env node
600
+ const fs = require("node:fs/promises");
601
+
602
+ async function main() {
603
+ const payload = {
604
+ args: process.argv.slice(2),
605
+ env: {
606
+ OPENCODE_CONFIG: process.env.OPENCODE_CONFIG,
607
+ OPENCODE_TUI_CONFIG: process.env.OPENCODE_TUI_CONFIG,
608
+ TEXTCORTEX_API_KEY: process.env.TEXTCORTEX_API_KEY,
609
+ },
610
+ };
611
+ await fs.writeFile(process.env.RUNTIME_LOG_PATH, JSON.stringify(payload), "utf-8");
612
+ }
613
+
614
+ main().catch((error) => {
615
+ console.error(error instanceof Error ? error.message : String(error));
616
+ process.exit(1);
617
+ });
618
+ `,
619
+ "utf-8",
620
+ );
621
+ await fs.chmod(fakeRuntimePath, 0o755);
622
+
623
+ const server = http.createServer((req, res) => {
624
+ if (
625
+ req.method === "POST" &&
626
+ req.url === "/internal/v1/fastapi/zenocode/oauth2/initiate"
627
+ ) {
628
+ res.writeHead(200, { "Content-Type": "application/json" });
629
+ res.end(
630
+ JSON.stringify({
631
+ data: {
632
+ device_code: "device-code",
633
+ user_code: "ABCD-1234",
634
+ verification_url_complete: "https://textcortex.example/verify",
635
+ interval: 0,
636
+ expires_in: 30,
637
+ },
638
+ }),
639
+ );
640
+ return;
641
+ }
642
+
643
+ if (
644
+ req.method === "POST" &&
645
+ req.url === "/internal/v1/fastapi/zenocode/oauth2/token"
646
+ ) {
647
+ res.writeHead(200, { "Content-Type": "application/json" });
648
+ res.end(
649
+ JSON.stringify({
650
+ data: {
651
+ access_token: "fresh-access",
652
+ refresh_token: "fresh-refresh",
653
+ auth_id: "auth_123",
654
+ },
655
+ }),
656
+ );
657
+ return;
658
+ }
659
+
660
+ if (
661
+ req.method === "GET" &&
662
+ req.url === "/internal/v1/fastapi/zenocode/models/api.json"
663
+ ) {
664
+ res.writeHead(200, { "Content-Type": "application/json" });
665
+ res.end(
666
+ JSON.stringify({
667
+ textcortex: {
668
+ models: {
669
+ "kimi-k2-5-thinking": {},
670
+ "glm-5": {},
671
+ },
672
+ },
673
+ }),
674
+ );
675
+ return;
676
+ }
677
+
678
+ res.writeHead(404, { "Content-Type": "application/json" });
679
+ res.end(JSON.stringify({ detail: "not found" }));
680
+ });
681
+
682
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
683
+ const address = server.address();
684
+ const baseUrl = `http://127.0.0.1:${address.port}`;
685
+
686
+ t.after(async () => {
687
+ await new Promise((resolve, reject) =>
688
+ server.close((error) => (error ? reject(error) : resolve())),
689
+ );
690
+ });
691
+
692
+ const result = await new Promise((resolve, reject) => {
693
+ const child = spawn(
694
+ process.execPath,
695
+ [
696
+ scriptPath.pathname,
697
+ "login",
698
+ "--email",
699
+ "person@example.com",
700
+ "--no-launch-browser",
701
+ ],
702
+ {
703
+ cwd: tempDir,
704
+ env: {
705
+ ...process.env,
706
+ ZENOCODE_HOME: zenocodeHome,
707
+ ZENOCODE_NO_BANNER: "1",
708
+ ZENOCODE_OPENCODE_BIN_PATH: fakeRuntimePath,
709
+ TEXTCORTEX_BASE_URL: baseUrl,
710
+ RUNTIME_LOG_PATH: runtimeLogPath,
711
+ },
712
+ stdio: ["ignore", "pipe", "pipe"],
713
+ },
714
+ );
715
+ let stdout = "";
716
+ let stderr = "";
717
+ child.stdout.on("data", (chunk) => {
718
+ stdout += String(chunk);
719
+ });
720
+ child.stderr.on("data", (chunk) => {
721
+ stderr += String(chunk);
722
+ });
723
+ child.on("error", reject);
724
+ child.on("exit", (code, signal) => resolve({ code, signal, stdout, stderr }));
725
+ });
726
+
727
+ assert.equal(result.code, 0);
728
+ assert.match(result.stdout, /Login successful for auth_123/);
729
+ assert.match(result.stdout, /Zenocode config ready at/);
730
+
731
+ const runtimeInvocation = JSON.parse(await fs.readFile(runtimeLogPath, "utf-8"));
732
+ assert.deepEqual(runtimeInvocation.args, []);
733
+ assert.equal(runtimeInvocation.env.TEXTCORTEX_API_KEY, "fresh-access");
734
+
735
+ const opencodeConfig = JSON.parse(
736
+ await fs.readFile(runtimeInvocation.env.OPENCODE_CONFIG, "utf-8"),
737
+ );
738
+ assert.equal(opencodeConfig.theme, "system");
739
+
740
+ const tuiConfig = JSON.parse(
741
+ await fs.readFile(runtimeInvocation.env.OPENCODE_TUI_CONFIG, "utf-8"),
742
+ );
743
+ assert.equal(tuiConfig.theme, "system");
744
+ });
745
+
555
746
  test("logout removes runtime credentials and blocks shared fallback credentials", async (t) => {
556
747
  const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "zenocode-logout-"));
557
748
  const zenocodeHome = path.join(tempDir, ".zenocode");