@textcortex/zenocode 0.1.12 → 0.1.14
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 +13 -3
- package/package.json +8 -3
- package/scripts/branding-patch.mjs +35 -1
- package/scripts/branding-patch.test.mjs +34 -1
- package/scripts/build-branded-opencode.mjs +149 -16
- package/scripts/build-branded-opencode.test.mjs +58 -1
- package/scripts/opencode-version.mjs +4 -0
- package/scripts/run-zenocode.mjs +211 -72
- package/scripts/run-zenocode.test.mjs +146 -14
package/README.md
CHANGED
|
@@ -4,6 +4,12 @@ Zenocode is the secure coding agent for TextCortex customers. It runs in your te
|
|
|
4
4
|
|
|
5
5
|
Built for teams that need speed, control, and trust, Zenocode combines a terminal-native coding workflow with workspace routing and enterprise security from day one.
|
|
6
6
|
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
Zenocode is available on the **pay-as-you-go (PAYG)** plan. Your TextCortex account — or the organization seat assigned to you — must be on PAYG. On any other plan (Free, subscription, Business, Unlimited), `zenocode login` and model discovery return **"Zenocode is only available on the pay-as-you-go plan."** Switch your account (or ask your workspace admin to assign you a PAYG seat) before installing.
|
|
10
|
+
|
|
11
|
+
If you already use pay-as-you-go and just ran out of wallet credits, top up under **Settings → Billing** (`/user/dashboard/settings/billing`). If you are not on pay-as-you-go yet, pick a plan on the **plan selection** page (`/billing/plans`). The in-app Zenocode dialog (sidebar → Zenocode) links you to the right one automatically based on your account.
|
|
12
|
+
|
|
7
13
|
## What This Package Does
|
|
8
14
|
|
|
9
15
|
- Launches Zenocode from your terminal with the `zenocode` or `codecortex` command
|
|
@@ -31,13 +37,17 @@ The first `zenocode login` launches Zenocode automatically after browser authent
|
|
|
31
37
|
|
|
32
38
|
If you already have an API key, you can also start Zenocode by setting `TEXTCORTEX_API_KEY` or `TEXTCORTEX_API_TOKEN`.
|
|
33
39
|
|
|
40
|
+
## Models
|
|
41
|
+
|
|
42
|
+
Zenocode uses **GPT-5.6 Luna** by default. You can switch to any other model available to your TextCortex workspace from the model picker inside the agent. Available models are discovered automatically from your account, so the default falls back to the next best coding model (for example Kimi K2.7 Code) when Luna is not available to you.
|
|
43
|
+
|
|
34
44
|
## Built For Security And Compliance
|
|
35
45
|
|
|
36
|
-
Zenocode is built for organizations that treat security, compliance, and data
|
|
46
|
+
Zenocode is built for organizations that treat security, compliance, and data governance as non-negotiable.
|
|
37
47
|
|
|
38
|
-
-
|
|
48
|
+
- TextCortex-managed routing with workspace-aware model availability
|
|
39
49
|
- SOC 2 and ISO 27001 certified
|
|
40
|
-
-
|
|
50
|
+
- Model region metadata is surfaced for routing and governance decisions
|
|
41
51
|
- Zero data training: your data is not used for model training or fine-tuning
|
|
42
52
|
- Optional services or LLMs hosted outside the EU are covered by the EU-US Data Privacy Framework or GDPR Standard Contractual Clauses
|
|
43
53
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@textcortex/zenocode",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Secure,
|
|
3
|
+
"version": "0.1.14",
|
|
4
|
+
"description": "Secure, TextCortex-managed coding agent for customers that runs in your terminal, edits files, runs scripts, and more.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
7
7
|
"agent",
|
|
@@ -11,12 +11,17 @@
|
|
|
11
11
|
"developer-tools",
|
|
12
12
|
"textcortex",
|
|
13
13
|
"secure",
|
|
14
|
-
"
|
|
14
|
+
"enterprise",
|
|
15
15
|
"gdpr"
|
|
16
16
|
],
|
|
17
17
|
"private": false,
|
|
18
18
|
"license": "UNLICENSED",
|
|
19
19
|
"type": "module",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/textcortex/platform.git",
|
|
23
|
+
"directory": "apps/zenocode"
|
|
24
|
+
},
|
|
20
25
|
"publishConfig": {
|
|
21
26
|
"access": "public"
|
|
22
27
|
},
|
|
@@ -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,31 @@ 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
|
+
export const trustedPublishingRepository = {
|
|
21
|
+
type: "git",
|
|
22
|
+
url: "git+https://github.com/textcortex/platform.git",
|
|
23
|
+
};
|
|
24
|
+
const displayNameOnlyBrandingRefs = new Set([defaultOpenCodeRef]);
|
|
15
25
|
|
|
16
26
|
const forkUrl =
|
|
17
27
|
process.env.ZENOCODE_OPENCODE_FORK_URL ||
|
|
18
28
|
process.env.CODECORTEX_OPENCODE_FORK_URL ||
|
|
19
|
-
|
|
20
|
-
const forkRef = (
|
|
29
|
+
defaultOpenCodeForkUrl;
|
|
30
|
+
const forkRef = (
|
|
31
|
+
process.env.ZENOCODE_OPENCODE_REF ||
|
|
32
|
+
process.env.CODECORTEX_OPENCODE_REF ||
|
|
33
|
+
defaultOpenCodeRef
|
|
34
|
+
).trim();
|
|
21
35
|
const outputDir =
|
|
22
36
|
process.env.ZENOCODE_BRANDED_OUTPUT_DIR ||
|
|
23
37
|
process.env.CODECORTEX_BRANDED_OUTPUT_DIR ||
|
|
@@ -36,16 +50,27 @@ const publishTag =
|
|
|
36
50
|
process.env.ZENOCODE_PUBLISH_TAG ||
|
|
37
51
|
process.env.CODECORTEX_PUBLISH_TAG ||
|
|
38
52
|
"latest";
|
|
39
|
-
const
|
|
53
|
+
const publishProvenance =
|
|
54
|
+
(process.env.ZENOCODE_NPM_PROVENANCE || process.env.CODECORTEX_NPM_PROVENANCE) === "1";
|
|
55
|
+
const configuredBuildArgs = (
|
|
40
56
|
process.env.ZENOCODE_OPENCODE_BUILD_ARGS ||
|
|
41
57
|
process.env.CODECORTEX_OPENCODE_BUILD_ARGS ||
|
|
42
58
|
""
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
.split(/\s+/)
|
|
46
|
-
|
|
59
|
+
).trim();
|
|
60
|
+
const buildArgs = configuredBuildArgs
|
|
61
|
+
? configuredBuildArgs.split(/\s+/).filter(Boolean)
|
|
62
|
+
: defaultOpenCodeBuildArgs;
|
|
47
63
|
const cliArgs = process.argv.slice(2);
|
|
48
64
|
|
|
65
|
+
export function deriveOpenCodeVersionFromRef(ref, fallbackVersion = defaultOpenCodeVersion) {
|
|
66
|
+
const match = ref.match(/^v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)(?:\+.*)?$/);
|
|
67
|
+
return match?.[1] || fallbackVersion;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function allowsDisplayNameOnlyBranding(ref) {
|
|
71
|
+
return displayNameOnlyBrandingRefs.has(ref);
|
|
72
|
+
}
|
|
73
|
+
|
|
49
74
|
function _command(name) {
|
|
50
75
|
if (process.platform === "win32" && ["npm", "pnpm", "npx"].includes(name)) {
|
|
51
76
|
return `${name}.cmd`;
|
|
@@ -207,6 +232,20 @@ async function writeJson(filePath, payload) {
|
|
|
207
232
|
await fs.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
|
|
208
233
|
}
|
|
209
234
|
|
|
235
|
+
export function selectLatestTarball(entries) {
|
|
236
|
+
const tarballs = entries.filter((entry) => entry.endsWith(".tgz")).sort((left, right) => {
|
|
237
|
+
const leftVersion = left.match(/-(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)(?:\+.*)?\.tgz$/)?.[1];
|
|
238
|
+
const rightVersion = right.match(/-(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)(?:\+.*)?\.tgz$/)?.[1];
|
|
239
|
+
if (leftVersion && rightVersion) {
|
|
240
|
+
return compareSemver(leftVersion, rightVersion);
|
|
241
|
+
}
|
|
242
|
+
if (leftVersion) return 1;
|
|
243
|
+
if (rightVersion) return -1;
|
|
244
|
+
return left.localeCompare(right);
|
|
245
|
+
});
|
|
246
|
+
return tarballs[tarballs.length - 1] || null;
|
|
247
|
+
}
|
|
248
|
+
|
|
210
249
|
async function maybePublishedVersion(packageName) {
|
|
211
250
|
const npmCommand = _command("npm");
|
|
212
251
|
try {
|
|
@@ -230,15 +269,18 @@ async function maybePublishedVersion(packageName) {
|
|
|
230
269
|
async function packPackage(packageDir) {
|
|
231
270
|
const npmCommand = _command("npm");
|
|
232
271
|
await run(npmCommand, ["pack"], { cwd: packageDir });
|
|
233
|
-
const
|
|
234
|
-
if (!
|
|
272
|
+
const tarball = selectLatestTarball(await fs.readdir(packageDir));
|
|
273
|
+
if (!tarball) {
|
|
235
274
|
throw new Error(`npm pack did not produce a tarball in ${packageDir}`);
|
|
236
275
|
}
|
|
237
|
-
return path.join(packageDir,
|
|
276
|
+
return path.join(packageDir, tarball);
|
|
238
277
|
}
|
|
239
278
|
|
|
240
|
-
export function buildPublishCommandArgs(tarballPath, { tag } = {}) {
|
|
279
|
+
export function buildPublishCommandArgs(tarballPath, { tag, provenance = false } = {}) {
|
|
241
280
|
const args = ["publish", tarballPath, "--access", "public"];
|
|
281
|
+
if (provenance) {
|
|
282
|
+
args.push("--provenance");
|
|
283
|
+
}
|
|
242
284
|
if (tag) {
|
|
243
285
|
args.push("--tag", tag);
|
|
244
286
|
}
|
|
@@ -262,14 +304,79 @@ async function publishTarballIfNeeded(packageName, version, tarballPath, cwd) {
|
|
|
262
304
|
}
|
|
263
305
|
|
|
264
306
|
const npmCommand = _command("npm");
|
|
265
|
-
await run(npmCommand, buildPublishCommandArgs(tarballPath, { tag: publishTag }), { cwd });
|
|
307
|
+
await run(npmCommand, buildPublishCommandArgs(tarballPath, { tag: publishTag, provenance: publishProvenance }), { cwd });
|
|
266
308
|
return { published: true, skipped: false };
|
|
267
309
|
}
|
|
268
310
|
|
|
311
|
+
async function readPublishTarget(packageDir) {
|
|
312
|
+
const pkg = await readJson(path.join(packageDir, "package.json"));
|
|
313
|
+
if (typeof pkg?.name !== "string" || typeof pkg?.version !== "string") {
|
|
314
|
+
throw new Error(`Invalid package metadata in ${packageDir}`);
|
|
315
|
+
}
|
|
316
|
+
const tarball = selectLatestTarball(await fs.readdir(packageDir));
|
|
317
|
+
if (!tarball) {
|
|
318
|
+
throw new Error(`Missing npm tarball in ${packageDir}`);
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
packageName: pkg.name,
|
|
322
|
+
version: pkg.version,
|
|
323
|
+
tarballPath: path.join(packageDir, tarball),
|
|
324
|
+
cwd: packageDir,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function publishExistingPackages(artifactDir) {
|
|
329
|
+
const packageRoot = path.join(artifactDir, "npm-binaries");
|
|
330
|
+
const entries = await fs.readdir(packageRoot, { withFileTypes: true });
|
|
331
|
+
const binaryPackageDirs = entries
|
|
332
|
+
.filter((entry) => entry.isDirectory())
|
|
333
|
+
.map((entry) => path.join(packageRoot, entry.name))
|
|
334
|
+
.sort();
|
|
335
|
+
if (!binaryPackageDirs.length) {
|
|
336
|
+
throw new Error(`No existing binary packages found under ${packageRoot}`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const targets = [];
|
|
340
|
+
for (const packageDir of binaryPackageDirs) {
|
|
341
|
+
targets.push(await readPublishTarget(packageDir));
|
|
342
|
+
}
|
|
343
|
+
targets.push(await readPublishTarget(path.join(artifactDir, "npm-package")));
|
|
344
|
+
|
|
345
|
+
const published = [];
|
|
346
|
+
const skipped = [];
|
|
347
|
+
for (const target of targets) {
|
|
348
|
+
const result = await publishTarballIfNeeded(
|
|
349
|
+
target.packageName,
|
|
350
|
+
target.version,
|
|
351
|
+
target.tarballPath,
|
|
352
|
+
target.cwd,
|
|
353
|
+
);
|
|
354
|
+
if (result.published) {
|
|
355
|
+
published.push(target.packageName);
|
|
356
|
+
} else if (result.skipped) {
|
|
357
|
+
skipped.push(target.packageName);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (published.length) {
|
|
362
|
+
console.log(`Published packages: ${published.join(", ")}`);
|
|
363
|
+
}
|
|
364
|
+
if (skipped.length) {
|
|
365
|
+
console.log(`Skipped already-published packages: ${skipped.join(", ")}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
269
369
|
function _binaryFilename() {
|
|
270
370
|
return process.platform === "win32" ? "opencode.exe" : "opencode";
|
|
271
371
|
}
|
|
272
372
|
|
|
373
|
+
async function adHocSignBinary(binaryPath) {
|
|
374
|
+
if (process.platform !== "darwin") return;
|
|
375
|
+
await run("codesign", ["--force", "--sign", "-", binaryPath], {
|
|
376
|
+
stdio: "ignore",
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
273
380
|
async function patchBinaryAtPath(binaryPath) {
|
|
274
381
|
const buffer = await fs.readFile(binaryPath);
|
|
275
382
|
const originalLength = buffer.length;
|
|
@@ -277,6 +384,12 @@ async function patchBinaryAtPath(binaryPath) {
|
|
|
277
384
|
if (!patch.patched) {
|
|
278
385
|
throw new Error(`Branding patch did not match binary ${binaryPath}`);
|
|
279
386
|
}
|
|
387
|
+
const patchedMandatoryBranding = patch.patches.logo || patch.patches.footer;
|
|
388
|
+
if (!patchedMandatoryBranding && !allowsDisplayNameOnlyBranding(forkRef)) {
|
|
389
|
+
throw new Error(
|
|
390
|
+
`Branding patch only matched display names for ${binaryPath}; update the logo/footer patch signatures before building ${forkRef}.`,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
280
393
|
const nextBuffer = Buffer.from(patch.text, "latin1");
|
|
281
394
|
if (nextBuffer.length !== originalLength) {
|
|
282
395
|
throw new Error(`Branding patch changed binary length for ${binaryPath}`);
|
|
@@ -285,6 +398,7 @@ async function patchBinaryAtPath(binaryPath) {
|
|
|
285
398
|
if (process.platform !== "win32") {
|
|
286
399
|
await fs.chmod(binaryPath, 0o755);
|
|
287
400
|
}
|
|
401
|
+
await adHocSignBinary(binaryPath);
|
|
288
402
|
}
|
|
289
403
|
|
|
290
404
|
function _buildWrapperExecutable({ runtimePackageName, binName }) {
|
|
@@ -501,6 +615,7 @@ async function buildWrapperPackage({
|
|
|
501
615
|
version,
|
|
502
616
|
license: "MIT",
|
|
503
617
|
type: "commonjs",
|
|
618
|
+
repository: trustedPublishingRepository,
|
|
504
619
|
bin: buildWrapperBinMap(packageName, binName),
|
|
505
620
|
optionalDependencies: Object.fromEntries(
|
|
506
621
|
binaryPackages.map((pkg) => [pkg.brandedName, pkg.version]),
|
|
@@ -555,6 +670,7 @@ async function prepareBinaryPackages({ distDir, artifactDir, runtimePackageName
|
|
|
555
670
|
const targetDir = path.join(packageRoot, brandedName.replace("/", "__"));
|
|
556
671
|
await fs.cp(sourceDir, targetDir, { recursive: true });
|
|
557
672
|
pkg.name = brandedName;
|
|
673
|
+
pkg.repository = trustedPublishingRepository;
|
|
558
674
|
await writeJson(path.join(targetDir, "package.json"), pkg);
|
|
559
675
|
await patchBinaryAtPath(path.join(targetDir, "bin", _binaryFilename()));
|
|
560
676
|
|
|
@@ -608,21 +724,29 @@ async function main() {
|
|
|
608
724
|
console.log(" ZENOCODE_OPENCODE_BUILD_ARGS Additional args for upstream build script");
|
|
609
725
|
console.log(" ZENOCODE_PUBLISH=1 Publish packages to npm");
|
|
610
726
|
console.log(` ZENOCODE_PUBLISH_TAG npm tag (default: ${publishTag})`);
|
|
727
|
+
console.log(" --publish-existing Publish tarballs from the output directory without rebuilding");
|
|
611
728
|
console.log(" Legacy CODECORTEX_* env names are still accepted for compatibility.");
|
|
612
729
|
return;
|
|
613
730
|
}
|
|
614
731
|
|
|
732
|
+
const npmCommand = _command("npm");
|
|
733
|
+
await requireCommand(npmCommand);
|
|
734
|
+
|
|
735
|
+
const artifactDir = path.resolve(outputDir);
|
|
736
|
+
|
|
737
|
+
if (cliArgs.includes("--publish-existing")) {
|
|
738
|
+
await publishExistingPackages(artifactDir);
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
|
|
615
742
|
const gitCommand = _command("git");
|
|
616
743
|
const bunCommand = _command("bun");
|
|
617
|
-
const npmCommand = _command("npm");
|
|
618
744
|
await requireCommand(gitCommand);
|
|
619
745
|
await requireCommand(bunCommand);
|
|
620
|
-
await requireCommand(npmCommand);
|
|
621
746
|
|
|
622
747
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "zenocode-opencode-"));
|
|
623
748
|
const checkoutDir = path.join(tempRoot, "opencode");
|
|
624
749
|
const distDir = path.join(checkoutDir, "packages", "opencode", "dist");
|
|
625
|
-
const artifactDir = path.resolve(outputDir);
|
|
626
750
|
|
|
627
751
|
try {
|
|
628
752
|
const cloneArgs = ["clone", "--depth=1"];
|
|
@@ -636,7 +760,16 @@ async function main() {
|
|
|
636
760
|
await run(
|
|
637
761
|
bunCommand,
|
|
638
762
|
["./packages/opencode/script/build.ts", "--single", ...buildArgs],
|
|
639
|
-
{
|
|
763
|
+
{
|
|
764
|
+
cwd: checkoutDir,
|
|
765
|
+
env: {
|
|
766
|
+
...process.env,
|
|
767
|
+
OPENCODE_CHANNEL: process.env.OPENCODE_CHANNEL || "latest",
|
|
768
|
+
OPENCODE_VERSION:
|
|
769
|
+
process.env.OPENCODE_VERSION ||
|
|
770
|
+
deriveOpenCodeVersionFromRef(forkRef, defaultOpenCodeVersion),
|
|
771
|
+
},
|
|
772
|
+
},
|
|
640
773
|
);
|
|
641
774
|
|
|
642
775
|
await fs.mkdir(artifactDir, { recursive: true });
|
|
@@ -3,9 +3,35 @@ 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,
|
|
13
|
+
selectLatestTarball,
|
|
14
|
+
trustedPublishingRepository,
|
|
7
15
|
} from "./build-branded-opencode.mjs";
|
|
8
16
|
|
|
17
|
+
test("branded OpenCode build defaults to the latest stable upstream release", () => {
|
|
18
|
+
assert.equal(defaultOpenCodeForkUrl, "https://github.com/sst/opencode");
|
|
19
|
+
assert.equal(defaultOpenCodeRef, "v1.17.6");
|
|
20
|
+
assert.equal(defaultOpenCodeVersion, "1.17.6");
|
|
21
|
+
assert.deepEqual(defaultOpenCodeBuildArgs, ["--skip-install"]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("deriveOpenCodeVersionFromRef follows explicit tag overrides", () => {
|
|
25
|
+
assert.equal(deriveOpenCodeVersionFromRef("v1.17.5"), "1.17.5");
|
|
26
|
+
assert.equal(deriveOpenCodeVersionFromRef("1.18.0-beta.1"), "1.18.0-beta.1");
|
|
27
|
+
assert.equal(deriveOpenCodeVersionFromRef("main", "1.17.4"), "1.17.4");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("display-name-only branding is limited to reviewed upstream refs", () => {
|
|
31
|
+
assert.equal(allowsDisplayNameOnlyBranding(defaultOpenCodeRef), true);
|
|
32
|
+
assert.equal(allowsDisplayNameOnlyBranding("v1.17.7"), false);
|
|
33
|
+
});
|
|
34
|
+
|
|
9
35
|
test("mapBrandedBinaryPackageName scopes runtime binaries under zenocode", () => {
|
|
10
36
|
assert.equal(
|
|
11
37
|
mapBrandedBinaryPackageName("opencode-darwin-arm64", "@textcortex/zenocode-ai"),
|
|
@@ -25,7 +51,26 @@ test("buildWrapperBinMap includes package, opencode, and zenocode entrypoints",
|
|
|
25
51
|
});
|
|
26
52
|
});
|
|
27
53
|
|
|
28
|
-
test("
|
|
54
|
+
test("generated runtime packages point trusted publishing at this repository", () => {
|
|
55
|
+
assert.deepEqual(trustedPublishingRepository, {
|
|
56
|
+
type: "git",
|
|
57
|
+
url: "git+https://github.com/textcortex/platform.git",
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("buildPublishCommandArgs enables npm provenance for trusted publishing", () => {
|
|
62
|
+
assert.deepEqual(buildPublishCommandArgs("package.tgz", { tag: "latest", provenance: true }), [
|
|
63
|
+
"publish",
|
|
64
|
+
"package.tgz",
|
|
65
|
+
"--access",
|
|
66
|
+
"public",
|
|
67
|
+
"--provenance",
|
|
68
|
+
"--tag",
|
|
69
|
+
"latest",
|
|
70
|
+
]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("buildPublishCommandArgs omits npm provenance unless requested", () => {
|
|
29
74
|
assert.deepEqual(buildPublishCommandArgs("package.tgz", { tag: "latest" }), [
|
|
30
75
|
"publish",
|
|
31
76
|
"package.tgz",
|
|
@@ -44,3 +89,15 @@ test("buildPublishCommandArgs omits tag arguments when no tag is provided", () =
|
|
|
44
89
|
"public",
|
|
45
90
|
]);
|
|
46
91
|
});
|
|
92
|
+
|
|
93
|
+
test("selectLatestTarball returns the sorted newest tarball", () => {
|
|
94
|
+
assert.equal(
|
|
95
|
+
selectLatestTarball([
|
|
96
|
+
"README.md",
|
|
97
|
+
"textcortex-zenocode-linux-x64-1.9.0.tgz",
|
|
98
|
+
"textcortex-zenocode-linux-x64-1.10.0.tgz",
|
|
99
|
+
]),
|
|
100
|
+
"textcortex-zenocode-linux-x64-1.10.0.tgz",
|
|
101
|
+
);
|
|
102
|
+
assert.equal(selectLatestTarball(["README.md"]), null);
|
|
103
|
+
});
|
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);
|
|
@@ -40,19 +44,29 @@ const configuredOpencodePackage =
|
|
|
40
44
|
process.env.CODECORTEX_OPENCODE_PACKAGE ||
|
|
41
45
|
process.env.OPENCODE_PACKAGE ||
|
|
42
46
|
null;
|
|
43
|
-
const defaultBrandedOpencodePackage =
|
|
47
|
+
const defaultBrandedOpencodePackage = openCodePackageSpec;
|
|
44
48
|
const legacyBrandedOpencodePackage = "@textcortex/opencode-ai";
|
|
45
|
-
const fallbackOpencodePackage =
|
|
49
|
+
const fallbackOpencodePackage = openCodePackageName;
|
|
46
50
|
const opencodePackage = configuredOpencodePackage || defaultBrandedOpencodePackage;
|
|
47
51
|
const opencodeBinaryPath =
|
|
48
52
|
process.env.ZENOCODE_OPENCODE_BIN_PATH ||
|
|
49
53
|
process.env.CODECORTEX_OPENCODE_BIN_PATH ||
|
|
50
54
|
"";
|
|
51
|
-
const oauthInitiatePath = "/internal/
|
|
52
|
-
const oauthTokenPath = "/internal/
|
|
55
|
+
const oauthInitiatePath = "/internal/v2/fastapi/zenocode/oauth2/initiate";
|
|
56
|
+
const oauthTokenPath = "/internal/v2/fastapi/zenocode/oauth2/token";
|
|
53
57
|
const defaultOrder = [
|
|
58
|
+
"gpt-5-6-luna",
|
|
59
|
+
"kimi-k2p7-code-non-thinking",
|
|
60
|
+
"kimi-k2p7-code-thinking",
|
|
61
|
+
"minimax-m3-thinking",
|
|
62
|
+
"kimi-k2-6-non-thinking",
|
|
63
|
+
"kimi-k2-6-thinking",
|
|
64
|
+
"kimi-k2-5",
|
|
54
65
|
"kimi-k2-5-thinking",
|
|
55
|
-
"glm-5",
|
|
66
|
+
"glm-5-2-non-thinking",
|
|
67
|
+
"glm-5-2-thinking",
|
|
68
|
+
"glm-5-1-non-thinking",
|
|
69
|
+
"glm-5-1-thinking",
|
|
56
70
|
"gpt-5-2",
|
|
57
71
|
"gpt-5-1",
|
|
58
72
|
"gpt-5",
|
|
@@ -387,7 +401,8 @@ export function chooseDefaults(models) {
|
|
|
387
401
|
const ids = Object.keys(models);
|
|
388
402
|
if (!ids.length) throw new Error("No models were returned by the backend.");
|
|
389
403
|
const model = defaultOrder.find((id) => ids.includes(id)) || ids[0];
|
|
390
|
-
const smallModel =
|
|
404
|
+
const smallModel =
|
|
405
|
+
ids.find((id) => id.includes("-mini") || id.includes("haiku")) || model;
|
|
391
406
|
return { model, smallModel };
|
|
392
407
|
}
|
|
393
408
|
|
|
@@ -403,7 +418,7 @@ export function buildOpenCodeConfig({ baseUrl, providerID, model, smallModel })
|
|
|
403
418
|
[providerID]: {
|
|
404
419
|
name: "Zenocode",
|
|
405
420
|
options: {
|
|
406
|
-
baseURL: new URL("/internal/
|
|
421
|
+
baseURL: new URL("/internal/v2/fastapi/zenocode/v1", baseUrl).toString(),
|
|
407
422
|
},
|
|
408
423
|
},
|
|
409
424
|
// Older fallback opencode-ai builds can load the Codex auth plugin when
|
|
@@ -464,7 +479,7 @@ async function requestJson(url, init) {
|
|
|
464
479
|
}
|
|
465
480
|
|
|
466
481
|
async function prepareRuntime(baseUrl, token) {
|
|
467
|
-
const modelsUrl = new URL("/internal/
|
|
482
|
+
const modelsUrl = new URL("/internal/v2/fastapi/zenocode/models/api.json", baseUrl).toString();
|
|
468
483
|
const { response, payload, text } = await requestJson(modelsUrl, {
|
|
469
484
|
headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
|
|
470
485
|
});
|
|
@@ -954,11 +969,26 @@ function _runtimeBrandedBinaryPath() {
|
|
|
954
969
|
return path.join(runtimeDir, "bin", binaryName);
|
|
955
970
|
}
|
|
956
971
|
|
|
972
|
+
export function packageNameFromSpecifier(packageSpecifier) {
|
|
973
|
+
if (!packageSpecifier.includes("@")) {
|
|
974
|
+
return packageSpecifier;
|
|
975
|
+
}
|
|
976
|
+
if (!packageSpecifier.startsWith("@")) {
|
|
977
|
+
return packageSpecifier.split("@")[0];
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
const versionSeparator = packageSpecifier.lastIndexOf("@");
|
|
981
|
+
return versionSeparator > packageSpecifier.indexOf("/")
|
|
982
|
+
? packageSpecifier.slice(0, versionSeparator)
|
|
983
|
+
: packageSpecifier;
|
|
984
|
+
}
|
|
985
|
+
|
|
957
986
|
function shouldPatchOpencodeRuntimePackage(packageName) {
|
|
987
|
+
const runtimePackageName = packageNameFromSpecifier(packageName);
|
|
958
988
|
return (
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
989
|
+
runtimePackageName.endsWith("/opencode-ai") ||
|
|
990
|
+
runtimePackageName.endsWith("/zenocode-ai") ||
|
|
991
|
+
runtimePackageName === openCodePackageName
|
|
962
992
|
);
|
|
963
993
|
}
|
|
964
994
|
|
|
@@ -999,6 +1029,88 @@ async function _collectPnpmDlxPnpmDirs(rootDir) {
|
|
|
999
1029
|
return pnpmDirs;
|
|
1000
1030
|
}
|
|
1001
1031
|
|
|
1032
|
+
async function _collectNodeModulesDirs(rootDir) {
|
|
1033
|
+
const queue = [{ dir: rootDir, depth: 0 }];
|
|
1034
|
+
const nodeModulesDirs = [];
|
|
1035
|
+
|
|
1036
|
+
while (queue.length) {
|
|
1037
|
+
const current = queue.shift();
|
|
1038
|
+
const nodeModulesPath = path.join(current.dir, "node_modules");
|
|
1039
|
+
if (await _pathExists(nodeModulesPath)) {
|
|
1040
|
+
nodeModulesDirs.push(nodeModulesPath);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
if (current.depth >= 3) continue;
|
|
1044
|
+
let entries = [];
|
|
1045
|
+
try {
|
|
1046
|
+
entries = await fs.readdir(current.dir, { withFileTypes: true });
|
|
1047
|
+
} catch {
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
for (const entry of entries) {
|
|
1051
|
+
if (!entry.isDirectory()) continue;
|
|
1052
|
+
if (entry.name === "node_modules") continue;
|
|
1053
|
+
queue.push({ dir: path.join(current.dir, entry.name), depth: current.depth + 1 });
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
return nodeModulesDirs;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
async function _collectOpencodeBinariesFromNodeModules(nodeModulesPath) {
|
|
1061
|
+
const binaryName = process.platform === "win32" ? "opencode.exe" : "opencode";
|
|
1062
|
+
const hiddenCachedBinaryName = ".opencode";
|
|
1063
|
+
const candidates = [];
|
|
1064
|
+
|
|
1065
|
+
let topLevel = [];
|
|
1066
|
+
try {
|
|
1067
|
+
topLevel = await fs.readdir(nodeModulesPath, { withFileTypes: true });
|
|
1068
|
+
} catch {
|
|
1069
|
+
return candidates;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
for (const moduleEntry of topLevel) {
|
|
1073
|
+
if (!moduleEntry.isDirectory()) continue;
|
|
1074
|
+
|
|
1075
|
+
if (moduleEntry.name === openCodePackageName) {
|
|
1076
|
+
const cachedPath = path.join(nodeModulesPath, openCodePackageName, "bin", hiddenCachedBinaryName);
|
|
1077
|
+
if (await _pathExists(cachedPath)) candidates.push(cachedPath);
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
if (moduleEntry.name.startsWith("opencode-")) {
|
|
1082
|
+
const binaryPath = path.join(nodeModulesPath, moduleEntry.name, "bin", binaryName);
|
|
1083
|
+
if (await _pathExists(binaryPath)) candidates.push(binaryPath);
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
if (!moduleEntry.name.startsWith("@")) continue;
|
|
1088
|
+
const scopePath = path.join(nodeModulesPath, moduleEntry.name);
|
|
1089
|
+
let scopedPackages = [];
|
|
1090
|
+
try {
|
|
1091
|
+
scopedPackages = await fs.readdir(scopePath, { withFileTypes: true });
|
|
1092
|
+
} catch {
|
|
1093
|
+
continue;
|
|
1094
|
+
}
|
|
1095
|
+
for (const scopedPackage of scopedPackages) {
|
|
1096
|
+
if (!scopedPackage.isDirectory()) continue;
|
|
1097
|
+
const scopedNodeModulesPath = path.join(scopePath, scopedPackage.name);
|
|
1098
|
+
|
|
1099
|
+
if (scopedPackage.name === "opencode-ai") {
|
|
1100
|
+
const cachedPath = path.join(scopedNodeModulesPath, "bin", hiddenCachedBinaryName);
|
|
1101
|
+
if (await _pathExists(cachedPath)) candidates.push(cachedPath);
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
if (!scopedPackage.name.startsWith("opencode-")) continue;
|
|
1106
|
+
const binaryPath = path.join(scopedNodeModulesPath, "bin", binaryName);
|
|
1107
|
+
if (await _pathExists(binaryPath)) candidates.push(binaryPath);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
return candidates;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1002
1114
|
async function _collectPnpmDlxOpencodeBinaries() {
|
|
1003
1115
|
const dlxRoots = [
|
|
1004
1116
|
path.join(os.homedir(), "Library", "Caches", "pnpm", "dlx"),
|
|
@@ -1006,8 +1118,6 @@ async function _collectPnpmDlxOpencodeBinaries() {
|
|
|
1006
1118
|
path.join(process.env.LOCALAPPDATA || "", "pnpm", "dlx"),
|
|
1007
1119
|
].filter(Boolean);
|
|
1008
1120
|
|
|
1009
|
-
const binaryName = process.platform === "win32" ? "opencode.exe" : "opencode";
|
|
1010
|
-
const hiddenCachedBinaryName = ".opencode";
|
|
1011
1121
|
const candidates = [];
|
|
1012
1122
|
|
|
1013
1123
|
for (const root of dlxRoots) {
|
|
@@ -1026,51 +1136,7 @@ async function _collectPnpmDlxOpencodeBinaries() {
|
|
|
1026
1136
|
const nodeModulesPath = path.join(pnpmDir, entry.name, "node_modules");
|
|
1027
1137
|
if (!(await _pathExists(nodeModulesPath))) continue;
|
|
1028
1138
|
|
|
1029
|
-
|
|
1030
|
-
try {
|
|
1031
|
-
topLevel = await fs.readdir(nodeModulesPath, { withFileTypes: true });
|
|
1032
|
-
} catch {
|
|
1033
|
-
continue;
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
for (const moduleEntry of topLevel) {
|
|
1037
|
-
if (!moduleEntry.isDirectory()) continue;
|
|
1038
|
-
|
|
1039
|
-
if (moduleEntry.name === "opencode-ai") {
|
|
1040
|
-
const cachedPath = path.join(nodeModulesPath, "opencode-ai", "bin", hiddenCachedBinaryName);
|
|
1041
|
-
if (await _pathExists(cachedPath)) candidates.push(cachedPath);
|
|
1042
|
-
continue;
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
if (moduleEntry.name.startsWith("opencode-")) {
|
|
1046
|
-
const binaryPath = path.join(nodeModulesPath, moduleEntry.name, "bin", binaryName);
|
|
1047
|
-
if (await _pathExists(binaryPath)) candidates.push(binaryPath);
|
|
1048
|
-
continue;
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
if (!moduleEntry.name.startsWith("@")) continue;
|
|
1052
|
-
const scopePath = path.join(nodeModulesPath, moduleEntry.name);
|
|
1053
|
-
let scopedPackages = [];
|
|
1054
|
-
try {
|
|
1055
|
-
scopedPackages = await fs.readdir(scopePath, { withFileTypes: true });
|
|
1056
|
-
} catch {
|
|
1057
|
-
continue;
|
|
1058
|
-
}
|
|
1059
|
-
for (const scopedPackage of scopedPackages) {
|
|
1060
|
-
if (!scopedPackage.isDirectory()) continue;
|
|
1061
|
-
const scopedNodeModulesPath = path.join(scopePath, scopedPackage.name);
|
|
1062
|
-
|
|
1063
|
-
if (scopedPackage.name === "opencode-ai") {
|
|
1064
|
-
const cachedPath = path.join(scopedNodeModulesPath, "bin", hiddenCachedBinaryName);
|
|
1065
|
-
if (await _pathExists(cachedPath)) candidates.push(cachedPath);
|
|
1066
|
-
continue;
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
if (!scopedPackage.name.startsWith("opencode-")) continue;
|
|
1070
|
-
const binaryPath = path.join(scopedNodeModulesPath, "bin", binaryName);
|
|
1071
|
-
if (await _pathExists(binaryPath)) candidates.push(binaryPath);
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1139
|
+
candidates.push(...await _collectOpencodeBinariesFromNodeModules(nodeModulesPath));
|
|
1074
1140
|
}
|
|
1075
1141
|
}
|
|
1076
1142
|
}
|
|
@@ -1078,6 +1144,44 @@ async function _collectPnpmDlxOpencodeBinaries() {
|
|
|
1078
1144
|
return [...new Set(candidates)];
|
|
1079
1145
|
}
|
|
1080
1146
|
|
|
1147
|
+
async function _collectNpxOpencodeBinaries() {
|
|
1148
|
+
const npxRoots = [
|
|
1149
|
+
path.join(os.homedir(), ".npm", "_npx"),
|
|
1150
|
+
path.join(process.env.LOCALAPPDATA || "", "npm-cache", "_npx"),
|
|
1151
|
+
].filter(Boolean);
|
|
1152
|
+
const candidates = [];
|
|
1153
|
+
|
|
1154
|
+
for (const root of npxRoots) {
|
|
1155
|
+
if (!(await _pathExists(root))) continue;
|
|
1156
|
+
const nodeModulesDirs = await _collectNodeModulesDirs(root);
|
|
1157
|
+
for (const nodeModulesDir of nodeModulesDirs) {
|
|
1158
|
+
candidates.push(...await _collectOpencodeBinariesFromNodeModules(nodeModulesDir));
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
return [...new Set(candidates)];
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
function _buildPackageWarmupArgs(packageName, runner) {
|
|
1166
|
+
if (runner.command === _runnerCommand("pnpm")) {
|
|
1167
|
+
return ["dlx", packageName, "--version"];
|
|
1168
|
+
}
|
|
1169
|
+
if (runner.command === _runnerCommand("npx")) {
|
|
1170
|
+
return ["--yes", packageName, "--version"];
|
|
1171
|
+
}
|
|
1172
|
+
return null;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
async function _collectRunnerOpencodeBinaries(runner) {
|
|
1176
|
+
if (runner.command === _runnerCommand("pnpm")) {
|
|
1177
|
+
return _collectPnpmDlxOpencodeBinaries();
|
|
1178
|
+
}
|
|
1179
|
+
if (runner.command === _runnerCommand("npx")) {
|
|
1180
|
+
return _collectNpxOpencodeBinaries();
|
|
1181
|
+
}
|
|
1182
|
+
return [];
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1081
1185
|
async function _adHocSignBinary(binaryPath) {
|
|
1082
1186
|
if (process.platform !== "darwin") return;
|
|
1083
1187
|
try {
|
|
@@ -1143,9 +1247,6 @@ async function _ensurePatchedOpencodeDlxBinaries(packageName, runner, options) {
|
|
|
1143
1247
|
if (!shouldPatchOpencodeRuntimePackage(packageName)) {
|
|
1144
1248
|
return null;
|
|
1145
1249
|
}
|
|
1146
|
-
if (runner.command !== _runnerCommand("pnpm")) {
|
|
1147
|
-
return null;
|
|
1148
|
-
}
|
|
1149
1250
|
if (
|
|
1150
1251
|
process.env.ZENOCODE_DISABLE_OPENCODE_LOGO_PATCH === "1" ||
|
|
1151
1252
|
process.env.CODECORTEX_DISABLE_OPENCODE_LOGO_PATCH === "1"
|
|
@@ -1153,19 +1254,18 @@ async function _ensurePatchedOpencodeDlxBinaries(packageName, runner, options) {
|
|
|
1153
1254
|
return null;
|
|
1154
1255
|
}
|
|
1155
1256
|
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
await runChild(runner.command, ["dlx", packageName, "--version"], {
|
|
1257
|
+
const warmupArgs = _buildPackageWarmupArgs(packageName, runner);
|
|
1258
|
+
try {
|
|
1259
|
+
if (warmupArgs) {
|
|
1260
|
+
await runChild(runner.command, warmupArgs, {
|
|
1161
1261
|
...options,
|
|
1162
1262
|
stdio: "ignore",
|
|
1163
1263
|
});
|
|
1164
|
-
} catch {
|
|
1165
|
-
// ignore warm-up failures and continue with best effort patching
|
|
1166
1264
|
}
|
|
1167
|
-
|
|
1265
|
+
} catch {
|
|
1266
|
+
// ignore warm-up failures and continue with best effort patching
|
|
1168
1267
|
}
|
|
1268
|
+
const binaryCandidates = await _collectRunnerOpencodeBinaries(runner);
|
|
1169
1269
|
|
|
1170
1270
|
if (!binaryCandidates.length) {
|
|
1171
1271
|
return null;
|
|
@@ -1198,6 +1298,22 @@ export function buildPackageLauncherChildOptions(options, pinnedRuntimePath) {
|
|
|
1198
1298
|
};
|
|
1199
1299
|
}
|
|
1200
1300
|
|
|
1301
|
+
export function buildPackageLauncherInvocation({ runner, args, pinnedRuntimePath }) {
|
|
1302
|
+
if (pinnedRuntimePath) {
|
|
1303
|
+
return {
|
|
1304
|
+
command: pinnedRuntimePath,
|
|
1305
|
+
args,
|
|
1306
|
+
usePinnedRuntime: true,
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
return {
|
|
1311
|
+
command: runner.command,
|
|
1312
|
+
args: runner.args,
|
|
1313
|
+
usePinnedRuntime: false,
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1201
1317
|
async function runPackageLauncher(packageName, args, options) {
|
|
1202
1318
|
const runners = [
|
|
1203
1319
|
{ command: _runnerCommand("pnpm"), args: ["dlx", packageName, ...args] },
|
|
@@ -1215,9 +1331,17 @@ async function runPackageLauncher(packageName, args, options) {
|
|
|
1215
1331
|
}
|
|
1216
1332
|
}
|
|
1217
1333
|
|
|
1218
|
-
const
|
|
1334
|
+
const invocation = buildPackageLauncherInvocation({
|
|
1335
|
+
runner,
|
|
1336
|
+
args,
|
|
1337
|
+
pinnedRuntimePath,
|
|
1338
|
+
});
|
|
1339
|
+
const childOptions = buildPackageLauncherChildOptions(
|
|
1340
|
+
options,
|
|
1341
|
+
invocation.usePinnedRuntime ? null : pinnedRuntimePath,
|
|
1342
|
+
);
|
|
1219
1343
|
|
|
1220
|
-
const result = await runChild(
|
|
1344
|
+
const result = await runChild(invocation.command, invocation.args, childOptions);
|
|
1221
1345
|
if (result.signal) {
|
|
1222
1346
|
process.kill(process.pid, result.signal);
|
|
1223
1347
|
return;
|
|
@@ -1363,10 +1487,11 @@ export async function runRuntimeWithSessionRecovery({
|
|
|
1363
1487
|
}
|
|
1364
1488
|
|
|
1365
1489
|
async function packageExistsOnNpm(packageName) {
|
|
1490
|
+
const packageSpecName = packageNameFromSpecifier(packageName);
|
|
1366
1491
|
const controller = new AbortController();
|
|
1367
1492
|
const timeout = setTimeout(() => controller.abort(), 4_000);
|
|
1368
1493
|
try {
|
|
1369
|
-
const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(
|
|
1494
|
+
const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageSpecName)}`, {
|
|
1370
1495
|
method: "GET",
|
|
1371
1496
|
headers: { Accept: "application/json" },
|
|
1372
1497
|
signal: controller.signal,
|
|
@@ -1424,6 +1549,11 @@ function shouldRenderBanner(args) {
|
|
|
1424
1549
|
return !args.some((arg) => suppressFlags.has(arg));
|
|
1425
1550
|
}
|
|
1426
1551
|
|
|
1552
|
+
export function shouldBypassZenocodePreparation(args) {
|
|
1553
|
+
const metadataFlags = new Set(["--help", "-h", "--version", "-v", "completion"]);
|
|
1554
|
+
return metadataFlags.has(args[0]);
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1427
1557
|
function maybeRenderBanner(args) {
|
|
1428
1558
|
if (!shouldRenderBanner(args)) {
|
|
1429
1559
|
return;
|
|
@@ -1617,6 +1747,15 @@ async function main() {
|
|
|
1617
1747
|
}
|
|
1618
1748
|
|
|
1619
1749
|
maybeRenderBanner(runtimeArgs);
|
|
1750
|
+
if (shouldBypassZenocodePreparation(runtimeArgs)) {
|
|
1751
|
+
const launchPackage = await resolveLaunchPackage();
|
|
1752
|
+
await runPackageLauncher(launchPackage, runtimeArgs, {
|
|
1753
|
+
cwd: process.cwd(),
|
|
1754
|
+
env: { ...process.env },
|
|
1755
|
+
});
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1620
1759
|
const tokenResolution = await resolveTokenWithAutoLogin(baseUrl, runtimeArgs, {
|
|
1621
1760
|
preferLocalhost,
|
|
1622
1761
|
});
|
|
@@ -11,32 +11,99 @@ 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 GPT-5.6 Luna as the Zenocode default", () => {
|
|
28
31
|
const defaults = chooseDefaults({
|
|
29
|
-
"
|
|
30
|
-
"
|
|
32
|
+
"kimi-k2p7-code-non-thinking": {},
|
|
33
|
+
"gpt-5-6-luna": {},
|
|
34
|
+
"glm-5-2-non-thinking": {},
|
|
35
|
+
"minimax-m3-thinking": {},
|
|
31
36
|
"gpt-5-2": {},
|
|
32
37
|
});
|
|
33
38
|
|
|
34
39
|
assert.deepEqual(defaults, {
|
|
35
|
-
model: "
|
|
36
|
-
smallModel: "
|
|
40
|
+
model: "gpt-5-6-luna",
|
|
41
|
+
smallModel: "gpt-5-6-luna",
|
|
37
42
|
});
|
|
38
43
|
});
|
|
39
44
|
|
|
45
|
+
test("chooseDefaults falls back to Kimi K2.7 Code when GPT-5.6 Luna is unavailable", () => {
|
|
46
|
+
const defaults = chooseDefaults({
|
|
47
|
+
"kimi-k2p7-code-non-thinking": {},
|
|
48
|
+
"glm-5-2-non-thinking": {},
|
|
49
|
+
"glm-5-1-non-thinking": {},
|
|
50
|
+
"kimi-k2-6-non-thinking": {},
|
|
51
|
+
"minimax-m3-thinking": {},
|
|
52
|
+
"gpt-5-2": {},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
assert.deepEqual(defaults, {
|
|
56
|
+
model: "kimi-k2p7-code-non-thinking",
|
|
57
|
+
smallModel: "kimi-k2p7-code-non-thinking",
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("chooseDefaults prefers GLM 5.2 over GLM 5.1 for Zenocode fallback", () => {
|
|
62
|
+
const defaults = chooseDefaults({
|
|
63
|
+
"glm-5-1-non-thinking": {},
|
|
64
|
+
"glm-5-2-non-thinking": {},
|
|
65
|
+
"gpt-5-mini": {},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
assert.deepEqual(defaults, {
|
|
69
|
+
model: "glm-5-2-non-thinking",
|
|
70
|
+
smallModel: "gpt-5-mini",
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("chooseDefaults falls back to explicit thinking variants when base models are absent", () => {
|
|
75
|
+
const defaults = chooseDefaults({
|
|
76
|
+
"kimi-k2p7-code-thinking": {},
|
|
77
|
+
"glm-5-2-non-thinking": {},
|
|
78
|
+
"kimi-k2-6-thinking": {},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
assert.deepEqual(defaults, {
|
|
82
|
+
model: "kimi-k2p7-code-thinking",
|
|
83
|
+
smallModel: "kimi-k2p7-code-thinking",
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("packageNameFromSpecifier strips npm versions without breaking scopes", () => {
|
|
88
|
+
assert.equal(packageNameFromSpecifier("opencode-ai@1.17.6"), "opencode-ai");
|
|
89
|
+
assert.equal(
|
|
90
|
+
packageNameFromSpecifier("@textcortex/zenocode-ai@1.17.6"),
|
|
91
|
+
"@textcortex/zenocode-ai",
|
|
92
|
+
);
|
|
93
|
+
assert.equal(
|
|
94
|
+
packageNameFromSpecifier("@textcortex/zenocode-ai"),
|
|
95
|
+
"@textcortex/zenocode-ai",
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("shouldBypassZenocodePreparation skips auth for runtime metadata commands", () => {
|
|
100
|
+
assert.equal(shouldBypassZenocodePreparation(["--version"]), true);
|
|
101
|
+
assert.equal(shouldBypassZenocodePreparation(["completion", "zsh"]), true);
|
|
102
|
+
assert.equal(shouldBypassZenocodePreparation(["run"]), false);
|
|
103
|
+
assert.equal(shouldBypassZenocodePreparation(["run", "implement", "completion"]), false);
|
|
104
|
+
assert.equal(shouldBypassZenocodePreparation(["run", "--help"]), false);
|
|
105
|
+
});
|
|
106
|
+
|
|
40
107
|
test("resolveTextCortexBaseUrl defaults to the cloud API for packaged usage", () => {
|
|
41
108
|
assert.equal(
|
|
42
109
|
resolveTextCortexBaseUrl({
|
|
@@ -128,12 +195,13 @@ test("buildOpenCodeConfig includes an openai stub for fallback runtime auth plug
|
|
|
128
195
|
const config = buildOpenCodeConfig({
|
|
129
196
|
baseUrl: "http://127.0.0.1:8080",
|
|
130
197
|
providerID: "textcortex",
|
|
131
|
-
model: "
|
|
132
|
-
smallModel: "
|
|
198
|
+
model: "minimax-m3-thinking",
|
|
199
|
+
smallModel: "gpt-5-2",
|
|
133
200
|
});
|
|
134
201
|
|
|
135
202
|
assert.deepEqual(config.enabled_providers, ["textcortex"]);
|
|
136
|
-
assert.equal(config.model, "textcortex/
|
|
203
|
+
assert.equal(config.model, "textcortex/minimax-m3-thinking");
|
|
204
|
+
assert.equal(config.small_model, "textcortex/gpt-5-2");
|
|
137
205
|
assert.equal(config.theme, "system");
|
|
138
206
|
assert.ok(config.provider.openai);
|
|
139
207
|
assert.deepEqual(config.provider.openai.models, {});
|
|
@@ -156,6 +224,40 @@ test("buildPackageLauncherChildOptions keeps fallback package launchers attached
|
|
|
156
224
|
});
|
|
157
225
|
});
|
|
158
226
|
|
|
227
|
+
test("buildPackageLauncherInvocation runs pinned runtimes directly", () => {
|
|
228
|
+
const invocation = buildPackageLauncherInvocation({
|
|
229
|
+
runner: {
|
|
230
|
+
command: "npx",
|
|
231
|
+
args: ["--yes", "opencode-ai@1.17.6", "--version"],
|
|
232
|
+
},
|
|
233
|
+
args: ["--version"],
|
|
234
|
+
pinnedRuntimePath: "/tmp/zenocode-runtime",
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
assert.deepEqual(invocation, {
|
|
238
|
+
command: "/tmp/zenocode-runtime",
|
|
239
|
+
args: ["--version"],
|
|
240
|
+
usePinnedRuntime: true,
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("buildPackageLauncherInvocation uses package runners without a pinned runtime", () => {
|
|
245
|
+
const invocation = buildPackageLauncherInvocation({
|
|
246
|
+
runner: {
|
|
247
|
+
command: "pnpm",
|
|
248
|
+
args: ["dlx", "opencode-ai@1.17.6", "--version"],
|
|
249
|
+
},
|
|
250
|
+
args: ["--version"],
|
|
251
|
+
pinnedRuntimePath: null,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
assert.deepEqual(invocation, {
|
|
255
|
+
command: "pnpm",
|
|
256
|
+
args: ["dlx", "opencode-ai@1.17.6", "--version"],
|
|
257
|
+
usePinnedRuntime: false,
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
159
261
|
test("buildZenocodeBanner renders block logo art instead of plain text", () => {
|
|
160
262
|
const banner = buildZenocodeBanner();
|
|
161
263
|
|
|
@@ -510,7 +612,7 @@ test("prepare-only refreshes stored Zenocode credentials when the access token h
|
|
|
510
612
|
|
|
511
613
|
if (
|
|
512
614
|
req.method === "GET" &&
|
|
513
|
-
req.url === "/internal/
|
|
615
|
+
req.url === "/internal/v2/fastapi/zenocode/models/api.json"
|
|
514
616
|
) {
|
|
515
617
|
modelAuthHeader = req.headers.authorization || null;
|
|
516
618
|
if (modelAuthHeader === "Bearer expired-access") {
|
|
@@ -524,8 +626,15 @@ test("prepare-only refreshes stored Zenocode credentials when the access token h
|
|
|
524
626
|
JSON.stringify({
|
|
525
627
|
textcortex: {
|
|
526
628
|
models: {
|
|
629
|
+
"minimax-m3-thinking": {},
|
|
630
|
+
"kimi-k2-6-non-thinking": {},
|
|
631
|
+
"kimi-k2-6-thinking": {},
|
|
632
|
+
"kimi-k2-5": {},
|
|
527
633
|
"kimi-k2-5-thinking": {},
|
|
528
|
-
"glm-5": {},
|
|
634
|
+
"glm-5-2-non-thinking": {},
|
|
635
|
+
"glm-5-2-thinking": {},
|
|
636
|
+
"glm-5-1-non-thinking": {},
|
|
637
|
+
"glm-5-1-thinking": {},
|
|
529
638
|
},
|
|
530
639
|
},
|
|
531
640
|
}),
|
|
@@ -580,6 +689,16 @@ test("prepare-only refreshes stored Zenocode credentials when the access token h
|
|
|
580
689
|
const savedCredentials = JSON.parse(await fs.readFile(credentialsPath, "utf-8"));
|
|
581
690
|
assert.equal(savedCredentials.access_token, "fresh-access");
|
|
582
691
|
assert.equal(savedCredentials.refresh_token, "fresh-refresh");
|
|
692
|
+
|
|
693
|
+
const savedConfig = JSON.parse(
|
|
694
|
+
await fs.readFile(path.join(zenocodeHome, "opencode.jsonc"), "utf-8"),
|
|
695
|
+
);
|
|
696
|
+
assert.equal(savedConfig.model, "textcortex/minimax-m3-thinking");
|
|
697
|
+
|
|
698
|
+
const savedModels = JSON.parse(
|
|
699
|
+
await fs.readFile(path.join(zenocodeHome, "models.json"), "utf-8"),
|
|
700
|
+
);
|
|
701
|
+
assert.ok(savedModels.textcortex.models["glm-5-2-non-thinking"]);
|
|
583
702
|
});
|
|
584
703
|
|
|
585
704
|
test("login launches the runtime immediately with system TUI theming", async (t) => {
|
|
@@ -623,7 +742,7 @@ main().catch((error) => {
|
|
|
623
742
|
const server = http.createServer((req, res) => {
|
|
624
743
|
if (
|
|
625
744
|
req.method === "POST" &&
|
|
626
|
-
req.url === "/internal/
|
|
745
|
+
req.url === "/internal/v2/fastapi/zenocode/oauth2/initiate"
|
|
627
746
|
) {
|
|
628
747
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
629
748
|
res.end(
|
|
@@ -642,7 +761,7 @@ main().catch((error) => {
|
|
|
642
761
|
|
|
643
762
|
if (
|
|
644
763
|
req.method === "POST" &&
|
|
645
|
-
req.url === "/internal/
|
|
764
|
+
req.url === "/internal/v2/fastapi/zenocode/oauth2/token"
|
|
646
765
|
) {
|
|
647
766
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
648
767
|
res.end(
|
|
@@ -659,15 +778,22 @@ main().catch((error) => {
|
|
|
659
778
|
|
|
660
779
|
if (
|
|
661
780
|
req.method === "GET" &&
|
|
662
|
-
req.url === "/internal/
|
|
781
|
+
req.url === "/internal/v2/fastapi/zenocode/models/api.json"
|
|
663
782
|
) {
|
|
664
783
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
665
784
|
res.end(
|
|
666
785
|
JSON.stringify({
|
|
667
786
|
textcortex: {
|
|
668
787
|
models: {
|
|
788
|
+
"minimax-m3-thinking": {},
|
|
789
|
+
"kimi-k2-6-non-thinking": {},
|
|
790
|
+
"kimi-k2-6-thinking": {},
|
|
791
|
+
"kimi-k2-5": {},
|
|
669
792
|
"kimi-k2-5-thinking": {},
|
|
670
|
-
"glm-5": {},
|
|
793
|
+
"glm-5-2-non-thinking": {},
|
|
794
|
+
"glm-5-2-thinking": {},
|
|
795
|
+
"glm-5-1-non-thinking": {},
|
|
796
|
+
"glm-5-1-thinking": {},
|
|
671
797
|
},
|
|
672
798
|
},
|
|
673
799
|
}),
|
|
@@ -736,6 +862,12 @@ main().catch((error) => {
|
|
|
736
862
|
await fs.readFile(runtimeInvocation.env.OPENCODE_CONFIG, "utf-8"),
|
|
737
863
|
);
|
|
738
864
|
assert.equal(opencodeConfig.theme, "system");
|
|
865
|
+
assert.equal(opencodeConfig.model, "textcortex/minimax-m3-thinking");
|
|
866
|
+
|
|
867
|
+
const modelsConfig = JSON.parse(
|
|
868
|
+
await fs.readFile(path.join(zenocodeHome, "models.json"), "utf-8"),
|
|
869
|
+
);
|
|
870
|
+
assert.ok(modelsConfig.textcortex.models["glm-5-2-non-thinking"]);
|
|
739
871
|
|
|
740
872
|
const tuiConfig = JSON.parse(
|
|
741
873
|
await fs.readFile(runtimeInvocation.env.OPENCODE_TUI_CONFIG, "utf-8"),
|