@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 +2 -1
- package/package.json +1 -1
- package/scripts/branding-patch.mjs +35 -1
- package/scripts/branding-patch.test.mjs +34 -1
- package/scripts/build-branded-opencode.mjs +50 -8
- package/scripts/build-branded-opencode.test.mjs +24 -0
- package/scripts/opencode-version.mjs +4 -0
- package/scripts/run-zenocode.mjs +216 -73
- package/scripts/run-zenocode.test.mjs +241 -10
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
|
@@ -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
|
-
|
|
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 {
|
|
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
|
-
|
|
20
|
-
const forkRef = (
|
|
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
|
|
49
|
+
const configuredBuildArgs = (
|
|
40
50
|
process.env.ZENOCODE_OPENCODE_BUILD_ARGS ||
|
|
41
51
|
process.env.CODECORTEX_OPENCODE_BUILD_ARGS ||
|
|
42
52
|
""
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
.split(/\s+/)
|
|
46
|
-
|
|
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
|
-
{
|
|
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"),
|
package/scripts/run-zenocode.mjs
CHANGED
|
@@ -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 =
|
|
47
|
+
const defaultBrandedOpencodePackage = openCodePackageSpec;
|
|
43
48
|
const legacyBrandedOpencodePackage = "@textcortex/opencode-ai";
|
|
44
|
-
const fallbackOpencodePackage =
|
|
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/
|
|
51
|
-
const oauthTokenPath = "/internal/
|
|
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/
|
|
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/
|
|
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
|
-
|
|
948
|
-
|
|
949
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
30
|
+
test("chooseDefaults prefers MiniMax M3 thinking for Zenocode", () => {
|
|
28
31
|
const defaults = chooseDefaults({
|
|
29
|
-
"glm-5": {},
|
|
30
|
-
"kimi-k2-
|
|
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: "
|
|
36
|
-
smallModel: "
|
|
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: "
|
|
132
|
-
smallModel: "
|
|
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/
|
|
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/
|
|
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) => {
|