@textcortex/zenocode 0.1.10 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@textcortex/zenocode",
3
- "version": "0.1.10",
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"
@@ -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
+ });
@@ -576,6 +576,16 @@ export function resolveTextCortexBaseUrl({
576
576
  return envBaseUrl || storedBaseUrl || cloudBaseUrlDefault;
577
577
  }
578
578
 
579
+ export function resolveLoginBaseUrl({
580
+ envBaseUrl,
581
+ preferLocalhost = false,
582
+ } = {}) {
583
+ if (preferLocalhost) {
584
+ return localBaseUrlDefault;
585
+ }
586
+ return envBaseUrl || cloudBaseUrlDefault;
587
+ }
588
+
579
589
  function _loginConnectivityHelp(baseUrl) {
580
590
  return [
581
591
  `Cannot reach Zenocode auth endpoint at ${baseUrl}.`,
@@ -1578,7 +1588,14 @@ async function main() {
1578
1588
  const subcommand = runtimeArgs[0];
1579
1589
 
1580
1590
  if (subcommand === "login") {
1581
- await runLoginCommand(baseUrl, runtimeArgs.slice(1), { preferLocalhost });
1591
+ await runLoginCommand(
1592
+ resolveLoginBaseUrl({
1593
+ envBaseUrl: process.env.TEXTCORTEX_BASE_URL,
1594
+ preferLocalhost,
1595
+ }),
1596
+ runtimeArgs.slice(1),
1597
+ { preferLocalhost },
1598
+ );
1582
1599
  return;
1583
1600
  }
1584
1601
 
@@ -16,6 +16,7 @@ import {
16
16
  buildZenocodeBanner,
17
17
  chooseDefaults,
18
18
  hasLocalBaseUrlFlag,
19
+ resolveLoginBaseUrl,
19
20
  resolveLoginSuccessIdentifier,
20
21
  resolveTextCortexBaseUrl,
21
22
  runRuntimeWithSessionRecovery,
@@ -67,6 +68,34 @@ test("resolveTextCortexBaseUrl prefers localhost when the local flag is enabled"
67
68
  );
68
69
  });
69
70
 
71
+ test("resolveLoginBaseUrl ignores stored credentials and defaults explicit login to cloud", () => {
72
+ assert.equal(
73
+ resolveLoginBaseUrl({
74
+ envBaseUrl: "",
75
+ }),
76
+ "https://api.textcortex.com",
77
+ );
78
+ });
79
+
80
+ test("resolveLoginBaseUrl still respects explicit env overrides", () => {
81
+ assert.equal(
82
+ resolveLoginBaseUrl({
83
+ envBaseUrl: "https://staging.textcortex.com",
84
+ }),
85
+ "https://staging.textcortex.com",
86
+ );
87
+ });
88
+
89
+ test("resolveLoginBaseUrl prefers localhost when the local flag is enabled", () => {
90
+ assert.equal(
91
+ resolveLoginBaseUrl({
92
+ envBaseUrl: "https://staging.textcortex.com",
93
+ preferLocalhost: true,
94
+ }),
95
+ "http://127.0.0.1:8080",
96
+ );
97
+ });
98
+
70
99
  test("hasLocalBaseUrlFlag detects localhost flags", () => {
71
100
  assert.equal(hasLocalBaseUrlFlag(["login", "--local"]), true);
72
101
  assert.equal(hasLocalBaseUrlFlag(["--localhost", "run"]), true);