@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.
|
|
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
|
+
});
|
package/scripts/run-zenocode.mjs
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
1582
|
-
|
|
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");
|