@tuannvm/ccodex 0.3.0 → 0.3.3
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/dist/aliases.d.ts.map +1 -1
- package/dist/aliases.js +31 -31
- package/dist/aliases.js.map +1 -1
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +87 -82
- package/dist/claude.js.map +1 -1
- package/dist/cli.js +60 -59
- package/dist/cli.js.map +1 -1
- package/dist/config.js +7 -7
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/powershell.d.ts.map +1 -1
- package/dist/powershell.js +11 -13
- package/dist/powershell.js.map +1 -1
- package/dist/proxy.d.ts +1 -1
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +230 -203
- package/dist/proxy.js.map +1 -1
- package/dist/status.d.ts +1 -1
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +28 -21
- package/dist/status.js.map +1 -1
- package/dist/types.d.ts +3 -3
- package/dist/utils.d.ts +2 -2
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +82 -87
- package/dist/utils.js.map +1 -1
- package/package.json +9 -1
package/dist/proxy.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { join, delimiter, normalize, sep, resolve, dirname, isAbsolute } from
|
|
2
|
-
import { homedir } from
|
|
3
|
-
import { createHash, randomUUID } from
|
|
4
|
-
import chalk from
|
|
5
|
-
import { hasCommand, getCommandPath, execCommand, httpGet, sleep, ensureDir, fileExists, safeJsonParse, debugLog, runCmdBounded, requireTrustedCommand, withInstallLock } from
|
|
6
|
-
import { CONFIG, getProxyUrl, getAuthDir, getLogFilePath } from
|
|
1
|
+
import { join, delimiter, normalize, sep, resolve, dirname, isAbsolute } from "path";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { createHash, randomUUID } from "crypto";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { hasCommand, getCommandPath, execCommand, httpGet, sleep, ensureDir, fileExists, safeJsonParse, debugLog, runCmdBounded, requireTrustedCommand, withInstallLock, } from "./utils.js";
|
|
6
|
+
import { CONFIG, getProxyUrl, getAuthDir, getLogFilePath } from "./config.js";
|
|
7
7
|
// Track installed proxy binary path for this process
|
|
8
8
|
let installedProxyPath = null;
|
|
9
9
|
/**
|
|
@@ -13,15 +13,15 @@ let installedProxyPath = null;
|
|
|
13
13
|
function getAllowedInstallDirs() {
|
|
14
14
|
const home = homedir();
|
|
15
15
|
const allowed = [
|
|
16
|
-
join(home,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
join(home,
|
|
16
|
+
join(home, ".local", "bin"), // User local bin
|
|
17
|
+
"/usr/local/bin", // System local bin
|
|
18
|
+
"/opt/homebrew/bin", // Homebrew Apple Silicon
|
|
19
|
+
"/usr/local/bin", // Homebrew Intel
|
|
20
|
+
join(home, "go", "bin"), // Go user bin
|
|
21
21
|
];
|
|
22
22
|
// Add common Windows paths if on Windows
|
|
23
|
-
if (process.platform ===
|
|
24
|
-
allowed.push(join(process.env.LOCALAPPDATA ||
|
|
23
|
+
if (process.platform === "win32") {
|
|
24
|
+
allowed.push(join(process.env.LOCALAPPDATA || "", "Programs"), join(process.env.APPDATA || "", "Programs"));
|
|
25
25
|
}
|
|
26
26
|
return allowed;
|
|
27
27
|
}
|
|
@@ -34,18 +34,18 @@ async function validateProxyPath(proxyPath) {
|
|
|
34
34
|
if (!isAbsolute(proxyPath)) {
|
|
35
35
|
throw new Error(`Proxy binary path is not absolute: ${proxyPath}`);
|
|
36
36
|
}
|
|
37
|
-
const fs = await import(
|
|
37
|
+
const fs = await import("fs/promises");
|
|
38
38
|
// Use realpath to resolve symlinks and get the actual file location
|
|
39
39
|
const realPath = await fs.realpath(proxyPath);
|
|
40
40
|
const allowedDirs = getAllowedInstallDirs();
|
|
41
|
-
const isAllowed = allowedDirs.some(allowedDir => {
|
|
41
|
+
const isAllowed = allowedDirs.some((allowedDir) => {
|
|
42
42
|
const resolvedAllowed = resolve(allowedDir);
|
|
43
43
|
return realPath.startsWith(resolvedAllowed + sep) || realPath === resolvedAllowed;
|
|
44
44
|
});
|
|
45
45
|
if (!isAllowed) {
|
|
46
46
|
throw new Error(`Proxy binary not from trusted location.\n` +
|
|
47
47
|
`Path: ${realPath}\n` +
|
|
48
|
-
`Allowed directories: ${allowedDirs.join(
|
|
48
|
+
`Allowed directories: ${allowedDirs.join(", ")}\n\n` +
|
|
49
49
|
`For security, only proxy binaries from trusted locations are executed.\n` +
|
|
50
50
|
`If you installed CLIProxyAPI manually, move it to ~/.local/bin or install via Homebrew.`);
|
|
51
51
|
}
|
|
@@ -63,9 +63,9 @@ export async function requireTrustedProxyCommand() {
|
|
|
63
63
|
}
|
|
64
64
|
// Use detected path
|
|
65
65
|
if (!cmdResult.path) {
|
|
66
|
-
throw new Error(
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
throw new Error("CLIProxyAPI not found. Install it first:\n" +
|
|
67
|
+
" 1. Run: npx -y @tuannvm/ccodex\n" +
|
|
68
|
+
" 2. Or install manually: brew install cli-proxy-api");
|
|
69
69
|
}
|
|
70
70
|
await validateProxyPath(cmdResult.path);
|
|
71
71
|
return cmdResult.path;
|
|
@@ -78,10 +78,14 @@ export async function requireTrustedProxyCommand() {
|
|
|
78
78
|
export async function detectProxyCommand() {
|
|
79
79
|
// Prefer locally installed binary from this process
|
|
80
80
|
if (installedProxyPath && fileExists(installedProxyPath)) {
|
|
81
|
-
return { cmd:
|
|
81
|
+
return { cmd: "cli-proxy-api", path: installedProxyPath };
|
|
82
82
|
}
|
|
83
83
|
// Try new name first, then legacy names
|
|
84
|
-
const commandNames = [
|
|
84
|
+
const commandNames = [
|
|
85
|
+
"cli-proxy-api",
|
|
86
|
+
"CLIProxyAPI",
|
|
87
|
+
"cliproxy",
|
|
88
|
+
];
|
|
85
89
|
for (const name of commandNames) {
|
|
86
90
|
if (await hasCommand(name)) {
|
|
87
91
|
const resolved = await getCommandPath(name);
|
|
@@ -109,21 +113,21 @@ export async function isProxyRunning() {
|
|
|
109
113
|
export async function checkAuthConfigured() {
|
|
110
114
|
const authDir = getAuthDir();
|
|
111
115
|
// Check for auth files
|
|
112
|
-
const fs = await import(
|
|
116
|
+
const fs = await import("fs/promises");
|
|
113
117
|
let hasAuthFiles = false;
|
|
114
118
|
try {
|
|
115
119
|
const files = await fs.readdir(authDir);
|
|
116
|
-
hasAuthFiles = files.some(f => f.startsWith(
|
|
120
|
+
hasAuthFiles = files.some((f) => f.startsWith("codex-") && f.endsWith(".json"));
|
|
117
121
|
}
|
|
118
122
|
catch {
|
|
119
123
|
// Directory doesn't exist
|
|
120
|
-
debugLog(
|
|
124
|
+
debugLog("Auth directory does not exist:", authDir);
|
|
121
125
|
}
|
|
122
126
|
// Check auth via proxy status
|
|
123
127
|
let hasAuthEntries = false;
|
|
124
128
|
try {
|
|
125
129
|
const proxyExe = await requireTrustedProxyCommand();
|
|
126
|
-
const output = await execCommand(proxyExe, [
|
|
130
|
+
const output = await execCommand(proxyExe, ["status"]);
|
|
127
131
|
// Match "N auth entries" or "N auth files" where N > 0
|
|
128
132
|
const match = output.match(/(\d+)\s+(auth entries|auth files)/);
|
|
129
133
|
if (match) {
|
|
@@ -132,7 +136,7 @@ export async function checkAuthConfigured() {
|
|
|
132
136
|
}
|
|
133
137
|
}
|
|
134
138
|
catch (error) {
|
|
135
|
-
debugLog(
|
|
139
|
+
debugLog("Failed to check proxy status:", error);
|
|
136
140
|
}
|
|
137
141
|
// Check via API
|
|
138
142
|
let hasModels = false;
|
|
@@ -141,12 +145,12 @@ export async function checkAuthConfigured() {
|
|
|
141
145
|
const response = await httpGet(`${proxyUrl}/v1/models`);
|
|
142
146
|
if (response.status === 200) {
|
|
143
147
|
const data = safeJsonParse(response.body);
|
|
144
|
-
hasModels = data?.object ===
|
|
148
|
+
hasModels = data?.object === "list" && Array.isArray(data.data) && data.data.length > 0;
|
|
145
149
|
}
|
|
146
150
|
}
|
|
147
151
|
catch {
|
|
148
152
|
// Proxy not running or not authenticated
|
|
149
|
-
debugLog(
|
|
153
|
+
debugLog("Failed to check models via API");
|
|
150
154
|
}
|
|
151
155
|
return {
|
|
152
156
|
hasAuthFiles,
|
|
@@ -157,12 +161,6 @@ export async function checkAuthConfigured() {
|
|
|
157
161
|
configured: hasModels || (hasAuthEntries && hasAuthFiles),
|
|
158
162
|
};
|
|
159
163
|
}
|
|
160
|
-
/**
|
|
161
|
-
* Compute SHA-256 hash of binary data
|
|
162
|
-
*/
|
|
163
|
-
function sha256Hex(data) {
|
|
164
|
-
return createHash('sha256').update(data).digest('hex');
|
|
165
|
-
}
|
|
166
164
|
/**
|
|
167
165
|
* Parse checksum file to find expected hash for a specific file
|
|
168
166
|
* Supports common checksum formats:
|
|
@@ -173,7 +171,7 @@ function sha256Hex(data) {
|
|
|
173
171
|
*/
|
|
174
172
|
function parseExpectedSha256(content, fileName) {
|
|
175
173
|
// First pass: try exact filename match (with or without path separators)
|
|
176
|
-
for (const line of content.split(
|
|
174
|
+
for (const line of content.split("\n")) {
|
|
177
175
|
const trimmed = line.trim();
|
|
178
176
|
if (!trimmed)
|
|
179
177
|
continue;
|
|
@@ -183,7 +181,7 @@ function parseExpectedSha256(content, fileName) {
|
|
|
183
181
|
const [, hash, name] = match;
|
|
184
182
|
const normalizedName = name.trim();
|
|
185
183
|
// Normalize path separators to / and strip any path prefix
|
|
186
|
-
const normalizedBase = normalizedName.replace(/\\/g,
|
|
184
|
+
const normalizedBase = normalizedName.replace(/\\/g, "/").replace(/^.*\//, "");
|
|
187
185
|
if (normalizedBase === fileName) {
|
|
188
186
|
// Found exact basename match
|
|
189
187
|
return hash.toLowerCase();
|
|
@@ -197,12 +195,12 @@ function parseExpectedSha256(content, fileName) {
|
|
|
197
195
|
* Returns the exact tag name to avoid moving 'latest' redirects
|
|
198
196
|
*/
|
|
199
197
|
async function getLatestReleaseTag() {
|
|
200
|
-
const apiUrl =
|
|
198
|
+
const apiUrl = "https://api.github.com/repos/router-for-me/CLIProxyAPI/releases/latest";
|
|
201
199
|
try {
|
|
202
200
|
const response = await fetch(apiUrl, {
|
|
203
201
|
headers: {
|
|
204
|
-
|
|
205
|
-
|
|
202
|
+
Accept: "application/vnd.github.v3+json",
|
|
203
|
+
"User-Agent": "@tuannvm/ccodex",
|
|
206
204
|
},
|
|
207
205
|
signal: AbortSignal.timeout(10000), // 10 second timeout
|
|
208
206
|
});
|
|
@@ -211,14 +209,14 @@ async function getLatestReleaseTag() {
|
|
|
211
209
|
}
|
|
212
210
|
const data = await safeJsonParse(await response.text());
|
|
213
211
|
if (!data?.tag_name) {
|
|
214
|
-
throw new Error(
|
|
212
|
+
throw new Error("Invalid GitHub API response");
|
|
215
213
|
}
|
|
216
214
|
return data.tag_name;
|
|
217
215
|
}
|
|
218
216
|
catch (error) {
|
|
219
|
-
debugLog(
|
|
217
|
+
debugLog("Failed to fetch latest release tag:", error);
|
|
220
218
|
throw new Error(`Failed to resolve latest release tag from GitHub API: ${error instanceof Error ? error.message : String(error)}\n\n` +
|
|
221
|
-
|
|
219
|
+
"Please check your internet connection or install CLIProxyAPI manually.");
|
|
222
220
|
}
|
|
223
221
|
}
|
|
224
222
|
/**
|
|
@@ -228,15 +226,15 @@ function isUnsafeArchivePath(raw) {
|
|
|
228
226
|
if (!raw)
|
|
229
227
|
return true;
|
|
230
228
|
// Normalize zip/tar separators and strip leading "./"
|
|
231
|
-
const p = raw.replace(/\\/g,
|
|
229
|
+
const p = raw.replace(/\\/g, "/").replace(/^\.\/+/, "");
|
|
232
230
|
if (!p)
|
|
233
231
|
return true;
|
|
234
232
|
// Reject absolute, drive-letter, UNC-like
|
|
235
|
-
if (p.startsWith(
|
|
233
|
+
if (p.startsWith("/") || /^[a-zA-Z]:\//.test(p) || p.startsWith("//"))
|
|
236
234
|
return true;
|
|
237
235
|
// Reject traversal segments
|
|
238
|
-
const parts = p.split(
|
|
239
|
-
if (parts.some(seg => seg ===
|
|
236
|
+
const parts = p.split("/").filter(Boolean);
|
|
237
|
+
if (parts.some((seg) => seg === ".."))
|
|
240
238
|
return true;
|
|
241
239
|
return false;
|
|
242
240
|
}
|
|
@@ -264,13 +262,13 @@ function parseTarVerboseLine(line) {
|
|
|
264
262
|
return null;
|
|
265
263
|
const firstChar = line.charAt(0);
|
|
266
264
|
const fileTypeMap = {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
265
|
+
"-": "file",
|
|
266
|
+
d: "dir",
|
|
267
|
+
l: "symlink",
|
|
268
|
+
h: "hardlink",
|
|
269
|
+
c: "char",
|
|
270
|
+
b: "block",
|
|
271
|
+
p: "fifo",
|
|
274
272
|
};
|
|
275
273
|
const type = fileTypeMap[firstChar];
|
|
276
274
|
if (!type) {
|
|
@@ -283,13 +281,13 @@ function parseTarVerboseLine(line) {
|
|
|
283
281
|
const parts = line.split(/\s+/);
|
|
284
282
|
// For very long lines with many fields, the path might be split further
|
|
285
283
|
// Find the part that looks like a path (contains '/', or ends with ' -> ', or is just a name)
|
|
286
|
-
let pathWithTarget =
|
|
284
|
+
let pathWithTarget = "";
|
|
287
285
|
let foundPath = false;
|
|
288
286
|
// Iterate from the end to find the path
|
|
289
287
|
for (let i = parts.length - 1; i >= 0; i--) {
|
|
290
288
|
const part = parts[i];
|
|
291
|
-
if (part.includes(
|
|
292
|
-
pathWithTarget = part + (pathWithTarget ?
|
|
289
|
+
if (part.includes("/") || part.includes(" -> ") || (part.length > 0 && !foundPath)) {
|
|
290
|
+
pathWithTarget = part + (pathWithTarget ? " " + pathWithTarget : "");
|
|
293
291
|
foundPath = true;
|
|
294
292
|
}
|
|
295
293
|
else if (foundPath) {
|
|
@@ -300,7 +298,7 @@ function parseTarVerboseLine(line) {
|
|
|
300
298
|
if (!pathWithTarget)
|
|
301
299
|
return null;
|
|
302
300
|
// Extract just the path (before " -> " for symlinks)
|
|
303
|
-
const arrowIndex = pathWithTarget.indexOf(
|
|
301
|
+
const arrowIndex = pathWithTarget.indexOf(" -> ");
|
|
304
302
|
const path = arrowIndex >= 0 ? pathWithTarget.substring(0, arrowIndex).trim() : pathWithTarget.trim();
|
|
305
303
|
if (!path)
|
|
306
304
|
return null;
|
|
@@ -310,22 +308,25 @@ function parseTarVerboseLine(line) {
|
|
|
310
308
|
* List entries in a tar archive with resource limits and link type validation
|
|
311
309
|
*/
|
|
312
310
|
async function listTarEntries(archivePath) {
|
|
313
|
-
const fs = await import(
|
|
311
|
+
const fs = await import("fs/promises");
|
|
314
312
|
// Check archive file size before extraction
|
|
315
313
|
const archiveStat = await fs.stat(archivePath);
|
|
316
314
|
if (archiveStat.size > MAX_ARCHIVE_BYTES) {
|
|
317
315
|
throw new Error(`Archive file is too large (${(archiveStat.size / 1024 / 1024).toFixed(1)} MB > ${MAX_ARCHIVE_BYTES / 1024 / 1024} MB). ` +
|
|
318
|
-
|
|
316
|
+
"This may be a tar bomb.");
|
|
319
317
|
}
|
|
320
318
|
// Use verbose mode with -z to explicitly handle gzip compression
|
|
321
319
|
// Some tar versions auto-detect compression, but -z ensures consistency
|
|
322
320
|
// Use trusted tar path to avoid PATH hijacking
|
|
323
|
-
const tarPath = await requireTrustedCommand(
|
|
324
|
-
const result = await runCmdBounded(tarPath, [
|
|
321
|
+
const tarPath = await requireTrustedCommand("tar");
|
|
322
|
+
const result = await runCmdBounded(tarPath, ["-ztvf", archivePath], 30000); // 30 second timeout
|
|
325
323
|
if (result.code !== 0) {
|
|
326
324
|
throw new Error(`tar list failed with code ${result.code}`);
|
|
327
325
|
}
|
|
328
|
-
const lines = result.stdout
|
|
326
|
+
const lines = result.stdout
|
|
327
|
+
.split("\n")
|
|
328
|
+
.map((s) => s.trim())
|
|
329
|
+
.filter(Boolean);
|
|
329
330
|
const entries = [];
|
|
330
331
|
// Resource limits: prevent tar/zip bombs
|
|
331
332
|
if (lines.length > MAX_ENTRIES) {
|
|
@@ -343,20 +344,38 @@ async function listTarEntries(archivePath) {
|
|
|
343
344
|
throw new Error(`Unsafe archive path: ${parsed.path}`);
|
|
344
345
|
}
|
|
345
346
|
// Reject symlinks and hardlinks for security
|
|
346
|
-
if (parsed.type ===
|
|
347
|
+
if (parsed.type === "symlink" || parsed.type === "hardlink") {
|
|
347
348
|
throw new Error(`Archive contains forbidden link entry: ${parsed.path} (${parsed.type})`);
|
|
348
349
|
}
|
|
349
350
|
// Reject character/block devices, fifos (unusual in CLIProxyAPI archives)
|
|
350
|
-
if (parsed.type ===
|
|
351
|
+
if (parsed.type === "char" || parsed.type === "block" || parsed.type === "fifo") {
|
|
351
352
|
throw new Error(`Archive contains unusual entry type: ${parsed.path} (${parsed.type})`);
|
|
352
353
|
}
|
|
353
354
|
entries.push(parsed.path);
|
|
354
355
|
}
|
|
355
356
|
if (entries.length === 0) {
|
|
356
|
-
throw new Error(
|
|
357
|
+
throw new Error("Archive is empty or contains no valid entries");
|
|
357
358
|
}
|
|
358
359
|
return entries;
|
|
359
360
|
}
|
|
361
|
+
/**
|
|
362
|
+
* Compute directory size recursively
|
|
363
|
+
*/
|
|
364
|
+
async function getDirSize(dir, fs) {
|
|
365
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
366
|
+
let size = 0;
|
|
367
|
+
for (const ent of entries) {
|
|
368
|
+
const full = join(dir, ent.name);
|
|
369
|
+
const stat = await fs.stat(full);
|
|
370
|
+
if (ent.isDirectory()) {
|
|
371
|
+
size += await getDirSize(full, fs);
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
size += stat.size;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return size;
|
|
378
|
+
}
|
|
360
379
|
/**
|
|
361
380
|
* Validate archive listing for unsafe paths and link types
|
|
362
381
|
* Note: Windows installation is not currently supported (throws early in installProxyApi)
|
|
@@ -371,7 +390,7 @@ async function validateArchiveListing(archivePath) {
|
|
|
371
390
|
* Symlinks and hardlinks are already rejected during tar parsing
|
|
372
391
|
*/
|
|
373
392
|
async function assertRealpathConfinement(rootDir) {
|
|
374
|
-
const fs = await import(
|
|
393
|
+
const fs = await import("fs/promises");
|
|
375
394
|
const rootReal = await fs.realpath(rootDir);
|
|
376
395
|
const stack = [rootDir];
|
|
377
396
|
while (stack.length > 0) {
|
|
@@ -400,82 +419,86 @@ async function assertRealpathConfinement(rootDir) {
|
|
|
400
419
|
* Install CLIProxyAPI via Homebrew or Go binary fallback
|
|
401
420
|
*/
|
|
402
421
|
export async function installProxyApi() {
|
|
403
|
-
const { homedir } = await import(
|
|
422
|
+
const { homedir } = await import("os");
|
|
404
423
|
const home = homedir();
|
|
405
424
|
if (!home) {
|
|
406
|
-
throw new Error(
|
|
425
|
+
throw new Error("Cannot determine home directory. Please set HOME environment variable.");
|
|
407
426
|
}
|
|
408
427
|
// Install lock file path (in the target install directory)
|
|
409
|
-
const lockPath = join(home,
|
|
428
|
+
const lockPath = join(home, ".local", "bin", ".cli-proxy-api.install.lock");
|
|
410
429
|
// Check platform
|
|
411
430
|
const platform = process.platform;
|
|
412
431
|
const arch = process.arch;
|
|
413
|
-
if (platform ===
|
|
414
|
-
throw new Error(
|
|
415
|
-
|
|
416
|
-
|
|
432
|
+
if (platform === "win32") {
|
|
433
|
+
throw new Error("CLIProxyAPI installation on Windows requires manual setup.\n" +
|
|
434
|
+
"Please install CLIProxyAPI manually and ensure it's in your PATH.\n" +
|
|
435
|
+
"See CLIProxyAPI documentation for Windows installation instructions.");
|
|
417
436
|
}
|
|
418
437
|
// Ensure lock directory exists before acquiring lock
|
|
419
|
-
const installDir = join(home,
|
|
438
|
+
const installDir = join(home, ".local", "bin");
|
|
420
439
|
await ensureDir(installDir);
|
|
421
440
|
// Wrap entire installation process (both Homebrew and Go binary paths) in lock
|
|
422
441
|
await withInstallLock(lockPath, async () => {
|
|
423
442
|
// Try Homebrew first (preferred)
|
|
424
|
-
if (await hasCommand(
|
|
425
|
-
console.log(
|
|
426
|
-
const brewPath = await requireTrustedCommand(
|
|
427
|
-
const spawnCmd = (await import(
|
|
443
|
+
if (await hasCommand("brew")) {
|
|
444
|
+
console.log("Installing CLIProxyAPI via Homebrew...");
|
|
445
|
+
const brewPath = await requireTrustedCommand("brew");
|
|
446
|
+
const spawnCmd = (await import("cross-spawn")).default;
|
|
428
447
|
try {
|
|
429
448
|
await new Promise((resolve, reject) => {
|
|
430
|
-
const child = spawnCmd(brewPath, [
|
|
431
|
-
stdio:
|
|
449
|
+
const child = spawnCmd(brewPath, ["install", "cli-proxy-api"], {
|
|
450
|
+
stdio: "inherit",
|
|
432
451
|
});
|
|
433
|
-
child.on(
|
|
452
|
+
child.on("close", (code) => {
|
|
434
453
|
if (code === 0) {
|
|
435
|
-
console.log(
|
|
454
|
+
console.log("CLIProxyAPI installed successfully via Homebrew");
|
|
436
455
|
resolve();
|
|
437
456
|
}
|
|
438
457
|
else {
|
|
439
|
-
reject(new Error(
|
|
458
|
+
reject(new Error("Failed to install CLIProxyAPI via Homebrew"));
|
|
440
459
|
}
|
|
441
460
|
});
|
|
442
|
-
child.on(
|
|
461
|
+
child.on("error", (error) => reject(error));
|
|
443
462
|
});
|
|
444
463
|
return;
|
|
445
464
|
}
|
|
446
465
|
catch (error) {
|
|
447
|
-
debugLog(
|
|
466
|
+
debugLog("Homebrew installation failed, falling back to Go binary:", error);
|
|
448
467
|
// Fall through to Go binary installation
|
|
449
468
|
}
|
|
450
469
|
}
|
|
451
470
|
// Fallback: Install Go binary directly
|
|
452
|
-
console.log(
|
|
471
|
+
console.log("Installing CLIProxyAPI via Go binary...");
|
|
453
472
|
// Determine platform/arch for CLIProxyAPI release asset format
|
|
454
473
|
// CLIProxyAPI uses: CLIProxyAPI_{version}_{platform}_{arch}.{ext}
|
|
455
|
-
const platformMap = {
|
|
456
|
-
|
|
474
|
+
const platformMap = {
|
|
475
|
+
darwin: "darwin",
|
|
476
|
+
linux: "linux",
|
|
477
|
+
win32: "windows",
|
|
478
|
+
};
|
|
479
|
+
const archMap = { arm64: "arm64", x64: "amd64" };
|
|
457
480
|
const cliPlatform = platformMap[platform];
|
|
458
481
|
const cliArch = archMap[arch];
|
|
459
482
|
if (!cliPlatform) {
|
|
460
483
|
throw new Error(`Unsupported platform: ${platform}\n` +
|
|
461
|
-
`Supported platforms: ${Object.keys(platformMap).join(
|
|
484
|
+
`Supported platforms: ${Object.keys(platformMap).join(", ")}`);
|
|
462
485
|
}
|
|
463
486
|
if (!cliArch) {
|
|
464
487
|
throw new Error(`Unsupported architecture: ${arch}\n` +
|
|
465
|
-
`Supported architectures: ${Object.keys(archMap).join(
|
|
488
|
+
`Supported architectures: ${Object.keys(archMap).join(", ")}`);
|
|
466
489
|
}
|
|
467
490
|
// Resolve exact release tag first for security (avoid moving 'latest' redirects)
|
|
468
|
-
console.log(
|
|
491
|
+
console.log("Resolving latest release tag from GitHub API...");
|
|
469
492
|
const releaseTag = await getLatestReleaseTag();
|
|
470
493
|
console.log(`Latest release: ${releaseTag}`);
|
|
471
494
|
// Strip 'v' prefix from tag for version (e.g., v6.9.5 -> 6.9.5)
|
|
472
|
-
const version = releaseTag.startsWith(
|
|
495
|
+
const version = releaseTag.startsWith("v") ? releaseTag.slice(1) : releaseTag;
|
|
473
496
|
// Determine archive extension
|
|
474
|
-
const isWindows = platform ===
|
|
475
|
-
const archiveExt = isWindows ?
|
|
497
|
+
const isWindows = platform === "win32";
|
|
498
|
+
const archiveExt = isWindows ? "zip" : "tar.gz";
|
|
476
499
|
const archiveFileName = `CLIProxyAPI_${version}_${cliPlatform}_${cliArch}.${archiveExt}`;
|
|
477
500
|
// installDir is already defined and ensured at function start
|
|
478
|
-
const binaryName = isWindows ?
|
|
501
|
+
const binaryName = isWindows ? "cli-proxy-api.exe" : "cli-proxy-api";
|
|
479
502
|
const binaryPath = join(installDir, binaryName);
|
|
480
503
|
// Use crypto.randomUUID() for temp files to avoid collision in concurrent installs
|
|
481
504
|
const randomSuffix = randomUUID();
|
|
@@ -484,7 +507,7 @@ export async function installProxyApi() {
|
|
|
484
507
|
const baseUrl = `https://github.com/router-for-me/CLIProxyAPI/releases/download/${releaseTag}`;
|
|
485
508
|
const archiveUrl = `${baseUrl}/${archiveFileName}`;
|
|
486
509
|
console.log(`Downloading ${archiveFileName} from GitHub releases...`);
|
|
487
|
-
const fs = await import(
|
|
510
|
+
const fs = await import("fs/promises");
|
|
488
511
|
try {
|
|
489
512
|
// Download archive with streaming and byte limits
|
|
490
513
|
console.log(`Downloading ${archiveFileName} from GitHub releases...`);
|
|
@@ -516,24 +539,24 @@ export async function installProxyApi() {
|
|
|
516
539
|
`Platform/Arch: ${cliPlatform}_${cliArch}`);
|
|
517
540
|
}
|
|
518
541
|
// Pre-check content-length if available
|
|
519
|
-
const contentLength = Number(response.headers.get(
|
|
542
|
+
const contentLength = Number(response.headers.get("content-length") || 0);
|
|
520
543
|
if (contentLength > MAX_ARCHIVE_BYTES) {
|
|
521
544
|
throw new Error(`Archive too large (Content-Length: ${(contentLength / 1024 / 1024).toFixed(1)} MB > ${MAX_ARCHIVE_BYTES / 1024 / 1024} MB). ` +
|
|
522
|
-
|
|
545
|
+
"This may be a tar bomb.");
|
|
523
546
|
}
|
|
524
547
|
// Stream download with byte limit and incremental hash
|
|
525
548
|
if (!response.body) {
|
|
526
|
-
throw new Error(
|
|
549
|
+
throw new Error("Response body is null");
|
|
527
550
|
}
|
|
528
|
-
const fileHandle = await fs.open(archivePath,
|
|
529
|
-
const hash = createHash(
|
|
551
|
+
const fileHandle = await fs.open(archivePath, "w");
|
|
552
|
+
const hash = createHash("sha256");
|
|
530
553
|
let downloadedBytes = 0;
|
|
531
554
|
try {
|
|
532
555
|
for await (const chunk of response.body) {
|
|
533
556
|
downloadedBytes += chunk.byteLength;
|
|
534
557
|
if (downloadedBytes > MAX_ARCHIVE_BYTES) {
|
|
535
558
|
throw new Error(`Archive exceeded size limit during download (${(downloadedBytes / 1024 / 1024).toFixed(1)} MB > ${MAX_ARCHIVE_BYTES / 1024 / 1024} MB). ` +
|
|
536
|
-
|
|
559
|
+
"This may be a tar bomb.");
|
|
537
560
|
}
|
|
538
561
|
hash.update(chunk);
|
|
539
562
|
await fileHandle.write(chunk);
|
|
@@ -544,7 +567,7 @@ export async function installProxyApi() {
|
|
|
544
567
|
}
|
|
545
568
|
console.log(`Downloaded ${(downloadedBytes / 1024 / 1024).toFixed(1)} MB`);
|
|
546
569
|
// Get the calculated hash
|
|
547
|
-
const actualHash = hash.digest(
|
|
570
|
+
const actualHash = hash.digest("hex");
|
|
548
571
|
// Try to download and verify checksums file
|
|
549
572
|
let checksumVerified = false;
|
|
550
573
|
let checksumMismatchError = null;
|
|
@@ -562,7 +585,7 @@ export async function installProxyApi() {
|
|
|
562
585
|
const expectedHash = parseExpectedSha256(checksumContent, archiveFileName);
|
|
563
586
|
if (expectedHash) {
|
|
564
587
|
if (actualHash === expectedHash) {
|
|
565
|
-
console.log(chalk.green(
|
|
588
|
+
console.log(chalk.green("✓ Checksum verification passed"));
|
|
566
589
|
checksumVerified = true;
|
|
567
590
|
break;
|
|
568
591
|
}
|
|
@@ -582,7 +605,7 @@ export async function installProxyApi() {
|
|
|
582
605
|
catch (checksumError) {
|
|
583
606
|
// Only catch network/parsing errors - let checksum mismatches fail hard
|
|
584
607
|
const errorMsg = checksumError instanceof Error ? checksumError.message : String(checksumError);
|
|
585
|
-
if (errorMsg.includes(
|
|
608
|
+
if (errorMsg.includes("Checksum verification failed")) {
|
|
586
609
|
// Re-throw checksum mismatch errors
|
|
587
610
|
throw checksumError;
|
|
588
611
|
}
|
|
@@ -598,41 +621,36 @@ export async function installProxyApi() {
|
|
|
598
621
|
// This prevents installation of potentially tampered binaries
|
|
599
622
|
if (!checksumVerified) {
|
|
600
623
|
await fs.unlink(archivePath).catch(() => { });
|
|
601
|
-
throw new Error(chalk.red(
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
624
|
+
throw new Error(chalk.red("Checksum verification required but failed.\n\n") +
|
|
625
|
+
"The downloaded archive could not be verified against a checksum file.\n" +
|
|
626
|
+
"This is a security requirement to prevent installation of tampered binaries.\n\n" +
|
|
627
|
+
"Possible reasons:\n" +
|
|
628
|
+
" - Network issues prevented checksum file download\n" +
|
|
629
|
+
" - Checksum files are not published for this release\n" +
|
|
630
|
+
" - GitHub releases are temporarily unavailable\n\n" +
|
|
631
|
+
"To install CLIProxyAPI safely:\n" +
|
|
609
632
|
` 1. Visit ${baseUrl}/\n` +
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
633
|
+
" 2. Download the archive and checksum files manually\n" +
|
|
634
|
+
" 3. Verify the checksums match\n" +
|
|
635
|
+
" 4. Extract the archive\n" +
|
|
636
|
+
" 5. Place the binary in a directory in your PATH\n" +
|
|
637
|
+
" 6. Make it executable: chmod +x cli-proxy-api\n\n" +
|
|
638
|
+
"Then run ccodex again.");
|
|
616
639
|
}
|
|
617
640
|
// Archive was already written to disk during streaming download
|
|
618
641
|
// Extract archive using hardened extraction strategy
|
|
619
642
|
console.log(`Extracting ${archiveExt} archive...`);
|
|
620
643
|
await ensureDir(extractDir);
|
|
621
644
|
// Preflight: validate archive listing before extraction
|
|
622
|
-
console.log(
|
|
645
|
+
console.log("Validating archive contents...");
|
|
623
646
|
await validateArchiveListing(archivePath);
|
|
624
647
|
try {
|
|
625
648
|
// Unix/macOS: use tar with portable hardened flags
|
|
626
649
|
// Note: --no-same-owner and --no-same-permissions are supported by both GNU and BSD tar
|
|
627
650
|
// We avoid GNU-specific flags like --delay-directory-restore for macOS compatibility
|
|
628
651
|
// Use bounded execution with timeout (60 seconds for extraction) and trusted tar path
|
|
629
|
-
const tarPath = await requireTrustedCommand(
|
|
630
|
-
const result = await runCmdBounded(tarPath, [
|
|
631
|
-
'-xzf', archivePath,
|
|
632
|
-
'-C', extractDir,
|
|
633
|
-
'--no-same-owner',
|
|
634
|
-
'--no-same-permissions',
|
|
635
|
-
], 60000);
|
|
652
|
+
const tarPath = await requireTrustedCommand("tar");
|
|
653
|
+
const result = await runCmdBounded(tarPath, ["-xzf", archivePath, "-C", extractDir, "--no-same-owner", "--no-same-permissions"], 60000);
|
|
636
654
|
if (result.code !== 0) {
|
|
637
655
|
throw new Error(`tar extraction failed with code ${result.code}`);
|
|
638
656
|
}
|
|
@@ -642,31 +660,16 @@ export async function installProxyApi() {
|
|
|
642
660
|
await fs.unlink(archivePath).catch(() => { });
|
|
643
661
|
await fs.rm(extractDir, { recursive: true, force: true }).catch(() => { });
|
|
644
662
|
throw new Error(`Failed to extract archive: ${extractError instanceof Error ? extractError.message : String(extractError)}\n\n` +
|
|
645
|
-
|
|
663
|
+
"The archive may be corrupted or incompatible with your system.");
|
|
646
664
|
}
|
|
647
665
|
// Post-extraction: validate extracted size (prevent zip bomb)
|
|
648
|
-
console.log(
|
|
666
|
+
console.log("Validating extracted size...");
|
|
649
667
|
let extractedBytes = 0;
|
|
650
|
-
async function getDirSize(dir) {
|
|
651
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
652
|
-
let size = 0;
|
|
653
|
-
for (const ent of entries) {
|
|
654
|
-
const full = join(dir, ent.name);
|
|
655
|
-
const stat = await fs.stat(full);
|
|
656
|
-
if (ent.isDirectory()) {
|
|
657
|
-
size += await getDirSize(full);
|
|
658
|
-
}
|
|
659
|
-
else {
|
|
660
|
-
size += stat.size;
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
return size;
|
|
664
|
-
}
|
|
665
668
|
try {
|
|
666
|
-
extractedBytes = await getDirSize(extractDir);
|
|
669
|
+
extractedBytes = await getDirSize(extractDir, fs);
|
|
667
670
|
if (extractedBytes > MAX_EXTRACTED_BYTES) {
|
|
668
671
|
throw new Error(`Extracted content is too large (${(extractedBytes / 1024 / 1024).toFixed(1)} MB > ${MAX_EXTRACTED_BYTES / 1024 / 1024} MB). ` +
|
|
669
|
-
|
|
672
|
+
"This may be a zip bomb.");
|
|
670
673
|
}
|
|
671
674
|
}
|
|
672
675
|
catch (sizeError) {
|
|
@@ -677,7 +680,7 @@ export async function installProxyApi() {
|
|
|
677
680
|
}
|
|
678
681
|
// Post-extraction: validate realpath confinement
|
|
679
682
|
// This detects path traversal via symlinks, hardlinks, or other escape mechanisms
|
|
680
|
-
console.log(
|
|
683
|
+
console.log("Validating extraction safety...");
|
|
681
684
|
try {
|
|
682
685
|
await assertRealpathConfinement(extractDir);
|
|
683
686
|
}
|
|
@@ -692,16 +695,16 @@ export async function installProxyApi() {
|
|
|
692
695
|
// On Windows it may have .exe extension
|
|
693
696
|
const extractedFiles = await fs.readdir(extractDir);
|
|
694
697
|
const binaryNames = isWindows
|
|
695
|
-
? [
|
|
696
|
-
: [
|
|
697
|
-
const extractedBinary = extractedFiles.find(f => binaryNames.includes(f));
|
|
698
|
+
? ["cli-proxy-api.exe", "CLIProxyAPI.exe"]
|
|
699
|
+
: ["cli-proxy-api", "CLIProxyAPI"];
|
|
700
|
+
const extractedBinary = extractedFiles.find((f) => binaryNames.includes(f));
|
|
698
701
|
if (!extractedBinary) {
|
|
699
702
|
// Clean up
|
|
700
703
|
await fs.unlink(archivePath).catch(() => { });
|
|
701
704
|
await fs.rm(extractDir, { recursive: true, force: true }).catch(() => { });
|
|
702
705
|
throw new Error(`Could not find CLIProxyAPI binary in extracted archive.\n` +
|
|
703
|
-
`Files found: ${extractedFiles.join(
|
|
704
|
-
|
|
706
|
+
`Files found: ${extractedFiles.join(", ")}\n\n` +
|
|
707
|
+
"The archive format may have changed. Please report this issue.");
|
|
705
708
|
}
|
|
706
709
|
const extractedBinaryPath = join(extractDir, extractedBinary);
|
|
707
710
|
// Set executable permission on extracted binary before validation (Unix/macOS only)
|
|
@@ -710,17 +713,41 @@ export async function installProxyApi() {
|
|
|
710
713
|
await fs.chmod(extractedBinaryPath, 0o755);
|
|
711
714
|
}
|
|
712
715
|
catch (chmodError) {
|
|
713
|
-
debugLog(
|
|
716
|
+
debugLog("Warning: Could not set executable permission on extracted binary:", chmodError);
|
|
714
717
|
// Continue anyway - the archive may already have execute bits set
|
|
715
718
|
}
|
|
716
719
|
}
|
|
717
720
|
// Validate the extracted binary works by running it
|
|
718
|
-
console.log(
|
|
721
|
+
console.log("Validating extracted binary...");
|
|
719
722
|
try {
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
723
|
+
// Try multiple version flags that the binary might support
|
|
724
|
+
const versionFlags = [["--version"], ["-v"], ["version"]];
|
|
725
|
+
let success = false;
|
|
726
|
+
for (const args of versionFlags) {
|
|
727
|
+
try {
|
|
728
|
+
const testResult = await runCmdBounded(extractedBinaryPath, args, 5000);
|
|
729
|
+
if (testResult.code === 0) {
|
|
730
|
+
success = true;
|
|
731
|
+
debugLog(`Binary validated with flag: ${args[0]}`);
|
|
732
|
+
break;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
catch {
|
|
736
|
+
// Try next flag
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
// If all version flags failed, try running without arguments (may show usage)
|
|
741
|
+
if (!success) {
|
|
742
|
+
const testResult = await runCmdBounded(extractedBinaryPath, [], 3000);
|
|
743
|
+
// If it runs at all (even with usage error), consider it valid
|
|
744
|
+
if (testResult.code === 0 || testResult.code === 2) {
|
|
745
|
+
debugLog("Binary validated (executable but may not support --version flag)");
|
|
746
|
+
success = true;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
if (!success) {
|
|
750
|
+
throw new Error(`Binary validation failed: all test attempts failed`);
|
|
724
751
|
}
|
|
725
752
|
}
|
|
726
753
|
catch (validationError) {
|
|
@@ -728,7 +755,7 @@ export async function installProxyApi() {
|
|
|
728
755
|
await fs.unlink(archivePath).catch(() => { });
|
|
729
756
|
await fs.rm(extractDir, { recursive: true, force: true }).catch(() => { });
|
|
730
757
|
throw new Error(`Extracted binary failed validation: ${validationError instanceof Error ? validationError.message : String(validationError)}\n\n` +
|
|
731
|
-
|
|
758
|
+
"The binary may be corrupted or incompatible with your system.");
|
|
732
759
|
}
|
|
733
760
|
// Backup existing binary if present, but be ready to rollback
|
|
734
761
|
let backupPath = null;
|
|
@@ -755,7 +782,7 @@ export async function installProxyApi() {
|
|
|
755
782
|
await fs.rename(backupPath, binaryPath);
|
|
756
783
|
}
|
|
757
784
|
catch (rollbackError) {
|
|
758
|
-
debugLog(
|
|
785
|
+
debugLog("Failed to rollback after copy failure:", rollbackError);
|
|
759
786
|
}
|
|
760
787
|
}
|
|
761
788
|
// Clean up
|
|
@@ -765,59 +792,59 @@ export async function installProxyApi() {
|
|
|
765
792
|
}
|
|
766
793
|
// Clean up on success
|
|
767
794
|
await fs.unlink(archivePath).catch((err) => {
|
|
768
|
-
debugLog(
|
|
795
|
+
debugLog("Warning: Failed to cleanup archive file:", err);
|
|
769
796
|
});
|
|
770
797
|
await fs.rm(extractDir, { recursive: true, force: true }).catch((err) => {
|
|
771
|
-
debugLog(
|
|
798
|
+
debugLog("Warning: Failed to cleanup extract directory:", err);
|
|
772
799
|
});
|
|
773
800
|
// Clean up backup on success
|
|
774
801
|
if (backupPath) {
|
|
775
802
|
await fs.unlink(backupPath).catch((err) => {
|
|
776
|
-
debugLog(
|
|
803
|
+
debugLog("Warning: Failed to cleanup backup file:", err);
|
|
777
804
|
});
|
|
778
805
|
}
|
|
779
806
|
console.log(`CLIProxyAPI installed successfully to: ${binaryPath}`);
|
|
780
807
|
// Check if install dir is in PATH (use platform-specific delimiter and case-insensitive on Windows)
|
|
781
|
-
const pathEnv = process.env.PATH ||
|
|
808
|
+
const pathEnv = process.env.PATH || "";
|
|
782
809
|
// Filter empty segments to avoid false positives from '::' in PATH
|
|
783
|
-
const pathDirs = pathEnv.split(delimiter).filter(p => p.length > 0);
|
|
810
|
+
const pathDirs = pathEnv.split(delimiter).filter((p) => p.length > 0);
|
|
784
811
|
// Normalize paths for comparison: resolve to absolute paths, normalize separators, case-insensitive on Windows
|
|
785
812
|
const normalizePath = (p) => {
|
|
786
813
|
const resolved = resolve(p);
|
|
787
814
|
const normalized = normalize(resolved);
|
|
788
815
|
return isWindows ? normalized.toLowerCase() : normalized;
|
|
789
816
|
};
|
|
790
|
-
const binInPath = pathDirs.some(dir => normalizePath(dir) === normalizePath(installDir));
|
|
817
|
+
const binInPath = pathDirs.some((dir) => normalizePath(dir) === normalizePath(installDir));
|
|
791
818
|
if (!binInPath) {
|
|
792
|
-
console.log(
|
|
793
|
-
console.log(
|
|
794
|
-
console.log(
|
|
795
|
-
console.log(
|
|
796
|
-
console.log(
|
|
797
|
-
console.log(
|
|
819
|
+
console.log("");
|
|
820
|
+
console.log("⚠️ WARNING: ~/.local/bin is not in your PATH");
|
|
821
|
+
console.log("");
|
|
822
|
+
console.log("To use ccodex, add ~/.local/bin to your PATH:");
|
|
823
|
+
console.log("");
|
|
824
|
+
console.log(" For bash (add to ~/.bashrc):");
|
|
798
825
|
console.log(' export PATH="$HOME/.local/bin:$PATH"');
|
|
799
|
-
console.log(
|
|
800
|
-
console.log(
|
|
826
|
+
console.log("");
|
|
827
|
+
console.log(" For zsh (add to ~/.zshrc):");
|
|
801
828
|
console.log(' export PATH="$HOME/.local/bin:$PATH"');
|
|
802
|
-
console.log(
|
|
803
|
-
console.log(
|
|
829
|
+
console.log("");
|
|
830
|
+
console.log("Then reload your shell: source ~/.bashrc (or ~/.zshrc)");
|
|
804
831
|
}
|
|
805
832
|
}
|
|
806
833
|
catch (error) {
|
|
807
834
|
// Clean up archive and extract dir on error
|
|
808
835
|
await fs.unlink(archivePath).catch((err) => {
|
|
809
|
-
debugLog(
|
|
836
|
+
debugLog("Warning: Failed to cleanup archive file during error handling:", err);
|
|
810
837
|
});
|
|
811
838
|
await fs.rm(extractDir, { recursive: true, force: true }).catch((err) => {
|
|
812
|
-
debugLog(
|
|
839
|
+
debugLog("Warning: Failed to cleanup extract directory during error handling:", err);
|
|
813
840
|
});
|
|
814
841
|
throw new Error(`Failed to install CLIProxyAPI: ${error instanceof Error ? error.message : String(error)}\n\n` +
|
|
815
|
-
|
|
816
|
-
|
|
842
|
+
"Please install CLIProxyAPI manually:\n" +
|
|
843
|
+
" 1. Visit https://github.com/router-for-me/CLIProxyAPI/releases\n" +
|
|
817
844
|
` 2. Download ${archiveFileName} for your system\n` +
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
845
|
+
" 3. Extract the archive\n" +
|
|
846
|
+
" 4. Place the binary in a directory in your PATH\n" +
|
|
847
|
+
" 5. Make it executable: chmod +x cli-proxy-api");
|
|
821
848
|
}
|
|
822
849
|
});
|
|
823
850
|
}
|
|
@@ -829,42 +856,42 @@ export async function startProxy() {
|
|
|
829
856
|
return;
|
|
830
857
|
}
|
|
831
858
|
const proxyExe = await requireTrustedProxyCommand();
|
|
832
|
-
console.log(
|
|
859
|
+
console.log("Starting CLIProxyAPI in background...");
|
|
833
860
|
const logFile = getLogFilePath();
|
|
834
861
|
await ensureDir(dirname(logFile));
|
|
835
|
-
const { spawn } = await import(
|
|
836
|
-
const fs = await import(
|
|
862
|
+
const { spawn } = await import("child_process");
|
|
863
|
+
const fs = await import("fs/promises");
|
|
837
864
|
let out = null;
|
|
838
865
|
try {
|
|
839
866
|
// Create log with restrictive permissions (user read/write only)
|
|
840
|
-
out = await fs.open(logFile,
|
|
867
|
+
out = await fs.open(logFile, "a");
|
|
841
868
|
// Set restrictive permissions on Unix/macOS (0600 = user read/write only)
|
|
842
|
-
if (process.platform !==
|
|
869
|
+
if (process.platform !== "win32") {
|
|
843
870
|
try {
|
|
844
871
|
await fs.chmod(logFile, 0o600);
|
|
845
872
|
}
|
|
846
873
|
catch {
|
|
847
874
|
// If chmod fails, continue anyway - the file was created successfully
|
|
848
|
-
debugLog(
|
|
875
|
+
debugLog("Warning: Could not set restrictive permissions on log file");
|
|
849
876
|
}
|
|
850
877
|
}
|
|
851
878
|
const child = spawn(proxyExe, [], {
|
|
852
879
|
detached: true,
|
|
853
|
-
stdio: [
|
|
880
|
+
stdio: ["ignore", out.fd, out.fd],
|
|
854
881
|
});
|
|
855
882
|
// Handle spawn errors immediately (fail-fast)
|
|
856
883
|
await new Promise((resolve, reject) => {
|
|
857
|
-
child.once(
|
|
884
|
+
child.once("error", (error) => {
|
|
858
885
|
reject(new Error(`Failed to start CLIProxyAPI: ${error.message}`));
|
|
859
886
|
});
|
|
860
|
-
child.once(
|
|
887
|
+
child.once("spawn", () => resolve());
|
|
861
888
|
});
|
|
862
889
|
child.unref();
|
|
863
890
|
// Wait for proxy to be ready
|
|
864
891
|
for (let i = 0; i < CONFIG.PROXY_STARTUP_MAX_RETRIES; i++) {
|
|
865
892
|
await sleep(CONFIG.PROXY_STARTUP_RETRY_DELAY_MS);
|
|
866
893
|
if (await isProxyRunning()) {
|
|
867
|
-
console.log(
|
|
894
|
+
console.log("CLIProxyAPI is running.");
|
|
868
895
|
return;
|
|
869
896
|
}
|
|
870
897
|
}
|
|
@@ -881,36 +908,36 @@ export async function startProxy() {
|
|
|
881
908
|
*/
|
|
882
909
|
export async function launchLogin() {
|
|
883
910
|
const proxyExe = await requireTrustedProxyCommand();
|
|
884
|
-
console.log(
|
|
885
|
-
const spawnCmd = (await import(
|
|
911
|
+
console.log("Launching ChatGPT/Codex OAuth login in browser...");
|
|
912
|
+
const spawnCmd = (await import("cross-spawn")).default;
|
|
886
913
|
return new Promise((resolve, reject) => {
|
|
887
|
-
const child = spawnCmd(proxyExe, [
|
|
888
|
-
stdio:
|
|
914
|
+
const child = spawnCmd(proxyExe, ["-codex-login"], {
|
|
915
|
+
stdio: "inherit",
|
|
889
916
|
});
|
|
890
|
-
child.on(
|
|
917
|
+
child.on("close", (code) => {
|
|
891
918
|
if (code === 0) {
|
|
892
919
|
resolve();
|
|
893
920
|
}
|
|
894
921
|
else {
|
|
895
|
-
reject(new Error(
|
|
922
|
+
reject(new Error("Login failed"));
|
|
896
923
|
}
|
|
897
924
|
});
|
|
898
|
-
child.on(
|
|
925
|
+
child.on("error", (error) => reject(error));
|
|
899
926
|
});
|
|
900
927
|
}
|
|
901
928
|
/**
|
|
902
929
|
* Wait for auth to be configured after login
|
|
903
930
|
*/
|
|
904
931
|
export async function waitForAuth() {
|
|
905
|
-
console.log(
|
|
932
|
+
console.log("Waiting for authentication...");
|
|
906
933
|
for (let i = 0; i < CONFIG.AUTH_WAIT_MAX_RETRIES; i++) {
|
|
907
934
|
await sleep(CONFIG.AUTH_WAIT_RETRY_DELAY_MS);
|
|
908
935
|
const auth = await checkAuthConfigured();
|
|
909
936
|
if (auth.configured) {
|
|
910
|
-
console.log(
|
|
937
|
+
console.log("Authentication configured.");
|
|
911
938
|
return;
|
|
912
939
|
}
|
|
913
940
|
}
|
|
914
|
-
throw new Error(
|
|
941
|
+
throw new Error("Authentication still not configured after login.");
|
|
915
942
|
}
|
|
916
943
|
//# sourceMappingURL=proxy.js.map
|