@jsonstudio/rcc 0.89.1121 → 0.89.1189
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/build-info.js +2 -2
- package/dist/cli/commands/clean.d.ts +16 -0
- package/dist/cli/commands/clean.js +58 -0
- package/dist/cli/commands/clean.js.map +1 -0
- package/dist/cli/commands/code.d.ts +55 -0
- package/dist/cli/commands/code.js +376 -0
- package/dist/cli/commands/code.js.map +1 -0
- package/dist/cli/commands/config.d.ts +31 -0
- package/dist/cli/commands/config.js +168 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/env.d.ts +20 -0
- package/dist/cli/commands/env.js +73 -0
- package/dist/cli/commands/env.js.map +1 -0
- package/dist/cli/commands/examples.d.ts +5 -0
- package/dist/cli/commands/examples.js +66 -0
- package/dist/cli/commands/examples.js.map +1 -0
- package/dist/cli/commands/port.d.ts +24 -0
- package/dist/cli/commands/port.js +85 -0
- package/dist/cli/commands/port.js.map +1 -0
- package/dist/cli/commands/restart.d.ts +50 -0
- package/dist/cli/commands/restart.js +176 -0
- package/dist/cli/commands/restart.js.map +1 -0
- package/dist/cli/commands/start.d.ts +68 -0
- package/dist/cli/commands/start.js +295 -0
- package/dist/cli/commands/start.js.map +1 -0
- package/dist/cli/commands/status.d.ts +16 -0
- package/dist/cli/commands/status.js +104 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/stop.d.ts +35 -0
- package/dist/cli/commands/stop.js +95 -0
- package/dist/cli/commands/stop.js.map +1 -0
- package/dist/cli/logger.d.ts +8 -0
- package/dist/cli/logger.js +9 -0
- package/dist/cli/logger.js.map +1 -0
- package/dist/cli/main.d.ts +6 -0
- package/dist/cli/main.js +16 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/program.d.ts +8 -0
- package/dist/cli/program.js +16 -0
- package/dist/cli/program.js.map +1 -0
- package/dist/cli/register/basic-commands.d.ts +30 -0
- package/dist/cli/register/basic-commands.js +11 -0
- package/dist/cli/register/basic-commands.js.map +1 -0
- package/dist/cli/register/code-command.d.ts +3 -0
- package/dist/cli/register/code-command.js +5 -0
- package/dist/cli/register/code-command.js.map +1 -0
- package/dist/cli/register/restart-command.d.ts +3 -0
- package/dist/cli/register/restart-command.js +5 -0
- package/dist/cli/register/restart-command.js.map +1 -0
- package/dist/cli/register/start-command.d.ts +3 -0
- package/dist/cli/register/start-command.js +5 -0
- package/dist/cli/register/start-command.js.map +1 -0
- package/dist/cli/register/status-config-commands.d.ts +16 -0
- package/dist/cli/register/status-config-commands.js +7 -0
- package/dist/cli/register/status-config-commands.js.map +1 -0
- package/dist/cli/register/stop-command.d.ts +3 -0
- package/dist/cli/register/stop-command.js +5 -0
- package/dist/cli/register/stop-command.js.map +1 -0
- package/dist/cli/runtime.d.ts +5 -0
- package/dist/cli/runtime.js +11 -0
- package/dist/cli/runtime.js.map +1 -0
- package/dist/cli/server/port-utils.d.ts +52 -0
- package/dist/cli/server/port-utils.js +193 -0
- package/dist/cli/server/port-utils.js.map +1 -0
- package/dist/cli/spinner.d.ts +10 -0
- package/dist/cli/spinner.js +59 -0
- package/dist/cli/spinner.js.map +1 -0
- package/dist/cli/utils/normalize.d.ts +2 -0
- package/dist/cli/utils/normalize.js +22 -0
- package/dist/cli/utils/normalize.js.map +1 -0
- package/dist/cli/utils/safe-read-json.d.ts +1 -0
- package/dist/cli/utils/safe-read-json.js +11 -0
- package/dist/cli/utils/safe-read-json.js.map +1 -0
- package/dist/cli.js +149 -1738
- package/dist/cli.js.map +1 -1
- package/dist/client/anthropic/anthropic-protocol-client.js +4 -3
- package/dist/client/anthropic/anthropic-protocol-client.js.map +1 -1
- package/dist/client/gemini/gemini-protocol-client.js +5 -0
- package/dist/client/gemini/gemini-protocol-client.js.map +1 -1
- package/dist/client/gemini-cli/gemini-cli-protocol-client.d.ts +1 -1
- package/dist/client/gemini-cli/gemini-cli-protocol-client.js +10 -3
- package/dist/client/gemini-cli/gemini-cli-protocol-client.js.map +1 -1
- package/dist/commands/provider-update.js +355 -5
- package/dist/commands/provider-update.js.map +1 -1
- package/dist/commands/quota-daemon.js +2 -2
- package/dist/commands/quota-daemon.js.map +1 -1
- package/dist/config/provider-v2-loader.js +4 -2
- package/dist/config/provider-v2-loader.js.map +1 -1
- package/dist/docs/daemon-admin-ui.html +583 -87
- package/dist/index.js +32 -1
- package/dist/index.js.map +1 -1
- package/dist/manager/modules/quota/index.d.ts +19 -1
- package/dist/manager/modules/quota/index.js +130 -5
- package/dist/manager/modules/quota/index.js.map +1 -1
- package/dist/manager/modules/routing/index.js.map +1 -1
- package/dist/manager/storage/file-store.js +1 -1
- package/dist/manager/storage/file-store.js.map +1 -1
- package/dist/manager/types.d.ts +5 -0
- package/dist/providers/auth/oauth-lifecycle.js +2 -2
- package/dist/providers/auth/oauth-lifecycle.js.map +1 -1
- package/dist/providers/core/api/provider-config.d.ts +2 -0
- package/dist/providers/core/api/provider-types.d.ts +2 -0
- package/dist/providers/core/config/service-profiles.js +1 -1
- package/dist/providers/core/config/service-profiles.js.map +1 -1
- package/dist/providers/core/runtime/base-provider.js +21 -27
- package/dist/providers/core/runtime/base-provider.js.map +1 -1
- package/dist/providers/core/runtime/gemini-cli-http-provider.d.ts +1 -0
- package/dist/providers/core/runtime/gemini-cli-http-provider.js +37 -6
- package/dist/providers/core/runtime/gemini-cli-http-provider.js.map +1 -1
- package/dist/providers/core/runtime/http-request-executor.js +23 -29
- package/dist/providers/core/runtime/http-request-executor.js.map +1 -1
- package/dist/providers/core/runtime/http-transport-provider.js +46 -38
- package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
- package/dist/providers/core/utils/http-client.d.ts +9 -0
- package/dist/providers/core/utils/http-client.js +9 -11
- package/dist/providers/core/utils/http-client.js.map +1 -1
- package/dist/providers/core/utils/provider-error-reporter.js +2 -6
- package/dist/providers/core/utils/provider-error-reporter.js.map +1 -1
- package/dist/providers/mock/mock-provider-runtime.js +19 -5
- package/dist/providers/mock/mock-provider-runtime.js.map +1 -1
- package/dist/server/handlers/handler-utils.d.ts +1 -1
- package/dist/server/handlers/handler-utils.js +4 -4
- package/dist/server/handlers/handler-utils.js.map +1 -1
- package/dist/server/handlers/responses-handler.js +2 -1
- package/dist/server/handlers/responses-handler.js.map +1 -1
- package/dist/server/handlers/sse-dispatcher.js +1 -4
- package/dist/server/handlers/sse-dispatcher.js.map +1 -1
- package/dist/server/runtime/http-server/colored-logger.d.ts +1 -1
- package/dist/server/runtime/http-server/colored-logger.js +22 -10
- package/dist/server/runtime/http-server/colored-logger.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js +12 -6
- package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/providers-handler.js +116 -98
- package/dist/server/runtime/http-server/daemon-admin/providers-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/quota-handler.js +108 -15
- package/dist/server/runtime/http-server/daemon-admin/quota-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/restart-handler.js +2 -1
- package/dist/server/runtime/http-server/daemon-admin/restart-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/stats-handler.d.ts +3 -0
- package/dist/server/runtime/http-server/daemon-admin/stats-handler.js +56 -0
- package/dist/server/runtime/http-server/daemon-admin/stats-handler.js.map +1 -0
- package/dist/server/runtime/http-server/daemon-admin/status-handler.js +8 -4
- package/dist/server/runtime/http-server/daemon-admin/status-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin-routes.d.ts +9 -0
- package/dist/server/runtime/http-server/daemon-admin-routes.js +3 -0
- package/dist/server/runtime/http-server/daemon-admin-routes.js.map +1 -1
- package/dist/server/runtime/http-server/executor-provider.js +74 -0
- package/dist/server/runtime/http-server/executor-provider.js.map +1 -1
- package/dist/server/runtime/http-server/index.d.ts +2 -0
- package/dist/server/runtime/http-server/index.js +107 -17
- package/dist/server/runtime/http-server/index.js.map +1 -1
- package/dist/server/runtime/http-server/request-executor.js +18 -11
- package/dist/server/runtime/http-server/request-executor.js.map +1 -1
- package/dist/server/runtime/http-server/routes.d.ts +5 -0
- package/dist/server/runtime/http-server/routes.js +17 -4
- package/dist/server/runtime/http-server/routes.js.map +1 -1
- package/dist/server/runtime/http-server/stats-manager.d.ts +7 -0
- package/dist/server/runtime/http-server/stats-manager.js +31 -6
- package/dist/server/runtime/http-server/stats-manager.js.map +1 -1
- package/dist/server/runtime/http-server/types.d.ts +5 -0
- package/dist/server/utils/http-error-mapper.js +70 -9
- package/dist/server/utils/http-error-mapper.js.map +1 -1
- package/dist/server/utils/request-id-manager.js +9 -5
- package/dist/server/utils/request-id-manager.js.map +1 -1
- package/dist/server/utils/sse-request-parser.js +2 -1
- package/dist/server/utils/sse-request-parser.js.map +1 -1
- package/dist/server/utils/utf8-chunk-buffer.d.ts +15 -30
- package/dist/server/utils/utf8-chunk-buffer.js +78 -88
- package/dist/server/utils/utf8-chunk-buffer.js.map +1 -1
- package/dist/server/utils/warmup-storm-tracker.js +1 -1
- package/dist/server/utils/warmup-storm-tracker.js.map +1 -1
- package/dist/tools/provider-update/fetch-models.js +8 -5
- package/dist/tools/provider-update/fetch-models.js.map +1 -1
- package/dist/tools/provider-update/probe-context.d.ts +24 -0
- package/dist/tools/provider-update/probe-context.js +199 -0
- package/dist/tools/provider-update/probe-context.js.map +1 -0
- package/dist/tools/provider-update/types.d.ts +1 -0
- package/package.json +10 -4
- package/scripts/anthropic-compare-modes.mjs +40 -3
- package/scripts/antigravity-smoke.mjs +180 -0
- package/scripts/backfill-apply-patch-exec-errorsamples.mjs +225 -0
- package/scripts/compare-codex-rccx.mjs +59 -1
- package/scripts/compare-responses-request.mjs +50 -4
- package/scripts/lib/errorsamples.mjs +23 -0
- package/scripts/mock-provider/run-regressions.mjs +12 -2
- package/scripts/policy-violations-report.mjs +257 -0
- package/scripts/publish-rcc.mjs +16 -2
- package/scripts/scan-apply-patch-samples.mjs +148 -7
- package/scripts/tests/unified-hub-responses-enforce-safe.mjs +37 -0
- package/scripts/tests/unified-hub-shadow-regression.mjs +55 -0
- package/scripts/unified-hub-shadow-compare.mjs +359 -0
- package/scripts/verify-e2e-gemini-followup-sample.mjs +269 -0
- package/scripts/virtual-router-shadow-v2-real.mjs +71 -1
- package/scripts/virtual-router-shadow-v2.mjs +41 -0
package/dist/cli.js
CHANGED
|
@@ -4,71 +4,28 @@
|
|
|
4
4
|
* Multi-provider OpenAI proxy server command line interface
|
|
5
5
|
*/
|
|
6
6
|
import { Command } from 'commander';
|
|
7
|
-
import chalk from 'chalk';
|
|
8
7
|
import fs from 'fs';
|
|
9
8
|
import path from 'path';
|
|
10
|
-
import { homedir } from 'os';
|
|
11
|
-
import { spawnSync } from 'child_process';
|
|
9
|
+
import { homedir, tmpdir } from 'os';
|
|
10
|
+
import { spawn, spawnSync } from 'child_process';
|
|
12
11
|
import { fileURLToPath } from 'url';
|
|
13
12
|
import { LOCAL_HOSTS, HTTP_PROTOCOLS, API_PATHS, DEFAULT_CONFIG, API_ENDPOINTS } from './constants/index.js';
|
|
14
13
|
import { buildInfo } from './build-info.js';
|
|
15
14
|
import { ensureLocalTokenPortalEnv } from './token-portal/local-token-portal.js';
|
|
16
15
|
import { parseNetstatListeningPids } from './utils/windows-netstat.js';
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
let currentText = text;
|
|
28
|
-
const log = (prefix, msg) => {
|
|
29
|
-
const message = msg ?? currentText;
|
|
30
|
-
if (!message) {
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
console.log(`${prefix} ${message}`);
|
|
34
|
-
};
|
|
35
|
-
const stub = {
|
|
36
|
-
start(msg) {
|
|
37
|
-
if (msg) {
|
|
38
|
-
currentText = msg;
|
|
39
|
-
}
|
|
40
|
-
log('...', msg);
|
|
41
|
-
return stub;
|
|
42
|
-
},
|
|
43
|
-
succeed(msg) { log('✓', msg); },
|
|
44
|
-
fail(msg) { log('✗', msg); },
|
|
45
|
-
warn(msg) { log('⚠', msg); },
|
|
46
|
-
info(msg) { log('ℹ', msg); },
|
|
47
|
-
stop() { },
|
|
48
|
-
get text() { return currentText; },
|
|
49
|
-
set text(value) { currentText = value; }
|
|
50
|
-
};
|
|
51
|
-
return stub;
|
|
52
|
-
}
|
|
16
|
+
import { ensurePortAvailableImpl, findListeningPidsImpl, isServerHealthyQuickImpl, killPidBestEffortImpl } from './cli/server/port-utils.js';
|
|
17
|
+
import { registerBasicCommands } from './cli/register/basic-commands.js';
|
|
18
|
+
import { loadRouteCodexConfig } from './config/routecodex-config-loader.js';
|
|
19
|
+
import { createSpinner } from './cli/spinner.js';
|
|
20
|
+
import { logger } from './cli/logger.js';
|
|
21
|
+
import { registerStatusConfigCommands } from './cli/register/status-config-commands.js';
|
|
22
|
+
import { registerRestartCommand } from './cli/register/restart-command.js';
|
|
23
|
+
import { registerStopCommand } from './cli/register/stop-command.js';
|
|
24
|
+
import { registerStartCommand } from './cli/register/start-command.js';
|
|
25
|
+
import { registerCodeCommand } from './cli/register/code-command.js';
|
|
53
26
|
const __filename = fileURLToPath(import.meta.url);
|
|
54
27
|
const __dirname = path.dirname(__filename);
|
|
55
|
-
|
|
56
|
-
const logger = {
|
|
57
|
-
info: (msg) => console.log(`${chalk.blue('ℹ')} ${msg}`),
|
|
58
|
-
success: (msg) => console.log(`${chalk.green('✓')} ${msg}`),
|
|
59
|
-
warning: (msg) => console.log(`${chalk.yellow('⚠')} ${msg}`),
|
|
60
|
-
error: (msg) => console.log(`${chalk.red('✗')} ${msg}`),
|
|
61
|
-
debug: (msg) => console.log(`${chalk.gray('◉')} ${msg}`)
|
|
62
|
-
};
|
|
63
|
-
// Ensure llmswitch-core is resolvable(dev/worktree 场景下由 pipeline 加载 vendor)
|
|
64
|
-
async function dynamicImport(specifier) {
|
|
65
|
-
try {
|
|
66
|
-
return (await import(specifier));
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
return undefined;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
28
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
72
29
|
async function ensureCoreOrFail() {
|
|
73
30
|
// 在当前 worktree/dev 场景下:
|
|
74
31
|
// - llmswitch-core 直接通过 sharedmodule/llmswitch-core/dist 引用;
|
|
@@ -130,28 +87,12 @@ async function ensureTokenDaemonAutoStart() {
|
|
|
130
87
|
}
|
|
131
88
|
}
|
|
132
89
|
function killPidBestEffort(pid, opts) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
args.push('/F');
|
|
140
|
-
}
|
|
141
|
-
try {
|
|
142
|
-
spawnSync('taskkill', args, { stdio: 'ignore', encoding: 'utf8' });
|
|
143
|
-
}
|
|
144
|
-
catch {
|
|
145
|
-
// best-effort
|
|
146
|
-
}
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
try {
|
|
150
|
-
process.kill(pid, opts.force ? 'SIGKILL' : 'SIGTERM');
|
|
151
|
-
}
|
|
152
|
-
catch {
|
|
153
|
-
// best-effort
|
|
154
|
-
}
|
|
90
|
+
return killPidBestEffortImpl({
|
|
91
|
+
pid,
|
|
92
|
+
force: Boolean(opts?.force),
|
|
93
|
+
isWindows: IS_WINDOWS,
|
|
94
|
+
spawnSyncImpl: spawnSync
|
|
95
|
+
});
|
|
155
96
|
}
|
|
156
97
|
async function stopTokenDaemonIfRunning() {
|
|
157
98
|
try {
|
|
@@ -253,1379 +194,126 @@ try {
|
|
|
253
194
|
}
|
|
254
195
|
catch { /* optional */ }
|
|
255
196
|
// Code command - Launch Claude Code interface
|
|
256
|
-
program
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
.
|
|
264
|
-
.
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
.
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const parseServerUrl = (raw) => {
|
|
278
|
-
const trimmed = String(raw || '').trim();
|
|
279
|
-
if (!trimmed) {
|
|
280
|
-
throw new Error('--url is empty');
|
|
281
|
-
}
|
|
282
|
-
let parsed;
|
|
283
|
-
try {
|
|
284
|
-
parsed = new URL(trimmed);
|
|
285
|
-
}
|
|
286
|
-
catch {
|
|
287
|
-
parsed = new URL(`http://${trimmed}`);
|
|
288
|
-
}
|
|
289
|
-
const protocol = parsed.protocol === 'https:' ? 'https' : 'http';
|
|
290
|
-
const host = parsed.hostname;
|
|
291
|
-
const hasExplicitPort = Boolean(parsed.port && parsed.port.trim());
|
|
292
|
-
const port = hasExplicitPort ? Number(parsed.port) : null;
|
|
293
|
-
const rawPath = typeof parsed.pathname === 'string' ? parsed.pathname : '';
|
|
294
|
-
const basePath = rawPath && rawPath !== '/' ? rawPath.replace(/\/+$/, '') : '';
|
|
295
|
-
return { protocol, host, port: Number.isFinite(port) ? port : null, basePath };
|
|
296
|
-
};
|
|
297
|
-
const readConfigApiKey = (configPath) => {
|
|
298
|
-
try {
|
|
299
|
-
if (!configPath || !fs.existsSync(configPath)) {
|
|
300
|
-
return null;
|
|
301
|
-
}
|
|
302
|
-
const txt = fs.readFileSync(configPath, 'utf8');
|
|
303
|
-
const cfg = JSON.parse(txt);
|
|
304
|
-
const direct = cfg?.httpserver?.apikey ?? cfg?.modules?.httpserver?.config?.apikey ?? cfg?.server?.apikey;
|
|
305
|
-
const value = typeof direct === 'string' ? direct.trim() : '';
|
|
306
|
-
return value ? value : null;
|
|
307
|
-
}
|
|
308
|
-
catch {
|
|
309
|
-
return null;
|
|
310
|
-
}
|
|
311
|
-
};
|
|
312
|
-
// Resolve configuration and determine port
|
|
313
|
-
let configPath = options.config;
|
|
314
|
-
if (!configPath) {
|
|
315
|
-
configPath = path.join(homedir(), '.routecodex', 'config.json');
|
|
316
|
-
}
|
|
317
|
-
let actualProtocol = 'http';
|
|
318
|
-
let actualPort = options.port ? parseInt(options.port, 10) : null;
|
|
319
|
-
let actualHost = options.host;
|
|
320
|
-
let actualBasePath = '';
|
|
321
|
-
if (options.url && String(options.url).trim()) {
|
|
322
|
-
const parsed = parseServerUrl(options.url);
|
|
323
|
-
actualProtocol = parsed.protocol;
|
|
324
|
-
actualHost = parsed.host || actualHost;
|
|
325
|
-
actualPort = parsed.port ?? actualPort;
|
|
326
|
-
actualBasePath = parsed.basePath;
|
|
327
|
-
}
|
|
328
|
-
// Determine effective port for code command:
|
|
329
|
-
// - dev package (routecodex): env override, otherwise固定 5555,不读取配置端口
|
|
330
|
-
// - release package (rcc): 按配置/参数解析端口
|
|
331
|
-
if (IS_DEV_PACKAGE) {
|
|
332
|
-
if (!actualPort) {
|
|
333
|
-
const envPort = Number(process.env.ROUTECODEX_PORT || process.env.RCC_PORT || NaN);
|
|
334
|
-
actualPort = Number.isFinite(envPort) && envPort > 0 ? envPort : DEFAULT_DEV_PORT;
|
|
335
|
-
logger.info(`Using dev default port ${actualPort} for routecodex code (config ports ignored)`);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
else {
|
|
339
|
-
// 非 dev 包:若未显式指定端口,则从配置文件解析
|
|
340
|
-
if (!actualPort && fs.existsSync(configPath) && !(options.url && String(options.url).trim())) {
|
|
341
|
-
try {
|
|
342
|
-
const configContent = fs.readFileSync(configPath, 'utf8');
|
|
343
|
-
const config = JSON.parse(configContent);
|
|
344
|
-
actualPort = (config?.httpserver?.port ?? config?.server?.port ?? config?.port) || null;
|
|
345
|
-
actualHost = (config?.httpserver?.host || config?.server?.host || config?.host || actualHost);
|
|
346
|
-
}
|
|
347
|
-
catch (error) {
|
|
348
|
-
spinner.warn('Failed to read configuration file, using defaults');
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
// Require explicit port if not resolved (except when --url is used; default ports are implicit).
|
|
353
|
-
if (!(options.url && String(options.url).trim()) && !actualPort) {
|
|
354
|
-
spinner.fail('Invalid or missing port configuration for RouteCodex server');
|
|
355
|
-
logger.error('Please set httpserver.port in your configuration (e.g., ~/.routecodex/config.json) or use --port');
|
|
356
|
-
process.exit(1);
|
|
357
|
-
}
|
|
358
|
-
const configuredApiKey = (typeof options.apikey === 'string' && options.apikey.trim()
|
|
359
|
-
? options.apikey.trim()
|
|
360
|
-
: null)
|
|
361
|
-
?? (typeof process.env.ROUTECODEX_APIKEY === 'string' && process.env.ROUTECODEX_APIKEY.trim()
|
|
362
|
-
? process.env.ROUTECODEX_APIKEY.trim()
|
|
363
|
-
: null)
|
|
364
|
-
?? (typeof process.env.RCC_APIKEY === 'string' && process.env.RCC_APIKEY.trim()
|
|
365
|
-
? process.env.RCC_APIKEY.trim()
|
|
366
|
-
: null)
|
|
367
|
-
?? readConfigApiKey(configPath);
|
|
368
|
-
// Check if RouteCodex server needs to be started
|
|
369
|
-
if (options.ensureServer) {
|
|
370
|
-
spinner.text = 'Checking RouteCodex server status...';
|
|
371
|
-
const normalizeConnectHost = (h) => {
|
|
372
|
-
const v = String(h || '').toLowerCase();
|
|
373
|
-
if (v === '0.0.0.0') {
|
|
374
|
-
return LOCAL_HOSTS.IPV4;
|
|
375
|
-
}
|
|
376
|
-
if (v === '::' || v === '::1' || v === 'localhost') {
|
|
377
|
-
return LOCAL_HOSTS.IPV4;
|
|
378
|
-
}
|
|
379
|
-
return h || LOCAL_HOSTS.IPV4;
|
|
380
|
-
};
|
|
381
|
-
const connectHost = normalizeConnectHost(actualHost);
|
|
382
|
-
const portPart = actualPort ? `:${actualPort}` : '';
|
|
383
|
-
const serverUrl = `${actualProtocol}://${connectHost}${portPart}${actualBasePath}`;
|
|
384
|
-
try {
|
|
385
|
-
const controller = new AbortController();
|
|
386
|
-
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
|
387
|
-
const headers = configuredApiKey ? { 'x-api-key': configuredApiKey } : undefined;
|
|
388
|
-
const response = await fetch(`${serverUrl}/ready`, { signal: controller.signal, method: 'GET', headers });
|
|
389
|
-
clearTimeout(timeoutId);
|
|
390
|
-
if (!response.ok) {
|
|
391
|
-
throw new Error('Server not ready');
|
|
392
|
-
}
|
|
393
|
-
const j = await response.json().catch(() => ({}));
|
|
394
|
-
if (j?.status !== 'ready') {
|
|
395
|
-
throw new Error('Server reported not_ready');
|
|
396
|
-
}
|
|
397
|
-
spinner.succeed('RouteCodex server is ready');
|
|
398
|
-
}
|
|
399
|
-
catch (error) {
|
|
400
|
-
if (options.url && String(options.url).trim()) {
|
|
401
|
-
spinner.fail('RouteCodex server is not reachable (ensure-server with --url cannot auto-start)');
|
|
402
|
-
logger.error(error instanceof Error ? error.message : String(error));
|
|
403
|
-
process.exit(1);
|
|
404
|
-
}
|
|
405
|
-
spinner.info('RouteCodex server is not running, starting it...');
|
|
406
|
-
// Start RouteCodex server in background
|
|
407
|
-
const { spawn } = await import('child_process');
|
|
408
|
-
const modulesConfigPath = path.resolve(__dirname, '../config/modules.json');
|
|
409
|
-
const serverEntry = path.resolve(__dirname, 'index.js');
|
|
410
|
-
const serverProcess = spawn(process.execPath, [serverEntry, modulesConfigPath], {
|
|
411
|
-
stdio: 'pipe',
|
|
412
|
-
env: { ...process.env },
|
|
413
|
-
detached: true
|
|
414
|
-
});
|
|
415
|
-
serverProcess.unref();
|
|
416
|
-
// Wait for server to become ready (up to ~30s)
|
|
417
|
-
spinner.text = 'Waiting for RouteCodex server to become ready...';
|
|
418
|
-
let ready = false;
|
|
419
|
-
for (let i = 0; i < 30; i++) {
|
|
420
|
-
await sleep(1000);
|
|
421
|
-
try {
|
|
422
|
-
const headers = configuredApiKey ? { 'x-api-key': configuredApiKey } : undefined;
|
|
423
|
-
const res = await fetch(`${serverUrl}/ready`, { method: 'GET', headers });
|
|
424
|
-
if (res.ok) {
|
|
425
|
-
const jr = await res.json().catch(() => ({}));
|
|
426
|
-
if (jr?.status === 'ready') {
|
|
427
|
-
ready = true;
|
|
428
|
-
break;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
catch { /* ignore */ }
|
|
433
|
-
}
|
|
434
|
-
if (ready) {
|
|
435
|
-
spinner.succeed('RouteCodex server is ready');
|
|
436
|
-
}
|
|
437
|
-
else {
|
|
438
|
-
spinner.warn('RouteCodex server may not be fully ready, continuing...');
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
spinner.text = 'Launching Claude Code...';
|
|
443
|
-
// Prepare environment variables for Claude Code
|
|
444
|
-
const resolvedBaseHost = String((() => {
|
|
445
|
-
const v = String(actualHost || '').toLowerCase();
|
|
446
|
-
if (v === '0.0.0.0') {
|
|
447
|
-
return LOCAL_HOSTS.IPV4;
|
|
448
|
-
}
|
|
449
|
-
if (v === '::' || v === '::1' || v === 'localhost') {
|
|
450
|
-
return LOCAL_HOSTS.IPV4;
|
|
451
|
-
}
|
|
452
|
-
return actualHost || LOCAL_HOSTS.IPV4;
|
|
453
|
-
})());
|
|
454
|
-
const portPart = actualPort ? `:${actualPort}` : '';
|
|
455
|
-
const anthropicBase = `${actualProtocol}://${resolvedBaseHost}${portPart}${actualBasePath}`;
|
|
456
|
-
const currentCwd = (() => {
|
|
457
|
-
try {
|
|
458
|
-
const d = options.cwd ? String(options.cwd) : process.cwd();
|
|
459
|
-
const resolved = path.resolve(d);
|
|
460
|
-
if (fs.existsSync(resolved)) {
|
|
461
|
-
return resolved;
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
catch {
|
|
465
|
-
return process.cwd();
|
|
466
|
-
}
|
|
467
|
-
return process.cwd();
|
|
468
|
-
})();
|
|
469
|
-
const claudeEnv = {
|
|
470
|
-
...process.env,
|
|
471
|
-
// Normalize working directory context for downstream tools
|
|
472
|
-
PWD: currentCwd,
|
|
473
|
-
RCC_WORKDIR: currentCwd,
|
|
474
|
-
ROUTECODEX_WORKDIR: currentCwd,
|
|
475
|
-
CLAUDE_WORKDIR: currentCwd,
|
|
476
|
-
// Cover both common env var names used by Anthropic SDK / tools
|
|
477
|
-
ANTHROPIC_BASE_URL: anthropicBase,
|
|
478
|
-
ANTHROPIC_API_URL: anthropicBase,
|
|
479
|
-
ANTHROPIC_API_KEY: configuredApiKey || 'rcc-proxy-key'
|
|
480
|
-
};
|
|
481
|
-
// Avoid auth conflict: prefer API key routed via RouteCodex; remove shell tokens
|
|
482
|
-
try {
|
|
483
|
-
delete claudeEnv['ANTHROPIC_AUTH_TOKEN'];
|
|
484
|
-
}
|
|
485
|
-
catch { /* ignore */ }
|
|
486
|
-
try {
|
|
487
|
-
delete claudeEnv['ANTHROPIC_TOKEN'];
|
|
488
|
-
}
|
|
489
|
-
catch { /* ignore */ }
|
|
490
|
-
logger.info('Unset ANTHROPIC_AUTH_TOKEN/ANTHROPIC_TOKEN for Claude process to avoid conflicts');
|
|
491
|
-
logger.info(`Setting Anthropic base URL to: ${anthropicBase}`);
|
|
492
|
-
// Prepare Claude Code command arguments(将 rcc code 后面的原始参数默认透传给 Claude)
|
|
493
|
-
const claudeArgs = [];
|
|
494
|
-
if (options.model) {
|
|
495
|
-
claudeArgs.push('--model', options.model);
|
|
496
|
-
}
|
|
497
|
-
if (options.profile) {
|
|
498
|
-
claudeArgs.push('--profile', options.profile);
|
|
499
|
-
}
|
|
500
|
-
// 透传用户紧随 `rcc code` 之后的参数(默认行为)
|
|
501
|
-
try {
|
|
502
|
-
const rawArgv = process.argv.slice(2); // drop node/bin and script
|
|
503
|
-
const idxCode = rawArgv.findIndex(a => a === 'code');
|
|
504
|
-
const afterCode = idxCode >= 0 ? rawArgv.slice(idxCode + 1) : [];
|
|
505
|
-
// 支持显式分隔符 -- :其后的所有参数原样传给 Claude
|
|
506
|
-
const sepIndex = afterCode.indexOf('--');
|
|
507
|
-
const tail = sepIndex >= 0 ? afterCode.slice(sepIndex + 1) : afterCode;
|
|
508
|
-
// 过滤本命令自身已识别的选项,剩余的作为透传参数
|
|
509
|
-
const knownOpts = new Set([
|
|
510
|
-
'-p',
|
|
511
|
-
'--port',
|
|
512
|
-
'-h',
|
|
513
|
-
'--host',
|
|
514
|
-
'--url',
|
|
515
|
-
'-c',
|
|
516
|
-
'--config',
|
|
517
|
-
'--apikey',
|
|
518
|
-
'--claude-path',
|
|
519
|
-
'--model',
|
|
520
|
-
'--profile',
|
|
521
|
-
'--ensure-server'
|
|
522
|
-
]);
|
|
523
|
-
const requireValue = new Set([
|
|
524
|
-
'-p',
|
|
525
|
-
'--port',
|
|
526
|
-
'-h',
|
|
527
|
-
'--host',
|
|
528
|
-
'--url',
|
|
529
|
-
'-c',
|
|
530
|
-
'--config',
|
|
531
|
-
'--apikey',
|
|
532
|
-
'--claude-path',
|
|
533
|
-
'--model',
|
|
534
|
-
'--profile'
|
|
535
|
-
]);
|
|
536
|
-
const passThrough = [];
|
|
537
|
-
for (let i = 0; i < tail.length; i++) {
|
|
538
|
-
const tok = tail[i];
|
|
539
|
-
if (knownOpts.has(tok)) {
|
|
540
|
-
if (requireValue.has(tok)) {
|
|
541
|
-
i++;
|
|
542
|
-
}
|
|
543
|
-
continue;
|
|
544
|
-
}
|
|
545
|
-
// 若是组合形式 --opt=value 且 opt 为已识别的,跳过
|
|
546
|
-
if (tok.startsWith('--')) {
|
|
547
|
-
const eq = tok.indexOf('=');
|
|
548
|
-
if (eq > 2) {
|
|
549
|
-
const optName = tok.slice(0, eq);
|
|
550
|
-
if (knownOpts.has(optName)) {
|
|
551
|
-
continue;
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
passThrough.push(tok);
|
|
556
|
-
}
|
|
557
|
-
// 合并 Commander 捕获到的额外参数(多数为位置参数),与我们手动解析的尾参数,去重保序
|
|
558
|
-
const merged = [];
|
|
559
|
-
const seen = new Set();
|
|
560
|
-
const pushUnique = (arr) => { for (const t of arr) {
|
|
561
|
-
if (!seen.has(t)) {
|
|
562
|
-
seen.add(t);
|
|
563
|
-
merged.push(t);
|
|
564
|
-
}
|
|
565
|
-
} };
|
|
566
|
-
pushUnique(extraArgsFromCommander);
|
|
567
|
-
pushUnique(passThrough);
|
|
568
|
-
if (merged.length) {
|
|
569
|
-
claudeArgs.push(...merged);
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
catch {
|
|
573
|
-
// ignore passthrough errors
|
|
574
|
-
void 0;
|
|
575
|
-
}
|
|
576
|
-
// Launch Claude Code
|
|
577
|
-
const { spawn } = await import('child_process');
|
|
578
|
-
const claudeBin = (() => {
|
|
579
|
-
try {
|
|
580
|
-
const v = String(options?.claudePath || '').trim();
|
|
581
|
-
if (v) {
|
|
582
|
-
return v;
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
catch {
|
|
586
|
-
// ignore
|
|
587
|
-
}
|
|
588
|
-
const envPath = String(process.env.CLAUDE_PATH || '').trim();
|
|
589
|
-
return envPath || 'claude';
|
|
590
|
-
})();
|
|
591
|
-
// Windows: Node spawn does not resolve .cmd shims unless using a shell. Prefer shell for bare commands.
|
|
592
|
-
const shouldUseShell = IS_WINDOWS &&
|
|
593
|
-
!path.extname(claudeBin) &&
|
|
594
|
-
!claudeBin.includes('/') &&
|
|
595
|
-
!claudeBin.includes('\\');
|
|
596
|
-
const claudeProcess = spawn(claudeBin, claudeArgs, {
|
|
597
|
-
stdio: 'inherit',
|
|
598
|
-
env: claudeEnv,
|
|
599
|
-
cwd: currentCwd,
|
|
600
|
-
shell: shouldUseShell
|
|
601
|
-
});
|
|
602
|
-
spinner.succeed('Claude Code launched with RouteCodex proxy');
|
|
603
|
-
// Log normalized IPv4 host to avoid confusion (do not print ::/localhost)
|
|
604
|
-
logger.info(`Using RouteCodex server at: http://${resolvedBaseHost}:${actualPort}`);
|
|
605
|
-
logger.info(`Claude binary: ${claudeBin}`);
|
|
606
|
-
logger.info(`Working directory for Claude: ${currentCwd}`);
|
|
607
|
-
logger.info('Press Ctrl+C to exit Claude Code');
|
|
608
|
-
// Handle graceful shutdown
|
|
609
|
-
const shutdown = async (sig) => {
|
|
610
|
-
try {
|
|
611
|
-
claudeProcess.kill(sig);
|
|
612
|
-
}
|
|
613
|
-
catch { /* ignore */ }
|
|
614
|
-
try {
|
|
615
|
-
process.exit(0);
|
|
616
|
-
}
|
|
617
|
-
catch { /* ignore */ }
|
|
618
|
-
};
|
|
619
|
-
process.on('SIGINT', () => { void shutdown('SIGINT'); });
|
|
620
|
-
process.on('SIGTERM', () => { void shutdown('SIGTERM'); });
|
|
621
|
-
claudeProcess.on('error', (err) => {
|
|
622
|
-
try {
|
|
623
|
-
logger.error(`Failed to launch Claude Code (${claudeBin}): ${err instanceof Error ? err.message : String(err)}`);
|
|
624
|
-
if (IS_WINDOWS && shouldUseShell) {
|
|
625
|
-
logger.error('Tip: If Claude is installed via npm, ensure the shim is in PATH (e.g. claude.cmd).');
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
catch { /* ignore */ }
|
|
629
|
-
process.exit(1);
|
|
630
|
-
});
|
|
631
|
-
claudeProcess.on('exit', (code, signal) => {
|
|
632
|
-
if (signal) {
|
|
633
|
-
process.exit(0);
|
|
634
|
-
}
|
|
635
|
-
else {
|
|
636
|
-
process.exit(code ?? 0);
|
|
637
|
-
}
|
|
638
|
-
});
|
|
639
|
-
// Keep process alive
|
|
640
|
-
await new Promise(() => {
|
|
641
|
-
// Keep process alive until interrupted
|
|
642
|
-
return;
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
catch (error) {
|
|
646
|
-
spinner.fail('Failed to launch Claude Code');
|
|
647
|
-
logger.error(error instanceof Error ? error.message : String(error));
|
|
648
|
-
process.exit(1);
|
|
649
|
-
}
|
|
197
|
+
registerCodeCommand(program, {
|
|
198
|
+
isDevPackage: IS_DEV_PACKAGE,
|
|
199
|
+
isWindows: IS_WINDOWS,
|
|
200
|
+
defaultDevPort: DEFAULT_DEV_PORT,
|
|
201
|
+
nodeBin: process.execPath,
|
|
202
|
+
createSpinner,
|
|
203
|
+
logger,
|
|
204
|
+
env: process.env,
|
|
205
|
+
rawArgv: process.argv.slice(2),
|
|
206
|
+
homedir,
|
|
207
|
+
cwd: () => process.cwd(),
|
|
208
|
+
sleep,
|
|
209
|
+
fetch,
|
|
210
|
+
spawn: (cmd, args, opts) => spawn(cmd, args, opts),
|
|
211
|
+
getModulesConfigPath,
|
|
212
|
+
resolveServerEntryPath: () => path.resolve(__dirname, 'index.js'),
|
|
213
|
+
waitForever: () => new Promise(() => {
|
|
214
|
+
return;
|
|
215
|
+
}),
|
|
216
|
+
onSignal: (sig, cb) => process.on(sig, cb),
|
|
217
|
+
exit: (code) => process.exit(code)
|
|
650
218
|
});
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
else {
|
|
673
|
-
if (!Number.isFinite(port) && fs.existsSync(configPath)) {
|
|
674
|
-
const cfg = safeReadJson(configPath);
|
|
675
|
-
port = normalizePort(cfg?.httpserver?.port ?? cfg?.server?.port ?? cfg?.port);
|
|
676
|
-
host = typeof cfg?.httpserver?.host === 'string'
|
|
677
|
-
? cfg.httpserver.host
|
|
678
|
-
: typeof cfg?.server?.host === 'string'
|
|
679
|
-
? cfg.server.host
|
|
680
|
-
: cfg?.host ?? host;
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
if (!Number.isFinite(port) || !port || port <= 0) {
|
|
684
|
-
throw new Error('Missing port. Set via --port, env or config file');
|
|
685
|
-
}
|
|
686
|
-
const norm = (h) => {
|
|
687
|
-
const v = String(h || '').toLowerCase();
|
|
688
|
-
if (v === '0.0.0.0' || v === '::' || v === '::1' || v === 'localhost') {
|
|
689
|
-
return LOCAL_HOSTS.IPV4;
|
|
690
|
-
}
|
|
691
|
-
return h || LOCAL_HOSTS.IPV4;
|
|
692
|
-
};
|
|
693
|
-
const resolvedHost = norm(host);
|
|
694
|
-
const base = `http://${resolvedHost}:${port}`;
|
|
695
|
-
if (options.json) {
|
|
696
|
-
const out = {
|
|
697
|
-
ANTHROPIC_BASE_URL: base,
|
|
698
|
-
ANTHROPIC_API_URL: base,
|
|
699
|
-
ANTHROPIC_API_KEY: 'rcc-proxy-key',
|
|
700
|
-
UNSET: ['ANTHROPIC_TOKEN', 'ANTHROPIC_AUTH_TOKEN']
|
|
701
|
-
};
|
|
702
|
-
console.log(JSON.stringify(out, null, 2));
|
|
703
|
-
}
|
|
704
|
-
else {
|
|
705
|
-
console.log(`export ANTHROPIC_BASE_URL=${base}`);
|
|
706
|
-
console.log(`export ANTHROPIC_API_URL=${base}`);
|
|
707
|
-
console.log(`export ANTHROPIC_API_KEY=rcc-proxy-key`);
|
|
708
|
-
// Ensure conflicting tokens are not picked up by client tools
|
|
709
|
-
console.log('unset ANTHROPIC_TOKEN');
|
|
710
|
-
console.log('unset ANTHROPIC_AUTH_TOKEN');
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
catch (error) {
|
|
714
|
-
logger.error(error instanceof Error ? error.message : String(error));
|
|
715
|
-
process.exit(1);
|
|
219
|
+
registerBasicCommands(program, {
|
|
220
|
+
env: {
|
|
221
|
+
isDevPackage: IS_DEV_PACKAGE,
|
|
222
|
+
defaultDevPort: DEFAULT_DEV_PORT,
|
|
223
|
+
log: (line) => console.log(line),
|
|
224
|
+
error: (line) => logger.error(line),
|
|
225
|
+
exit: (code) => process.exit(code)
|
|
226
|
+
},
|
|
227
|
+
clean: { logger },
|
|
228
|
+
examples: { log: (line) => console.log(line) },
|
|
229
|
+
port: {
|
|
230
|
+
defaultPort: DEFAULT_CONFIG.PORT,
|
|
231
|
+
createSpinner,
|
|
232
|
+
findListeningPids,
|
|
233
|
+
killPidBestEffort,
|
|
234
|
+
sleep,
|
|
235
|
+
log: (line) => console.log(line),
|
|
236
|
+
error: (line) => console.error(line),
|
|
237
|
+
exit: (code) => process.exit(code)
|
|
716
238
|
}
|
|
717
239
|
});
|
|
718
240
|
// Start command
|
|
719
|
-
program
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
.
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
.
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
const uaFromFlag = typeof options.ua === 'string' && options.ua.trim()
|
|
749
|
-
? options.ua.trim()
|
|
750
|
-
: null;
|
|
751
|
-
const uaMode = uaFromFlag || (options.codex ? 'codex' : null);
|
|
752
|
-
if (uaMode) {
|
|
753
|
-
process.env.ROUTECODEX_UA_MODE = uaMode;
|
|
754
|
-
}
|
|
755
|
-
if (options.snap && options.snapOff) {
|
|
756
|
-
spinner.fail('Flags --snap and --snap-off are mutually exclusive');
|
|
757
|
-
process.exit(1);
|
|
758
|
-
}
|
|
759
|
-
if (options.snap) {
|
|
760
|
-
process.env.ROUTECODEX_SNAPSHOT = '1';
|
|
761
|
-
}
|
|
762
|
-
else if (options.snapOff) {
|
|
763
|
-
process.env.ROUTECODEX_SNAPSHOT = '0';
|
|
764
|
-
}
|
|
765
|
-
if (options.verboseErrors && options.quietErrors) {
|
|
766
|
-
spinner.fail('Flags --verbose-errors and --quiet-errors are mutually exclusive');
|
|
767
|
-
process.exit(1);
|
|
768
|
-
}
|
|
769
|
-
if (options.verboseErrors) {
|
|
770
|
-
process.env.ROUTECODEX_VERBOSE_ERRORS = '1';
|
|
771
|
-
}
|
|
772
|
-
else if (options.quietErrors) {
|
|
773
|
-
process.env.ROUTECODEX_VERBOSE_ERRORS = '0';
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
catch { /* ignore */ }
|
|
777
|
-
// Resolve config path
|
|
778
|
-
let configPath = options.config;
|
|
779
|
-
if (!configPath) {
|
|
780
|
-
configPath = path.join(homedir(), '.routecodex', 'config.json');
|
|
781
|
-
}
|
|
782
|
-
// Ensure provided config path is a file (not a directory)
|
|
783
|
-
if (fs.existsSync(configPath)) {
|
|
784
|
-
const stats = fs.statSync(configPath);
|
|
785
|
-
if (stats.isDirectory()) {
|
|
786
|
-
spinner.fail(`Configuration path must be a file, received directory: ${configPath}`);
|
|
787
|
-
process.exit(1);
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
// Check if config exists; do NOT create defaults
|
|
791
|
-
if (!fs.existsSync(configPath)) {
|
|
792
|
-
spinner.fail(`Configuration file not found: ${configPath}`);
|
|
793
|
-
logger.error('Please create a RouteCodex user config first (e.g., ~/.routecodex/config.json).');
|
|
794
|
-
logger.error('Or initialize via CLI:');
|
|
795
|
-
logger.error(' rcc config init');
|
|
796
|
-
logger.error('Or specify a custom configuration file:');
|
|
797
|
-
logger.error(' rcc start --config ./my-config.json');
|
|
798
|
-
process.exit(1);
|
|
799
|
-
}
|
|
800
|
-
// Load and validate configuration (non-dev packages rely on config port)
|
|
801
|
-
let config;
|
|
802
|
-
try {
|
|
803
|
-
const configContent = fs.readFileSync(configPath, 'utf8');
|
|
804
|
-
config = JSON.parse(configContent);
|
|
805
|
-
}
|
|
806
|
-
catch (error) {
|
|
807
|
-
spinner.fail('Failed to parse configuration file');
|
|
808
|
-
logger.error(`Invalid JSON in configuration file: ${configPath}`);
|
|
809
|
-
process.exit(1);
|
|
810
|
-
}
|
|
811
|
-
// Determine effective port:
|
|
812
|
-
// - dev package (`routecodex`): env override, otherwise固定端口 5555(完全忽略配置中的端口)
|
|
813
|
-
// - release package (`rcc`): 严格按配置文件端口启动
|
|
814
|
-
let resolvedPort;
|
|
815
|
-
if (IS_DEV_PACKAGE) {
|
|
816
|
-
const flagPort = typeof options.port === 'string' ? Number(options.port) : NaN;
|
|
817
|
-
if (!Number.isNaN(flagPort) && flagPort > 0) {
|
|
818
|
-
logger.info(`Using port ${flagPort} from --port flag [dev package: routecodex]`);
|
|
819
|
-
resolvedPort = flagPort;
|
|
820
|
-
}
|
|
821
|
-
else {
|
|
822
|
-
const envPort = Number(process.env.ROUTECODEX_PORT || process.env.RCC_PORT || NaN);
|
|
823
|
-
if (!Number.isNaN(envPort) && envPort > 0) {
|
|
824
|
-
logger.info(`Using port ${envPort} from environment (ROUTECODEX_PORT/RCC_PORT) [dev package: routecodex]`);
|
|
825
|
-
resolvedPort = envPort;
|
|
826
|
-
}
|
|
827
|
-
else {
|
|
828
|
-
resolvedPort = DEFAULT_DEV_PORT;
|
|
829
|
-
logger.info(`Using dev default port ${resolvedPort} (routecodex dev package)`);
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
else {
|
|
834
|
-
const port = (config?.httpserver?.port ?? config?.server?.port ?? config?.port);
|
|
835
|
-
if (!port || typeof port !== 'number' || port <= 0) {
|
|
836
|
-
spinner.fail('Invalid or missing port configuration');
|
|
837
|
-
logger.error('Please set a valid port (httpserver.port or top-level port) in your configuration');
|
|
838
|
-
process.exit(1);
|
|
839
|
-
}
|
|
840
|
-
resolvedPort = port;
|
|
841
|
-
}
|
|
842
|
-
// Ensure port state aligns with requested behavior (always take over to avoid duplicates)
|
|
843
|
-
await ensurePortAvailable(resolvedPort, spinner, { restart: true });
|
|
844
|
-
const resolveServerHost = () => {
|
|
845
|
-
if (typeof config?.httpserver?.host === 'string' && config.httpserver.host.trim()) {
|
|
846
|
-
return config.httpserver.host;
|
|
847
|
-
}
|
|
848
|
-
if (typeof config?.server?.host === 'string' && config.server.host.trim()) {
|
|
849
|
-
return config.server.host;
|
|
850
|
-
}
|
|
851
|
-
if (typeof config?.host === 'string' && config.host.trim()) {
|
|
852
|
-
return config.host;
|
|
853
|
-
}
|
|
854
|
-
return LOCAL_HOSTS.LOCALHOST;
|
|
855
|
-
};
|
|
856
|
-
const serverHost = resolveServerHost();
|
|
857
|
-
process.env.ROUTECODEX_PORT = String(resolvedPort);
|
|
858
|
-
process.env.RCC_PORT = String(resolvedPort);
|
|
859
|
-
process.env.ROUTECODEX_HTTP_HOST = serverHost;
|
|
860
|
-
process.env.ROUTECODEX_HTTP_PORT = String(resolvedPort);
|
|
861
|
-
await ensureLocalTokenPortalEnv();
|
|
862
|
-
// Best-effort auto-start of token daemon (can be disabled via env)
|
|
863
|
-
await ensureTokenDaemonAutoStart();
|
|
864
|
-
// simple-log application removed
|
|
865
|
-
// Resolve modules config path
|
|
866
|
-
const modulesConfigPath = getModulesConfigPath();
|
|
867
|
-
if (!fs.existsSync(modulesConfigPath)) {
|
|
868
|
-
spinner.fail(`Modules configuration file not found: ${modulesConfigPath}`);
|
|
869
|
-
process.exit(1);
|
|
870
|
-
}
|
|
871
|
-
// resolvedPort already determined above
|
|
872
|
-
// Spawn child Node process to run the server entry; forward signals
|
|
873
|
-
const nodeBin = process.execPath; // current Node
|
|
874
|
-
const serverEntry = path.resolve(__dirname, 'index.js');
|
|
875
|
-
// Use spawn (not spawnSync); import child_process at top already
|
|
876
|
-
const { spawn } = await import('child_process');
|
|
877
|
-
const env = { ...process.env };
|
|
878
|
-
// Ensure server process picks the intended user config path
|
|
879
|
-
env.ROUTECODEX_CONFIG = configPath;
|
|
880
|
-
env.ROUTECODEX_CONFIG_PATH = configPath;
|
|
881
|
-
// 对 dev 包(routecodex),强制通过环境变量传递端口,确保服务器与 CLI 使用同一个 5555/自定义端口
|
|
882
|
-
if (IS_DEV_PACKAGE) {
|
|
883
|
-
env.ROUTECODEX_PORT = String(resolvedPort);
|
|
884
|
-
}
|
|
885
|
-
const args = [serverEntry, modulesConfigPath];
|
|
886
|
-
const childProc = spawn(nodeBin, args, { stdio: 'inherit', env });
|
|
887
|
-
// Persist child pid for out-of-band stop diagnostics
|
|
888
|
-
try {
|
|
889
|
-
const pidFile = path.join(homedir(), '.routecodex', 'server.cli.pid');
|
|
890
|
-
fs.writeFileSync(pidFile, String(childProc.pid ?? ''), 'utf8');
|
|
891
|
-
}
|
|
892
|
-
catch (error) { /* ignore */ }
|
|
893
|
-
const host = serverHost;
|
|
894
|
-
spinner.succeed(`RouteCodex server starting on ${host}:${resolvedPort}`);
|
|
895
|
-
logger.info(`Configuration loaded from: ${configPath}`);
|
|
896
|
-
logger.info(`Server will run on port: ${resolvedPort}`);
|
|
897
|
-
logger.info('Press Ctrl+C to stop the server');
|
|
898
|
-
// Forward signals to child
|
|
899
|
-
const shutdown = async (sig) => {
|
|
900
|
-
// 1) Ask server to shutdown over HTTP
|
|
901
|
-
try {
|
|
902
|
-
await fetch(`${HTTP_PROTOCOLS.HTTP}${LOCAL_HOSTS.IPV4}:${resolvedPort}${API_PATHS.SHUTDOWN}`, { method: 'POST' }).catch(() => { });
|
|
903
|
-
}
|
|
904
|
-
catch (error) { /* ignore */ }
|
|
905
|
-
// 2) Forward signal to child
|
|
906
|
-
try {
|
|
907
|
-
childProc.kill(sig);
|
|
908
|
-
}
|
|
909
|
-
catch (error) { /* ignore */ }
|
|
910
|
-
if (!IS_WINDOWS) {
|
|
911
|
-
try {
|
|
912
|
-
if (childProc.pid) {
|
|
913
|
-
process.kill(-childProc.pid, sig);
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
catch (error) { /* ignore */ }
|
|
917
|
-
}
|
|
918
|
-
// 3) Wait briefly; if still listening, try SIGTERM/SIGKILL by port
|
|
919
|
-
const deadline = Date.now() + 3500;
|
|
920
|
-
while (Date.now() < deadline) {
|
|
921
|
-
if (findListeningPids(resolvedPort).length === 0) {
|
|
922
|
-
break;
|
|
923
|
-
}
|
|
924
|
-
await sleep(120);
|
|
925
|
-
}
|
|
926
|
-
const remain = findListeningPids(resolvedPort);
|
|
927
|
-
if (remain.length) {
|
|
928
|
-
for (const pid of remain) {
|
|
929
|
-
killPidBestEffort(pid, { force: false });
|
|
930
|
-
}
|
|
931
|
-
const killDeadline = Date.now() + 1500;
|
|
932
|
-
while (Date.now() < killDeadline) {
|
|
933
|
-
if (findListeningPids(resolvedPort).length === 0) {
|
|
934
|
-
break;
|
|
935
|
-
}
|
|
936
|
-
await sleep(100);
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
const still = findListeningPids(resolvedPort);
|
|
940
|
-
if (still.length) {
|
|
941
|
-
for (const pid of still) {
|
|
942
|
-
killPidBestEffort(pid, { force: true });
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
if (IS_DEV_PACKAGE) {
|
|
946
|
-
await stopTokenDaemonIfRunning();
|
|
947
|
-
}
|
|
948
|
-
// Ensure parent exits even if child fails to exit
|
|
949
|
-
try {
|
|
950
|
-
process.exit(0);
|
|
951
|
-
}
|
|
952
|
-
catch { /* ignore */ }
|
|
953
|
-
};
|
|
954
|
-
process.on('SIGINT', () => { void shutdown('SIGINT'); });
|
|
955
|
-
process.on('SIGTERM', () => { void shutdown('SIGTERM'); });
|
|
956
|
-
// Fallback keypress handler: capture Ctrl+C / q when some environments swallow SIGINT
|
|
957
|
-
const cleanupKeypress = setupKeypress(() => { void shutdown('SIGINT'); });
|
|
958
|
-
childProc.on('exit', (code, signal) => {
|
|
959
|
-
// Propagate exit code
|
|
960
|
-
try {
|
|
961
|
-
cleanupKeypress();
|
|
962
|
-
}
|
|
963
|
-
catch { /* ignore */ }
|
|
964
|
-
if (signal) {
|
|
965
|
-
process.exit(0);
|
|
966
|
-
}
|
|
967
|
-
else {
|
|
968
|
-
process.exit(code ?? 0);
|
|
969
|
-
}
|
|
970
|
-
});
|
|
971
|
-
// Do not exit parent; keep process alive to relay signals
|
|
972
|
-
await new Promise(() => {
|
|
973
|
-
// Keep supervisor alive until shutdown completes
|
|
974
|
-
return;
|
|
975
|
-
});
|
|
976
|
-
}
|
|
977
|
-
catch (error) {
|
|
978
|
-
spinner.fail('Failed to start server');
|
|
979
|
-
logger.error(error instanceof Error ? error.message : String(error));
|
|
980
|
-
process.exit(1);
|
|
981
|
-
}
|
|
241
|
+
registerStartCommand(program, {
|
|
242
|
+
isDevPackage: IS_DEV_PACKAGE,
|
|
243
|
+
isWindows: IS_WINDOWS,
|
|
244
|
+
defaultDevPort: DEFAULT_DEV_PORT,
|
|
245
|
+
nodeBin: process.execPath,
|
|
246
|
+
createSpinner,
|
|
247
|
+
logger,
|
|
248
|
+
env: process.env,
|
|
249
|
+
fsImpl: fs,
|
|
250
|
+
pathImpl: path,
|
|
251
|
+
homedir,
|
|
252
|
+
tmpdir,
|
|
253
|
+
sleep,
|
|
254
|
+
ensureLocalTokenPortalEnv,
|
|
255
|
+
ensureTokenDaemonAutoStart,
|
|
256
|
+
stopTokenDaemonIfRunning,
|
|
257
|
+
ensurePortAvailable,
|
|
258
|
+
findListeningPids,
|
|
259
|
+
killPidBestEffort,
|
|
260
|
+
getModulesConfigPath,
|
|
261
|
+
resolveServerEntryPath: () => path.resolve(__dirname, 'index.js'),
|
|
262
|
+
spawn: (cmd, args, opts) => spawn(cmd, args, opts),
|
|
263
|
+
fetch,
|
|
264
|
+
setupKeypress,
|
|
265
|
+
waitForever: () => new Promise(() => {
|
|
266
|
+
return;
|
|
267
|
+
}),
|
|
268
|
+
onSignal: (sig, cb) => process.on(sig, cb),
|
|
269
|
+
exit: (code) => process.exit(code)
|
|
982
270
|
});
|
|
983
271
|
// Config command
|
|
984
|
-
program
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
.action(async (action, options) => {
|
|
992
|
-
try {
|
|
993
|
-
const configPath = options.config || path.join(homedir(), '.routecodex', 'config.json');
|
|
994
|
-
switch (action) {
|
|
995
|
-
case 'init':
|
|
996
|
-
await initializeConfig(configPath, options.template, options.force);
|
|
997
|
-
break;
|
|
998
|
-
case 'show':
|
|
999
|
-
if (fs.existsSync(configPath)) {
|
|
1000
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
1001
|
-
console.log(JSON.stringify(config, null, 2));
|
|
1002
|
-
}
|
|
1003
|
-
else {
|
|
1004
|
-
logger.error('Configuration file not found');
|
|
1005
|
-
}
|
|
1006
|
-
break;
|
|
1007
|
-
case 'edit': {
|
|
1008
|
-
const editor = process.env.EDITOR || 'nano';
|
|
1009
|
-
const { spawn } = await import('child_process');
|
|
1010
|
-
spawn(editor, [configPath], { stdio: 'inherit' });
|
|
1011
|
-
break;
|
|
1012
|
-
}
|
|
1013
|
-
case 'validate': {
|
|
1014
|
-
if (fs.existsSync(configPath)) {
|
|
1015
|
-
try {
|
|
1016
|
-
JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
1017
|
-
logger.success('Configuration is valid');
|
|
1018
|
-
}
|
|
1019
|
-
catch (error) {
|
|
1020
|
-
logger.error(`Configuration is invalid: ${error instanceof Error ? error.message : String(error)}`);
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
else {
|
|
1024
|
-
logger.error('Configuration file not found');
|
|
1025
|
-
}
|
|
1026
|
-
break;
|
|
1027
|
-
}
|
|
1028
|
-
default:
|
|
1029
|
-
logger.error('Unknown action. Use: show, edit, validate, init');
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
catch (error) {
|
|
1033
|
-
logger.error(`Config command failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
272
|
+
registerStatusConfigCommands(program, {
|
|
273
|
+
config: { logger, createSpinner },
|
|
274
|
+
status: {
|
|
275
|
+
logger,
|
|
276
|
+
log: (line) => console.log(line),
|
|
277
|
+
loadConfig: () => loadRouteCodexConfig(),
|
|
278
|
+
fetch
|
|
1034
279
|
}
|
|
1035
280
|
});
|
|
1036
|
-
// Initialize configuration helper function
|
|
1037
|
-
async function initializeConfig(configPath, template, force = false) {
|
|
1038
|
-
const spinner = await createSpinner('Initializing configuration...');
|
|
1039
|
-
try {
|
|
1040
|
-
// Create config directory if it doesn't exist
|
|
1041
|
-
const configDir = path.dirname(configPath);
|
|
1042
|
-
if (!fs.existsSync(configDir)) {
|
|
1043
|
-
fs.mkdirSync(configDir, { recursive: true });
|
|
1044
|
-
}
|
|
1045
|
-
// Check if config already exists
|
|
1046
|
-
if (fs.existsSync(configPath) && !force) {
|
|
1047
|
-
spinner.warn('Configuration file already exists');
|
|
1048
|
-
spinner.info('Use --force flag to overwrite or choose a different path');
|
|
1049
|
-
return;
|
|
1050
|
-
}
|
|
1051
|
-
// Load template
|
|
1052
|
-
let templateConfig;
|
|
1053
|
-
switch (template) {
|
|
1054
|
-
case 'lmstudio':
|
|
1055
|
-
templateConfig = {
|
|
1056
|
-
server: {
|
|
1057
|
-
port: DEFAULT_CONFIG.PORT,
|
|
1058
|
-
host: LOCAL_HOSTS.LOCALHOST
|
|
1059
|
-
},
|
|
1060
|
-
logging: {
|
|
1061
|
-
level: "info"
|
|
1062
|
-
},
|
|
1063
|
-
providers: {
|
|
1064
|
-
lmstudio: {
|
|
1065
|
-
type: "lmstudio",
|
|
1066
|
-
baseUrl: `${HTTP_PROTOCOLS.HTTP}${LOCAL_HOSTS.LOCALHOST}:${DEFAULT_CONFIG.LM_STUDIO_PORT}`,
|
|
1067
|
-
apiKey: "${LM_STUDIO_API_KEY:-}",
|
|
1068
|
-
models: {
|
|
1069
|
-
"gpt-oss-20b-mlx": {
|
|
1070
|
-
maxTokens: 8192,
|
|
1071
|
-
temperature: 0.7,
|
|
1072
|
-
supportsStreaming: true,
|
|
1073
|
-
supportsTools: true
|
|
1074
|
-
},
|
|
1075
|
-
"qwen2.5-7b-instruct": {
|
|
1076
|
-
maxTokens: 32768,
|
|
1077
|
-
temperature: 0.7,
|
|
1078
|
-
supportsStreaming: true,
|
|
1079
|
-
supportsTools: true
|
|
1080
|
-
}
|
|
1081
|
-
},
|
|
1082
|
-
timeout: 60000,
|
|
1083
|
-
retryAttempts: 3
|
|
1084
|
-
}
|
|
1085
|
-
},
|
|
1086
|
-
routing: {
|
|
1087
|
-
default: "lmstudio",
|
|
1088
|
-
models: {
|
|
1089
|
-
"gpt-4": "gpt-oss-20b-mlx",
|
|
1090
|
-
"gpt-4-turbo": "gpt-oss-20b-mlx",
|
|
1091
|
-
"gpt-3.5-turbo": "gpt-oss-20b-mlx",
|
|
1092
|
-
"claude-3-haiku": "qwen2.5-7b-instruct",
|
|
1093
|
-
"claude-3-sonnet": "gpt-oss-20b-mlx"
|
|
1094
|
-
}
|
|
1095
|
-
},
|
|
1096
|
-
features: {
|
|
1097
|
-
tools: {
|
|
1098
|
-
enabled: true,
|
|
1099
|
-
maxTools: 10
|
|
1100
|
-
},
|
|
1101
|
-
streaming: {
|
|
1102
|
-
enabled: true,
|
|
1103
|
-
chunkSize: 1024
|
|
1104
|
-
},
|
|
1105
|
-
oauth: {
|
|
1106
|
-
enabled: true,
|
|
1107
|
-
providers: ["qwen", "iflow"]
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
};
|
|
1111
|
-
break;
|
|
1112
|
-
case 'oauth':
|
|
1113
|
-
templateConfig = {
|
|
1114
|
-
server: {
|
|
1115
|
-
port: DEFAULT_CONFIG.PORT,
|
|
1116
|
-
host: LOCAL_HOSTS.LOCALHOST
|
|
1117
|
-
},
|
|
1118
|
-
logging: {
|
|
1119
|
-
level: "info"
|
|
1120
|
-
},
|
|
1121
|
-
providers: {
|
|
1122
|
-
qwen: {
|
|
1123
|
-
type: "qwen-provider",
|
|
1124
|
-
baseUrl: "https://chat.qwen.ai",
|
|
1125
|
-
oauth: {
|
|
1126
|
-
clientId: "f0304373b74a44d2b584a3fb70ca9e56",
|
|
1127
|
-
deviceCodeUrl: "https://chat.qwen.ai/api/v1/oauth2/device/code",
|
|
1128
|
-
tokenUrl: "https://chat.qwen.ai/api/v1/oauth2/token",
|
|
1129
|
-
scopes: ["openid", "profile", "email", "model.completion"]
|
|
1130
|
-
},
|
|
1131
|
-
models: {
|
|
1132
|
-
"qwen3-coder-plus": {
|
|
1133
|
-
maxTokens: 32768,
|
|
1134
|
-
temperature: 0.7,
|
|
1135
|
-
supportsStreaming: true,
|
|
1136
|
-
supportsTools: true
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
},
|
|
1141
|
-
routing: {
|
|
1142
|
-
default: "qwen",
|
|
1143
|
-
models: {
|
|
1144
|
-
"gpt-4": "qwen3-coder-plus",
|
|
1145
|
-
"gpt-3.5-turbo": "qwen3-coder-plus"
|
|
1146
|
-
}
|
|
1147
|
-
},
|
|
1148
|
-
features: {
|
|
1149
|
-
tools: {
|
|
1150
|
-
enabled: true,
|
|
1151
|
-
maxTools: 10
|
|
1152
|
-
},
|
|
1153
|
-
streaming: {
|
|
1154
|
-
enabled: true,
|
|
1155
|
-
chunkSize: 1024
|
|
1156
|
-
},
|
|
1157
|
-
oauth: {
|
|
1158
|
-
enabled: true,
|
|
1159
|
-
autoRefresh: true,
|
|
1160
|
-
sharedCredentials: true
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
};
|
|
1164
|
-
break;
|
|
1165
|
-
default:
|
|
1166
|
-
templateConfig = {
|
|
1167
|
-
server: {
|
|
1168
|
-
port: DEFAULT_CONFIG.PORT,
|
|
1169
|
-
host: LOCAL_HOSTS.LOCALHOST
|
|
1170
|
-
},
|
|
1171
|
-
logging: {
|
|
1172
|
-
level: "info"
|
|
1173
|
-
},
|
|
1174
|
-
providers: {
|
|
1175
|
-
openai: {
|
|
1176
|
-
type: "openai",
|
|
1177
|
-
apiKey: "${OPENAI_API_KEY}",
|
|
1178
|
-
baseUrl: API_ENDPOINTS.OPENAI,
|
|
1179
|
-
models: {
|
|
1180
|
-
"gpt-4": {
|
|
1181
|
-
maxTokens: 8192,
|
|
1182
|
-
temperature: 0.7
|
|
1183
|
-
},
|
|
1184
|
-
"gpt-3.5-turbo": {
|
|
1185
|
-
maxTokens: 4096,
|
|
1186
|
-
temperature: 0.7
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
},
|
|
1191
|
-
routing: {
|
|
1192
|
-
default: "openai"
|
|
1193
|
-
},
|
|
1194
|
-
features: {
|
|
1195
|
-
tools: {
|
|
1196
|
-
enabled: true,
|
|
1197
|
-
maxTools: 10
|
|
1198
|
-
},
|
|
1199
|
-
streaming: {
|
|
1200
|
-
enabled: true,
|
|
1201
|
-
chunkSize: 1024
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
};
|
|
1205
|
-
}
|
|
1206
|
-
// Write configuration file
|
|
1207
|
-
fs.writeFileSync(configPath, JSON.stringify(templateConfig, null, 2));
|
|
1208
|
-
spinner.succeed(`Configuration initialized: ${configPath}`);
|
|
1209
|
-
logger.info(`Template used: ${template || 'default'}`);
|
|
1210
|
-
logger.info('You can now start the server with: rcc start');
|
|
1211
|
-
}
|
|
1212
|
-
catch (error) {
|
|
1213
|
-
spinner.fail('Failed to initialize configuration');
|
|
1214
|
-
logger.error(`Initialization failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
281
|
// Stop command
|
|
1218
|
-
program
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
resolvedPort = envPort;
|
|
1230
|
-
}
|
|
1231
|
-
else {
|
|
1232
|
-
resolvedPort = DEFAULT_DEV_PORT;
|
|
1233
|
-
logger.info(`Using dev default port ${resolvedPort} (routecodex dev package)`);
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
else {
|
|
1237
|
-
// Resolve config path and port
|
|
1238
|
-
const configPath = path.join(homedir(), '.routecodex', 'config.json');
|
|
1239
|
-
// Check if config exists
|
|
1240
|
-
if (!fs.existsSync(configPath)) {
|
|
1241
|
-
spinner.fail(`Configuration file not found: ${configPath}`);
|
|
1242
|
-
logger.error('Cannot determine server port without configuration file');
|
|
1243
|
-
logger.info('Please create a configuration file first:');
|
|
1244
|
-
logger.info(' rcc config init');
|
|
1245
|
-
process.exit(1);
|
|
1246
|
-
}
|
|
1247
|
-
// Load configuration to get port
|
|
1248
|
-
let config;
|
|
1249
|
-
try {
|
|
1250
|
-
const configContent = fs.readFileSync(configPath, 'utf8');
|
|
1251
|
-
config = JSON.parse(configContent);
|
|
1252
|
-
}
|
|
1253
|
-
catch (error) {
|
|
1254
|
-
spinner.fail('Failed to parse configuration file');
|
|
1255
|
-
logger.error(`Invalid JSON in configuration file: ${configPath}`);
|
|
1256
|
-
process.exit(1);
|
|
1257
|
-
}
|
|
1258
|
-
const port = (config?.httpserver?.port ?? config?.server?.port ?? config?.port);
|
|
1259
|
-
if (!port || typeof port !== 'number' || port <= 0) {
|
|
1260
|
-
spinner.fail('Invalid or missing port configuration');
|
|
1261
|
-
logger.error('Configuration file must specify a valid port number');
|
|
1262
|
-
process.exit(1);
|
|
1263
|
-
}
|
|
1264
|
-
resolvedPort = port;
|
|
1265
|
-
}
|
|
1266
|
-
const pids = findListeningPids(resolvedPort);
|
|
1267
|
-
if (!pids.length) {
|
|
1268
|
-
spinner.succeed(`No server listening on ${resolvedPort}.`);
|
|
1269
|
-
if (IS_DEV_PACKAGE) {
|
|
1270
|
-
await stopTokenDaemonIfRunning();
|
|
1271
|
-
}
|
|
1272
|
-
return;
|
|
1273
|
-
}
|
|
1274
|
-
for (const pid of pids) {
|
|
1275
|
-
killPidBestEffort(pid, { force: false });
|
|
1276
|
-
}
|
|
1277
|
-
const deadline = Date.now() + 3000;
|
|
1278
|
-
while (Date.now() < deadline) {
|
|
1279
|
-
if (findListeningPids(resolvedPort).length === 0) {
|
|
1280
|
-
spinner.succeed(`Stopped server on ${resolvedPort}.`);
|
|
1281
|
-
if (IS_DEV_PACKAGE) {
|
|
1282
|
-
await stopTokenDaemonIfRunning();
|
|
1283
|
-
}
|
|
1284
|
-
return;
|
|
1285
|
-
}
|
|
1286
|
-
await sleep(100);
|
|
1287
|
-
}
|
|
1288
|
-
const remain = findListeningPids(resolvedPort);
|
|
1289
|
-
if (remain.length) {
|
|
1290
|
-
for (const pid of remain) {
|
|
1291
|
-
killPidBestEffort(pid, { force: true });
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
spinner.succeed(`Force stopped server on ${resolvedPort}.`);
|
|
1295
|
-
if (IS_DEV_PACKAGE) {
|
|
1296
|
-
await stopTokenDaemonIfRunning();
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
catch (e) {
|
|
1300
|
-
spinner.fail(`Failed to stop: ${e.message}`);
|
|
1301
|
-
process.exit(1);
|
|
1302
|
-
}
|
|
282
|
+
registerStopCommand(program, {
|
|
283
|
+
isDevPackage: IS_DEV_PACKAGE,
|
|
284
|
+
defaultDevPort: DEFAULT_DEV_PORT,
|
|
285
|
+
createSpinner,
|
|
286
|
+
logger,
|
|
287
|
+
findListeningPids,
|
|
288
|
+
killPidBestEffort,
|
|
289
|
+
sleep,
|
|
290
|
+
stopTokenDaemonIfRunning,
|
|
291
|
+
env: process.env,
|
|
292
|
+
exit: (code) => process.exit(code)
|
|
1303
293
|
});
|
|
1304
294
|
// Restart command (stop + start with same environment)
|
|
1305
|
-
program
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
resolvedPort = envPort;
|
|
1322
|
-
}
|
|
1323
|
-
else {
|
|
1324
|
-
resolvedPort = DEFAULT_DEV_PORT;
|
|
1325
|
-
logger.info(`Using dev default port ${resolvedPort} (routecodex dev package)`);
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
else {
|
|
1329
|
-
// Resolve config path
|
|
1330
|
-
const configPath = options.config || path.join(homedir(), '.routecodex', 'config.json');
|
|
1331
|
-
// Check if config exists
|
|
1332
|
-
if (!fs.existsSync(configPath)) {
|
|
1333
|
-
spinner.fail(`Configuration file not found: ${configPath}`);
|
|
1334
|
-
logger.error('Cannot determine server port without configuration file');
|
|
1335
|
-
logger.info('Please create a configuration file first:');
|
|
1336
|
-
logger.info(' rcc config init');
|
|
1337
|
-
process.exit(1);
|
|
1338
|
-
}
|
|
1339
|
-
// Load configuration to get port
|
|
1340
|
-
let config;
|
|
1341
|
-
try {
|
|
1342
|
-
const configContent = fs.readFileSync(configPath, 'utf8');
|
|
1343
|
-
config = JSON.parse(configContent);
|
|
1344
|
-
}
|
|
1345
|
-
catch (error) {
|
|
1346
|
-
spinner.fail('Failed to parse configuration file');
|
|
1347
|
-
logger.error(`Invalid JSON in configuration file: ${configPath}`);
|
|
1348
|
-
process.exit(1);
|
|
1349
|
-
}
|
|
1350
|
-
const port = (config?.httpserver?.port ?? config?.server?.port ?? config?.port);
|
|
1351
|
-
if (!port || typeof port !== 'number' || port <= 0) {
|
|
1352
|
-
spinner.fail('Invalid or missing port configuration');
|
|
1353
|
-
logger.error('Configuration file must specify a valid port number');
|
|
1354
|
-
process.exit(1);
|
|
1355
|
-
}
|
|
1356
|
-
resolvedPort = port;
|
|
1357
|
-
resolvedHost =
|
|
1358
|
-
(config?.httpserver?.host || config?.server?.host || config?.host || LOCAL_HOSTS.LOCALHOST);
|
|
1359
|
-
}
|
|
1360
|
-
// Stop current instance (if any)
|
|
1361
|
-
const pids = findListeningPids(resolvedPort);
|
|
1362
|
-
if (pids.length) {
|
|
1363
|
-
for (const pid of pids) {
|
|
1364
|
-
killPidBestEffort(pid, { force: false });
|
|
1365
|
-
}
|
|
1366
|
-
const deadline = Date.now() + 3500;
|
|
1367
|
-
while (Date.now() < deadline) {
|
|
1368
|
-
if (findListeningPids(resolvedPort).length === 0) {
|
|
1369
|
-
break;
|
|
1370
|
-
}
|
|
1371
|
-
await sleep(120);
|
|
1372
|
-
}
|
|
1373
|
-
const remain = findListeningPids(resolvedPort);
|
|
1374
|
-
for (const pid of remain) {
|
|
1375
|
-
killPidBestEffort(pid, { force: true });
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
spinner.text = 'Starting RouteCodex server...';
|
|
1379
|
-
// Delegate to start command behavior with --restart semantics
|
|
1380
|
-
const nodeBin = process.execPath;
|
|
1381
|
-
const serverEntry = path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'index.js');
|
|
1382
|
-
const { spawn } = await import('child_process');
|
|
1383
|
-
// Prompt source flags
|
|
1384
|
-
if (options.codex && options.claude) {
|
|
1385
|
-
spinner.fail('Flags --codex and --claude are mutually exclusive');
|
|
1386
|
-
process.exit(1);
|
|
1387
|
-
}
|
|
1388
|
-
const restartPrompt = options.codex ? 'codex' : (options.claude ? 'claude' : null);
|
|
1389
|
-
if (restartPrompt) {
|
|
1390
|
-
process.env.ROUTECODEX_SYSTEM_PROMPT_SOURCE = restartPrompt;
|
|
1391
|
-
process.env.ROUTECODEX_SYSTEM_PROMPT_ENABLE = '1';
|
|
1392
|
-
}
|
|
1393
|
-
const modulesConfigPath = getModulesConfigPath();
|
|
1394
|
-
const env = { ...process.env };
|
|
1395
|
-
const args = [serverEntry, modulesConfigPath];
|
|
1396
|
-
const child = spawn(nodeBin, args, { stdio: 'inherit', env });
|
|
1397
|
-
try {
|
|
1398
|
-
fs.writeFileSync(path.join(homedir(), '.routecodex', 'server.cli.pid'), String(child.pid ?? ''), 'utf8');
|
|
1399
|
-
}
|
|
1400
|
-
catch (error) { /* ignore */ }
|
|
1401
|
-
spinner.succeed(`RouteCodex server restarting on ${resolvedHost}:${resolvedPort}`);
|
|
1402
|
-
logger.info(`Server will run on port: ${resolvedPort}`);
|
|
1403
|
-
logger.info('Press Ctrl+C to stop the server');
|
|
1404
|
-
const shutdown = async (sig) => {
|
|
1405
|
-
try {
|
|
1406
|
-
await fetch(`${HTTP_PROTOCOLS.HTTP}${LOCAL_HOSTS.IPV4}:${resolvedPort}${API_PATHS.SHUTDOWN}`, { method: 'POST' }).catch(() => { });
|
|
1407
|
-
}
|
|
1408
|
-
catch (error) { /* ignore */ }
|
|
1409
|
-
try {
|
|
1410
|
-
child.kill(sig);
|
|
1411
|
-
}
|
|
1412
|
-
catch (error) { /* ignore */ }
|
|
1413
|
-
if (!IS_WINDOWS) {
|
|
1414
|
-
try {
|
|
1415
|
-
if (child.pid) {
|
|
1416
|
-
process.kill(-child.pid, sig);
|
|
1417
|
-
}
|
|
1418
|
-
}
|
|
1419
|
-
catch (error) { /* ignore */ }
|
|
1420
|
-
}
|
|
1421
|
-
const deadline = Date.now() + 3500;
|
|
1422
|
-
while (Date.now() < deadline) {
|
|
1423
|
-
if (findListeningPids(resolvedPort).length === 0) {
|
|
1424
|
-
break;
|
|
1425
|
-
}
|
|
1426
|
-
await sleep(120);
|
|
1427
|
-
}
|
|
1428
|
-
const remain = findListeningPids(resolvedPort);
|
|
1429
|
-
for (const pid of remain) {
|
|
1430
|
-
killPidBestEffort(pid, { force: false });
|
|
1431
|
-
}
|
|
1432
|
-
const killDeadline = Date.now() + 1500;
|
|
1433
|
-
while (Date.now() < killDeadline) {
|
|
1434
|
-
if (findListeningPids(resolvedPort).length === 0) {
|
|
1435
|
-
break;
|
|
1436
|
-
}
|
|
1437
|
-
await sleep(100);
|
|
1438
|
-
}
|
|
1439
|
-
const still = findListeningPids(resolvedPort);
|
|
1440
|
-
for (const pid of still) {
|
|
1441
|
-
killPidBestEffort(pid, { force: true });
|
|
1442
|
-
}
|
|
1443
|
-
// Ensure parent exits in any case
|
|
1444
|
-
try {
|
|
1445
|
-
process.exit(0);
|
|
1446
|
-
}
|
|
1447
|
-
catch (error) { /* ignore */ }
|
|
1448
|
-
};
|
|
1449
|
-
process.on('SIGINT', () => { void shutdown('SIGINT'); });
|
|
1450
|
-
process.on('SIGTERM', () => { void shutdown('SIGTERM'); });
|
|
1451
|
-
// Fallback keypress handler for restart mode as well
|
|
1452
|
-
const cleanupKeypress2 = setupKeypress(() => { void shutdown('SIGINT'); });
|
|
1453
|
-
child.on('exit', (code, signal) => {
|
|
1454
|
-
try {
|
|
1455
|
-
cleanupKeypress2();
|
|
1456
|
-
}
|
|
1457
|
-
catch { /* ignore */ }
|
|
1458
|
-
if (signal) {
|
|
1459
|
-
process.exit(0);
|
|
1460
|
-
}
|
|
1461
|
-
else {
|
|
1462
|
-
process.exit(code ?? 0);
|
|
1463
|
-
}
|
|
1464
|
-
});
|
|
1465
|
-
await new Promise(() => {
|
|
1466
|
-
// Keep CLI alive (never resolves)
|
|
1467
|
-
return;
|
|
1468
|
-
});
|
|
1469
|
-
}
|
|
1470
|
-
catch (e) {
|
|
1471
|
-
spinner.fail(`Failed to restart: ${e.message}`);
|
|
1472
|
-
process.exit(1);
|
|
1473
|
-
}
|
|
1474
|
-
});
|
|
1475
|
-
// Status command
|
|
1476
|
-
program
|
|
1477
|
-
.command('status')
|
|
1478
|
-
.description('Show server status')
|
|
1479
|
-
.option('-j, --json', 'Output in JSON format')
|
|
1480
|
-
.action(async (options) => {
|
|
1481
|
-
try {
|
|
1482
|
-
// Resolve config path and get configuration
|
|
1483
|
-
const configPath = path.join(homedir(), '.routecodex', 'config.json');
|
|
1484
|
-
// Check if config exists
|
|
1485
|
-
if (!fs.existsSync(configPath)) {
|
|
1486
|
-
logger.error('Configuration file not found');
|
|
1487
|
-
logger.info('Please create a configuration file first:');
|
|
1488
|
-
logger.info(' rcc config init');
|
|
1489
|
-
if (options.json) {
|
|
1490
|
-
console.log(JSON.stringify({ error: 'Configuration file not found' }, null, 2));
|
|
1491
|
-
}
|
|
1492
|
-
return;
|
|
1493
|
-
}
|
|
1494
|
-
let port;
|
|
1495
|
-
let host;
|
|
1496
|
-
// Load configuration to get port and host
|
|
1497
|
-
try {
|
|
1498
|
-
const configContent = fs.readFileSync(configPath, 'utf8');
|
|
1499
|
-
const config = JSON.parse(configContent);
|
|
1500
|
-
port = config?.port || config?.server?.port;
|
|
1501
|
-
host = config?.server?.host || config?.host || LOCAL_HOSTS.LOCALHOST;
|
|
1502
|
-
if (!port || typeof port !== 'number' || port <= 0) {
|
|
1503
|
-
const errorMsg = 'Invalid or missing port configuration in configuration file';
|
|
1504
|
-
logger.error(errorMsg);
|
|
1505
|
-
if (options.json) {
|
|
1506
|
-
console.log(JSON.stringify({ error: errorMsg }, null, 2));
|
|
1507
|
-
}
|
|
1508
|
-
return;
|
|
1509
|
-
}
|
|
1510
|
-
}
|
|
1511
|
-
catch (error) {
|
|
1512
|
-
const errorMsg = `Failed to parse configuration file: ${configPath}`;
|
|
1513
|
-
logger.error(errorMsg);
|
|
1514
|
-
if (options.json) {
|
|
1515
|
-
console.log(JSON.stringify({ error: errorMsg }, null, 2));
|
|
1516
|
-
}
|
|
1517
|
-
return;
|
|
1518
|
-
}
|
|
1519
|
-
// Check if server is running by trying to connect (HTTP)
|
|
1520
|
-
const { get } = await import('http');
|
|
1521
|
-
const checkServer = (port, host) => {
|
|
1522
|
-
return new Promise((resolve) => {
|
|
1523
|
-
const req = get({
|
|
1524
|
-
hostname: host,
|
|
1525
|
-
port: port,
|
|
1526
|
-
path: '/health',
|
|
1527
|
-
method: 'GET',
|
|
1528
|
-
timeout: 5000
|
|
1529
|
-
}, (res) => {
|
|
1530
|
-
let data = '';
|
|
1531
|
-
res.on('data', chunk => data += chunk);
|
|
1532
|
-
res.on('end', () => {
|
|
1533
|
-
try {
|
|
1534
|
-
const health = JSON.parse(data);
|
|
1535
|
-
// Ensure required fields in case health payload differs
|
|
1536
|
-
resolve({
|
|
1537
|
-
status: health?.status || 'unknown',
|
|
1538
|
-
port,
|
|
1539
|
-
host
|
|
1540
|
-
});
|
|
1541
|
-
}
|
|
1542
|
-
catch {
|
|
1543
|
-
resolve({ status: 'unknown', port, host });
|
|
1544
|
-
}
|
|
1545
|
-
});
|
|
1546
|
-
});
|
|
1547
|
-
req.on('error', () => {
|
|
1548
|
-
resolve({ status: 'stopped', port, host });
|
|
1549
|
-
});
|
|
1550
|
-
req.on('timeout', () => {
|
|
1551
|
-
req.destroy();
|
|
1552
|
-
resolve({ status: 'timeout', port, host });
|
|
1553
|
-
});
|
|
1554
|
-
req.end();
|
|
1555
|
-
});
|
|
1556
|
-
};
|
|
1557
|
-
const status = await checkServer(port, host);
|
|
1558
|
-
if (options.json) {
|
|
1559
|
-
console.log(JSON.stringify(status, null, 2));
|
|
1560
|
-
}
|
|
1561
|
-
else {
|
|
1562
|
-
switch (status.status) {
|
|
1563
|
-
case 'running':
|
|
1564
|
-
logger.success(`Server is running on ${host}:${port}`);
|
|
1565
|
-
break;
|
|
1566
|
-
case 'stopped':
|
|
1567
|
-
logger.error('Server is not running');
|
|
1568
|
-
break;
|
|
1569
|
-
case 'error':
|
|
1570
|
-
logger.error('Server is in error state');
|
|
1571
|
-
break;
|
|
1572
|
-
default:
|
|
1573
|
-
logger.warning('Server status unknown');
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
|
-
}
|
|
1577
|
-
catch (error) {
|
|
1578
|
-
logger.error(`Status check failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1579
|
-
}
|
|
1580
|
-
});
|
|
1581
|
-
// Clean command: purge local capture and debug data for fresh runs
|
|
1582
|
-
program
|
|
1583
|
-
.command('clean')
|
|
1584
|
-
.description('Clean captured data and debug logs')
|
|
1585
|
-
.option('-y, --yes', 'Confirm deletion without prompt')
|
|
1586
|
-
.option('--what <targets>', 'Targets to clean: captures,logs,all', 'all')
|
|
1587
|
-
.action(async (options) => {
|
|
1588
|
-
const confirm = Boolean(options.yes);
|
|
1589
|
-
const what = String(options.what || 'all');
|
|
1590
|
-
if (!confirm) {
|
|
1591
|
-
logger.warning("Add --yes to confirm deletion.");
|
|
1592
|
-
logger.info("Example: rcc clean --yes --what all");
|
|
295
|
+
registerRestartCommand(program, {
|
|
296
|
+
isDevPackage: IS_DEV_PACKAGE,
|
|
297
|
+
isWindows: IS_WINDOWS,
|
|
298
|
+
defaultDevPort: DEFAULT_DEV_PORT,
|
|
299
|
+
createSpinner,
|
|
300
|
+
logger,
|
|
301
|
+
findListeningPids,
|
|
302
|
+
killPidBestEffort,
|
|
303
|
+
sleep,
|
|
304
|
+
getModulesConfigPath,
|
|
305
|
+
resolveServerEntryPath: () => path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'index.js'),
|
|
306
|
+
nodeBin: process.execPath,
|
|
307
|
+
spawn: (cmd, args, opts) => spawn(cmd, args, opts),
|
|
308
|
+
fetch,
|
|
309
|
+
setupKeypress,
|
|
310
|
+
waitForever: () => new Promise(() => {
|
|
1593
311
|
return;
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
}
|
|
1600
|
-
if (what === 'logs' || what === 'all') {
|
|
1601
|
-
targets.push({ path: path.join(process.cwd(), 'debug-logs'), label: 'debug-logs' });
|
|
1602
|
-
targets.push({ path: path.join(home, '.routecodex', 'logs'), label: 'user-logs' });
|
|
1603
|
-
}
|
|
1604
|
-
let removedAny = false;
|
|
1605
|
-
for (const t of targets) {
|
|
1606
|
-
try {
|
|
1607
|
-
if (fs.existsSync(t.path)) {
|
|
1608
|
-
const entries = fs.readdirSync(t.path);
|
|
1609
|
-
for (const name of entries) {
|
|
1610
|
-
const p = path.join(t.path, name);
|
|
1611
|
-
try {
|
|
1612
|
-
// Recursively remove files/folders
|
|
1613
|
-
fs.rmSync(p, { recursive: true, force: true });
|
|
1614
|
-
removedAny = true;
|
|
1615
|
-
}
|
|
1616
|
-
catch (e) {
|
|
1617
|
-
logger.warning(`Failed to remove ${p}: ${e.message}`);
|
|
1618
|
-
}
|
|
1619
|
-
}
|
|
1620
|
-
logger.success(`Cleared ${t.label} at ${t.path}`);
|
|
1621
|
-
}
|
|
1622
|
-
}
|
|
1623
|
-
catch (e) {
|
|
1624
|
-
logger.warning(`Unable to access ${t.label} at ${t.path}: ${e.message}`);
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
if (!removedAny) {
|
|
1628
|
-
logger.info('Nothing to clean.');
|
|
312
|
+
}),
|
|
313
|
+
env: process.env,
|
|
314
|
+
exit: (code) => process.exit(code),
|
|
315
|
+
onSignal: (sig, cb) => {
|
|
316
|
+
process.on(sig, cb);
|
|
1629
317
|
}
|
|
1630
318
|
});
|
|
1631
319
|
// Import commands at top level
|
|
@@ -1635,215 +323,30 @@ program
|
|
|
1635
323
|
// dry-run commands removed
|
|
1636
324
|
// offline-log command disabled
|
|
1637
325
|
// simple-log command removed
|
|
1638
|
-
// Examples command
|
|
1639
|
-
program
|
|
1640
|
-
.command('examples')
|
|
1641
|
-
.description('Show usage examples')
|
|
1642
|
-
.action(() => {
|
|
1643
|
-
console.log(chalk.cyan('RouteCodex Usage Examples'));
|
|
1644
|
-
console.log('='.repeat(40));
|
|
1645
|
-
console.log('');
|
|
1646
|
-
console.log(chalk.yellow('1. Initialize Configuration:'));
|
|
1647
|
-
console.log(' # Create default configuration');
|
|
1648
|
-
console.log(' rcc config init');
|
|
1649
|
-
console.log('');
|
|
1650
|
-
console.log(' # Create LMStudio configuration');
|
|
1651
|
-
console.log(' rcc config init --template lmstudio');
|
|
1652
|
-
console.log('');
|
|
1653
|
-
console.log(' # Create OAuth configuration');
|
|
1654
|
-
console.log(' rcc config init --template oauth');
|
|
1655
|
-
console.log('');
|
|
1656
|
-
console.log(chalk.yellow('2. Start Server:'));
|
|
1657
|
-
console.log(' # Start with default config');
|
|
1658
|
-
console.log(' rcc start');
|
|
1659
|
-
console.log('');
|
|
1660
|
-
console.log(' # Start with custom config');
|
|
1661
|
-
console.log(' rcc start --config ./config/lmstudio-config.json');
|
|
1662
|
-
console.log('');
|
|
1663
|
-
console.log(' # Note: Port must be specified in configuration file');
|
|
1664
|
-
console.log(' # Server will not start without valid port configuration');
|
|
1665
|
-
console.log('');
|
|
1666
|
-
console.log(chalk.yellow('3. Launch Claude Code:'));
|
|
1667
|
-
console.log(' # Launch Claude Code with automatic server start');
|
|
1668
|
-
console.log(' rcc code --ensure-server');
|
|
1669
|
-
console.log('');
|
|
1670
|
-
console.log(' # Launch Claude Code with specific model');
|
|
1671
|
-
console.log(' rcc code --model claude-3-haiku');
|
|
1672
|
-
console.log('');
|
|
1673
|
-
console.log(' # Launch Claude Code with custom profile');
|
|
1674
|
-
console.log(' rcc code --profile my-profile');
|
|
1675
|
-
console.log('');
|
|
1676
|
-
console.log(chalk.yellow('4. Configuration Management:'));
|
|
1677
|
-
console.log(' # Show current configuration');
|
|
1678
|
-
console.log(' rcc config show');
|
|
1679
|
-
console.log('');
|
|
1680
|
-
console.log(' # Edit configuration');
|
|
1681
|
-
console.log(' rcc config edit');
|
|
1682
|
-
console.log('');
|
|
1683
|
-
console.log(' # Validate configuration');
|
|
1684
|
-
console.log(' rcc config validate');
|
|
1685
|
-
console.log('');
|
|
1686
|
-
// Dry-Run examples removed
|
|
1687
|
-
console.log(chalk.yellow('6. Environment Variables:'));
|
|
1688
|
-
console.log(' # Set LM Studio API Key');
|
|
1689
|
-
console.log(' export LM_STUDIO_API_KEY="your-api-key"');
|
|
1690
|
-
console.log('');
|
|
1691
|
-
console.log(' # Set OpenAI API Key');
|
|
1692
|
-
console.log(' export OPENAI_API_KEY="your-api-key"');
|
|
1693
|
-
console.log('');
|
|
1694
|
-
console.log(chalk.yellow('7. Testing:'));
|
|
1695
|
-
console.log(' # Test with curl');
|
|
1696
|
-
console.log(' curl -X POST http://localhost:5506/v1/chat/completions \\');
|
|
1697
|
-
console.log(' -H "Content-Type: application/json" \\');
|
|
1698
|
-
console.log(' -H "Authorization: Bearer test-key" \\');
|
|
1699
|
-
console.log(' -d \'{"model": "gpt-4", "messages": [{"role": "user", "content": "Hello!"}]}\'');
|
|
1700
|
-
console.log('');
|
|
1701
|
-
});
|
|
1702
326
|
async function ensurePortAvailable(port, parentSpinner, opts = {}) {
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
clearTimeout(t);
|
|
1718
|
-
}
|
|
1719
|
-
catch (error) { /* ignore */ }
|
|
1720
|
-
}
|
|
1721
|
-
await sleep(300);
|
|
1722
|
-
}
|
|
1723
|
-
catch { /* ignore */ }
|
|
1724
|
-
const initialPids = findListeningPids(port);
|
|
1725
|
-
if (initialPids.length === 0) {
|
|
1726
|
-
return;
|
|
1727
|
-
}
|
|
1728
|
-
// If a healthy server is already running and no restart requested, report and exit gracefully
|
|
1729
|
-
const healthy = await isServerHealthyQuick(port);
|
|
1730
|
-
if (healthy && !opts.restart) {
|
|
1731
|
-
parentSpinner.stop();
|
|
1732
|
-
logger.success(`RouteCodex is already running on port ${port}.`);
|
|
1733
|
-
logger.info(`Use 'rcc stop' or 'rcc start --restart' to restart.`);
|
|
1734
|
-
process.exit(0);
|
|
1735
|
-
}
|
|
1736
|
-
parentSpinner.stop();
|
|
1737
|
-
logger.warning(`Port ${port} is in use by PID(s): ${initialPids.join(', ')}`);
|
|
1738
|
-
const stopSpinner = await createSpinner(`Port ${port} is in use on 0.0.0.0. Attempting graceful stop...`);
|
|
1739
|
-
const gracefulTimeout = Number(process.env.ROUTECODEX_STOP_TIMEOUT_MS ?? 5000);
|
|
1740
|
-
const killTimeout = Number(process.env.ROUTECODEX_KILL_TIMEOUT_MS ?? 3000);
|
|
1741
|
-
const pollInterval = 150;
|
|
1742
|
-
for (const pid of initialPids) {
|
|
1743
|
-
try {
|
|
1744
|
-
killPidBestEffort(pid, { force: false });
|
|
1745
|
-
}
|
|
1746
|
-
catch (error) {
|
|
1747
|
-
stopSpinner.warn(`Failed to send SIGTERM to PID ${pid}: ${error.message}`);
|
|
1748
|
-
}
|
|
1749
|
-
}
|
|
1750
|
-
const gracefulDeadline = Date.now() + gracefulTimeout;
|
|
1751
|
-
while (Date.now() < gracefulDeadline) {
|
|
1752
|
-
if (findListeningPids(port).length === 0) {
|
|
1753
|
-
stopSpinner.succeed(`Port ${port} freed after graceful stop.`);
|
|
1754
|
-
logger.success(`Port ${port} freed after graceful stop.`);
|
|
1755
|
-
parentSpinner.start('Starting RouteCodex server...');
|
|
1756
|
-
return;
|
|
1757
|
-
}
|
|
1758
|
-
await sleep(pollInterval);
|
|
1759
|
-
}
|
|
1760
|
-
let remaining = findListeningPids(port);
|
|
1761
|
-
if (remaining.length) {
|
|
1762
|
-
stopSpinner.warn(`Graceful stop timed out, sending SIGKILL to PID(s): ${remaining.join(', ')}`);
|
|
1763
|
-
logger.warning(`Graceful stop timed out. Forcing SIGKILL to PID(s): ${remaining.join(', ')}`);
|
|
1764
|
-
for (const pid of remaining) {
|
|
1765
|
-
try {
|
|
1766
|
-
killPidBestEffort(pid, { force: true });
|
|
1767
|
-
}
|
|
1768
|
-
catch (error) {
|
|
1769
|
-
const message = error.message;
|
|
1770
|
-
stopSpinner.warn(`Failed to send SIGKILL to PID ${pid}: ${message}`);
|
|
1771
|
-
logger.error(`Failed to SIGKILL PID ${pid}: ${message}`);
|
|
1772
|
-
}
|
|
1773
|
-
}
|
|
1774
|
-
const killDeadline = Date.now() + killTimeout;
|
|
1775
|
-
while (Date.now() < killDeadline) {
|
|
1776
|
-
if (findListeningPids(port).length === 0) {
|
|
1777
|
-
stopSpinner.succeed(`Port ${port} freed after SIGKILL.`);
|
|
1778
|
-
logger.success(`Port ${port} freed after SIGKILL.`);
|
|
1779
|
-
parentSpinner.start('Starting RouteCodex server...');
|
|
1780
|
-
return;
|
|
1781
|
-
}
|
|
1782
|
-
await sleep(pollInterval);
|
|
1783
|
-
}
|
|
1784
|
-
}
|
|
1785
|
-
remaining = findListeningPids(port);
|
|
1786
|
-
if (remaining.length) {
|
|
1787
|
-
stopSpinner.fail(`Failed to free port ${port}. Still held by PID(s): ${remaining.join(', ')}`);
|
|
1788
|
-
logger.error(`Failed to free port ${port}. Still held by PID(s): ${remaining.join(', ')}`);
|
|
1789
|
-
throw new Error(`Failed to free port ${port}`);
|
|
1790
|
-
}
|
|
1791
|
-
stopSpinner.succeed(`Port ${port} freed.`);
|
|
1792
|
-
logger.success(`Port ${port} freed.`);
|
|
1793
|
-
parentSpinner.start('Starting RouteCodex server...');
|
|
327
|
+
return ensurePortAvailableImpl({
|
|
328
|
+
port,
|
|
329
|
+
parentSpinner,
|
|
330
|
+
opts,
|
|
331
|
+
fetchImpl: fetch,
|
|
332
|
+
sleep,
|
|
333
|
+
env: process.env,
|
|
334
|
+
logger,
|
|
335
|
+
createSpinner,
|
|
336
|
+
findListeningPids,
|
|
337
|
+
killPidBestEffort,
|
|
338
|
+
isServerHealthyQuick,
|
|
339
|
+
exit: (code) => process.exit(code)
|
|
340
|
+
});
|
|
1794
341
|
}
|
|
1795
342
|
function findListeningPids(port) {
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
return parseNetstatListeningPids(result.stdout || '', port);
|
|
1804
|
-
}
|
|
1805
|
-
// macOS/BSD lsof expects either "-i TCP:port" or "-tiTCP:port" as a single argument.
|
|
1806
|
-
// Use the compact form to avoid treating ":port" as a filename.
|
|
1807
|
-
const result = spawnSync('lsof', [`-tiTCP:${port}`, '-sTCP:LISTEN'], { encoding: 'utf8' });
|
|
1808
|
-
if (result.error) {
|
|
1809
|
-
logger.warning(`lsof not available to inspect port usage: ${result.error.message}`);
|
|
1810
|
-
return [];
|
|
1811
|
-
}
|
|
1812
|
-
const stdout = (result.stdout || '').trim();
|
|
1813
|
-
if (!stdout) {
|
|
1814
|
-
return [];
|
|
1815
|
-
}
|
|
1816
|
-
return stdout
|
|
1817
|
-
.split(/\s+/)
|
|
1818
|
-
.map((value) => parseInt(value, 10))
|
|
1819
|
-
.filter((pid) => !Number.isNaN(pid));
|
|
1820
|
-
}
|
|
1821
|
-
catch (error) {
|
|
1822
|
-
logger.warning(`Failed to inspect port ${port}: ${error.message}`);
|
|
1823
|
-
return [];
|
|
1824
|
-
}
|
|
1825
|
-
}
|
|
1826
|
-
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
1827
|
-
function normalizePort(value) {
|
|
1828
|
-
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
1829
|
-
return value;
|
|
1830
|
-
}
|
|
1831
|
-
if (typeof value === 'string' && value.trim()) {
|
|
1832
|
-
const parsed = Number(value);
|
|
1833
|
-
if (Number.isFinite(parsed)) {
|
|
1834
|
-
return parsed;
|
|
1835
|
-
}
|
|
1836
|
-
}
|
|
1837
|
-
return NaN;
|
|
1838
|
-
}
|
|
1839
|
-
function safeReadJson(filePath) {
|
|
1840
|
-
try {
|
|
1841
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
1842
|
-
return JSON.parse(content);
|
|
1843
|
-
}
|
|
1844
|
-
catch {
|
|
1845
|
-
return null;
|
|
1846
|
-
}
|
|
343
|
+
return findListeningPidsImpl({
|
|
344
|
+
port,
|
|
345
|
+
isWindows: IS_WINDOWS,
|
|
346
|
+
spawnSyncImpl: spawnSync,
|
|
347
|
+
logger,
|
|
348
|
+
parseNetstatListeningPids
|
|
349
|
+
});
|
|
1847
350
|
}
|
|
1848
351
|
// Fallback keypress setup: capture Ctrl+C and 'q' to trigger shutdown when SIGINT is not delivered
|
|
1849
352
|
function setupKeypress(onInterrupt) {
|
|
@@ -1897,103 +400,11 @@ function setupKeypress(onInterrupt) {
|
|
|
1897
400
|
};
|
|
1898
401
|
}
|
|
1899
402
|
async function isServerHealthyQuick(port) {
|
|
1900
|
-
|
|
1901
|
-
const controller = new AbortController();
|
|
1902
|
-
const t = setTimeout(() => { try {
|
|
1903
|
-
controller.abort();
|
|
1904
|
-
}
|
|
1905
|
-
catch { /* ignore */ } }, 800);
|
|
1906
|
-
const res = await fetch(`${HTTP_PROTOCOLS.HTTP}${LOCAL_HOSTS.IPV4}:${port}${API_PATHS.HEALTH}`, { method: 'GET', signal: controller.signal });
|
|
1907
|
-
clearTimeout(t);
|
|
1908
|
-
if (!res.ok) {
|
|
1909
|
-
return false;
|
|
1910
|
-
}
|
|
1911
|
-
const data = await res.json().catch(() => null);
|
|
1912
|
-
return !!data && (data.status === 'healthy' || data.status === 'ready');
|
|
1913
|
-
}
|
|
1914
|
-
catch (error) {
|
|
1915
|
-
return false;
|
|
1916
|
-
}
|
|
403
|
+
return isServerHealthyQuickImpl({ port, fetchImpl: fetch });
|
|
1917
404
|
}
|
|
1918
405
|
function getModulesConfigPath() {
|
|
1919
406
|
return path.resolve(__dirname, '../config/modules.json');
|
|
1920
407
|
}
|
|
1921
|
-
// Port utilities: doctor
|
|
1922
|
-
program
|
|
1923
|
-
.command('port')
|
|
1924
|
-
.description('Port utilities (doctor)')
|
|
1925
|
-
.argument('<sub>', 'Subcommand: doctor')
|
|
1926
|
-
.argument('[port]', 'Port number (e.g., ${DEFAULT_CONFIG.PORT})')
|
|
1927
|
-
.option('--kill', 'Kill all listeners on the port')
|
|
1928
|
-
.action(async (sub, portArg, opts) => {
|
|
1929
|
-
if ((sub || '').toLowerCase() !== 'doctor') {
|
|
1930
|
-
console.error(chalk.red("Unknown subcommand. Use: rcc port doctor [port] [--kill]"));
|
|
1931
|
-
process.exit(2);
|
|
1932
|
-
}
|
|
1933
|
-
const spinner = await createSpinner('Inspecting port...');
|
|
1934
|
-
try {
|
|
1935
|
-
let port = Number(portArg || 0);
|
|
1936
|
-
if (!Number.isFinite(port) || port <= 0) {
|
|
1937
|
-
// fallback to user config
|
|
1938
|
-
const cfgPath = path.join(homedir(), '.routecodex', 'config.json');
|
|
1939
|
-
if (fs.existsSync(cfgPath)) {
|
|
1940
|
-
try {
|
|
1941
|
-
const raw = fs.readFileSync(cfgPath, 'utf8');
|
|
1942
|
-
const cfg = JSON.parse(raw);
|
|
1943
|
-
port = (cfg?.httpserver?.port ?? cfg?.server?.port ?? cfg?.port) || port;
|
|
1944
|
-
}
|
|
1945
|
-
catch { /* ignore */ }
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
if (!Number.isFinite(port) || port <= 0) {
|
|
1949
|
-
spinner.fail('Missing port. Provide an explicit port or set it in ~/.routecodex/config.json');
|
|
1950
|
-
process.exit(1);
|
|
1951
|
-
}
|
|
1952
|
-
const pids = findListeningPids(port);
|
|
1953
|
-
spinner.stop();
|
|
1954
|
-
console.log(chalk.cyan(`Port ${port} listeners:`));
|
|
1955
|
-
if (!pids.length) {
|
|
1956
|
-
console.log(' (none)');
|
|
1957
|
-
}
|
|
1958
|
-
else {
|
|
1959
|
-
for (const pid of pids) {
|
|
1960
|
-
let cmd = '';
|
|
1961
|
-
try {
|
|
1962
|
-
cmd = spawnSync('ps', ['-o', 'command=', '-p', String(pid)], { encoding: 'utf8' }).stdout.trim();
|
|
1963
|
-
}
|
|
1964
|
-
catch {
|
|
1965
|
-
cmd = '';
|
|
1966
|
-
}
|
|
1967
|
-
const origin = /node\s+.*routecodex-worktree/.test(cmd) ? 'local-dev' : (/node\s+.*lib\/node_modules\/routecodex/.test(cmd) ? 'global' : 'unknown');
|
|
1968
|
-
console.log(` PID ${pid} [${origin}] ${cmd}`);
|
|
1969
|
-
}
|
|
1970
|
-
}
|
|
1971
|
-
if (opts.kill && pids.length) {
|
|
1972
|
-
const ksp = await createSpinner(`Killing ${pids.length} listener(s) on ${port}...`);
|
|
1973
|
-
for (const pid of pids) {
|
|
1974
|
-
try {
|
|
1975
|
-
killPidBestEffort(pid, { force: true });
|
|
1976
|
-
}
|
|
1977
|
-
catch (e) {
|
|
1978
|
-
ksp.warn(`Failed to kill ${pid}: ${e.message}`);
|
|
1979
|
-
}
|
|
1980
|
-
}
|
|
1981
|
-
// brief wait
|
|
1982
|
-
await sleep(300);
|
|
1983
|
-
const remain = findListeningPids(port);
|
|
1984
|
-
if (remain.length) {
|
|
1985
|
-
ksp.fail(`Some listeners remain: ${remain.join(', ')}`);
|
|
1986
|
-
process.exit(1);
|
|
1987
|
-
}
|
|
1988
|
-
ksp.succeed(`Port ${port} is now free.`);
|
|
1989
|
-
}
|
|
1990
|
-
}
|
|
1991
|
-
catch (e) {
|
|
1992
|
-
spinner.fail('Port inspection failed');
|
|
1993
|
-
console.error(e instanceof Error ? e.message : String(e));
|
|
1994
|
-
process.exit(1);
|
|
1995
|
-
}
|
|
1996
|
-
});
|
|
1997
408
|
// Parse command line arguments (must be last)
|
|
1998
409
|
program.parse();
|
|
1999
410
|
//# sourceMappingURL=cli.js.map
|