@textcortex/zenocode 0.1.9 → 0.1.11
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/package.json +2 -1
- package/scripts/build-branded-opencode.mjs +9 -5
- package/scripts/build-branded-opencode.test.mjs +21 -0
- package/scripts/prepare-release-version.mjs +164 -0
- package/scripts/prepare-release-version.test.mjs +96 -0
- package/scripts/run-zenocode.mjs +122 -30
- package/scripts/run-zenocode.test.mjs +146 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@textcortex/zenocode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Secure, EU-hosted coding agent for TextCortex customers that runs in your terminal, edits files, runs scripts, and more.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"scripts": {
|
|
31
31
|
"build-branded-opencode": "node scripts/build-branded-opencode.mjs",
|
|
32
32
|
"prepare-config": "node scripts/run-zenocode.mjs --prepare-only",
|
|
33
|
+
"prepare-release-version": "node scripts/prepare-release-version.mjs",
|
|
33
34
|
"dev": "node scripts/run-zenocode.mjs",
|
|
34
35
|
"login": "node scripts/run-zenocode.mjs login",
|
|
35
36
|
"logout": "node scripts/run-zenocode.mjs logout"
|
|
@@ -237,6 +237,14 @@ async function packPackage(packageDir) {
|
|
|
237
237
|
return path.join(packageDir, tarballs[tarballs.length - 1]);
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
export function buildPublishCommandArgs(tarballPath, { tag } = {}) {
|
|
241
|
+
const args = ["publish", tarballPath, "--access", "public"];
|
|
242
|
+
if (tag) {
|
|
243
|
+
args.push("--tag", tag);
|
|
244
|
+
}
|
|
245
|
+
return args;
|
|
246
|
+
}
|
|
247
|
+
|
|
240
248
|
async function publishTarballIfNeeded(packageName, version, tarballPath, cwd) {
|
|
241
249
|
if (!publishEnabled) return { published: false, skipped: false };
|
|
242
250
|
|
|
@@ -254,11 +262,7 @@ async function publishTarballIfNeeded(packageName, version, tarballPath, cwd) {
|
|
|
254
262
|
}
|
|
255
263
|
|
|
256
264
|
const npmCommand = _command("npm");
|
|
257
|
-
await run(
|
|
258
|
-
npmCommand,
|
|
259
|
-
["publish", tarballPath, "--access", "public", "--tag", publishTag, "--provenance"],
|
|
260
|
-
{ cwd },
|
|
261
|
-
);
|
|
265
|
+
await run(npmCommand, buildPublishCommandArgs(tarballPath, { tag: publishTag }), { cwd });
|
|
262
266
|
return { published: true, skipped: false };
|
|
263
267
|
}
|
|
264
268
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import {
|
|
4
|
+
buildPublishCommandArgs,
|
|
4
5
|
buildWrapperBinMap,
|
|
5
6
|
mapBrandedBinaryPackageName,
|
|
6
7
|
} from "./build-branded-opencode.mjs";
|
|
@@ -23,3 +24,23 @@ test("buildWrapperBinMap includes package, opencode, and zenocode entrypoints",
|
|
|
23
24
|
zenocode: "./bin/opencode",
|
|
24
25
|
});
|
|
25
26
|
});
|
|
27
|
+
|
|
28
|
+
test("buildPublishCommandArgs omits npm provenance for runtime tarball publishing", () => {
|
|
29
|
+
assert.deepEqual(buildPublishCommandArgs("package.tgz", { tag: "latest" }), [
|
|
30
|
+
"publish",
|
|
31
|
+
"package.tgz",
|
|
32
|
+
"--access",
|
|
33
|
+
"public",
|
|
34
|
+
"--tag",
|
|
35
|
+
"latest",
|
|
36
|
+
]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("buildPublishCommandArgs omits tag arguments when no tag is provided", () => {
|
|
40
|
+
assert.deepEqual(buildPublishCommandArgs("package.tgz"), [
|
|
41
|
+
"publish",
|
|
42
|
+
"package.tgz",
|
|
43
|
+
"--access",
|
|
44
|
+
"public",
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const defaultPackageJsonPath = path.resolve(__dirname, "..", "package.json");
|
|
9
|
+
|
|
10
|
+
export function parseSemver(value) {
|
|
11
|
+
const match = value.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+.*)?$/);
|
|
12
|
+
if (!match) {
|
|
13
|
+
throw new Error(`Invalid semver: ${value}`);
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
major: Number(match[1]),
|
|
17
|
+
minor: Number(match[2]),
|
|
18
|
+
patch: Number(match[3]),
|
|
19
|
+
prerelease: match[4] ?? null,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function compareSemver(left, right) {
|
|
24
|
+
const lhs = parseSemver(left);
|
|
25
|
+
const rhs = parseSemver(right);
|
|
26
|
+
|
|
27
|
+
for (const key of ["major", "minor", "patch"]) {
|
|
28
|
+
if (lhs[key] > rhs[key]) return 1;
|
|
29
|
+
if (lhs[key] < rhs[key]) return -1;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (lhs.prerelease === rhs.prerelease) return 0;
|
|
33
|
+
if (lhs.prerelease === null) return 1;
|
|
34
|
+
if (rhs.prerelease === null) return -1;
|
|
35
|
+
|
|
36
|
+
const leftParts = lhs.prerelease.split(".");
|
|
37
|
+
const rightParts = rhs.prerelease.split(".");
|
|
38
|
+
const length = Math.max(leftParts.length, rightParts.length);
|
|
39
|
+
for (let index = 0; index < length; index += 1) {
|
|
40
|
+
const leftPart = leftParts[index];
|
|
41
|
+
const rightPart = rightParts[index];
|
|
42
|
+
if (leftPart === undefined) return -1;
|
|
43
|
+
if (rightPart === undefined) return 1;
|
|
44
|
+
const leftNumeric = /^\d+$/.test(leftPart);
|
|
45
|
+
const rightNumeric = /^\d+$/.test(rightPart);
|
|
46
|
+
if (leftNumeric && rightNumeric) {
|
|
47
|
+
const leftValue = Number(leftPart);
|
|
48
|
+
const rightValue = Number(rightPart);
|
|
49
|
+
if (leftValue > rightValue) return 1;
|
|
50
|
+
if (leftValue < rightValue) return -1;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (leftNumeric && !rightNumeric) return -1;
|
|
54
|
+
if (!leftNumeric && rightNumeric) return 1;
|
|
55
|
+
if (leftPart > rightPart) return 1;
|
|
56
|
+
if (leftPart < rightPart) return -1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function incrementPatchVersion(version) {
|
|
63
|
+
const parsed = parseSemver(version);
|
|
64
|
+
return `${parsed.major}.${parsed.minor}.${parsed.patch + 1}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function resolveReleaseVersion(localVersion, publishedVersion) {
|
|
68
|
+
if (!publishedVersion) {
|
|
69
|
+
return localVersion;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const comparison = compareSemver(localVersion, publishedVersion);
|
|
73
|
+
if (comparison > 0) {
|
|
74
|
+
return localVersion;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return incrementPatchVersion(publishedVersion);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function prepareReleasePackageVersion({
|
|
81
|
+
packageJsonPath = defaultPackageJsonPath,
|
|
82
|
+
publishedVersion = null,
|
|
83
|
+
} = {}) {
|
|
84
|
+
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf-8"));
|
|
85
|
+
const packageName = String(packageJson.name || "").trim();
|
|
86
|
+
const localVersion = String(packageJson.version || "").trim();
|
|
87
|
+
|
|
88
|
+
if (!packageName) {
|
|
89
|
+
throw new Error(`Missing package name in ${packageJsonPath}`);
|
|
90
|
+
}
|
|
91
|
+
if (!localVersion) {
|
|
92
|
+
throw new Error(`Missing package version in ${packageJsonPath}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
parseSemver(localVersion);
|
|
96
|
+
if (publishedVersion) {
|
|
97
|
+
parseSemver(publishedVersion);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const nextVersion = resolveReleaseVersion(localVersion, publishedVersion);
|
|
101
|
+
const updated = nextVersion !== localVersion;
|
|
102
|
+
|
|
103
|
+
if (updated) {
|
|
104
|
+
packageJson.version = nextVersion;
|
|
105
|
+
await fs.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, "utf-8");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
packageName,
|
|
110
|
+
localVersion,
|
|
111
|
+
nextVersion,
|
|
112
|
+
publishedVersion,
|
|
113
|
+
updated,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function parseArgs(argv) {
|
|
118
|
+
const options = {
|
|
119
|
+
packageJsonPath: defaultPackageJsonPath,
|
|
120
|
+
publishedVersion: null,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
124
|
+
const arg = argv[index];
|
|
125
|
+
if (arg === "--package-json") {
|
|
126
|
+
if (!argv[index + 1]) {
|
|
127
|
+
throw new Error("--package-json requires a value");
|
|
128
|
+
}
|
|
129
|
+
options.packageJsonPath = path.resolve(argv[index + 1]);
|
|
130
|
+
index += 1;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (arg === "--published-version") {
|
|
134
|
+
options.publishedVersion = (argv[index + 1] || "").trim() || null;
|
|
135
|
+
index += 1;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return options;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function main() {
|
|
145
|
+
const result = await prepareReleasePackageVersion(parseArgs(process.argv.slice(2)));
|
|
146
|
+
console.log(
|
|
147
|
+
JSON.stringify(
|
|
148
|
+
{
|
|
149
|
+
package_name: result.packageName,
|
|
150
|
+
local_version: result.localVersion,
|
|
151
|
+
next_version: result.nextVersion,
|
|
152
|
+
published_version: result.publishedVersion,
|
|
153
|
+
updated: result.updated,
|
|
154
|
+
},
|
|
155
|
+
null,
|
|
156
|
+
2,
|
|
157
|
+
),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : "";
|
|
162
|
+
if (invokedPath === fileURLToPath(import.meta.url)) {
|
|
163
|
+
await main();
|
|
164
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
parseArgs,
|
|
9
|
+
prepareReleasePackageVersion,
|
|
10
|
+
resolveReleaseVersion,
|
|
11
|
+
} from "./prepare-release-version.mjs";
|
|
12
|
+
|
|
13
|
+
test("resolveReleaseVersion keeps the local version when nothing is published yet", () => {
|
|
14
|
+
assert.equal(resolveReleaseVersion("0.1.10", null), "0.1.10");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("resolveReleaseVersion bumps the patch version when the local version matches the published version", () => {
|
|
18
|
+
assert.equal(resolveReleaseVersion("0.1.10", "0.1.10"), "0.1.11");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("resolveReleaseVersion bumps from the published version when the local version is behind", () => {
|
|
22
|
+
assert.equal(resolveReleaseVersion("0.1.9", "0.1.10"), "0.1.11");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("resolveReleaseVersion keeps an already-ahead local version", () => {
|
|
26
|
+
assert.equal(resolveReleaseVersion("0.1.11", "0.1.10"), "0.1.11");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("resolveReleaseVersion rejects invalid semver values", () => {
|
|
30
|
+
assert.throws(() => resolveReleaseVersion("invalid", "0.1.10"), /Invalid semver/);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("prepareReleasePackageVersion rewrites package.json when an automatic bump is required", async () => {
|
|
34
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "zenocode-version-"));
|
|
35
|
+
const packageJsonPath = path.join(tempDir, "package.json");
|
|
36
|
+
await fs.writeFile(
|
|
37
|
+
packageJsonPath,
|
|
38
|
+
`${JSON.stringify(
|
|
39
|
+
{
|
|
40
|
+
name: "@textcortex/zenocode",
|
|
41
|
+
version: "0.1.10",
|
|
42
|
+
},
|
|
43
|
+
null,
|
|
44
|
+
2,
|
|
45
|
+
)}\n`,
|
|
46
|
+
"utf-8",
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const result = await prepareReleasePackageVersion({
|
|
50
|
+
packageJsonPath,
|
|
51
|
+
publishedVersion: "0.1.10",
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf-8"));
|
|
55
|
+
assert.deepEqual(result, {
|
|
56
|
+
packageName: "@textcortex/zenocode",
|
|
57
|
+
localVersion: "0.1.10",
|
|
58
|
+
nextVersion: "0.1.11",
|
|
59
|
+
publishedVersion: "0.1.10",
|
|
60
|
+
updated: true,
|
|
61
|
+
});
|
|
62
|
+
assert.equal(packageJson.version, "0.1.11");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("prepareReleasePackageVersion leaves package.json untouched when the local version is already ahead", async () => {
|
|
66
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "zenocode-version-"));
|
|
67
|
+
const packageJsonPath = path.join(tempDir, "package.json");
|
|
68
|
+
const originalContents = `${JSON.stringify(
|
|
69
|
+
{
|
|
70
|
+
name: "@textcortex/zenocode",
|
|
71
|
+
version: "0.1.11",
|
|
72
|
+
},
|
|
73
|
+
null,
|
|
74
|
+
2,
|
|
75
|
+
)}\n`;
|
|
76
|
+
await fs.writeFile(packageJsonPath, originalContents, "utf-8");
|
|
77
|
+
|
|
78
|
+
const result = await prepareReleasePackageVersion({
|
|
79
|
+
packageJsonPath,
|
|
80
|
+
publishedVersion: "0.1.10",
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const packageJson = await fs.readFile(packageJsonPath, "utf-8");
|
|
84
|
+
assert.deepEqual(result, {
|
|
85
|
+
packageName: "@textcortex/zenocode",
|
|
86
|
+
localVersion: "0.1.11",
|
|
87
|
+
nextVersion: "0.1.11",
|
|
88
|
+
publishedVersion: "0.1.10",
|
|
89
|
+
updated: false,
|
|
90
|
+
});
|
|
91
|
+
assert.equal(packageJson, originalContents);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("parseArgs requires a value after --package-json", () => {
|
|
95
|
+
assert.throws(() => parseArgs(["--package-json"]), /--package-json requires a value/);
|
|
96
|
+
});
|
package/scripts/run-zenocode.mjs
CHANGED
|
@@ -31,6 +31,7 @@ const modelsPath = path.join(runtimeDir, "models.json");
|
|
|
31
31
|
const configPath = path.join(runtimeDir, "opencode.jsonc");
|
|
32
32
|
const localBaseUrlDefault = "http://127.0.0.1:8080";
|
|
33
33
|
const cloudBaseUrlDefault = "https://api.textcortex.com";
|
|
34
|
+
const localBaseUrlFlags = new Set(["--local", "--localhost"]);
|
|
34
35
|
|
|
35
36
|
const providerID = "textcortex";
|
|
36
37
|
const configuredOpencodePackage =
|
|
@@ -492,6 +493,15 @@ function sleep(ms) {
|
|
|
492
493
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
493
494
|
}
|
|
494
495
|
|
|
496
|
+
export function hasLocalBaseUrlFlag(args = []) {
|
|
497
|
+
const normalizedArgs = args[0] === "--" ? args.slice(1) : args;
|
|
498
|
+
return normalizedArgs.some((arg) => localBaseUrlFlags.has(arg));
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function stripLocalBaseUrlFlags(args = []) {
|
|
502
|
+
return args.filter((arg) => !localBaseUrlFlags.has(arg));
|
|
503
|
+
}
|
|
504
|
+
|
|
495
505
|
async function parseLoginArgs(args) {
|
|
496
506
|
const normalizedArgs = args[0] === "--" ? args.slice(1) : args;
|
|
497
507
|
let emailHint = process.env.TEXTCORTEX_LOGIN_EMAIL || null;
|
|
@@ -499,6 +509,9 @@ async function parseLoginArgs(args) {
|
|
|
499
509
|
|
|
500
510
|
for (let idx = 0; idx < normalizedArgs.length; idx += 1) {
|
|
501
511
|
const arg = normalizedArgs[idx];
|
|
512
|
+
if (localBaseUrlFlags.has(arg)) {
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
502
515
|
if (arg === "--no-launch-browser") {
|
|
503
516
|
launchBrowser = false;
|
|
504
517
|
continue;
|
|
@@ -515,6 +528,7 @@ async function parseLoginArgs(args) {
|
|
|
515
528
|
if (arg === "--help" || arg === "-h") {
|
|
516
529
|
console.log("Zenocode login options:");
|
|
517
530
|
console.log(" --email <address> Optional email hint for tenant/SSO routing");
|
|
531
|
+
console.log(` --local Use the local FastAPI backend at ${localBaseUrlDefault}`);
|
|
518
532
|
console.log(" --no-launch-browser Do not open browser automatically");
|
|
519
533
|
process.exit(0);
|
|
520
534
|
}
|
|
@@ -543,10 +557,39 @@ function isLoginRouteNotFoundError(error) {
|
|
|
543
557
|
return message.includes("Login initiate failed (404)");
|
|
544
558
|
}
|
|
545
559
|
|
|
560
|
+
export function shouldFallbackLoginToCloud({ baseUrl, hasExplicitBaseUrl, error }) {
|
|
561
|
+
if (hasExplicitBaseUrl || baseUrl !== localBaseUrlDefault) {
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return isFetchFailedError(error) || isLoginRouteNotFoundError(error);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
export function resolveTextCortexBaseUrl({
|
|
569
|
+
envBaseUrl,
|
|
570
|
+
storedBaseUrl,
|
|
571
|
+
preferLocalhost = false,
|
|
572
|
+
} = {}) {
|
|
573
|
+
if (preferLocalhost) {
|
|
574
|
+
return localBaseUrlDefault;
|
|
575
|
+
}
|
|
576
|
+
return envBaseUrl || storedBaseUrl || cloudBaseUrlDefault;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
export function resolveLoginBaseUrl({
|
|
580
|
+
envBaseUrl,
|
|
581
|
+
preferLocalhost = false,
|
|
582
|
+
} = {}) {
|
|
583
|
+
if (preferLocalhost) {
|
|
584
|
+
return localBaseUrlDefault;
|
|
585
|
+
}
|
|
586
|
+
return envBaseUrl || cloudBaseUrlDefault;
|
|
587
|
+
}
|
|
588
|
+
|
|
546
589
|
function _loginConnectivityHelp(baseUrl) {
|
|
547
590
|
return [
|
|
548
591
|
`Cannot reach Zenocode auth endpoint at ${baseUrl}.`,
|
|
549
|
-
|
|
592
|
+
`Set TEXTCORTEX_BASE_URL to ${cloudBaseUrlDefault} or, for local development, run local backend FastAPI (\`cd backend && uv run dev_fastapi\`).`,
|
|
550
593
|
].join(" ");
|
|
551
594
|
}
|
|
552
595
|
|
|
@@ -655,18 +698,20 @@ async function saveRuntimeCredentials(baseUrl, tokenData) {
|
|
|
655
698
|
await clearLogoutMarker();
|
|
656
699
|
}
|
|
657
700
|
|
|
658
|
-
async function runLoginCommand(baseUrl, args) {
|
|
701
|
+
async function runLoginCommand(baseUrl, args, options = {}) {
|
|
702
|
+
const preferLocalhost = options.preferLocalhost === true;
|
|
659
703
|
const { emailHint, launchBrowser } = await parseLoginArgs(args);
|
|
660
|
-
let resolvedBaseUrl = baseUrl;
|
|
704
|
+
let resolvedBaseUrl = preferLocalhost ? localBaseUrlDefault : baseUrl;
|
|
661
705
|
let login;
|
|
662
706
|
try {
|
|
663
707
|
login = await initiateDeviceLogin(resolvedBaseUrl, emailHint);
|
|
664
708
|
} catch (error) {
|
|
665
|
-
if (
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
709
|
+
if (shouldFallbackLoginToCloud({
|
|
710
|
+
baseUrl: resolvedBaseUrl,
|
|
711
|
+
hasExplicitBaseUrl:
|
|
712
|
+
Boolean(process.env.TEXTCORTEX_BASE_URL) || preferLocalhost,
|
|
713
|
+
error,
|
|
714
|
+
})) {
|
|
670
715
|
resolvedBaseUrl = cloudBaseUrlDefault;
|
|
671
716
|
console.log(
|
|
672
717
|
`Local backend not reachable at ${localBaseUrlDefault}. Falling back to ${cloudBaseUrlDefault}.`,
|
|
@@ -1225,6 +1270,7 @@ export async function runRuntimeWithSessionRecovery({
|
|
|
1225
1270
|
token,
|
|
1226
1271
|
childOptions,
|
|
1227
1272
|
canAutoLoginRuntime,
|
|
1273
|
+
preferLocalhost = false,
|
|
1228
1274
|
refreshTokenFn = refreshStoredCredentials,
|
|
1229
1275
|
runLogin = runLoginCommand,
|
|
1230
1276
|
resolveTokenFn = resolveToken,
|
|
@@ -1283,9 +1329,16 @@ export async function runRuntimeWithSessionRecovery({
|
|
|
1283
1329
|
}
|
|
1284
1330
|
|
|
1285
1331
|
console.log("Zenocode session expired. Starting login flow...\n");
|
|
1286
|
-
await runLogin(activeBaseUrl, buildAutoLoginArgs()
|
|
1332
|
+
await runLogin(activeBaseUrl, buildAutoLoginArgs({ preferLocalhost }), {
|
|
1333
|
+
preferLocalhost,
|
|
1334
|
+
});
|
|
1287
1335
|
activeToken = await resolveTokenFn();
|
|
1288
|
-
activeBaseUrl =
|
|
1336
|
+
activeBaseUrl =
|
|
1337
|
+
resolveTextCortexBaseUrl({
|
|
1338
|
+
envBaseUrl: process.env.TEXTCORTEX_BASE_URL,
|
|
1339
|
+
storedBaseUrl: await resolveStoredBaseUrlFn(),
|
|
1340
|
+
preferLocalhost,
|
|
1341
|
+
}) || activeBaseUrl;
|
|
1289
1342
|
await prepareRuntimeFn(activeBaseUrl, activeToken);
|
|
1290
1343
|
const retryDelayMs = Math.min(
|
|
1291
1344
|
Math.max(recoveryDelayMs, 0) * recoveryAttempts,
|
|
@@ -1366,11 +1419,18 @@ function maybeRenderBanner(args) {
|
|
|
1366
1419
|
console.log(`${buildZenocodeBanner()}\n`);
|
|
1367
1420
|
}
|
|
1368
1421
|
|
|
1369
|
-
function buildAutoLoginArgs() {
|
|
1370
|
-
|
|
1422
|
+
function buildAutoLoginArgs({ preferLocalhost = false } = {}) {
|
|
1423
|
+
const loginArgs = [];
|
|
1424
|
+
if (preferLocalhost) {
|
|
1425
|
+
loginArgs.push("--local");
|
|
1426
|
+
}
|
|
1427
|
+
if (
|
|
1428
|
+
process.env.ZENOCODE_AUTO_LOGIN_NO_BROWSER === "1" ||
|
|
1371
1429
|
process.env.CODECORTEX_AUTO_LOGIN_NO_BROWSER === "1"
|
|
1372
|
-
|
|
1373
|
-
|
|
1430
|
+
) {
|
|
1431
|
+
loginArgs.push("--no-launch-browser");
|
|
1432
|
+
}
|
|
1433
|
+
return loginArgs;
|
|
1374
1434
|
}
|
|
1375
1435
|
|
|
1376
1436
|
function isMissingTokenError(error) {
|
|
@@ -1443,7 +1503,8 @@ function shouldAttemptAutoLogin(error, args) {
|
|
|
1443
1503
|
return canAutoLogin(args);
|
|
1444
1504
|
}
|
|
1445
1505
|
|
|
1446
|
-
async function resolveTokenWithAutoLogin(baseUrl, args) {
|
|
1506
|
+
async function resolveTokenWithAutoLogin(baseUrl, args, options = {}) {
|
|
1507
|
+
const preferLocalhost = options.preferLocalhost === true;
|
|
1447
1508
|
try {
|
|
1448
1509
|
const token = await resolveToken();
|
|
1449
1510
|
return { token, baseUrl };
|
|
@@ -1453,14 +1514,21 @@ async function resolveTokenWithAutoLogin(baseUrl, args) {
|
|
|
1453
1514
|
}
|
|
1454
1515
|
|
|
1455
1516
|
console.log("No local Zenocode credentials found. Starting login flow...\n");
|
|
1456
|
-
await runLoginCommand(baseUrl, buildAutoLoginArgs()
|
|
1517
|
+
await runLoginCommand(baseUrl, buildAutoLoginArgs({ preferLocalhost }), {
|
|
1518
|
+
preferLocalhost,
|
|
1519
|
+
});
|
|
1457
1520
|
const token = await resolveToken();
|
|
1458
|
-
const persistedBaseUrl =
|
|
1521
|
+
const persistedBaseUrl = resolveTextCortexBaseUrl({
|
|
1522
|
+
envBaseUrl: process.env.TEXTCORTEX_BASE_URL,
|
|
1523
|
+
storedBaseUrl: await resolveStoredBaseUrl(),
|
|
1524
|
+
preferLocalhost,
|
|
1525
|
+
});
|
|
1459
1526
|
return { token, baseUrl: persistedBaseUrl };
|
|
1460
1527
|
}
|
|
1461
1528
|
}
|
|
1462
1529
|
|
|
1463
|
-
async function prepareRuntimeWithAutoLogin(baseUrl, token, args) {
|
|
1530
|
+
async function prepareRuntimeWithAutoLogin(baseUrl, token, args, options = {}) {
|
|
1531
|
+
const preferLocalhost = options.preferLocalhost === true;
|
|
1464
1532
|
try {
|
|
1465
1533
|
const model = await prepareRuntime(baseUrl, token);
|
|
1466
1534
|
return { model, token, baseUrl };
|
|
@@ -1484,9 +1552,15 @@ async function prepareRuntimeWithAutoLogin(baseUrl, token, args) {
|
|
|
1484
1552
|
}
|
|
1485
1553
|
|
|
1486
1554
|
console.log("Zenocode session expired. Starting login flow...\n");
|
|
1487
|
-
await runLoginCommand(baseUrl, buildAutoLoginArgs()
|
|
1555
|
+
await runLoginCommand(baseUrl, buildAutoLoginArgs({ preferLocalhost }), {
|
|
1556
|
+
preferLocalhost,
|
|
1557
|
+
});
|
|
1488
1558
|
const refreshedToken = await resolveToken();
|
|
1489
|
-
const refreshedBaseUrl =
|
|
1559
|
+
const refreshedBaseUrl = resolveTextCortexBaseUrl({
|
|
1560
|
+
envBaseUrl: process.env.TEXTCORTEX_BASE_URL,
|
|
1561
|
+
storedBaseUrl: await resolveStoredBaseUrl(),
|
|
1562
|
+
preferLocalhost,
|
|
1563
|
+
});
|
|
1490
1564
|
const model = await prepareRuntime(refreshedBaseUrl, refreshedToken);
|
|
1491
1565
|
return { model, token: refreshedToken, baseUrl: refreshedBaseUrl };
|
|
1492
1566
|
}
|
|
@@ -1503,12 +1577,25 @@ async function main() {
|
|
|
1503
1577
|
passthrough.shift();
|
|
1504
1578
|
}
|
|
1505
1579
|
|
|
1580
|
+
const preferLocalhost = hasLocalBaseUrlFlag(passthrough);
|
|
1581
|
+
const runtimeArgs = stripLocalBaseUrlFlags(passthrough);
|
|
1506
1582
|
const storedBaseUrl = await resolveStoredBaseUrl();
|
|
1507
|
-
const baseUrl =
|
|
1508
|
-
|
|
1583
|
+
const baseUrl = resolveTextCortexBaseUrl({
|
|
1584
|
+
envBaseUrl: process.env.TEXTCORTEX_BASE_URL,
|
|
1585
|
+
storedBaseUrl,
|
|
1586
|
+
preferLocalhost,
|
|
1587
|
+
});
|
|
1588
|
+
const subcommand = runtimeArgs[0];
|
|
1509
1589
|
|
|
1510
1590
|
if (subcommand === "login") {
|
|
1511
|
-
await runLoginCommand(
|
|
1591
|
+
await runLoginCommand(
|
|
1592
|
+
resolveLoginBaseUrl({
|
|
1593
|
+
envBaseUrl: process.env.TEXTCORTEX_BASE_URL,
|
|
1594
|
+
preferLocalhost,
|
|
1595
|
+
}),
|
|
1596
|
+
runtimeArgs.slice(1),
|
|
1597
|
+
{ preferLocalhost },
|
|
1598
|
+
);
|
|
1512
1599
|
return;
|
|
1513
1600
|
}
|
|
1514
1601
|
|
|
@@ -1517,12 +1604,15 @@ async function main() {
|
|
|
1517
1604
|
return;
|
|
1518
1605
|
}
|
|
1519
1606
|
|
|
1520
|
-
maybeRenderBanner(
|
|
1521
|
-
const tokenResolution = await resolveTokenWithAutoLogin(baseUrl,
|
|
1607
|
+
maybeRenderBanner(runtimeArgs);
|
|
1608
|
+
const tokenResolution = await resolveTokenWithAutoLogin(baseUrl, runtimeArgs, {
|
|
1609
|
+
preferLocalhost,
|
|
1610
|
+
});
|
|
1522
1611
|
const runtime = await prepareRuntimeWithAutoLogin(
|
|
1523
1612
|
tokenResolution.baseUrl,
|
|
1524
1613
|
tokenResolution.token,
|
|
1525
|
-
|
|
1614
|
+
runtimeArgs,
|
|
1615
|
+
{ preferLocalhost },
|
|
1526
1616
|
);
|
|
1527
1617
|
const token = runtime.token;
|
|
1528
1618
|
const model = runtime.model;
|
|
@@ -1539,14 +1629,15 @@ async function main() {
|
|
|
1539
1629
|
TEXTCORTEX_API_KEY: token,
|
|
1540
1630
|
},
|
|
1541
1631
|
};
|
|
1542
|
-
const monitorRuntimeSession = canAutoLogin(
|
|
1632
|
+
const monitorRuntimeSession = canAutoLogin(runtimeArgs);
|
|
1543
1633
|
|
|
1544
1634
|
if (opencodeBinaryPath) {
|
|
1545
1635
|
const result = await runRuntimeWithSessionRecovery({
|
|
1546
|
-
args:
|
|
1636
|
+
args: runtimeArgs,
|
|
1547
1637
|
baseUrl: runtime.baseUrl,
|
|
1548
1638
|
token,
|
|
1549
1639
|
childOptions,
|
|
1640
|
+
preferLocalhost,
|
|
1550
1641
|
canAutoLoginRuntime: monitorRuntimeSession,
|
|
1551
1642
|
launchRuntimeFn: ({ args, childOptions }) =>
|
|
1552
1643
|
runRuntimeBinary(opencodeBinaryPath, args, childOptions, monitorRuntimeSession),
|
|
@@ -1559,10 +1650,11 @@ async function main() {
|
|
|
1559
1650
|
const pinnedRuntimePath = await resolvePinnedRuntimeBinary(launchPackage, childOptions);
|
|
1560
1651
|
if (pinnedRuntimePath) {
|
|
1561
1652
|
const result = await runRuntimeWithSessionRecovery({
|
|
1562
|
-
args:
|
|
1653
|
+
args: runtimeArgs,
|
|
1563
1654
|
baseUrl: runtime.baseUrl,
|
|
1564
1655
|
token,
|
|
1565
1656
|
childOptions,
|
|
1657
|
+
preferLocalhost,
|
|
1566
1658
|
canAutoLoginRuntime: monitorRuntimeSession,
|
|
1567
1659
|
launchRuntimeFn: ({ args, childOptions }) =>
|
|
1568
1660
|
runRuntimeBinary(pinnedRuntimePath, args, childOptions, monitorRuntimeSession),
|
|
@@ -1570,7 +1662,7 @@ async function main() {
|
|
|
1570
1662
|
exitWithChildResult(result);
|
|
1571
1663
|
return;
|
|
1572
1664
|
}
|
|
1573
|
-
await runPackageLauncher(launchPackage,
|
|
1665
|
+
await runPackageLauncher(launchPackage, runtimeArgs, childOptions);
|
|
1574
1666
|
}
|
|
1575
1667
|
|
|
1576
1668
|
const resolveExecutablePath = (value) => {
|
|
@@ -15,8 +15,12 @@ import {
|
|
|
15
15
|
canRecoverRuntimeSessionFromTranscript,
|
|
16
16
|
buildZenocodeBanner,
|
|
17
17
|
chooseDefaults,
|
|
18
|
+
hasLocalBaseUrlFlag,
|
|
19
|
+
resolveLoginBaseUrl,
|
|
18
20
|
resolveLoginSuccessIdentifier,
|
|
21
|
+
resolveTextCortexBaseUrl,
|
|
19
22
|
runRuntimeWithSessionRecovery,
|
|
23
|
+
shouldFallbackLoginToCloud,
|
|
20
24
|
writePrivateJsonFile,
|
|
21
25
|
} from "./run-zenocode.mjs";
|
|
22
26
|
|
|
@@ -33,6 +37,93 @@ test("chooseDefaults prefers kimi k2.5 thinking for Zenocode", () => {
|
|
|
33
37
|
});
|
|
34
38
|
});
|
|
35
39
|
|
|
40
|
+
test("resolveTextCortexBaseUrl defaults to the cloud API for packaged usage", () => {
|
|
41
|
+
assert.equal(
|
|
42
|
+
resolveTextCortexBaseUrl({
|
|
43
|
+
envBaseUrl: "",
|
|
44
|
+
storedBaseUrl: null,
|
|
45
|
+
}),
|
|
46
|
+
"https://api.textcortex.com",
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("resolveTextCortexBaseUrl prefers the explicit env var over stored credentials", () => {
|
|
51
|
+
assert.equal(
|
|
52
|
+
resolveTextCortexBaseUrl({
|
|
53
|
+
envBaseUrl: "https://staging.textcortex.com",
|
|
54
|
+
storedBaseUrl: "http://127.0.0.1:8080",
|
|
55
|
+
}),
|
|
56
|
+
"https://staging.textcortex.com",
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("resolveTextCortexBaseUrl prefers localhost when the local flag is enabled", () => {
|
|
61
|
+
assert.equal(
|
|
62
|
+
resolveTextCortexBaseUrl({
|
|
63
|
+
envBaseUrl: "https://staging.textcortex.com",
|
|
64
|
+
storedBaseUrl: "https://api.textcortex.com",
|
|
65
|
+
preferLocalhost: true,
|
|
66
|
+
}),
|
|
67
|
+
"http://127.0.0.1:8080",
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("resolveLoginBaseUrl ignores stored credentials and defaults explicit login to cloud", () => {
|
|
72
|
+
assert.equal(
|
|
73
|
+
resolveLoginBaseUrl({
|
|
74
|
+
envBaseUrl: "",
|
|
75
|
+
}),
|
|
76
|
+
"https://api.textcortex.com",
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("resolveLoginBaseUrl still respects explicit env overrides", () => {
|
|
81
|
+
assert.equal(
|
|
82
|
+
resolveLoginBaseUrl({
|
|
83
|
+
envBaseUrl: "https://staging.textcortex.com",
|
|
84
|
+
}),
|
|
85
|
+
"https://staging.textcortex.com",
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("resolveLoginBaseUrl prefers localhost when the local flag is enabled", () => {
|
|
90
|
+
assert.equal(
|
|
91
|
+
resolveLoginBaseUrl({
|
|
92
|
+
envBaseUrl: "https://staging.textcortex.com",
|
|
93
|
+
preferLocalhost: true,
|
|
94
|
+
}),
|
|
95
|
+
"http://127.0.0.1:8080",
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("hasLocalBaseUrlFlag detects localhost flags", () => {
|
|
100
|
+
assert.equal(hasLocalBaseUrlFlag(["login", "--local"]), true);
|
|
101
|
+
assert.equal(hasLocalBaseUrlFlag(["--localhost", "run"]), true);
|
|
102
|
+
assert.equal(hasLocalBaseUrlFlag(["run", "--help"]), false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("shouldFallbackLoginToCloud retries the cloud API when the local auth route returns 404", () => {
|
|
106
|
+
assert.equal(
|
|
107
|
+
shouldFallbackLoginToCloud({
|
|
108
|
+
baseUrl: "http://127.0.0.1:8080",
|
|
109
|
+
hasExplicitBaseUrl: false,
|
|
110
|
+
error: new Error("Login initiate failed (404): not found"),
|
|
111
|
+
}),
|
|
112
|
+
true,
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("shouldFallbackLoginToCloud does not override an explicit base URL", () => {
|
|
117
|
+
assert.equal(
|
|
118
|
+
shouldFallbackLoginToCloud({
|
|
119
|
+
baseUrl: "http://127.0.0.1:8080",
|
|
120
|
+
hasExplicitBaseUrl: true,
|
|
121
|
+
error: new Error("fetch failed"),
|
|
122
|
+
}),
|
|
123
|
+
false,
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
36
127
|
test("buildOpenCodeConfig includes an openai stub for fallback runtime auth plugins", () => {
|
|
37
128
|
const config = buildOpenCodeConfig({
|
|
38
129
|
baseUrl: "http://127.0.0.1:8080",
|
|
@@ -253,6 +344,61 @@ test("runRuntimeWithSessionRecovery refreshes stored credentials before forcing
|
|
|
253
344
|
assert.deepEqual(result, { code: 0, signal: null, expiredSession: false });
|
|
254
345
|
});
|
|
255
346
|
|
|
347
|
+
test("runRuntimeWithSessionRecovery preserves explicit localhost preference during login recovery", async () => {
|
|
348
|
+
const events = [];
|
|
349
|
+
let launchCount = 0;
|
|
350
|
+
|
|
351
|
+
const result = await runRuntimeWithSessionRecovery({
|
|
352
|
+
args: ["run"],
|
|
353
|
+
baseUrl: "http://127.0.0.1:8080",
|
|
354
|
+
token: "token-1",
|
|
355
|
+
childOptions: {
|
|
356
|
+
cwd: process.cwd(),
|
|
357
|
+
env: {},
|
|
358
|
+
},
|
|
359
|
+
canAutoLoginRuntime: true,
|
|
360
|
+
preferLocalhost: true,
|
|
361
|
+
refreshTokenFn: async (baseUrl) => {
|
|
362
|
+
events.push(["refresh", baseUrl]);
|
|
363
|
+
throw new Error("refresh failed");
|
|
364
|
+
},
|
|
365
|
+
runLogin: async (baseUrl, loginArgs, options) => {
|
|
366
|
+
events.push(["login", baseUrl, loginArgs, options]);
|
|
367
|
+
},
|
|
368
|
+
resolveTokenFn: async () => {
|
|
369
|
+
events.push(["resolve-token"]);
|
|
370
|
+
return "token-2";
|
|
371
|
+
},
|
|
372
|
+
resolveStoredBaseUrlFn: async () => {
|
|
373
|
+
events.push(["resolve-base-url"]);
|
|
374
|
+
return "https://api.textcortex.com";
|
|
375
|
+
},
|
|
376
|
+
prepareRuntimeFn: async (baseUrl, token) => {
|
|
377
|
+
events.push(["prepare", baseUrl, token]);
|
|
378
|
+
return "kimi-k2-5-thinking";
|
|
379
|
+
},
|
|
380
|
+
launchRuntimeFn: async ({ childOptions }) => {
|
|
381
|
+
launchCount += 1;
|
|
382
|
+
events.push(["launch", launchCount, childOptions.env.TEXTCORTEX_API_KEY]);
|
|
383
|
+
if (launchCount === 1) {
|
|
384
|
+
return { expiredSession: true };
|
|
385
|
+
}
|
|
386
|
+
return { code: 0, signal: null, expiredSession: false };
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
assert.deepEqual(events, [
|
|
391
|
+
["launch", 1, "token-1"],
|
|
392
|
+
["refresh", "http://127.0.0.1:8080"],
|
|
393
|
+
["login", "http://127.0.0.1:8080", ["--local"], { preferLocalhost: true }],
|
|
394
|
+
["resolve-token"],
|
|
395
|
+
["resolve-base-url"],
|
|
396
|
+
["prepare", "http://127.0.0.1:8080", "token-2"],
|
|
397
|
+
["launch", 2, "token-2"],
|
|
398
|
+
]);
|
|
399
|
+
assert.deepEqual(result, { code: 0, signal: null, expiredSession: false });
|
|
400
|
+
});
|
|
401
|
+
|
|
256
402
|
test("runRuntimeWithSessionRecovery stops after repeated recovery failures", async () => {
|
|
257
403
|
const events = [];
|
|
258
404
|
let launchCount = 0;
|