@textcortex/zenocode 0.1.14 → 0.1.15
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 +36 -1
- package/package.json +1 -1
- package/scripts/build-branded-opencode.mjs +292 -215
- package/scripts/build-branded-opencode.test.mjs +85 -59
- package/scripts/opencode-version.mjs +5 -3
- package/scripts/run-zenocode.mjs +600 -763
- package/scripts/run-zenocode.test.mjs +625 -430
- package/scripts/source-branding.mjs +608 -0
- package/scripts/source-branding.test.mjs +76 -0
- package/scripts/branding-patch.mjs +0 -193
- package/scripts/branding-patch.test.mjs +0 -89
package/scripts/run-zenocode.mjs
CHANGED
|
@@ -1,83 +1,86 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { spawn } from
|
|
3
|
-
import { realpathSync } from
|
|
4
|
-
import fs from
|
|
5
|
-
import os from
|
|
6
|
-
import path from
|
|
7
|
-
import process from
|
|
8
|
-
import readline from
|
|
9
|
-
import { fileURLToPath } from
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
zenocodeLogo,
|
|
13
|
-
} from "./branding-patch.mjs";
|
|
14
|
-
import {
|
|
15
|
-
openCodePackageName,
|
|
16
|
-
openCodePackageSpec,
|
|
17
|
-
} from "./opencode-version.mjs";
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
4
|
+
import fs from 'node:fs/promises';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import process from 'node:process';
|
|
8
|
+
import readline from 'node:readline/promises';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { zenocodeRuntimePackageSpec } from './opencode-version.mjs';
|
|
11
|
+
import { zenocodeLogo } from './source-branding.mjs';
|
|
18
12
|
|
|
19
13
|
const currentFilePath = realpathSync(fileURLToPath(import.meta.url));
|
|
20
|
-
const __dirname = path.dirname(currentFilePath);
|
|
21
|
-
const appRoot = path.resolve(__dirname, "..");
|
|
22
14
|
const runtimeDir =
|
|
23
15
|
process.env.ZENOCODE_HOME ||
|
|
24
16
|
process.env.CODECORTEX_HOME ||
|
|
25
|
-
path.join(os.homedir(),
|
|
17
|
+
path.join(os.homedir(), '.zenocode');
|
|
26
18
|
const legacyRuntimeDir =
|
|
27
|
-
process.env.CODECORTEX_HOME || path.join(os.homedir(),
|
|
28
|
-
const runtimeCredentialsPath = path.join(runtimeDir,
|
|
19
|
+
process.env.CODECORTEX_HOME || path.join(os.homedir(), '.codecortex');
|
|
20
|
+
const runtimeCredentialsPath = path.join(runtimeDir, 'credentials.json');
|
|
29
21
|
const legacyRuntimeCredentialsPath = path.join(
|
|
30
22
|
legacyRuntimeDir,
|
|
31
|
-
|
|
23
|
+
'credentials.json'
|
|
32
24
|
);
|
|
33
|
-
const logoutMarkerPath = path.join(runtimeDir,
|
|
34
|
-
const modelsPath = path.join(runtimeDir,
|
|
35
|
-
const configPath = path.join(runtimeDir,
|
|
36
|
-
const tuiConfigPath = path.join(runtimeDir,
|
|
37
|
-
const localBaseUrlDefault =
|
|
38
|
-
const cloudBaseUrlDefault =
|
|
39
|
-
const localBaseUrlFlags = new Set([
|
|
40
|
-
|
|
41
|
-
const providerID =
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
const opencodePackage = configuredOpencodePackage || defaultBrandedOpencodePackage;
|
|
25
|
+
const logoutMarkerPath = path.join(runtimeDir, 'logout-marker.json');
|
|
26
|
+
const modelsPath = path.join(runtimeDir, 'models.json');
|
|
27
|
+
const configPath = path.join(runtimeDir, 'zenocode.jsonc');
|
|
28
|
+
const tuiConfigPath = path.join(runtimeDir, 'zenocode.tui.json');
|
|
29
|
+
const localBaseUrlDefault = 'http://127.0.0.1:8080';
|
|
30
|
+
const cloudBaseUrlDefault = 'https://api.textcortex.com';
|
|
31
|
+
const localBaseUrlFlags = new Set(['--local', '--localhost']);
|
|
32
|
+
|
|
33
|
+
const providerID = 'textcortex';
|
|
34
|
+
export function resolveConfiguredRuntimePackage(env = process.env) {
|
|
35
|
+
return (
|
|
36
|
+
env.ZENOCODE_OPENCODE_PACKAGE || env.CODECORTEX_OPENCODE_PACKAGE || null
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const configuredOpencodePackage = resolveConfiguredRuntimePackage();
|
|
41
|
+
const opencodePackage = configuredOpencodePackage || zenocodeRuntimePackageSpec;
|
|
51
42
|
const opencodeBinaryPath =
|
|
52
43
|
process.env.ZENOCODE_OPENCODE_BIN_PATH ||
|
|
53
44
|
process.env.CODECORTEX_OPENCODE_BIN_PATH ||
|
|
54
|
-
|
|
55
|
-
const oauthInitiatePath =
|
|
56
|
-
const oauthTokenPath =
|
|
45
|
+
'';
|
|
46
|
+
const oauthInitiatePath = '/internal/v2/fastapi/zenocode/oauth2/initiate';
|
|
47
|
+
const oauthTokenPath = '/internal/v2/fastapi/zenocode/oauth2/token';
|
|
57
48
|
const defaultOrder = [
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
49
|
+
'gpt-6-luna',
|
|
50
|
+
'gpt-5-6-luna',
|
|
51
|
+
'kimi-k3-non-thinking',
|
|
52
|
+
'kimi-k3-thinking',
|
|
53
|
+
'kimi-k2p7-code-non-thinking',
|
|
54
|
+
'kimi-k2p7-code-thinking',
|
|
55
|
+
'minimax-m3-thinking',
|
|
56
|
+
'kimi-k2-6-non-thinking',
|
|
57
|
+
'kimi-k2-6-thinking',
|
|
58
|
+
'kimi-k2-5',
|
|
59
|
+
'kimi-k2-5-thinking',
|
|
60
|
+
'glm-5-3',
|
|
61
|
+
'glm-5-2-non-thinking',
|
|
62
|
+
'glm-5-2-thinking',
|
|
63
|
+
'gpt-5-2',
|
|
64
|
+
'gpt-5-1',
|
|
65
|
+
'gpt-5',
|
|
66
|
+
'claude-sonnet-5-5',
|
|
67
|
+
'claude-5-sonnet',
|
|
68
|
+
'claude-opus-5-5',
|
|
69
|
+
'claude-opus-5',
|
|
70
|
+
'gpt-5-mini',
|
|
75
71
|
];
|
|
72
|
+
const retiredModelIDs = new Set([
|
|
73
|
+
'glm-4-7',
|
|
74
|
+
'glm-5',
|
|
75
|
+
'glm-5-1',
|
|
76
|
+
'glm-5-1-non-thinking',
|
|
77
|
+
'glm-5-1-thinking',
|
|
78
|
+
]);
|
|
76
79
|
const devCredentialFiles =
|
|
77
|
-
process.env.ZENOCODE_DEV_MODE ===
|
|
80
|
+
process.env.ZENOCODE_DEV_MODE === '1'
|
|
78
81
|
? [
|
|
79
|
-
path.join(process.cwd(),
|
|
80
|
-
path.join(process.cwd(),
|
|
82
|
+
path.join(process.cwd(), 'backend', '.credentials.json'),
|
|
83
|
+
path.join(process.cwd(), '.credentials.json'),
|
|
81
84
|
]
|
|
82
85
|
: [];
|
|
83
86
|
const runtimeCredentialFiles = [
|
|
@@ -85,53 +88,61 @@ const runtimeCredentialFiles = [
|
|
|
85
88
|
legacyRuntimeCredentialsPath,
|
|
86
89
|
].filter((value, index, values) => values.indexOf(value) === index);
|
|
87
90
|
const fallbackCredentialFiles = [
|
|
88
|
-
path.join(os.homedir(),
|
|
91
|
+
path.join(os.homedir(), '.credentials.json'),
|
|
89
92
|
...devCredentialFiles,
|
|
90
93
|
].filter((value, index, values) => values.indexOf(value) === index);
|
|
91
94
|
const credentialFiles = [...runtimeCredentialFiles, ...fallbackCredentialFiles];
|
|
92
95
|
|
|
93
96
|
function _decodeEscapedLogoLine(line) {
|
|
94
97
|
return line.replace(/\\u([0-9a-fA-F]{4})/g, (_, codePoint) =>
|
|
95
|
-
String.fromCharCode(Number.parseInt(codePoint, 16))
|
|
98
|
+
String.fromCharCode(Number.parseInt(codePoint, 16))
|
|
96
99
|
);
|
|
97
100
|
}
|
|
98
101
|
|
|
99
102
|
export function buildZenocodeBanner() {
|
|
100
103
|
const bannerLines = zenocodeLogo.left
|
|
101
104
|
.map((leftLine, index) => {
|
|
102
|
-
const rightLine = zenocodeLogo.right[index] ||
|
|
105
|
+
const rightLine = zenocodeLogo.right[index] || '';
|
|
103
106
|
return `${_decodeEscapedLogoLine(leftLine)} ${_decodeEscapedLogoLine(rightLine)}`.trimEnd();
|
|
104
107
|
})
|
|
105
108
|
.filter((line) => line.trim());
|
|
106
|
-
return `\n${bannerLines.join(
|
|
109
|
+
return `\n${bannerLines.join('\n')}\n`;
|
|
107
110
|
}
|
|
108
111
|
const privateDirectoryMode = 0o700;
|
|
109
112
|
const privateFileMode = 0o600;
|
|
110
113
|
|
|
114
|
+
async function _pathExists(pathToCheck) {
|
|
115
|
+
try {
|
|
116
|
+
await fs.access(pathToCheck);
|
|
117
|
+
return true;
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
111
123
|
async function readJson(filePath) {
|
|
112
124
|
try {
|
|
113
|
-
return JSON.parse(await fs.readFile(filePath,
|
|
125
|
+
return JSON.parse(await fs.readFile(filePath, 'utf-8'));
|
|
114
126
|
} catch (error) {
|
|
115
|
-
if (error?.code ===
|
|
127
|
+
if (error?.code === 'ENOENT') return null;
|
|
116
128
|
throw error;
|
|
117
129
|
}
|
|
118
130
|
}
|
|
119
131
|
|
|
120
132
|
async function ensurePrivateDirectory(dirPath) {
|
|
121
133
|
await fs.mkdir(dirPath, { recursive: true, mode: privateDirectoryMode });
|
|
122
|
-
if (process.platform !==
|
|
134
|
+
if (process.platform !== 'win32') {
|
|
123
135
|
await fs.chmod(dirPath, privateDirectoryMode);
|
|
124
136
|
}
|
|
125
137
|
}
|
|
126
138
|
|
|
127
139
|
export async function writePrivateJsonFile(filePath, payload) {
|
|
128
140
|
await ensurePrivateDirectory(path.dirname(filePath));
|
|
129
|
-
await fs.writeFile(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
)
|
|
134
|
-
if (process.platform !== "win32") {
|
|
141
|
+
await fs.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, {
|
|
142
|
+
encoding: 'utf-8',
|
|
143
|
+
mode: privateFileMode,
|
|
144
|
+
});
|
|
145
|
+
if (process.platform !== 'win32') {
|
|
135
146
|
await fs.chmod(filePath, privateFileMode);
|
|
136
147
|
}
|
|
137
148
|
}
|
|
@@ -140,7 +151,7 @@ async function clearLogoutMarker() {
|
|
|
140
151
|
try {
|
|
141
152
|
await fs.unlink(logoutMarkerPath);
|
|
142
153
|
} catch (error) {
|
|
143
|
-
if (error?.code !==
|
|
154
|
+
if (error?.code !== 'ENOENT') {
|
|
144
155
|
throw error;
|
|
145
156
|
}
|
|
146
157
|
}
|
|
@@ -151,24 +162,31 @@ async function writeLogoutMarker() {
|
|
|
151
162
|
await fs.writeFile(
|
|
152
163
|
logoutMarkerPath,
|
|
153
164
|
`${JSON.stringify({ logged_out_at: new Date().toISOString() }, null, 2)}\n`,
|
|
154
|
-
{
|
|
165
|
+
{
|
|
166
|
+
encoding: 'utf-8',
|
|
167
|
+
mode: privateFileMode,
|
|
168
|
+
}
|
|
155
169
|
);
|
|
156
|
-
if (process.platform !==
|
|
170
|
+
if (process.platform !== 'win32') {
|
|
157
171
|
await fs.chmod(logoutMarkerPath, privateFileMode);
|
|
158
172
|
}
|
|
159
173
|
}
|
|
160
174
|
|
|
161
175
|
function extractTokenFromCredentialPayload(parsed) {
|
|
162
176
|
if (!parsed) return null;
|
|
163
|
-
if (typeof parsed?.access_token ===
|
|
177
|
+
if (typeof parsed?.access_token === 'string' && parsed.access_token) {
|
|
164
178
|
return parsed.access_token;
|
|
165
179
|
}
|
|
166
180
|
if (Array.isArray(parsed?.accounts)) {
|
|
167
|
-
const active = parsed.accounts.find(
|
|
181
|
+
const active = parsed.accounts.find(
|
|
182
|
+
(entry) => typeof entry?.access_token === 'string' && entry.access_token
|
|
183
|
+
);
|
|
168
184
|
if (active) return active.access_token;
|
|
169
185
|
}
|
|
170
186
|
if (Array.isArray(parsed)) {
|
|
171
|
-
const match = parsed.find(
|
|
187
|
+
const match = parsed.find(
|
|
188
|
+
(entry) => typeof entry?.access_token === 'string' && entry.access_token
|
|
189
|
+
);
|
|
172
190
|
if (match) return match.access_token;
|
|
173
191
|
}
|
|
174
192
|
return null;
|
|
@@ -176,17 +194,19 @@ function extractTokenFromCredentialPayload(parsed) {
|
|
|
176
194
|
|
|
177
195
|
function extractBaseUrlFromCredentialPayload(parsed) {
|
|
178
196
|
if (!parsed) return null;
|
|
179
|
-
if (typeof parsed?.base_url ===
|
|
197
|
+
if (typeof parsed?.base_url === 'string' && parsed.base_url) {
|
|
180
198
|
return parsed.base_url;
|
|
181
199
|
}
|
|
182
200
|
if (Array.isArray(parsed?.accounts)) {
|
|
183
201
|
const active = parsed.accounts.find(
|
|
184
|
-
(entry) => typeof entry?.base_url ===
|
|
202
|
+
(entry) => typeof entry?.base_url === 'string' && entry.base_url
|
|
185
203
|
);
|
|
186
204
|
if (active) return active.base_url;
|
|
187
205
|
}
|
|
188
206
|
if (Array.isArray(parsed)) {
|
|
189
|
-
const match = parsed.find(
|
|
207
|
+
const match = parsed.find(
|
|
208
|
+
(entry) => typeof entry?.base_url === 'string' && entry.base_url
|
|
209
|
+
);
|
|
190
210
|
if (match) return match.base_url;
|
|
191
211
|
}
|
|
192
212
|
return null;
|
|
@@ -194,17 +214,17 @@ function extractBaseUrlFromCredentialPayload(parsed) {
|
|
|
194
214
|
|
|
195
215
|
export function resolveLoginSuccessIdentifier(tokenData) {
|
|
196
216
|
const authId =
|
|
197
|
-
typeof tokenData?.auth_id ===
|
|
198
|
-
return authId ||
|
|
217
|
+
typeof tokenData?.auth_id === 'string' ? tokenData.auth_id.trim() : '';
|
|
218
|
+
return authId || 'unknown';
|
|
199
219
|
}
|
|
200
220
|
|
|
201
221
|
function _isCredentialEntry(entry) {
|
|
202
222
|
return Boolean(
|
|
203
223
|
entry &&
|
|
204
|
-
typeof entry ===
|
|
224
|
+
typeof entry === 'object' &&
|
|
205
225
|
!Array.isArray(entry) &&
|
|
206
|
-
(typeof entry.access_token ===
|
|
207
|
-
typeof entry.refresh_token ===
|
|
226
|
+
(typeof entry.access_token === 'string' ||
|
|
227
|
+
typeof entry.refresh_token === 'string')
|
|
208
228
|
);
|
|
209
229
|
}
|
|
210
230
|
|
|
@@ -219,7 +239,9 @@ function _selectCredentialEntry(parsed) {
|
|
|
219
239
|
}
|
|
220
240
|
|
|
221
241
|
if (Array.isArray(parsed?.accounts)) {
|
|
222
|
-
const index = parsed.accounts.findIndex((entry) =>
|
|
242
|
+
const index = parsed.accounts.findIndex((entry) =>
|
|
243
|
+
_isCredentialEntry(entry)
|
|
244
|
+
);
|
|
223
245
|
if (index !== -1) {
|
|
224
246
|
return {
|
|
225
247
|
entry: parsed.accounts[index],
|
|
@@ -281,14 +303,14 @@ async function findStoredCredentialState() {
|
|
|
281
303
|
}
|
|
282
304
|
|
|
283
305
|
function _decodeBase64Url(value) {
|
|
284
|
-
const normalized = value.replaceAll(
|
|
285
|
-
const padding =
|
|
286
|
-
return Buffer.from(`${normalized}${padding}`,
|
|
306
|
+
const normalized = value.replaceAll('-', '+').replaceAll('_', '/');
|
|
307
|
+
const padding = '='.repeat((4 - (normalized.length % 4)) % 4);
|
|
308
|
+
return Buffer.from(`${normalized}${padding}`, 'base64').toString('utf-8');
|
|
287
309
|
}
|
|
288
310
|
|
|
289
311
|
function extractAccessTokenExpiry(accessToken) {
|
|
290
312
|
try {
|
|
291
|
-
const [, payloadSegment] = String(accessToken ||
|
|
313
|
+
const [, payloadSegment] = String(accessToken || '').split('.');
|
|
292
314
|
if (!payloadSegment) return null;
|
|
293
315
|
const payload = JSON.parse(_decodeBase64Url(payloadSegment));
|
|
294
316
|
const exp = Number(payload?.exp);
|
|
@@ -302,24 +324,24 @@ function extractAccessTokenExpiry(accessToken) {
|
|
|
302
324
|
}
|
|
303
325
|
|
|
304
326
|
async function exchangeRefreshToken(baseUrl, refreshToken) {
|
|
305
|
-
const refreshUrl = new URL(
|
|
327
|
+
const refreshUrl = new URL('/auth/token/refresh/', baseUrl).toString();
|
|
306
328
|
const { response, payload, text } = await requestJson(refreshUrl, {
|
|
307
|
-
method:
|
|
329
|
+
method: 'POST',
|
|
308
330
|
headers: {
|
|
309
331
|
Authorization: `Bearer ${refreshToken}`,
|
|
310
|
-
Accept:
|
|
311
|
-
|
|
332
|
+
Accept: 'application/json',
|
|
333
|
+
'Content-Type': 'application/json',
|
|
312
334
|
},
|
|
313
335
|
});
|
|
314
336
|
|
|
315
337
|
if (!response.ok) {
|
|
316
|
-
const message = extractErrorMessage(payload, text ||
|
|
338
|
+
const message = extractErrorMessage(payload, text || 'request failed');
|
|
317
339
|
throw new Error(`Token refresh failed (${response.status}): ${message}`);
|
|
318
340
|
}
|
|
319
341
|
|
|
320
342
|
const data = unwrapData(payload);
|
|
321
343
|
if (!data?.access_token) {
|
|
322
|
-
throw new Error(
|
|
344
|
+
throw new Error('Token refresh failed: invalid response payload.');
|
|
323
345
|
}
|
|
324
346
|
|
|
325
347
|
return data;
|
|
@@ -328,20 +350,20 @@ async function exchangeRefreshToken(baseUrl, refreshToken) {
|
|
|
328
350
|
async function refreshStoredCredentials(baseUrl) {
|
|
329
351
|
const state = await findStoredCredentialState();
|
|
330
352
|
const refreshToken =
|
|
331
|
-
typeof state?.entry?.refresh_token ===
|
|
353
|
+
typeof state?.entry?.refresh_token === 'string' && state.entry.refresh_token
|
|
332
354
|
? state.entry.refresh_token
|
|
333
355
|
: null;
|
|
334
356
|
|
|
335
357
|
if (!state || !refreshToken) {
|
|
336
|
-
throw new Error(
|
|
358
|
+
throw new Error('No refresh token available.');
|
|
337
359
|
}
|
|
338
360
|
|
|
339
361
|
const resolvedBaseUrl =
|
|
340
362
|
process.env.TEXTCORTEX_BASE_URL ||
|
|
341
|
-
(typeof state.entry.base_url ===
|
|
363
|
+
(typeof state.entry.base_url === 'string' && state.entry.base_url) ||
|
|
342
364
|
baseUrl;
|
|
343
365
|
if (!resolvedBaseUrl) {
|
|
344
|
-
throw new Error(
|
|
366
|
+
throw new Error('Missing base URL for token refresh.');
|
|
345
367
|
}
|
|
346
368
|
|
|
347
369
|
const refreshed = await exchangeRefreshToken(resolvedBaseUrl, refreshToken);
|
|
@@ -350,7 +372,7 @@ async function refreshStoredCredentials(baseUrl) {
|
|
|
350
372
|
base_url: resolvedBaseUrl,
|
|
351
373
|
access_token: refreshed.access_token,
|
|
352
374
|
refresh_token:
|
|
353
|
-
typeof refreshed.refresh_token ===
|
|
375
|
+
typeof refreshed.refresh_token === 'string' && refreshed.refresh_token
|
|
354
376
|
? refreshed.refresh_token
|
|
355
377
|
: refreshToken,
|
|
356
378
|
auth_id: refreshed.auth_id || state.entry.auth_id || null,
|
|
@@ -371,7 +393,8 @@ async function refreshStoredCredentials(baseUrl) {
|
|
|
371
393
|
}
|
|
372
394
|
|
|
373
395
|
async function resolveToken() {
|
|
374
|
-
const envToken =
|
|
396
|
+
const envToken =
|
|
397
|
+
process.env.TEXTCORTEX_API_KEY || process.env.TEXTCORTEX_API_TOKEN;
|
|
375
398
|
if (envToken) return envToken;
|
|
376
399
|
|
|
377
400
|
for (const filePath of await resolveCredentialFiles()) {
|
|
@@ -382,10 +405,10 @@ async function resolveToken() {
|
|
|
382
405
|
|
|
383
406
|
throw new Error(
|
|
384
407
|
[
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
].join(
|
|
408
|
+
'Missing API token.',
|
|
409
|
+
'Run `zenocode login` first,',
|
|
410
|
+
'or set TEXTCORTEX_API_KEY / TEXTCORTEX_API_TOKEN.',
|
|
411
|
+
].join(' ')
|
|
389
412
|
);
|
|
390
413
|
}
|
|
391
414
|
|
|
@@ -397,35 +420,45 @@ async function resolveStoredBaseUrl() {
|
|
|
397
420
|
return baseUrl || null;
|
|
398
421
|
}
|
|
399
422
|
|
|
423
|
+
/** Choose runtime defaults without reviving retired rows from stale catalogs. */
|
|
400
424
|
export function chooseDefaults(models) {
|
|
401
|
-
const ids = Object.keys(models);
|
|
402
|
-
if (!ids.length) throw new Error(
|
|
425
|
+
const ids = Object.keys(models).filter((id) => !retiredModelIDs.has(id));
|
|
426
|
+
if (!ids.length) throw new Error('No models were returned by the backend.');
|
|
403
427
|
const model = defaultOrder.find((id) => ids.includes(id)) || ids[0];
|
|
404
428
|
const smallModel =
|
|
405
|
-
ids.find((id) => id.includes(
|
|
429
|
+
ids.find((id) => id.includes('-mini') || id.includes('haiku')) || model;
|
|
406
430
|
return { model, smallModel };
|
|
407
431
|
}
|
|
408
432
|
|
|
409
|
-
export function buildOpenCodeConfig({
|
|
433
|
+
export function buildOpenCodeConfig({
|
|
434
|
+
baseUrl,
|
|
435
|
+
providerID,
|
|
436
|
+
model,
|
|
437
|
+
smallModel,
|
|
438
|
+
}) {
|
|
410
439
|
return {
|
|
411
|
-
$schema:
|
|
440
|
+
$schema: 'https://opencode.ai/config.json',
|
|
412
441
|
enabled_providers: [providerID],
|
|
442
|
+
// Zenocode owns runtime updates. Its source-branded runtime is released with
|
|
443
|
+
// the launcher, so an independently updating runtime would split the release.
|
|
444
|
+
autoupdate: false,
|
|
413
445
|
// Keep the legacy theme key populated for older OpenCode builds.
|
|
414
|
-
theme:
|
|
446
|
+
theme: 'system',
|
|
415
447
|
model: `${providerID}/${model}`,
|
|
416
448
|
small_model: `${providerID}/${smallModel}`,
|
|
417
449
|
provider: {
|
|
418
450
|
[providerID]: {
|
|
419
|
-
name:
|
|
451
|
+
name: 'Zenocode',
|
|
420
452
|
options: {
|
|
421
|
-
baseURL: new URL(
|
|
453
|
+
baseURL: new URL(
|
|
454
|
+
'/internal/v2/fastapi/zenocode/v1',
|
|
455
|
+
baseUrl
|
|
456
|
+
).toString(),
|
|
422
457
|
},
|
|
423
458
|
},
|
|
424
|
-
//
|
|
425
|
-
//
|
|
426
|
-
//
|
|
427
|
-
// empty provider entry around even though Zenocode only enables
|
|
428
|
-
// `textcortex`.
|
|
459
|
+
// The inherited Codex auth plugin assumes `database.openai` exists when a
|
|
460
|
+
// local OpenAI OAuth session is present. Keep a harmless empty entry even
|
|
461
|
+
// though Zenocode enables only `textcortex`.
|
|
429
462
|
openai: {
|
|
430
463
|
models: {},
|
|
431
464
|
},
|
|
@@ -435,27 +468,35 @@ export function buildOpenCodeConfig({ baseUrl, providerID, model, smallModel })
|
|
|
435
468
|
|
|
436
469
|
export function buildOpenCodeTuiConfig() {
|
|
437
470
|
return {
|
|
438
|
-
$schema:
|
|
439
|
-
theme:
|
|
471
|
+
$schema: 'https://opencode.ai/tui.json',
|
|
472
|
+
theme: 'system',
|
|
440
473
|
};
|
|
441
474
|
}
|
|
442
475
|
|
|
443
476
|
function unwrapData(payload) {
|
|
444
|
-
if (
|
|
477
|
+
if (
|
|
478
|
+
payload &&
|
|
479
|
+
typeof payload === 'object' &&
|
|
480
|
+
payload.data &&
|
|
481
|
+
typeof payload.data === 'object'
|
|
482
|
+
) {
|
|
445
483
|
return payload.data;
|
|
446
484
|
}
|
|
447
485
|
return payload;
|
|
448
486
|
}
|
|
449
487
|
|
|
450
488
|
function extractErrorMessage(payload, fallbackMessage) {
|
|
451
|
-
if (payload && typeof payload ===
|
|
489
|
+
if (payload && typeof payload === 'object') {
|
|
452
490
|
const errorMessage = payload?.error?.message;
|
|
453
|
-
if (typeof errorMessage ===
|
|
454
|
-
if (typeof payload.message ===
|
|
455
|
-
|
|
456
|
-
if (payload.detail &&
|
|
491
|
+
if (typeof errorMessage === 'string' && errorMessage) return errorMessage;
|
|
492
|
+
if (typeof payload.message === 'string' && payload.message)
|
|
493
|
+
return payload.message;
|
|
494
|
+
if (typeof payload.detail === 'string' && payload.detail)
|
|
495
|
+
return payload.detail;
|
|
496
|
+
if (payload.detail && typeof payload.detail === 'object') {
|
|
457
497
|
const detailMessage = payload.detail.message;
|
|
458
|
-
if (typeof detailMessage ===
|
|
498
|
+
if (typeof detailMessage === 'string' && detailMessage)
|
|
499
|
+
return detailMessage;
|
|
459
500
|
}
|
|
460
501
|
try {
|
|
461
502
|
return JSON.stringify(payload);
|
|
@@ -479,28 +520,46 @@ async function requestJson(url, init) {
|
|
|
479
520
|
}
|
|
480
521
|
|
|
481
522
|
async function prepareRuntime(baseUrl, token) {
|
|
482
|
-
const modelsUrl = new URL(
|
|
523
|
+
const modelsUrl = new URL(
|
|
524
|
+
'/internal/v2/fastapi/zenocode/models/api.json',
|
|
525
|
+
baseUrl
|
|
526
|
+
).toString();
|
|
483
527
|
const { response, payload, text } = await requestJson(modelsUrl, {
|
|
484
|
-
headers: { Authorization: `Bearer ${token}`, Accept:
|
|
528
|
+
headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
|
|
485
529
|
});
|
|
486
530
|
|
|
487
531
|
if (!response.ok) {
|
|
488
532
|
const body = payload ?? text;
|
|
489
|
-
const message = extractErrorMessage(
|
|
490
|
-
|
|
533
|
+
const message = extractErrorMessage(
|
|
534
|
+
payload,
|
|
535
|
+
String(body || 'request failed')
|
|
536
|
+
);
|
|
537
|
+
if (response.status === 401 && message.includes('Email not confirmed')) {
|
|
491
538
|
throw new Error(
|
|
492
|
-
|
|
539
|
+
'Models fetch failed (401): Email is not confirmed for this account. Please verify the email address and try again.'
|
|
493
540
|
);
|
|
494
541
|
}
|
|
495
542
|
throw new Error(`Models fetch failed (${response.status}): ${message}`);
|
|
496
543
|
}
|
|
497
544
|
|
|
498
545
|
const provider = payload?.[providerID];
|
|
499
|
-
if (!provider?.models || typeof provider.models !==
|
|
500
|
-
throw new Error(
|
|
546
|
+
if (!provider?.models || typeof provider.models !== 'object') {
|
|
547
|
+
throw new Error(
|
|
548
|
+
'Backend models response is missing textcortex provider data.'
|
|
549
|
+
);
|
|
501
550
|
}
|
|
502
551
|
|
|
503
|
-
const
|
|
552
|
+
const supportedModels = Object.fromEntries(
|
|
553
|
+
Object.entries(provider.models).filter(([id]) => !retiredModelIDs.has(id))
|
|
554
|
+
);
|
|
555
|
+
const supportedPayload = {
|
|
556
|
+
...payload,
|
|
557
|
+
[providerID]: {
|
|
558
|
+
...provider,
|
|
559
|
+
models: supportedModels,
|
|
560
|
+
},
|
|
561
|
+
};
|
|
562
|
+
const { model, smallModel } = chooseDefaults(supportedModels);
|
|
504
563
|
const config = buildOpenCodeConfig({
|
|
505
564
|
baseUrl,
|
|
506
565
|
providerID,
|
|
@@ -510,9 +569,21 @@ async function prepareRuntime(baseUrl, token) {
|
|
|
510
569
|
const tuiConfig = buildOpenCodeTuiConfig();
|
|
511
570
|
|
|
512
571
|
await fs.mkdir(runtimeDir, { recursive: true });
|
|
513
|
-
await fs.writeFile(
|
|
514
|
-
|
|
515
|
-
|
|
572
|
+
await fs.writeFile(
|
|
573
|
+
modelsPath,
|
|
574
|
+
`${JSON.stringify(supportedPayload, null, 2)}\n`,
|
|
575
|
+
'utf-8'
|
|
576
|
+
);
|
|
577
|
+
await fs.writeFile(
|
|
578
|
+
configPath,
|
|
579
|
+
`${JSON.stringify(config, null, 2)}\n`,
|
|
580
|
+
'utf-8'
|
|
581
|
+
);
|
|
582
|
+
await fs.writeFile(
|
|
583
|
+
tuiConfigPath,
|
|
584
|
+
`${JSON.stringify(tuiConfig, null, 2)}\n`,
|
|
585
|
+
'utf-8'
|
|
586
|
+
);
|
|
516
587
|
return model;
|
|
517
588
|
}
|
|
518
589
|
|
|
@@ -521,7 +592,7 @@ function sleep(ms) {
|
|
|
521
592
|
}
|
|
522
593
|
|
|
523
594
|
export function hasLocalBaseUrlFlag(args = []) {
|
|
524
|
-
const normalizedArgs = args[0] ===
|
|
595
|
+
const normalizedArgs = args[0] === '--' ? args.slice(1) : args;
|
|
525
596
|
return normalizedArgs.some((arg) => localBaseUrlFlags.has(arg));
|
|
526
597
|
}
|
|
527
598
|
|
|
@@ -530,7 +601,7 @@ function stripLocalBaseUrlFlags(args = []) {
|
|
|
530
601
|
}
|
|
531
602
|
|
|
532
603
|
async function parseLoginArgs(args) {
|
|
533
|
-
const normalizedArgs = args[0] ===
|
|
604
|
+
const normalizedArgs = args[0] === '--' ? args.slice(1) : args;
|
|
534
605
|
let emailHint = process.env.TEXTCORTEX_LOGIN_EMAIL || null;
|
|
535
606
|
let launchBrowser = true;
|
|
536
607
|
|
|
@@ -539,32 +610,41 @@ async function parseLoginArgs(args) {
|
|
|
539
610
|
if (localBaseUrlFlags.has(arg)) {
|
|
540
611
|
continue;
|
|
541
612
|
}
|
|
542
|
-
if (arg ===
|
|
613
|
+
if (arg === '--no-launch-browser') {
|
|
543
614
|
launchBrowser = false;
|
|
544
615
|
continue;
|
|
545
616
|
}
|
|
546
|
-
if (arg ===
|
|
617
|
+
if (arg === '--email') {
|
|
547
618
|
emailHint = normalizedArgs[idx + 1] || null;
|
|
548
619
|
idx += 1;
|
|
549
620
|
continue;
|
|
550
621
|
}
|
|
551
|
-
if (arg.startsWith(
|
|
552
|
-
emailHint = arg.slice(
|
|
622
|
+
if (arg.startsWith('--email=')) {
|
|
623
|
+
emailHint = arg.slice('--email='.length) || null;
|
|
553
624
|
continue;
|
|
554
625
|
}
|
|
555
|
-
if (arg ===
|
|
556
|
-
console.log(
|
|
557
|
-
console.log(
|
|
558
|
-
|
|
559
|
-
|
|
626
|
+
if (arg === '--help' || arg === '-h') {
|
|
627
|
+
console.log('Zenocode login options:');
|
|
628
|
+
console.log(
|
|
629
|
+
' --email <address> Optional email hint for tenant/SSO routing'
|
|
630
|
+
);
|
|
631
|
+
console.log(
|
|
632
|
+
` --local Use the local FastAPI backend at ${localBaseUrlDefault}`
|
|
633
|
+
);
|
|
634
|
+
console.log(' --no-launch-browser Do not open browser automatically');
|
|
560
635
|
process.exit(0);
|
|
561
636
|
}
|
|
562
637
|
}
|
|
563
638
|
|
|
564
639
|
if (!emailHint && process.stdin.isTTY) {
|
|
565
|
-
const rl = readline.createInterface({
|
|
640
|
+
const rl = readline.createInterface({
|
|
641
|
+
input: process.stdin,
|
|
642
|
+
output: process.stdout,
|
|
643
|
+
});
|
|
566
644
|
try {
|
|
567
|
-
const response = (
|
|
645
|
+
const response = (
|
|
646
|
+
await rl.question('Email for tenant routing (optional): ')
|
|
647
|
+
).trim();
|
|
568
648
|
emailHint = response || null;
|
|
569
649
|
} finally {
|
|
570
650
|
rl.close();
|
|
@@ -576,15 +656,19 @@ async function parseLoginArgs(args) {
|
|
|
576
656
|
|
|
577
657
|
function isFetchFailedError(error) {
|
|
578
658
|
const message = error instanceof Error ? error.message : String(error);
|
|
579
|
-
return message.includes(
|
|
659
|
+
return message.includes('fetch failed');
|
|
580
660
|
}
|
|
581
661
|
|
|
582
662
|
function isLoginRouteNotFoundError(error) {
|
|
583
663
|
const message = error instanceof Error ? error.message : String(error);
|
|
584
|
-
return message.includes(
|
|
664
|
+
return message.includes('Login initiate failed (404)');
|
|
585
665
|
}
|
|
586
666
|
|
|
587
|
-
export function shouldFallbackLoginToCloud({
|
|
667
|
+
export function shouldFallbackLoginToCloud({
|
|
668
|
+
baseUrl,
|
|
669
|
+
hasExplicitBaseUrl,
|
|
670
|
+
error,
|
|
671
|
+
}) {
|
|
588
672
|
if (hasExplicitBaseUrl || baseUrl !== localBaseUrlDefault) {
|
|
589
673
|
return false;
|
|
590
674
|
}
|
|
@@ -617,28 +701,28 @@ function _loginConnectivityHelp(baseUrl) {
|
|
|
617
701
|
return [
|
|
618
702
|
`Cannot reach Zenocode auth endpoint at ${baseUrl}.`,
|
|
619
703
|
`Set TEXTCORTEX_BASE_URL to ${cloudBaseUrlDefault} or, for local development, run local backend FastAPI (\`cd backend && uv run dev_fastapi\`).`,
|
|
620
|
-
].join(
|
|
704
|
+
].join(' ');
|
|
621
705
|
}
|
|
622
706
|
|
|
623
707
|
async function openBrowser(url) {
|
|
624
708
|
let cmd;
|
|
625
709
|
let args;
|
|
626
|
-
if (process.platform ===
|
|
627
|
-
cmd =
|
|
710
|
+
if (process.platform === 'darwin') {
|
|
711
|
+
cmd = 'open';
|
|
628
712
|
args = [url];
|
|
629
|
-
} else if (process.platform ===
|
|
630
|
-
cmd =
|
|
631
|
-
args = [
|
|
713
|
+
} else if (process.platform === 'win32') {
|
|
714
|
+
cmd = 'cmd';
|
|
715
|
+
args = ['/c', 'start', '', url];
|
|
632
716
|
} else {
|
|
633
|
-
cmd =
|
|
717
|
+
cmd = 'xdg-open';
|
|
634
718
|
args = [url];
|
|
635
719
|
}
|
|
636
720
|
|
|
637
721
|
return new Promise((resolve) => {
|
|
638
722
|
try {
|
|
639
|
-
const child = spawn(cmd, args, { stdio:
|
|
640
|
-
child.on(
|
|
641
|
-
child.on(
|
|
723
|
+
const child = spawn(cmd, args, { stdio: 'ignore' });
|
|
724
|
+
child.on('error', () => resolve(false));
|
|
725
|
+
child.on('exit', (code) => resolve(code === 0));
|
|
642
726
|
} catch {
|
|
643
727
|
resolve(false);
|
|
644
728
|
}
|
|
@@ -649,49 +733,67 @@ async function initiateDeviceLogin(baseUrl, emailHint) {
|
|
|
649
733
|
const initiateUrl = new URL(oauthInitiatePath, baseUrl).toString();
|
|
650
734
|
const body = emailHint ? { email_hint: emailHint } : {};
|
|
651
735
|
const { response, payload, text } = await requestJson(initiateUrl, {
|
|
652
|
-
method:
|
|
653
|
-
headers: {
|
|
736
|
+
method: 'POST',
|
|
737
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
654
738
|
body: JSON.stringify(body),
|
|
655
739
|
});
|
|
656
740
|
|
|
657
741
|
if (!response.ok) {
|
|
658
|
-
const message = extractErrorMessage(payload, text ||
|
|
742
|
+
const message = extractErrorMessage(payload, text || 'request failed');
|
|
659
743
|
throw new Error(`Login initiate failed (${response.status}): ${message}`);
|
|
660
744
|
}
|
|
661
745
|
|
|
662
746
|
const data = unwrapData(payload);
|
|
663
|
-
if (!data || typeof data !==
|
|
664
|
-
throw new Error(
|
|
747
|
+
if (!data || typeof data !== 'object') {
|
|
748
|
+
throw new Error('Login initiate failed: invalid response payload.');
|
|
665
749
|
}
|
|
666
750
|
|
|
667
|
-
const deviceCode =
|
|
668
|
-
|
|
669
|
-
const
|
|
670
|
-
const
|
|
671
|
-
|
|
751
|
+
const deviceCode =
|
|
752
|
+
typeof data.device_code === 'string' ? data.device_code : null;
|
|
753
|
+
const userCode = typeof data.user_code === 'string' ? data.user_code : null;
|
|
754
|
+
const verificationUrl =
|
|
755
|
+
typeof data.verification_url_complete === 'string'
|
|
756
|
+
? data.verification_url_complete
|
|
757
|
+
: null;
|
|
758
|
+
const interval = Number.isFinite(Number(data.interval))
|
|
759
|
+
? Number(data.interval)
|
|
760
|
+
: 5;
|
|
761
|
+
const expiresIn = Number.isFinite(Number(data.expires_in))
|
|
762
|
+
? Number(data.expires_in)
|
|
763
|
+
: 600;
|
|
672
764
|
|
|
673
765
|
if (!deviceCode || !userCode || !verificationUrl) {
|
|
674
|
-
throw new Error(
|
|
766
|
+
throw new Error(
|
|
767
|
+
'Login initiate failed: incomplete device authorization payload.'
|
|
768
|
+
);
|
|
675
769
|
}
|
|
676
770
|
|
|
677
771
|
return { deviceCode, userCode, verificationUrl, interval, expiresIn };
|
|
678
772
|
}
|
|
679
773
|
|
|
680
|
-
async function pollDeviceToken(
|
|
774
|
+
async function pollDeviceToken(
|
|
775
|
+
baseUrl,
|
|
776
|
+
deviceCode,
|
|
777
|
+
intervalSeconds,
|
|
778
|
+
expiresInSeconds
|
|
779
|
+
) {
|
|
681
780
|
const deadline = Date.now() + Math.max(expiresInSeconds, 1) * 1000;
|
|
682
781
|
const pollUrl = new URL(oauthTokenPath, baseUrl).toString();
|
|
683
782
|
|
|
684
783
|
while (Date.now() <= deadline) {
|
|
685
784
|
const { response, payload, text } = await requestJson(pollUrl, {
|
|
686
|
-
method:
|
|
687
|
-
headers: {
|
|
785
|
+
method: 'POST',
|
|
786
|
+
headers: {
|
|
787
|
+
'Content-Type': 'application/json',
|
|
788
|
+
Accept: 'application/json',
|
|
789
|
+
},
|
|
688
790
|
body: JSON.stringify({ device_code: deviceCode }),
|
|
689
791
|
});
|
|
690
792
|
|
|
691
793
|
if (response.status === 200) {
|
|
692
794
|
const data = unwrapData(payload);
|
|
693
795
|
if (!data?.access_token || !data?.refresh_token) {
|
|
694
|
-
throw new Error(
|
|
796
|
+
throw new Error('Token endpoint returned incomplete credentials.');
|
|
695
797
|
}
|
|
696
798
|
return data;
|
|
697
799
|
}
|
|
@@ -701,11 +803,13 @@ async function pollDeviceToken(baseUrl, deviceCode, intervalSeconds, expiresInSe
|
|
|
701
803
|
continue;
|
|
702
804
|
}
|
|
703
805
|
|
|
704
|
-
const message = extractErrorMessage(payload, text ||
|
|
705
|
-
throw new Error(
|
|
806
|
+
const message = extractErrorMessage(payload, text || 'request failed');
|
|
807
|
+
throw new Error(
|
|
808
|
+
`Login token exchange failed (${response.status}): ${message}`
|
|
809
|
+
);
|
|
706
810
|
}
|
|
707
811
|
|
|
708
|
-
throw new Error(
|
|
812
|
+
throw new Error('Device code expired. Please run login again.');
|
|
709
813
|
}
|
|
710
814
|
|
|
711
815
|
async function saveRuntimeCredentials(baseUrl, tokenData) {
|
|
@@ -725,6 +829,7 @@ async function saveRuntimeCredentials(baseUrl, tokenData) {
|
|
|
725
829
|
await clearLogoutMarker();
|
|
726
830
|
}
|
|
727
831
|
|
|
832
|
+
/** Complete device login, persist its credentials, and return that exact session. */
|
|
728
833
|
async function runLoginCommand(baseUrl, args, options = {}) {
|
|
729
834
|
const preferLocalhost = options.preferLocalhost === true;
|
|
730
835
|
const { emailHint, launchBrowser } = await parseLoginArgs(args);
|
|
@@ -733,15 +838,17 @@ async function runLoginCommand(baseUrl, args, options = {}) {
|
|
|
733
838
|
try {
|
|
734
839
|
login = await initiateDeviceLogin(resolvedBaseUrl, emailHint);
|
|
735
840
|
} catch (error) {
|
|
736
|
-
if (
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
841
|
+
if (
|
|
842
|
+
shouldFallbackLoginToCloud({
|
|
843
|
+
baseUrl: resolvedBaseUrl,
|
|
844
|
+
hasExplicitBaseUrl:
|
|
845
|
+
Boolean(process.env.TEXTCORTEX_BASE_URL) || preferLocalhost,
|
|
846
|
+
error,
|
|
847
|
+
})
|
|
848
|
+
) {
|
|
742
849
|
resolvedBaseUrl = cloudBaseUrlDefault;
|
|
743
850
|
console.log(
|
|
744
|
-
`Local backend not reachable at ${localBaseUrlDefault}. Falling back to ${cloudBaseUrlDefault}
|
|
851
|
+
`Local backend not reachable at ${localBaseUrlDefault}. Falling back to ${cloudBaseUrlDefault}.`
|
|
745
852
|
);
|
|
746
853
|
try {
|
|
747
854
|
login = await initiateDeviceLogin(resolvedBaseUrl, emailHint);
|
|
@@ -762,16 +869,18 @@ async function runLoginCommand(baseUrl, args, options = {}) {
|
|
|
762
869
|
}
|
|
763
870
|
}
|
|
764
871
|
|
|
765
|
-
console.log(
|
|
872
|
+
console.log('\nComplete authorization in your browser:');
|
|
766
873
|
console.log(` ${login.verificationUrl}`);
|
|
767
874
|
console.log(`User code: ${login.userCode}\n`);
|
|
768
875
|
|
|
769
876
|
if (launchBrowser) {
|
|
770
877
|
const launched = await openBrowser(login.verificationUrl);
|
|
771
878
|
if (!launched) {
|
|
772
|
-
console.log(
|
|
879
|
+
console.log(
|
|
880
|
+
'Unable to open a browser automatically. Open the URL above manually.'
|
|
881
|
+
);
|
|
773
882
|
} else {
|
|
774
|
-
console.log(
|
|
883
|
+
console.log('Opened browser for authentication.');
|
|
775
884
|
}
|
|
776
885
|
}
|
|
777
886
|
|
|
@@ -779,13 +888,14 @@ async function runLoginCommand(baseUrl, args, options = {}) {
|
|
|
779
888
|
resolvedBaseUrl,
|
|
780
889
|
login.deviceCode,
|
|
781
890
|
login.interval,
|
|
782
|
-
login.expiresIn
|
|
891
|
+
login.expiresIn
|
|
783
892
|
);
|
|
784
893
|
await saveRuntimeCredentials(resolvedBaseUrl, tokenData);
|
|
785
894
|
|
|
786
895
|
const account = resolveLoginSuccessIdentifier(tokenData);
|
|
787
896
|
console.log(`Login successful for ${account}.`);
|
|
788
897
|
console.log(`Credentials saved to ${runtimeCredentialsPath}`);
|
|
898
|
+
return { token: tokenData.access_token, baseUrl: resolvedBaseUrl };
|
|
789
899
|
}
|
|
790
900
|
|
|
791
901
|
async function runLogoutCommand() {
|
|
@@ -795,7 +905,7 @@ async function runLogoutCommand() {
|
|
|
795
905
|
await fs.unlink(credentialsPath);
|
|
796
906
|
removedAnyCredentials = true;
|
|
797
907
|
} catch (error) {
|
|
798
|
-
if (error?.code ===
|
|
908
|
+
if (error?.code === 'ENOENT') {
|
|
799
909
|
continue;
|
|
800
910
|
}
|
|
801
911
|
throw error;
|
|
@@ -804,25 +914,27 @@ async function runLogoutCommand() {
|
|
|
804
914
|
|
|
805
915
|
const fallbackCredentialsPresent = (
|
|
806
916
|
await Promise.all(
|
|
807
|
-
fallbackCredentialFiles.map((credentialsPath) =>
|
|
917
|
+
fallbackCredentialFiles.map((credentialsPath) =>
|
|
918
|
+
_pathExists(credentialsPath)
|
|
919
|
+
)
|
|
808
920
|
)
|
|
809
921
|
).some(Boolean);
|
|
810
922
|
|
|
811
923
|
if (removedAnyCredentials || fallbackCredentialsPresent) {
|
|
812
924
|
await writeLogoutMarker();
|
|
813
|
-
console.log(
|
|
925
|
+
console.log('Zenocode session cleared.');
|
|
814
926
|
return;
|
|
815
927
|
}
|
|
816
928
|
|
|
817
|
-
console.log(
|
|
929
|
+
console.log('No local Zenocode credentials found.');
|
|
818
930
|
}
|
|
819
931
|
|
|
820
932
|
async function runChild(command, args, options) {
|
|
821
933
|
return new Promise((resolve, reject) => {
|
|
822
934
|
try {
|
|
823
935
|
const child = spawn(command, args, options);
|
|
824
|
-
child.on(
|
|
825
|
-
child.on(
|
|
936
|
+
child.on('error', reject);
|
|
937
|
+
child.on('exit', (code, signal) => resolve({ code, signal }));
|
|
826
938
|
} catch (error) {
|
|
827
939
|
reject(error);
|
|
828
940
|
}
|
|
@@ -830,19 +942,19 @@ async function runChild(command, args, options) {
|
|
|
830
942
|
}
|
|
831
943
|
|
|
832
944
|
function _buildScriptCommand(runtimeBinaryPath, args, transcriptPath) {
|
|
833
|
-
if (process.platform ===
|
|
945
|
+
if (process.platform === 'darwin') {
|
|
834
946
|
return {
|
|
835
|
-
command:
|
|
836
|
-
args: [
|
|
947
|
+
command: 'script',
|
|
948
|
+
args: ['-q', '-F', '-t', '0', transcriptPath, runtimeBinaryPath, ...args],
|
|
837
949
|
};
|
|
838
950
|
}
|
|
839
|
-
if (process.platform ===
|
|
951
|
+
if (process.platform === 'linux') {
|
|
840
952
|
const shellCommand = [runtimeBinaryPath, ...args]
|
|
841
953
|
.map((value) => `'${String(value).replaceAll("'", `'\\''`)}'`)
|
|
842
|
-
.join(
|
|
954
|
+
.join(' ');
|
|
843
955
|
return {
|
|
844
|
-
command:
|
|
845
|
-
args: [
|
|
956
|
+
command: 'script',
|
|
957
|
+
args: ['-q', '-f', '-c', shellCommand, transcriptPath],
|
|
846
958
|
};
|
|
847
959
|
}
|
|
848
960
|
return null;
|
|
@@ -851,10 +963,10 @@ function _buildScriptCommand(runtimeBinaryPath, args, transcriptPath) {
|
|
|
851
963
|
async function _readTranscriptUpdate(transcriptPath, offset) {
|
|
852
964
|
let handle;
|
|
853
965
|
try {
|
|
854
|
-
handle = await fs.open(transcriptPath,
|
|
966
|
+
handle = await fs.open(transcriptPath, 'r');
|
|
855
967
|
} catch (error) {
|
|
856
|
-
if (error?.code ===
|
|
857
|
-
return { offset, text:
|
|
968
|
+
if (error?.code === 'ENOENT') {
|
|
969
|
+
return { offset, text: '' };
|
|
858
970
|
}
|
|
859
971
|
throw error;
|
|
860
972
|
}
|
|
@@ -862,13 +974,13 @@ async function _readTranscriptUpdate(transcriptPath, offset) {
|
|
|
862
974
|
try {
|
|
863
975
|
const stats = await handle.stat();
|
|
864
976
|
if (stats.size <= offset) {
|
|
865
|
-
return { offset, text:
|
|
977
|
+
return { offset, text: '' };
|
|
866
978
|
}
|
|
867
979
|
|
|
868
980
|
const length = stats.size - offset;
|
|
869
981
|
const buffer = Buffer.alloc(length);
|
|
870
982
|
await handle.read(buffer, 0, length, offset);
|
|
871
|
-
return { offset: stats.size, text: buffer.toString(
|
|
983
|
+
return { offset: stats.size, text: buffer.toString('utf-8') };
|
|
872
984
|
} finally {
|
|
873
985
|
await handle.close();
|
|
874
986
|
}
|
|
@@ -879,44 +991,61 @@ async function _terminateChildProcess(child, exitPromise) {
|
|
|
879
991
|
return exitPromise;
|
|
880
992
|
}
|
|
881
993
|
|
|
882
|
-
child.kill(
|
|
883
|
-
const exited = await Promise.race([
|
|
994
|
+
child.kill('SIGTERM');
|
|
995
|
+
const exited = await Promise.race([
|
|
996
|
+
exitPromise,
|
|
997
|
+
sleep(1_500).then(() => null),
|
|
998
|
+
]);
|
|
884
999
|
if (exited) {
|
|
885
1000
|
return exited;
|
|
886
1001
|
}
|
|
887
1002
|
|
|
888
|
-
child.kill(
|
|
1003
|
+
child.kill('SIGKILL');
|
|
889
1004
|
return exitPromise;
|
|
890
1005
|
}
|
|
891
1006
|
|
|
892
|
-
async function _runRuntimeWithTranscriptMonitor(
|
|
893
|
-
|
|
894
|
-
|
|
1007
|
+
async function _runRuntimeWithTranscriptMonitor(
|
|
1008
|
+
runtimeBinaryPath,
|
|
1009
|
+
args,
|
|
1010
|
+
options
|
|
1011
|
+
) {
|
|
1012
|
+
const transcriptDir = await fs.mkdtemp(
|
|
1013
|
+
path.join(os.tmpdir(), 'zenocode-runtime-')
|
|
1014
|
+
);
|
|
1015
|
+
const transcriptPath = path.join(transcriptDir, 'session.log');
|
|
895
1016
|
const command = _buildScriptCommand(runtimeBinaryPath, args, transcriptPath);
|
|
896
1017
|
if (!command) {
|
|
897
|
-
const result = await runChild(runtimeBinaryPath, args, {
|
|
1018
|
+
const result = await runChild(runtimeBinaryPath, args, {
|
|
1019
|
+
...options,
|
|
1020
|
+
stdio: 'inherit',
|
|
1021
|
+
});
|
|
898
1022
|
return { ...result, expiredSession: false };
|
|
899
1023
|
}
|
|
900
1024
|
|
|
901
1025
|
try {
|
|
902
1026
|
const child = spawn(command.command, command.args, {
|
|
903
1027
|
...options,
|
|
904
|
-
stdio:
|
|
1028
|
+
stdio: 'inherit',
|
|
905
1029
|
});
|
|
906
1030
|
const exitPromise = new Promise((resolve, reject) => {
|
|
907
|
-
child.on(
|
|
908
|
-
child.on(
|
|
1031
|
+
child.on('error', reject);
|
|
1032
|
+
child.on('exit', (code, signal) =>
|
|
1033
|
+
resolve({ code, signal, expiredSession: false })
|
|
1034
|
+
);
|
|
909
1035
|
});
|
|
910
1036
|
let transcriptOffset = 0;
|
|
911
|
-
let transcriptTail =
|
|
1037
|
+
let transcriptTail = '';
|
|
912
1038
|
|
|
913
1039
|
while (true) {
|
|
914
1040
|
const outcome = await Promise.race([
|
|
915
|
-
exitPromise.then((result) => ({ type:
|
|
916
|
-
sleep(250).then(() => ({ type:
|
|
1041
|
+
exitPromise.then((result) => ({ type: 'exit', result })),
|
|
1042
|
+
sleep(250).then(() => ({ type: 'poll' })),
|
|
917
1043
|
]);
|
|
918
1044
|
|
|
919
|
-
const update = await _readTranscriptUpdate(
|
|
1045
|
+
const update = await _readTranscriptUpdate(
|
|
1046
|
+
transcriptPath,
|
|
1047
|
+
transcriptOffset
|
|
1048
|
+
);
|
|
920
1049
|
transcriptOffset = update.offset;
|
|
921
1050
|
if (update.text) {
|
|
922
1051
|
transcriptTail = _appendTranscriptTail(transcriptTail, update.text);
|
|
@@ -927,15 +1056,18 @@ async function _runRuntimeWithTranscriptMonitor(runtimeBinaryPath, args, options
|
|
|
927
1056
|
return { expiredSession: true };
|
|
928
1057
|
}
|
|
929
1058
|
|
|
930
|
-
if (outcome.type ===
|
|
1059
|
+
if (outcome.type === 'exit') {
|
|
931
1060
|
return canRecoverRuntimeSessionFromTranscript(transcriptTail)
|
|
932
1061
|
? { expiredSession: true }
|
|
933
1062
|
: outcome.result;
|
|
934
1063
|
}
|
|
935
1064
|
}
|
|
936
1065
|
} catch (error) {
|
|
937
|
-
if (error?.code ===
|
|
938
|
-
const result = await runChild(runtimeBinaryPath, args, {
|
|
1066
|
+
if (error?.code === 'ENOENT') {
|
|
1067
|
+
const result = await runChild(runtimeBinaryPath, args, {
|
|
1068
|
+
...options,
|
|
1069
|
+
stdio: 'inherit',
|
|
1070
|
+
});
|
|
939
1071
|
return { ...result, expiredSession: false };
|
|
940
1072
|
}
|
|
941
1073
|
throw error;
|
|
@@ -944,9 +1076,17 @@ async function _runRuntimeWithTranscriptMonitor(runtimeBinaryPath, args, options
|
|
|
944
1076
|
}
|
|
945
1077
|
}
|
|
946
1078
|
|
|
947
|
-
async function runRuntimeBinary(
|
|
1079
|
+
async function runRuntimeBinary(
|
|
1080
|
+
runtimeBinaryPath,
|
|
1081
|
+
args,
|
|
1082
|
+
options,
|
|
1083
|
+
monitorRuntimeSession
|
|
1084
|
+
) {
|
|
948
1085
|
if (!monitorRuntimeSession) {
|
|
949
|
-
const result = await runChild(runtimeBinaryPath, args, {
|
|
1086
|
+
const result = await runChild(runtimeBinaryPath, args, {
|
|
1087
|
+
...options,
|
|
1088
|
+
stdio: 'inherit',
|
|
1089
|
+
});
|
|
950
1090
|
return { ...result, expiredSession: false };
|
|
951
1091
|
}
|
|
952
1092
|
return _runRuntimeWithTranscriptMonitor(runtimeBinaryPath, args, options);
|
|
@@ -960,396 +1100,106 @@ function exitWithChildResult(result) {
|
|
|
960
1100
|
process.exit(result.code ?? 0);
|
|
961
1101
|
}
|
|
962
1102
|
|
|
963
|
-
function
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
const versionSeparator = packageSpecifier.lastIndexOf("@");
|
|
981
|
-
return versionSeparator > packageSpecifier.indexOf("/")
|
|
982
|
-
? packageSpecifier.slice(0, versionSeparator)
|
|
983
|
-
: packageSpecifier;
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
function shouldPatchOpencodeRuntimePackage(packageName) {
|
|
987
|
-
const runtimePackageName = packageNameFromSpecifier(packageName);
|
|
988
|
-
return (
|
|
989
|
-
runtimePackageName.endsWith("/opencode-ai") ||
|
|
990
|
-
runtimePackageName.endsWith("/zenocode-ai") ||
|
|
991
|
-
runtimePackageName === openCodePackageName
|
|
992
|
-
);
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
async function _pathExists(pathToCheck) {
|
|
996
|
-
try {
|
|
997
|
-
await fs.access(pathToCheck);
|
|
998
|
-
return true;
|
|
999
|
-
} catch {
|
|
1000
|
-
return false;
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
async function _collectPnpmDlxPnpmDirs(rootDir) {
|
|
1005
|
-
const queue = [{ dir: rootDir, depth: 0 }];
|
|
1006
|
-
const pnpmDirs = [];
|
|
1007
|
-
|
|
1008
|
-
while (queue.length) {
|
|
1009
|
-
const current = queue.shift();
|
|
1010
|
-
const nodeModulesPnpm = path.join(current.dir, "node_modules", ".pnpm");
|
|
1011
|
-
if (await _pathExists(nodeModulesPnpm)) {
|
|
1012
|
-
pnpmDirs.push(nodeModulesPnpm);
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
if (current.depth >= 3) continue;
|
|
1016
|
-
let entries = [];
|
|
1017
|
-
try {
|
|
1018
|
-
entries = await fs.readdir(current.dir, { withFileTypes: true });
|
|
1019
|
-
} catch {
|
|
1020
|
-
continue;
|
|
1021
|
-
}
|
|
1022
|
-
for (const entry of entries) {
|
|
1023
|
-
if (!entry.isDirectory()) continue;
|
|
1024
|
-
if (entry.name === "node_modules") continue;
|
|
1025
|
-
queue.push({ dir: path.join(current.dir, entry.name), depth: current.depth + 1 });
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
return pnpmDirs;
|
|
1030
|
-
}
|
|
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
|
-
}
|
|
1103
|
+
function _windowsNpxCliCandidates(execPath, env) {
|
|
1104
|
+
const candidates = [
|
|
1105
|
+
path.win32.join(
|
|
1106
|
+
path.win32.dirname(execPath),
|
|
1107
|
+
'node_modules',
|
|
1108
|
+
'npm',
|
|
1109
|
+
'bin',
|
|
1110
|
+
'npx-cli.js'
|
|
1111
|
+
),
|
|
1112
|
+
];
|
|
1113
|
+
const npmExecPath = env.npm_execpath || '';
|
|
1114
|
+
if (/npm-cli\.js$/i.test(npmExecPath)) {
|
|
1115
|
+
candidates.push(
|
|
1116
|
+
path.win32.join(path.win32.dirname(npmExecPath), 'npx-cli.js')
|
|
1117
|
+
);
|
|
1109
1118
|
}
|
|
1110
|
-
|
|
1111
1119
|
return candidates;
|
|
1112
1120
|
}
|
|
1113
1121
|
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
candidates.push(...await _collectOpencodeBinariesFromNodeModules(nodeModulesPath));
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
return [...new Set(candidates)];
|
|
1145
|
-
}
|
|
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();
|
|
1122
|
+
/**
|
|
1123
|
+
* Return the package runners to try, in order, for launching `packageName`.
|
|
1124
|
+
*
|
|
1125
|
+
* On Windows, pnpm/npx are installed as `.cmd` shims, and Node >= 18.20.2 /
|
|
1126
|
+
* 20.12.2 refuses to spawn batch files without a shell (CVE-2024-27980),
|
|
1127
|
+
* failing with `spawn EINVAL`. Windows runners are therefore real executables:
|
|
1128
|
+
* the standalone `pnpm.exe`, then npm's `npx-cli.js` run through the current
|
|
1129
|
+
* Node binary. A shell is deliberately avoided so user arguments never pass
|
|
1130
|
+
* through cmd.exe parsing.
|
|
1131
|
+
*/
|
|
1132
|
+
export function resolvePackageRunners(
|
|
1133
|
+
packageName,
|
|
1134
|
+
args,
|
|
1135
|
+
{
|
|
1136
|
+
platform = process.platform,
|
|
1137
|
+
execPath = process.execPath,
|
|
1138
|
+
env = process.env,
|
|
1139
|
+
fileExists = existsSync,
|
|
1140
|
+
} = {}
|
|
1141
|
+
) {
|
|
1142
|
+
if (platform !== 'win32') {
|
|
1143
|
+
return [
|
|
1144
|
+
{ command: 'pnpm', args: ['dlx', packageName, ...args] },
|
|
1145
|
+
{ command: 'npx', args: ['--yes', packageName, ...args] },
|
|
1146
|
+
];
|
|
1181
1147
|
}
|
|
1182
|
-
return [];
|
|
1183
|
-
}
|
|
1184
1148
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1149
|
+
const runners = [
|
|
1150
|
+
{ command: 'pnpm.exe', args: ['dlx', packageName, ...args] },
|
|
1151
|
+
];
|
|
1152
|
+
const npxCli = _windowsNpxCliCandidates(execPath, env).find((candidate) =>
|
|
1153
|
+
fileExists(candidate)
|
|
1154
|
+
);
|
|
1155
|
+
if (npxCli) {
|
|
1156
|
+
runners.push({
|
|
1157
|
+
command: execPath,
|
|
1158
|
+
args: [npxCli, '--yes', packageName, ...args],
|
|
1190
1159
|
});
|
|
1191
|
-
} catch {
|
|
1192
|
-
// If ad-hoc signing fails, keep going; runtime may still work on systems that do not enforce signatures.
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
async function _patchOpencodeBinaryBranding(binaryPath) {
|
|
1197
|
-
let buffer;
|
|
1198
|
-
try {
|
|
1199
|
-
buffer = await fs.readFile(binaryPath);
|
|
1200
|
-
} catch {
|
|
1201
|
-
return false;
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
const originalLength = buffer.length;
|
|
1205
|
-
let text = buffer.toString("latin1");
|
|
1206
|
-
const patch = patchZenocodeBinaryText(text);
|
|
1207
|
-
text = patch.text;
|
|
1208
|
-
const patched = patch.patched;
|
|
1209
|
-
|
|
1210
|
-
if (!patched) {
|
|
1211
|
-
return false;
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
buffer = Buffer.from(text, "latin1");
|
|
1215
|
-
if (buffer.length !== originalLength) {
|
|
1216
|
-
return false;
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
await fs.writeFile(binaryPath, buffer);
|
|
1220
|
-
if (process.platform !== "win32") {
|
|
1221
|
-
await fs.chmod(binaryPath, 0o755);
|
|
1222
|
-
}
|
|
1223
|
-
await _adHocSignBinary(binaryPath);
|
|
1224
|
-
return true;
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
async function _preparePinnedRuntimeBinary(binaryCandidates) {
|
|
1228
|
-
if (!binaryCandidates.length) return null;
|
|
1229
|
-
const runtimeBinaryPath = _runtimeBrandedBinaryPath();
|
|
1230
|
-
const stats = await Promise.all(binaryCandidates.map(async (binaryPath) => ({
|
|
1231
|
-
binaryPath,
|
|
1232
|
-
mtimeMs: (await fs.stat(binaryPath)).mtimeMs,
|
|
1233
|
-
})));
|
|
1234
|
-
stats.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1235
|
-
|
|
1236
|
-
await fs.mkdir(path.dirname(runtimeBinaryPath), { recursive: true });
|
|
1237
|
-
await fs.copyFile(stats[0].binaryPath, runtimeBinaryPath);
|
|
1238
|
-
if (process.platform !== "win32") {
|
|
1239
|
-
await fs.chmod(runtimeBinaryPath, 0o755);
|
|
1240
|
-
}
|
|
1241
|
-
await _patchOpencodeBinaryBranding(runtimeBinaryPath);
|
|
1242
|
-
await _adHocSignBinary(runtimeBinaryPath);
|
|
1243
|
-
return runtimeBinaryPath;
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
async function _ensurePatchedOpencodeDlxBinaries(packageName, runner, options) {
|
|
1247
|
-
if (!shouldPatchOpencodeRuntimePackage(packageName)) {
|
|
1248
|
-
return null;
|
|
1249
|
-
}
|
|
1250
|
-
if (
|
|
1251
|
-
process.env.ZENOCODE_DISABLE_OPENCODE_LOGO_PATCH === "1" ||
|
|
1252
|
-
process.env.CODECORTEX_DISABLE_OPENCODE_LOGO_PATCH === "1"
|
|
1253
|
-
) {
|
|
1254
|
-
return null;
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
const warmupArgs = _buildPackageWarmupArgs(packageName, runner);
|
|
1258
|
-
try {
|
|
1259
|
-
if (warmupArgs) {
|
|
1260
|
-
await runChild(runner.command, warmupArgs, {
|
|
1261
|
-
...options,
|
|
1262
|
-
stdio: "ignore",
|
|
1263
|
-
});
|
|
1264
|
-
}
|
|
1265
|
-
} catch {
|
|
1266
|
-
// ignore warm-up failures and continue with best effort patching
|
|
1267
1160
|
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
if (!binaryCandidates.length) {
|
|
1271
|
-
return null;
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
let patchedCount = 0;
|
|
1275
|
-
for (const binaryPath of binaryCandidates) {
|
|
1276
|
-
const patched = await _patchOpencodeBinaryBranding(binaryPath);
|
|
1277
|
-
if (patched) patchedCount += 1;
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
if (patchedCount > 0) {
|
|
1281
|
-
console.log(`Patched Zenocode branding in ${patchedCount} OpenCode runtime ${patchedCount > 1 ? "binaries" : "binary"}.`);
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
return _preparePinnedRuntimeBinary(binaryCandidates);
|
|
1161
|
+
return runners;
|
|
1285
1162
|
}
|
|
1286
1163
|
|
|
1287
|
-
|
|
1288
|
-
* Mirror direct runtime launches so fallback package runners stay interactive.
|
|
1289
|
-
*/
|
|
1290
|
-
export function buildPackageLauncherChildOptions(options, pinnedRuntimePath) {
|
|
1164
|
+
export function buildPackageLauncherChildOptions(options) {
|
|
1291
1165
|
return {
|
|
1292
1166
|
...options,
|
|
1293
|
-
stdio:
|
|
1167
|
+
stdio: 'inherit',
|
|
1294
1168
|
env: {
|
|
1295
1169
|
...(options.env || {}),
|
|
1296
|
-
...(pinnedRuntimePath ? { OPENCODE_BIN_PATH: pinnedRuntimePath } : {}),
|
|
1297
1170
|
},
|
|
1298
1171
|
};
|
|
1299
1172
|
}
|
|
1300
1173
|
|
|
1301
|
-
export function buildPackageLauncherInvocation({ runner
|
|
1302
|
-
if (pinnedRuntimePath) {
|
|
1303
|
-
return {
|
|
1304
|
-
command: pinnedRuntimePath,
|
|
1305
|
-
args,
|
|
1306
|
-
usePinnedRuntime: true,
|
|
1307
|
-
};
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1174
|
+
export function buildPackageLauncherInvocation({ runner }) {
|
|
1310
1175
|
return {
|
|
1311
1176
|
command: runner.command,
|
|
1312
1177
|
args: runner.args,
|
|
1313
|
-
usePinnedRuntime: false,
|
|
1314
1178
|
};
|
|
1315
1179
|
}
|
|
1316
1180
|
|
|
1317
|
-
async function runPackageLauncher(
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1181
|
+
async function runPackageLauncher(
|
|
1182
|
+
packageName,
|
|
1183
|
+
args,
|
|
1184
|
+
options,
|
|
1185
|
+
monitorRuntimeSession = false
|
|
1186
|
+
) {
|
|
1187
|
+
const runners = resolvePackageRunners(packageName, args);
|
|
1322
1188
|
const missingRunners = [];
|
|
1323
1189
|
|
|
1324
1190
|
for (const runner of runners) {
|
|
1325
1191
|
try {
|
|
1326
|
-
let pinnedRuntimePath = await _ensurePatchedOpencodeDlxBinaries(packageName, runner, options);
|
|
1327
|
-
if (!pinnedRuntimePath && shouldPatchOpencodeRuntimePackage(packageName)) {
|
|
1328
|
-
const existingPinnedPath = _runtimeBrandedBinaryPath();
|
|
1329
|
-
if (await _pathExists(existingPinnedPath)) {
|
|
1330
|
-
pinnedRuntimePath = existingPinnedPath;
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1334
1192
|
const invocation = buildPackageLauncherInvocation({
|
|
1335
1193
|
runner,
|
|
1336
|
-
args,
|
|
1337
|
-
pinnedRuntimePath,
|
|
1338
1194
|
});
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
invocation.
|
|
1195
|
+
return await runRuntimeBinary(
|
|
1196
|
+
invocation.command,
|
|
1197
|
+
invocation.args,
|
|
1198
|
+
buildPackageLauncherChildOptions(options),
|
|
1199
|
+
monitorRuntimeSession
|
|
1342
1200
|
);
|
|
1343
|
-
|
|
1344
|
-
const result = await runChild(invocation.command, invocation.args, childOptions);
|
|
1345
|
-
if (result.signal) {
|
|
1346
|
-
process.kill(process.pid, result.signal);
|
|
1347
|
-
return;
|
|
1348
|
-
}
|
|
1349
|
-
process.exit(result.code ?? 0);
|
|
1350
|
-
return;
|
|
1351
1201
|
} catch (error) {
|
|
1352
|
-
if (error?.code ===
|
|
1202
|
+
if (error?.code === 'ENOENT') {
|
|
1353
1203
|
missingRunners.push(runner.command);
|
|
1354
1204
|
continue;
|
|
1355
1205
|
}
|
|
@@ -1358,48 +1208,11 @@ async function runPackageLauncher(packageName, args, options) {
|
|
|
1358
1208
|
}
|
|
1359
1209
|
|
|
1360
1210
|
throw new Error(
|
|
1361
|
-
`No package runner found (${missingRunners.join(
|
|
1211
|
+
`No package runner found (${missingRunners.join(', ')}). Install pnpm or npm, or set ZENOCODE_OPENCODE_BIN_PATH/CODECORTEX_OPENCODE_BIN_PATH.`
|
|
1362
1212
|
);
|
|
1363
1213
|
}
|
|
1364
1214
|
|
|
1365
|
-
|
|
1366
|
-
const runners = [
|
|
1367
|
-
{ command: _runnerCommand("pnpm"), args: ["dlx", packageName] },
|
|
1368
|
-
{ command: _runnerCommand("npx"), args: ["--yes", packageName] },
|
|
1369
|
-
];
|
|
1370
|
-
const missingRunners = [];
|
|
1371
|
-
|
|
1372
|
-
for (const runner of runners) {
|
|
1373
|
-
try {
|
|
1374
|
-
let pinnedRuntimePath = await _ensurePatchedOpencodeDlxBinaries(packageName, runner, options);
|
|
1375
|
-
if (!pinnedRuntimePath && shouldPatchOpencodeRuntimePackage(packageName)) {
|
|
1376
|
-
const existingPinnedPath = _runtimeBrandedBinaryPath();
|
|
1377
|
-
if (await _pathExists(existingPinnedPath)) {
|
|
1378
|
-
pinnedRuntimePath = existingPinnedPath;
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
if (pinnedRuntimePath) {
|
|
1383
|
-
return pinnedRuntimePath;
|
|
1384
|
-
}
|
|
1385
|
-
} catch (error) {
|
|
1386
|
-
if (error?.code === "ENOENT") {
|
|
1387
|
-
missingRunners.push(runner.command);
|
|
1388
|
-
continue;
|
|
1389
|
-
}
|
|
1390
|
-
throw error;
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
if (missingRunners.length === runners.length) {
|
|
1395
|
-
throw new Error(
|
|
1396
|
-
`No package runner found (${missingRunners.join(", ")}). Install pnpm or npm, or set ZENOCODE_OPENCODE_BIN_PATH/CODECORTEX_OPENCODE_BIN_PATH.`,
|
|
1397
|
-
);
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
return null;
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1215
|
+
/** Relaunch the runtime with a refreshed or newly issued session after auth expiry. */
|
|
1403
1216
|
export async function runRuntimeWithSessionRecovery({
|
|
1404
1217
|
args,
|
|
1405
1218
|
baseUrl,
|
|
@@ -1440,7 +1253,7 @@ export async function runRuntimeWithSessionRecovery({
|
|
|
1440
1253
|
|
|
1441
1254
|
if (recoveryAttempts >= maxRecoveryAttempts) {
|
|
1442
1255
|
throw new Error(
|
|
1443
|
-
`Zenocode session recovery failed after ${maxRecoveryAttempts} attempts. Run \`zenocode login\` and try again
|
|
1256
|
+
`Zenocode session recovery failed after ${maxRecoveryAttempts} attempts. Run \`zenocode login\` and try again.`
|
|
1444
1257
|
);
|
|
1445
1258
|
}
|
|
1446
1259
|
recoveryAttempts += 1;
|
|
@@ -1452,7 +1265,7 @@ export async function runRuntimeWithSessionRecovery({
|
|
|
1452
1265
|
await prepareRuntimeFn(activeBaseUrl, activeToken);
|
|
1453
1266
|
const retryDelayMs = Math.min(
|
|
1454
1267
|
Math.max(recoveryDelayMs, 0) * recoveryAttempts,
|
|
1455
|
-
3_000
|
|
1268
|
+
3_000
|
|
1456
1269
|
);
|
|
1457
1270
|
if (retryDelayMs > 0) {
|
|
1458
1271
|
await sleepFn(retryDelayMs);
|
|
@@ -1464,21 +1277,30 @@ export async function runRuntimeWithSessionRecovery({
|
|
|
1464
1277
|
}
|
|
1465
1278
|
}
|
|
1466
1279
|
|
|
1467
|
-
console.log(
|
|
1468
|
-
await runLogin(
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
activeBaseUrl =
|
|
1473
|
-
resolveTextCortexBaseUrl({
|
|
1474
|
-
envBaseUrl: process.env.TEXTCORTEX_BASE_URL,
|
|
1475
|
-
storedBaseUrl: await resolveStoredBaseUrlFn(),
|
|
1280
|
+
console.log('Zenocode session expired. Starting login flow...\n');
|
|
1281
|
+
const loginResult = await runLogin(
|
|
1282
|
+
activeBaseUrl,
|
|
1283
|
+
buildAutoLoginArgs({ preferLocalhost }),
|
|
1284
|
+
{
|
|
1476
1285
|
preferLocalhost,
|
|
1477
|
-
}
|
|
1286
|
+
}
|
|
1287
|
+
);
|
|
1288
|
+
if (loginResult?.token && loginResult?.baseUrl) {
|
|
1289
|
+
activeToken = loginResult.token;
|
|
1290
|
+
activeBaseUrl = loginResult.baseUrl;
|
|
1291
|
+
} else {
|
|
1292
|
+
activeToken = await resolveTokenFn();
|
|
1293
|
+
activeBaseUrl =
|
|
1294
|
+
resolveTextCortexBaseUrl({
|
|
1295
|
+
envBaseUrl: process.env.TEXTCORTEX_BASE_URL,
|
|
1296
|
+
storedBaseUrl: await resolveStoredBaseUrlFn(),
|
|
1297
|
+
preferLocalhost,
|
|
1298
|
+
}) || activeBaseUrl;
|
|
1299
|
+
}
|
|
1478
1300
|
await prepareRuntimeFn(activeBaseUrl, activeToken);
|
|
1479
1301
|
const retryDelayMs = Math.min(
|
|
1480
1302
|
Math.max(recoveryDelayMs, 0) * recoveryAttempts,
|
|
1481
|
-
3_000
|
|
1303
|
+
3_000
|
|
1482
1304
|
);
|
|
1483
1305
|
if (retryDelayMs > 0) {
|
|
1484
1306
|
await sleepFn(retryDelayMs);
|
|
@@ -1486,71 +1308,57 @@ export async function runRuntimeWithSessionRecovery({
|
|
|
1486
1308
|
}
|
|
1487
1309
|
}
|
|
1488
1310
|
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
const controller = new AbortController();
|
|
1492
|
-
const timeout = setTimeout(() => controller.abort(), 4_000);
|
|
1493
|
-
try {
|
|
1494
|
-
const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageSpecName)}`, {
|
|
1495
|
-
method: "GET",
|
|
1496
|
-
headers: { Accept: "application/json" },
|
|
1497
|
-
signal: controller.signal,
|
|
1498
|
-
});
|
|
1499
|
-
return response.ok;
|
|
1500
|
-
} catch {
|
|
1501
|
-
return false;
|
|
1502
|
-
} finally {
|
|
1503
|
-
clearTimeout(timeout);
|
|
1504
|
-
}
|
|
1311
|
+
export function resolveLaunchPackage() {
|
|
1312
|
+
return opencodePackage;
|
|
1505
1313
|
}
|
|
1506
1314
|
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1315
|
+
/** Return launcher-owned guidance for runtime package-management commands. */
|
|
1316
|
+
export function zenocodeOwnedCommandMessage(args) {
|
|
1317
|
+
if (args[0] === 'upgrade') {
|
|
1318
|
+
return [
|
|
1319
|
+
'Zenocode updates are managed by the @textcortex/zenocode package.',
|
|
1320
|
+
'Run `npm install -g @textcortex/zenocode@latest` to update Zenocode and its managed runtime together.',
|
|
1321
|
+
].join('\n');
|
|
1510
1322
|
}
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
if (brandedAvailable) {
|
|
1514
|
-
return defaultBrandedOpencodePackage;
|
|
1323
|
+
if (args[0] === 'uninstall') {
|
|
1324
|
+
return 'Run `npm uninstall -g @textcortex/zenocode` to uninstall Zenocode.';
|
|
1515
1325
|
}
|
|
1516
|
-
|
|
1517
|
-
const legacyBrandedAvailable = await packageExistsOnNpm(
|
|
1518
|
-
legacyBrandedOpencodePackage,
|
|
1519
|
-
);
|
|
1520
|
-
if (legacyBrandedAvailable) {
|
|
1521
|
-
console.warn(
|
|
1522
|
-
`Package ${defaultBrandedOpencodePackage} was not found on npm. Falling back to ${legacyBrandedOpencodePackage}.`,
|
|
1523
|
-
);
|
|
1524
|
-
return legacyBrandedOpencodePackage;
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
console.warn(
|
|
1528
|
-
`Package ${defaultBrandedOpencodePackage} was not found on npm. Falling back to ${fallbackOpencodePackage}.`,
|
|
1529
|
-
);
|
|
1530
|
-
return fallbackOpencodePackage;
|
|
1326
|
+
return null;
|
|
1531
1327
|
}
|
|
1532
1328
|
|
|
1533
1329
|
function shouldRenderBanner(args) {
|
|
1534
1330
|
if (
|
|
1535
|
-
process.env.ZENOCODE_NO_BANNER ===
|
|
1536
|
-
process.env.CODECORTEX_NO_BANNER ===
|
|
1331
|
+
process.env.ZENOCODE_NO_BANNER === '1' ||
|
|
1332
|
+
process.env.CODECORTEX_NO_BANNER === '1'
|
|
1537
1333
|
) {
|
|
1538
1334
|
return false;
|
|
1539
1335
|
}
|
|
1540
1336
|
if (
|
|
1541
1337
|
!process.stdout.isTTY &&
|
|
1542
|
-
process.env.ZENOCODE_FORCE_BANNER !==
|
|
1543
|
-
process.env.CODECORTEX_FORCE_BANNER !==
|
|
1338
|
+
process.env.ZENOCODE_FORCE_BANNER !== '1' &&
|
|
1339
|
+
process.env.CODECORTEX_FORCE_BANNER !== '1'
|
|
1544
1340
|
) {
|
|
1545
1341
|
return false;
|
|
1546
1342
|
}
|
|
1547
1343
|
|
|
1548
|
-
const suppressFlags = new Set([
|
|
1344
|
+
const suppressFlags = new Set([
|
|
1345
|
+
'--help',
|
|
1346
|
+
'-h',
|
|
1347
|
+
'--version',
|
|
1348
|
+
'-v',
|
|
1349
|
+
'completion',
|
|
1350
|
+
]);
|
|
1549
1351
|
return !args.some((arg) => suppressFlags.has(arg));
|
|
1550
1352
|
}
|
|
1551
1353
|
|
|
1552
1354
|
export function shouldBypassZenocodePreparation(args) {
|
|
1553
|
-
const metadataFlags = new Set([
|
|
1355
|
+
const metadataFlags = new Set([
|
|
1356
|
+
'--help',
|
|
1357
|
+
'-h',
|
|
1358
|
+
'--version',
|
|
1359
|
+
'-v',
|
|
1360
|
+
'completion',
|
|
1361
|
+
]);
|
|
1554
1362
|
return metadataFlags.has(args[0]);
|
|
1555
1363
|
}
|
|
1556
1364
|
|
|
@@ -1564,39 +1372,48 @@ function maybeRenderBanner(args) {
|
|
|
1564
1372
|
function buildAutoLoginArgs({ preferLocalhost = false } = {}) {
|
|
1565
1373
|
const loginArgs = [];
|
|
1566
1374
|
if (preferLocalhost) {
|
|
1567
|
-
loginArgs.push(
|
|
1375
|
+
loginArgs.push('--local');
|
|
1568
1376
|
}
|
|
1569
1377
|
if (
|
|
1570
|
-
process.env.ZENOCODE_AUTO_LOGIN_NO_BROWSER ===
|
|
1571
|
-
process.env.CODECORTEX_AUTO_LOGIN_NO_BROWSER ===
|
|
1378
|
+
process.env.ZENOCODE_AUTO_LOGIN_NO_BROWSER === '1' ||
|
|
1379
|
+
process.env.CODECORTEX_AUTO_LOGIN_NO_BROWSER === '1'
|
|
1572
1380
|
) {
|
|
1573
|
-
loginArgs.push(
|
|
1381
|
+
loginArgs.push('--no-launch-browser');
|
|
1574
1382
|
}
|
|
1575
1383
|
return loginArgs;
|
|
1576
1384
|
}
|
|
1577
1385
|
|
|
1578
1386
|
function isMissingTokenError(error) {
|
|
1579
1387
|
const message = error instanceof Error ? error.message : String(error);
|
|
1580
|
-
return message.includes(
|
|
1388
|
+
return message.includes('Missing API token');
|
|
1581
1389
|
}
|
|
1582
1390
|
|
|
1583
|
-
|
|
1391
|
+
/** Return whether a models 401 can be repaired by refreshing or replacing auth. */
|
|
1392
|
+
export function isRecoverableSessionError(error) {
|
|
1584
1393
|
const message = error instanceof Error ? error.message : String(error);
|
|
1585
|
-
if (!message.includes(
|
|
1586
|
-
return /token has expired|expired token|token expired/i.test(
|
|
1394
|
+
if (!message.includes('Models fetch failed (401)')) return false;
|
|
1395
|
+
return /token has expired|expired token|token expired|invalid token format/i.test(
|
|
1396
|
+
message
|
|
1397
|
+
);
|
|
1587
1398
|
}
|
|
1588
1399
|
|
|
1589
1400
|
function canAutoLogin(args) {
|
|
1590
1401
|
if (
|
|
1591
|
-
process.env.ZENOCODE_AUTO_LOGIN ===
|
|
1592
|
-
process.env.CODECORTEX_AUTO_LOGIN ===
|
|
1402
|
+
process.env.ZENOCODE_AUTO_LOGIN === '0' ||
|
|
1403
|
+
process.env.CODECORTEX_AUTO_LOGIN === '0'
|
|
1593
1404
|
) {
|
|
1594
1405
|
return false;
|
|
1595
1406
|
}
|
|
1596
1407
|
if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
|
|
1597
|
-
if (process.env.CI ===
|
|
1598
|
-
|
|
1599
|
-
const suppressFlags = new Set([
|
|
1408
|
+
if (process.env.CI === '1' || process.env.CI === 'true') return false;
|
|
1409
|
+
|
|
1410
|
+
const suppressFlags = new Set([
|
|
1411
|
+
'--help',
|
|
1412
|
+
'-h',
|
|
1413
|
+
'--version',
|
|
1414
|
+
'-v',
|
|
1415
|
+
'completion',
|
|
1416
|
+
]);
|
|
1600
1417
|
if (args.some((arg) => suppressFlags.has(arg))) return false;
|
|
1601
1418
|
return true;
|
|
1602
1419
|
}
|
|
@@ -1605,16 +1422,16 @@ const ansiSequencePattern =
|
|
|
1605
1422
|
/\u001B(?:\[[0-?]*[ -/]*[@-~]|\][^\u0007]*(?:\u0007|\u001B\\)|[@-Z\\-_])/g;
|
|
1606
1423
|
|
|
1607
1424
|
function _stripTerminalControlCharacters(text) {
|
|
1608
|
-
let normalized =
|
|
1425
|
+
let normalized = '';
|
|
1609
1426
|
for (const char of text) {
|
|
1610
|
-
if (char ===
|
|
1427
|
+
if (char === '\b' || char === '\u007f') {
|
|
1611
1428
|
normalized = normalized.slice(0, -1);
|
|
1612
1429
|
continue;
|
|
1613
1430
|
}
|
|
1614
|
-
if (char ===
|
|
1431
|
+
if (char === '\r') {
|
|
1615
1432
|
continue;
|
|
1616
1433
|
}
|
|
1617
|
-
if (char <
|
|
1434
|
+
if (char < ' ' && char !== '\n' && char !== '\t') {
|
|
1618
1435
|
continue;
|
|
1619
1436
|
}
|
|
1620
1437
|
normalized += char;
|
|
@@ -1623,13 +1440,17 @@ function _stripTerminalControlCharacters(text) {
|
|
|
1623
1440
|
}
|
|
1624
1441
|
|
|
1625
1442
|
export function normalizeRuntimeTranscript(text) {
|
|
1626
|
-
return _stripTerminalControlCharacters(
|
|
1443
|
+
return _stripTerminalControlCharacters(
|
|
1444
|
+
text.replaceAll(ansiSequencePattern, '')
|
|
1445
|
+
);
|
|
1627
1446
|
}
|
|
1628
1447
|
|
|
1629
1448
|
export function canRecoverRuntimeSessionFromTranscript(text) {
|
|
1630
1449
|
const normalized = normalizeRuntimeTranscript(text);
|
|
1631
1450
|
return (
|
|
1632
|
-
/Unauthorized:\s*\{\s*"detail"\s*:\s*"Token has expired"\s*\}/i.test(
|
|
1451
|
+
/Unauthorized:\s*\{\s*"detail"\s*:\s*"Token has expired"\s*\}/i.test(
|
|
1452
|
+
normalized
|
|
1453
|
+
) ||
|
|
1633
1454
|
/authentication token may be missing or expired/i.test(normalized) ||
|
|
1634
1455
|
/server returned 401 after successful authentication/i.test(normalized)
|
|
1635
1456
|
);
|
|
@@ -1645,6 +1466,7 @@ function shouldAttemptAutoLogin(error, args) {
|
|
|
1645
1466
|
return canAutoLogin(args);
|
|
1646
1467
|
}
|
|
1647
1468
|
|
|
1469
|
+
/** Resolve credentials, returning the exact session produced by automatic login. */
|
|
1648
1470
|
async function resolveTokenWithAutoLogin(baseUrl, args, options = {}) {
|
|
1649
1471
|
const preferLocalhost = options.preferLocalhost === true;
|
|
1650
1472
|
try {
|
|
@@ -1655,27 +1477,23 @@ async function resolveTokenWithAutoLogin(baseUrl, args, options = {}) {
|
|
|
1655
1477
|
throw error;
|
|
1656
1478
|
}
|
|
1657
1479
|
|
|
1658
|
-
console.log(
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
})
|
|
1662
|
-
const token = await resolveToken();
|
|
1663
|
-
const persistedBaseUrl = resolveTextCortexBaseUrl({
|
|
1664
|
-
envBaseUrl: process.env.TEXTCORTEX_BASE_URL,
|
|
1665
|
-
storedBaseUrl: await resolveStoredBaseUrl(),
|
|
1480
|
+
console.log(
|
|
1481
|
+
'No local Zenocode credentials found. Starting login flow...\n'
|
|
1482
|
+
);
|
|
1483
|
+
return runLoginCommand(baseUrl, buildAutoLoginArgs({ preferLocalhost }), {
|
|
1666
1484
|
preferLocalhost,
|
|
1667
1485
|
});
|
|
1668
|
-
return { token, baseUrl: persistedBaseUrl };
|
|
1669
1486
|
}
|
|
1670
1487
|
}
|
|
1671
1488
|
|
|
1489
|
+
/** Prepare the runtime, refreshing or replacing rejected credentials when possible. */
|
|
1672
1490
|
async function prepareRuntimeWithAutoLogin(baseUrl, token, args, options = {}) {
|
|
1673
1491
|
const preferLocalhost = options.preferLocalhost === true;
|
|
1674
1492
|
try {
|
|
1675
1493
|
const model = await prepareRuntime(baseUrl, token);
|
|
1676
1494
|
return { model, token, baseUrl };
|
|
1677
1495
|
} catch (error) {
|
|
1678
|
-
if (!
|
|
1496
|
+
if (!isRecoverableSessionError(error)) {
|
|
1679
1497
|
throw error;
|
|
1680
1498
|
}
|
|
1681
1499
|
|
|
@@ -1689,33 +1507,31 @@ async function prepareRuntimeWithAutoLogin(baseUrl, token, args, options = {}) {
|
|
|
1689
1507
|
};
|
|
1690
1508
|
} catch {
|
|
1691
1509
|
if (!canAutoLogin(args)) {
|
|
1692
|
-
throw new Error(
|
|
1510
|
+
throw new Error(
|
|
1511
|
+
'Zenocode session expired. Run `zenocode login` and try again.'
|
|
1512
|
+
);
|
|
1693
1513
|
}
|
|
1694
1514
|
}
|
|
1695
1515
|
|
|
1696
|
-
console.log(
|
|
1697
|
-
await runLoginCommand(
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
preferLocalhost,
|
|
1705
|
-
});
|
|
1706
|
-
const model = await prepareRuntime(refreshedBaseUrl, refreshedToken);
|
|
1707
|
-
return { model, token: refreshedToken, baseUrl: refreshedBaseUrl };
|
|
1516
|
+
console.log('Zenocode session expired. Starting login flow...\n');
|
|
1517
|
+
const loginResult = await runLoginCommand(
|
|
1518
|
+
baseUrl,
|
|
1519
|
+
buildAutoLoginArgs({ preferLocalhost }),
|
|
1520
|
+
{ preferLocalhost }
|
|
1521
|
+
);
|
|
1522
|
+
const model = await prepareRuntime(loginResult.baseUrl, loginResult.token);
|
|
1523
|
+
return { model, token: loginResult.token, baseUrl: loginResult.baseUrl };
|
|
1708
1524
|
}
|
|
1709
1525
|
}
|
|
1710
1526
|
|
|
1711
1527
|
async function main() {
|
|
1712
1528
|
const args = process.argv.slice(2);
|
|
1713
|
-
const prepareOnly = args.includes(
|
|
1714
|
-
const passthrough = args.filter((arg) => arg !==
|
|
1529
|
+
const prepareOnly = args.includes('--prepare-only');
|
|
1530
|
+
const passthrough = args.filter((arg) => arg !== '--prepare-only');
|
|
1715
1531
|
|
|
1716
1532
|
// `pnpm run <script> -- ...` can forward a leading `--`; drop it so subcommands
|
|
1717
1533
|
// like `run` / `models` / `login` are parsed here first.
|
|
1718
|
-
if (passthrough[0] ===
|
|
1534
|
+
if (passthrough[0] === '--') {
|
|
1719
1535
|
passthrough.shift();
|
|
1720
1536
|
}
|
|
1721
1537
|
|
|
@@ -1728,42 +1544,54 @@ async function main() {
|
|
|
1728
1544
|
preferLocalhost,
|
|
1729
1545
|
});
|
|
1730
1546
|
const subcommand = runtimeArgs[0];
|
|
1547
|
+
let loginResult = null;
|
|
1731
1548
|
|
|
1732
|
-
|
|
1733
|
-
|
|
1549
|
+
const ownedCommandMessage = zenocodeOwnedCommandMessage(runtimeArgs);
|
|
1550
|
+
if (ownedCommandMessage) {
|
|
1551
|
+
console.log(ownedCommandMessage);
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
if (subcommand === 'login') {
|
|
1556
|
+
loginResult = await runLoginCommand(
|
|
1734
1557
|
resolveLoginBaseUrl({
|
|
1735
1558
|
envBaseUrl: process.env.TEXTCORTEX_BASE_URL,
|
|
1736
1559
|
preferLocalhost,
|
|
1737
1560
|
}),
|
|
1738
1561
|
runtimeArgs.slice(1),
|
|
1739
|
-
{ preferLocalhost }
|
|
1562
|
+
{ preferLocalhost }
|
|
1740
1563
|
);
|
|
1741
1564
|
runtimeArgs = [];
|
|
1742
1565
|
}
|
|
1743
1566
|
|
|
1744
|
-
if (subcommand ===
|
|
1567
|
+
if (subcommand === 'logout') {
|
|
1745
1568
|
await runLogoutCommand();
|
|
1746
1569
|
return;
|
|
1747
1570
|
}
|
|
1748
1571
|
|
|
1749
1572
|
maybeRenderBanner(runtimeArgs);
|
|
1750
1573
|
if (shouldBypassZenocodePreparation(runtimeArgs)) {
|
|
1751
|
-
const launchPackage =
|
|
1752
|
-
await runPackageLauncher(launchPackage, runtimeArgs, {
|
|
1574
|
+
const launchPackage = resolveLaunchPackage();
|
|
1575
|
+
const result = await runPackageLauncher(launchPackage, runtimeArgs, {
|
|
1753
1576
|
cwd: process.cwd(),
|
|
1754
1577
|
env: { ...process.env },
|
|
1755
1578
|
});
|
|
1579
|
+
exitWithChildResult(result);
|
|
1756
1580
|
return;
|
|
1757
1581
|
}
|
|
1758
1582
|
|
|
1759
|
-
const tokenResolution =
|
|
1760
|
-
|
|
1761
|
-
|
|
1583
|
+
const tokenResolution =
|
|
1584
|
+
loginResult ||
|
|
1585
|
+
(await resolveTokenWithAutoLogin(baseUrl, runtimeArgs, {
|
|
1586
|
+
preferLocalhost,
|
|
1587
|
+
}));
|
|
1762
1588
|
const runtime = await prepareRuntimeWithAutoLogin(
|
|
1763
1589
|
tokenResolution.baseUrl,
|
|
1764
1590
|
tokenResolution.token,
|
|
1765
1591
|
runtimeArgs,
|
|
1766
|
-
{
|
|
1592
|
+
{
|
|
1593
|
+
preferLocalhost,
|
|
1594
|
+
}
|
|
1767
1595
|
);
|
|
1768
1596
|
const token = runtime.token;
|
|
1769
1597
|
const model = runtime.model;
|
|
@@ -1778,6 +1606,7 @@ async function main() {
|
|
|
1778
1606
|
OPENCODE_MODELS_PATH: modelsPath,
|
|
1779
1607
|
OPENCODE_CONFIG: configPath,
|
|
1780
1608
|
OPENCODE_TUI_CONFIG: tuiConfigPath,
|
|
1609
|
+
OPENCODE_DISABLE_AUTOUPDATE: 'true',
|
|
1781
1610
|
TEXTCORTEX_API_KEY: token,
|
|
1782
1611
|
},
|
|
1783
1612
|
};
|
|
@@ -1792,29 +1621,34 @@ async function main() {
|
|
|
1792
1621
|
preferLocalhost,
|
|
1793
1622
|
canAutoLoginRuntime: monitorRuntimeSession,
|
|
1794
1623
|
launchRuntimeFn: ({ args, childOptions }) =>
|
|
1795
|
-
runRuntimeBinary(
|
|
1624
|
+
runRuntimeBinary(
|
|
1625
|
+
opencodeBinaryPath,
|
|
1626
|
+
args,
|
|
1627
|
+
childOptions,
|
|
1628
|
+
monitorRuntimeSession
|
|
1629
|
+
),
|
|
1796
1630
|
});
|
|
1797
1631
|
exitWithChildResult(result);
|
|
1798
1632
|
return;
|
|
1799
1633
|
}
|
|
1800
1634
|
|
|
1801
|
-
const launchPackage =
|
|
1802
|
-
const
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
}
|
|
1817
|
-
|
|
1635
|
+
const launchPackage = resolveLaunchPackage();
|
|
1636
|
+
const result = await runRuntimeWithSessionRecovery({
|
|
1637
|
+
args: runtimeArgs,
|
|
1638
|
+
baseUrl: runtime.baseUrl,
|
|
1639
|
+
token,
|
|
1640
|
+
childOptions,
|
|
1641
|
+
preferLocalhost,
|
|
1642
|
+
canAutoLoginRuntime: monitorRuntimeSession,
|
|
1643
|
+
launchRuntimeFn: ({ args, childOptions }) =>
|
|
1644
|
+
runPackageLauncher(
|
|
1645
|
+
launchPackage,
|
|
1646
|
+
args,
|
|
1647
|
+
childOptions,
|
|
1648
|
+
monitorRuntimeSession
|
|
1649
|
+
),
|
|
1650
|
+
});
|
|
1651
|
+
exitWithChildResult(result);
|
|
1818
1652
|
}
|
|
1819
1653
|
|
|
1820
1654
|
const resolveExecutablePath = (value) => {
|
|
@@ -1825,7 +1659,10 @@ const resolveExecutablePath = (value) => {
|
|
|
1825
1659
|
}
|
|
1826
1660
|
};
|
|
1827
1661
|
|
|
1828
|
-
if (
|
|
1662
|
+
if (
|
|
1663
|
+
process.argv[1] &&
|
|
1664
|
+
resolveExecutablePath(process.argv[1]) === currentFilePath
|
|
1665
|
+
) {
|
|
1829
1666
|
main().catch((error) => {
|
|
1830
1667
|
console.error(error instanceof Error ? error.message : String(error));
|
|
1831
1668
|
process.exit(1);
|