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