@lakphy/local-router 0.5.7 → 0.5.8

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/cli.js CHANGED
@@ -8708,6 +8708,8 @@ var init_errors = __esm(() => {
8708
8708
  APPLY_FAILED: 8,
8709
8709
  UPSTREAM_UNREACHABLE: 9,
8710
8710
  INTERACTIVE_REQUIRED: 10,
8711
+ TARGET_NOT_FOUND: 3,
8712
+ TARGET_UNREACHABLE: 9,
8711
8713
  UNKNOWN_ERROR: 1
8712
8714
  };
8713
8715
  CliError = class CliError extends Error {
@@ -8750,6 +8752,9 @@ function extractGlobalFlags(args) {
8750
8752
  let yes = false;
8751
8753
  let jsonAlias = false;
8752
8754
  let explain = false;
8755
+ let targetUrl;
8756
+ let targetHost;
8757
+ let targetPortRaw;
8753
8758
  for (let i = 0;i < args.length; i++) {
8754
8759
  const a = args[i];
8755
8760
  if (!a)
@@ -8796,6 +8801,21 @@ function extractGlobalFlags(args) {
8796
8801
  explain = true;
8797
8802
  continue;
8798
8803
  }
8804
+ if (a === "--url" || a.startsWith("--url=")) {
8805
+ targetUrl = a.startsWith("--url=") ? a.slice("--url=".length) : args[i + 1];
8806
+ rest.push(a);
8807
+ continue;
8808
+ }
8809
+ if (a === "--host" || a.startsWith("--host=")) {
8810
+ targetHost = a.startsWith("--host=") ? a.slice("--host=".length) : args[i + 1];
8811
+ rest.push(a);
8812
+ continue;
8813
+ }
8814
+ if (a === "--port" || a.startsWith("--port=")) {
8815
+ targetPortRaw = a.startsWith("--port=") ? a.slice("--port=".length) : args[i + 1];
8816
+ rest.push(a);
8817
+ continue;
8818
+ }
8799
8819
  rest.push(a);
8800
8820
  }
8801
8821
  const envFormat = pickOutput(process.env.LOCAL_ROUTER_FORMAT);
@@ -8813,6 +8833,16 @@ function extractGlobalFlags(args) {
8813
8833
  } else {
8814
8834
  output = envFormat ?? "markdown";
8815
8835
  }
8836
+ let targetPort;
8837
+ if (targetPortRaw !== undefined) {
8838
+ targetPort = Number.parseInt(targetPortRaw, 10);
8839
+ if (!Number.isFinite(targetPort) || targetPort <= 0 || targetPort > 65535) {
8840
+ throw new CliError("USAGE_ERROR", `\u65E0\u6548\u7AEF\u53E3: ${targetPortRaw}`, {
8841
+ hint: "\u7AEF\u53E3\u8303\u56F4 1-65535"
8842
+ });
8843
+ }
8844
+ }
8845
+ const target = targetUrl !== undefined || targetHost !== undefined || targetPort !== undefined ? { url: targetUrl, host: targetHost, port: targetPort } : undefined;
8816
8846
  return {
8817
8847
  flags: {
8818
8848
  output,
@@ -8821,7 +8851,8 @@ function extractGlobalFlags(args) {
8821
8851
  noColor: noColor || !!process.env.NO_COLOR,
8822
8852
  noInteractive: noInteractive || process.env.LOCAL_ROUTER_NO_INTERACTIVE === "1",
8823
8853
  yes,
8824
- explain: explain || process.env.LOCAL_ROUTER_EXPLAIN === "1"
8854
+ explain: explain || process.env.LOCAL_ROUTER_EXPLAIN === "1",
8855
+ target
8825
8856
  },
8826
8857
  rest
8827
8858
  };
@@ -52575,6 +52606,35 @@ var init_bun = __esm(() => {
52575
52606
  init_server();
52576
52607
  });
52577
52608
 
52609
+ // src/cli/asset-paths.ts
52610
+ async function findExisting(...candidates) {
52611
+ for (const url2 of candidates) {
52612
+ try {
52613
+ const exists = await Bun.file(url2).exists();
52614
+ if (exists)
52615
+ return url2;
52616
+ } catch {}
52617
+ }
52618
+ return null;
52619
+ }
52620
+ async function readPackageJson() {
52621
+ const url2 = await findExisting(new URL("../../package.json", import.meta.url), new URL("../package.json", import.meta.url), new URL("../../../package.json", import.meta.url));
52622
+ if (!url2)
52623
+ return null;
52624
+ try {
52625
+ return await Bun.file(url2).json();
52626
+ } catch {
52627
+ return null;
52628
+ }
52629
+ }
52630
+ async function findConfigSchemaUrl() {
52631
+ return findExisting(new URL("../../config.schema.json", import.meta.url), new URL("../config.schema.json", import.meta.url), new URL("../../../config.schema.json", import.meta.url));
52632
+ }
52633
+ async function readVersionString() {
52634
+ const pkg = await readPackageJson();
52635
+ return typeof pkg?.version === "string" ? pkg.version : "unknown";
52636
+ }
52637
+
52578
52638
  // src/cli/runtime.ts
52579
52639
  import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync3 } from "fs";
52580
52640
  import { homedir as homedir2 } from "os";
@@ -59325,6 +59385,13 @@ function createServerAddressInfo(listenHost, port) {
59325
59385
  // src/index.ts
59326
59386
  import { readFileSync as readFileSync8 } from "fs";
59327
59387
  import { dirname as dirname4, resolve as resolve8 } from "path";
59388
+ function readRestartCriticalServerFields(config2) {
59389
+ return {
59390
+ host: config2.server?.host ?? "0.0.0.0",
59391
+ port: config2.server?.port ?? 4099,
59392
+ idleTimeout: config2.server?.idleTimeout ?? 0
59393
+ };
59394
+ }
59328
59395
  function printIntegrationGuide(config2, listen = {}) {
59329
59396
  const host = listen.host ?? process.env.HOST ?? "0.0.0.0";
59330
59397
  const parsedPort = listen.port ?? Number.parseInt(process.env.PORT ?? "4099", 10);
@@ -59404,7 +59471,7 @@ function createChatProxyModel(providerName, providerConfig, model) {
59404
59471
  throw new Error(`\u6682\u4E0D\u652F\u6301\u7684 provider \u7C7B\u578B: ${providerConfig.type}`);
59405
59472
  }
59406
59473
  }
59407
- function createAdminApiRoutes(store, pluginManager, registerCleanup) {
59474
+ function createAdminApiRoutes(store, pluginManager, registerCleanup, serverControl, restartLogStorageTask, serviceVersion) {
59408
59475
  const api2 = new Hono2;
59409
59476
  const cryptoSessions = new Map;
59410
59477
  const CRYPTO_SESSION_TTL_MS = 2 * 60 * 1000;
@@ -59444,7 +59511,16 @@ function createAdminApiRoutes(store, pluginManager, registerCleanup) {
59444
59511
  }
59445
59512
  cryptoSessions.clear();
59446
59513
  });
59447
- api2.get("/health", (c) => c.json({ status: "ok", service: "local-router" }));
59514
+ api2.get("/health", (c) => {
59515
+ const addr = serverControl?.current ?? readRestartCriticalServerFields(store.get());
59516
+ return c.json({
59517
+ status: "ok",
59518
+ service: "local-router",
59519
+ version: serviceVersion ?? "unknown",
59520
+ host: addr.host,
59521
+ port: addr.port
59522
+ });
59523
+ });
59448
59524
  api2.post("/crypto/handshake", async (c) => {
59449
59525
  pruneExpiredCryptoSessions();
59450
59526
  if (cryptoSessions.size >= CRYPTO_SESSION_MAX) {
@@ -59517,18 +59593,30 @@ function createAdminApiRoutes(store, pluginManager, registerCleanup) {
59517
59593
  });
59518
59594
  api2.post("/config/apply", async (_c) => {
59519
59595
  try {
59596
+ const fallbackBefore = readRestartCriticalServerFields(store.get());
59597
+ const before = serverControl?.current ?? fallbackBefore;
59520
59598
  const config2 = store.reload();
59599
+ const after = readRestartCriticalServerFields(config2);
59521
59600
  if (config2.log) {
59522
59601
  const logBaseDir = resolveLogBaseDir(config2.log);
59523
59602
  initLogger(logBaseDir, config2.log);
59603
+ } else {
59604
+ resetLogger();
59524
59605
  }
59606
+ restartLogStorageTask?.(config2.log);
59525
59607
  const pluginResult = await pluginManager.reloadAll(config2.providers);
59608
+ const restartRequired = before.host !== after.host || before.port !== after.port || before.idleTimeout !== after.idleTimeout;
59526
59609
  return _c.json({
59527
59610
  ok: true,
59528
59611
  summary: {
59529
59612
  providers: Object.keys(config2.providers).length,
59530
59613
  routes: Object.keys(config2.routes).length
59531
59614
  },
59615
+ restartRequired,
59616
+ ...restartRequired && {
59617
+ listen: { host: after.host, port: after.port },
59618
+ canRestart: Boolean(serverControl)
59619
+ },
59532
59620
  ...pluginResult.failures.length > 0 && {
59533
59621
  pluginWarnings: pluginResult.failures
59534
59622
  }
@@ -59537,6 +59625,14 @@ function createAdminApiRoutes(store, pluginManager, registerCleanup) {
59537
59625
  return _c.json({ error: `\u5E94\u7528\u914D\u7F6E\u5931\u8D25: ${err instanceof Error ? err.message : err}` }, 500);
59538
59626
  }
59539
59627
  });
59628
+ api2.post("/restart", (c) => {
59629
+ if (!serverControl) {
59630
+ return c.json({ error: "\u5F53\u524D\u8FD0\u884C\u65B9\u5F0F\u4E0D\u652F\u6301\u81EA\u52A8\u91CD\u542F\uFF0C\u8BF7\u624B\u52A8\u6267\u884C local-router restart" }, 501);
59631
+ }
59632
+ const { host, port } = readRestartCriticalServerFields(store.get());
59633
+ serverControl.requestRestart();
59634
+ return c.json({ ok: true, listen: { host, port } });
59635
+ });
59540
59636
  api2.get("/config/meta", (c) => {
59541
59637
  return c.json({
59542
59638
  configPath: store.getPath(),
@@ -60082,6 +60178,7 @@ async function proxyAdminToDevServer(c, origin) {
60082
60178
  }
60083
60179
  async function createApp(store, options) {
60084
60180
  const config2 = store.get();
60181
+ const serviceVersion = await readVersionString();
60085
60182
  console.log(`\u5DF2\u52A0\u8F7D\u914D\u7F6E: ${store.getPath()}`);
60086
60183
  if (config2.log) {
60087
60184
  const logBaseDir = resolveLogBaseDir(config2.log);
@@ -60089,8 +60186,14 @@ async function createApp(store, options) {
60089
60186
  } else {
60090
60187
  resetLogger();
60091
60188
  }
60092
- const stopLogStorageTask = startLogStorageBackgroundTask(config2.log);
60093
- options?.registerCleanup?.(stopLogStorageTask);
60189
+ let stopLogStorageTask = startLogStorageBackgroundTask(config2.log);
60190
+ const restartLogStorageTask = (logConfig) => {
60191
+ try {
60192
+ stopLogStorageTask();
60193
+ } catch {}
60194
+ stopLogStorageTask = startLogStorageBackgroundTask(logConfig);
60195
+ };
60196
+ options?.registerCleanup?.(() => stopLogStorageTask());
60094
60197
  const configDir = dirname4(resolve8(store.getPath()));
60095
60198
  const pluginManager = new PluginManager(configDir);
60096
60199
  const reloadResult = await pluginManager.reloadAll(config2.providers);
@@ -60109,7 +60212,7 @@ async function createApp(store, options) {
60109
60212
  app.route(entry.mountPrefix, subApp);
60110
60213
  console.log(`\u5DF2\u6CE8\u518C\u8DEF\u7531: ${routeType} -> ${entry.mountPrefix}`);
60111
60214
  }
60112
- app.route("/api", createAdminApiRoutes(store, pluginManager, options?.registerCleanup));
60215
+ app.route("/api", createAdminApiRoutes(store, pluginManager, options?.registerCleanup, options?.serverControl, restartLogStorageTask, serviceVersion));
60113
60216
  console.log("\u5DF2\u6CE8\u518C\u7BA1\u7406 API: /api");
60114
60217
  app.get("/api/docs", middleware({ url: "/api/openapi.json" }));
60115
60218
  app.get("/api/openapi.json", (c) => c.json(openAPISpec));
@@ -60138,11 +60241,12 @@ async function createApp(store, options) {
60138
60241
  }
60139
60242
  return app;
60140
60243
  }
60141
- async function createAppRuntimeFromConfigPath(configPath, listen) {
60244
+ async function createAppRuntimeFromConfigPath(configPath, listen, serverControl) {
60142
60245
  const store = new ConfigStore(configPath);
60143
60246
  const cleanups = [];
60144
60247
  const app = await createApp(store, {
60145
60248
  listen,
60249
+ serverControl,
60146
60250
  registerCleanup: (cleanup) => {
60147
60251
  cleanups.push(cleanup);
60148
60252
  }
@@ -60244,11 +60348,15 @@ function resolveIdleTimeoutSeconds(explicit) {
60244
60348
  return DEFAULT_IDLE_TIMEOUT_SECONDS;
60245
60349
  }
60246
60350
  async function startServer(options) {
60351
+ const idleTimeout = resolveIdleTimeoutSeconds(options.idleTimeoutSeconds);
60352
+ const requestRestart = options.requestRestart;
60247
60353
  const runtime = await createAppRuntimeFromConfigPath(options.configPath, {
60248
60354
  host: options.host,
60249
60355
  port: options.port
60250
- });
60251
- const idleTimeout = resolveIdleTimeoutSeconds(options.idleTimeoutSeconds);
60356
+ }, requestRestart ? {
60357
+ requestRestart,
60358
+ current: { host: options.host, port: options.port, idleTimeout }
60359
+ } : undefined);
60252
60360
  const server = Bun.serve({
60253
60361
  fetch: (request, server2) => {
60254
60362
  const remoteAddress = server2.requestIP(request)?.address ?? null;
@@ -60291,6 +60399,7 @@ var exports_process = {};
60291
60399
  __export(exports_process, {
60292
60400
  stopProcess: () => stopProcess,
60293
60401
  startDaemon: () => startDaemon,
60402
+ spawnDetachedRestart: () => spawnDetachedRestart,
60294
60403
  runServerProcess: () => runServerProcess,
60295
60404
  readLogDelta: () => readLogDelta,
60296
60405
  parseSharedFlags: () => parseSharedFlags,
@@ -60448,7 +60557,8 @@ async function runServerProcess(opts) {
60448
60557
  configPath: ensured.path,
60449
60558
  host,
60450
60559
  port,
60451
- idleTimeoutSeconds: Number.isFinite(idleTimeoutSeconds) ? idleTimeoutSeconds : undefined
60560
+ idleTimeoutSeconds: Number.isFinite(idleTimeoutSeconds) ? idleTimeoutSeconds : undefined,
60561
+ requestRestart: () => spawnDetachedRestart({ configPath: ensured.path })
60452
60562
  });
60453
60563
  } catch (err) {
60454
60564
  const code = err?.code;
@@ -60552,6 +60662,32 @@ async function startDaemon(flags) {
60552
60662
  throw new Error(`\u540E\u53F0\u542F\u52A8\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u65E5\u5FD7: ${files.daemonLog}
60553
60663
  ${tail}`);
60554
60664
  }
60665
+ function spawnDetachedRestart(opts = {}) {
60666
+ const trigger = () => {
60667
+ try {
60668
+ ensureRuntimeDirs();
60669
+ const files = getRuntimeFiles();
60670
+ const fd = openSync3(files.daemonLog, "a");
60671
+ const cliScript = process.argv[1] ?? "src/cli.ts";
60672
+ const childArgs = [cliScript, "restart"];
60673
+ if (opts.configPath) {
60674
+ childArgs.push("--config", opts.configPath);
60675
+ }
60676
+ const child = Bun.spawn([process.execPath, ...childArgs], {
60677
+ stdin: "ignore",
60678
+ stdout: fd,
60679
+ stderr: fd,
60680
+ detached: true
60681
+ });
60682
+ closeSync3(fd);
60683
+ child.unref();
60684
+ } catch (err) {
60685
+ console.error(`[restart] \u81EA\u91CD\u542F\u89E6\u53D1\u5931\u8D25: ${err instanceof Error ? err.message : err}`);
60686
+ }
60687
+ };
60688
+ const timer = setTimeout(trigger, 400);
60689
+ timer.unref?.();
60690
+ }
60555
60691
  async function stopProcess(graceMs = 8000) {
60556
60692
  await cleanupIfStale();
60557
60693
  const state = readRuntimeState();
@@ -61194,6 +61330,10 @@ function normalizeOne(flag, raw2) {
61194
61330
  }
61195
61331
  function parseCommandArgs(def, args) {
61196
61332
  const options = toParseArgsOptions(def.flags);
61333
+ for (const g of GLOBAL_TARGET_FLAG_NAMES) {
61334
+ if (!(g in options))
61335
+ options[g] = { type: "string" };
61336
+ }
61197
61337
  let parsed;
61198
61338
  try {
61199
61339
  parsed = nodeParseArgs({
@@ -61205,7 +61345,7 @@ function parseCommandArgs(def, args) {
61205
61345
  } catch (err) {
61206
61346
  throw new CliError("USAGE_ERROR", err instanceof Error ? err.message : String(err));
61207
61347
  }
61208
- const known = new Set(def.flags?.map((f) => f.name) ?? []);
61348
+ const known = new Set([...def.flags?.map((f) => f.name) ?? [], ...GLOBAL_TARGET_FLAG_NAMES]);
61209
61349
  for (const a of args) {
61210
61350
  if (!a.startsWith("--"))
61211
61351
  continue;
@@ -61273,8 +61413,10 @@ function levenshtein(a, b) {
61273
61413
  }
61274
61414
  return dp[b.length];
61275
61415
  }
61416
+ var GLOBAL_TARGET_FLAG_NAMES;
61276
61417
  var init_parse_args = __esm(() => {
61277
61418
  init_errors();
61419
+ GLOBAL_TARGET_FLAG_NAMES = ["url", "host", "port"];
61278
61420
  });
61279
61421
 
61280
61422
  // src/cli/registry.ts
@@ -61570,9 +61712,298 @@ init_config_apply();
61570
61712
  init_errors();
61571
61713
  init_output();
61572
61714
  init_process();
61715
+ import { createInterface as createInterface5 } from "readline/promises";
61716
+ import { parseArgs as parseArgs3 } from "util";
61717
+
61718
+ // src/cli/target.ts
61719
+ init_errors();
61720
+ init_output();
61721
+ init_process();
61573
61722
  init_runtime();
61574
61723
  import { createInterface as createInterface4 } from "readline/promises";
61575
- import { parseArgs as parseArgs3 } from "util";
61724
+ var DEFAULT_HOST = "127.0.0.1";
61725
+ var DEFAULT_PORT = 4099;
61726
+ function normalizeHostForUrl(host) {
61727
+ const h = host.trim();
61728
+ if (h === "" || h === "0.0.0.0")
61729
+ return "127.0.0.1";
61730
+ if (h === "::" || h === "::1")
61731
+ return "[::1]";
61732
+ if (h.includes(":") && !h.startsWith("["))
61733
+ return `[${h}]`;
61734
+ return h;
61735
+ }
61736
+ function buildBaseUrl(host, port) {
61737
+ return `http://${normalizeHostForUrl(host)}:${port}`;
61738
+ }
61739
+ async function fingerprint(baseUrl, timeoutMs = 800) {
61740
+ const controller = new AbortController;
61741
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
61742
+ try {
61743
+ const res = await fetch(`${baseUrl}/api/health`, { method: "GET", signal: controller.signal });
61744
+ if (!res.ok)
61745
+ return { ok: false };
61746
+ const body = await res.json();
61747
+ if (body?.service !== "local-router")
61748
+ return { ok: false };
61749
+ return { ok: true, version: body.version, host: body.host, port: body.port };
61750
+ } catch {
61751
+ return { ok: false };
61752
+ } finally {
61753
+ clearTimeout(timer);
61754
+ }
61755
+ }
61756
+ function parseLsofPorts(output) {
61757
+ const ports = new Set;
61758
+ for (const line of output.split(`
61759
+ `)) {
61760
+ if (!line.includes("LISTEN"))
61761
+ continue;
61762
+ const m = line.match(/(?:\*|\[[^\]]+\]|[\d.]+):(\d{2,5})\s*\(LISTEN\)/);
61763
+ if (!m)
61764
+ continue;
61765
+ const port = Number.parseInt(m[1], 10);
61766
+ if (Number.isFinite(port))
61767
+ ports.add(port);
61768
+ }
61769
+ return [...ports];
61770
+ }
61771
+ function parseNetstatPorts(output) {
61772
+ const ports = new Set;
61773
+ for (const line of output.split(`
61774
+ `)) {
61775
+ if (!/LISTEN/i.test(line))
61776
+ continue;
61777
+ const m = line.match(/[.:](\d{2,5})\s+\S+\s+LISTEN/i) ?? line.match(/[.:](\d{2,5})\s+LISTEN/i);
61778
+ if (!m)
61779
+ continue;
61780
+ const port = Number.parseInt(m[1], 10);
61781
+ if (Number.isFinite(port))
61782
+ ports.add(port);
61783
+ }
61784
+ return [...ports];
61785
+ }
61786
+ async function runShellCommand(cmd) {
61787
+ try {
61788
+ const proc = Bun.spawn(cmd, { stdout: "pipe", stderr: "ignore", stdin: "ignore" });
61789
+ const out = await new Response(proc.stdout).text();
61790
+ await proc.exited;
61791
+ return out;
61792
+ } catch {
61793
+ return null;
61794
+ }
61795
+ }
61796
+ async function listListeningPorts() {
61797
+ const ports = new Set;
61798
+ const lsof = await runShellCommand(["lsof", "-nP", "-iTCP", "-sTCP:LISTEN"]);
61799
+ if (lsof) {
61800
+ for (const p of parseLsofPorts(lsof))
61801
+ ports.add(p);
61802
+ }
61803
+ if (ports.size === 0) {
61804
+ const netstat = await runShellCommand(["netstat", "-an"]);
61805
+ if (netstat) {
61806
+ for (const p of parseNetstatPorts(netstat))
61807
+ ports.add(p);
61808
+ }
61809
+ }
61810
+ return [...ports].sort((a, b) => a - b);
61811
+ }
61812
+ async function discoverLocalRouters(timeoutMs = 300) {
61813
+ const ports = await listListeningPorts();
61814
+ const hits = [];
61815
+ const concurrency = 8;
61816
+ let idx = 0;
61817
+ const worker = async () => {
61818
+ while (idx < ports.length) {
61819
+ const port = ports[idx++];
61820
+ const fp = await fingerprint(buildBaseUrl(DEFAULT_HOST, port), timeoutMs);
61821
+ if (fp.ok)
61822
+ hits.push({ port, version: fp.version });
61823
+ }
61824
+ };
61825
+ await Promise.all(Array.from({ length: Math.min(concurrency, ports.length) }, () => worker()));
61826
+ return hits.sort((a, b) => a.port - b.port);
61827
+ }
61828
+ async function fromExplicit(url2, host, port, source) {
61829
+ if (url2) {
61830
+ let u;
61831
+ try {
61832
+ u = new URL(url2);
61833
+ } catch {
61834
+ throw new CliError("USAGE_ERROR", `\u65E0\u6548 URL: ${url2}`, { hint: "\u5F62\u5982 http://127.0.0.1:4099" });
61835
+ }
61836
+ const baseUrl = u.origin;
61837
+ const fp = await fingerprint(baseUrl);
61838
+ if (!fp.ok) {
61839
+ throw new CliError("TARGET_UNREACHABLE", `\u76EE\u6807\u65E0\u6CD5\u8FDE\u901A: ${baseUrl}`, {
61840
+ hint: "\u786E\u8BA4\u5730\u5740\u6B63\u786E\u4E14 local-router \u6B63\u5728\u8BE5\u5730\u5740\u76D1\u542C"
61841
+ });
61842
+ }
61843
+ const portNum = u.port ? Number.parseInt(u.port, 10) : u.protocol === "https:" ? 443 : 80;
61844
+ return { baseUrl, host: u.hostname, port: portNum, version: fp.version, source };
61845
+ }
61846
+ if (port !== undefined || host !== undefined) {
61847
+ const h = host ?? DEFAULT_HOST;
61848
+ const p = port ?? DEFAULT_PORT;
61849
+ const baseUrl = buildBaseUrl(h, p);
61850
+ const fp = await fingerprint(baseUrl);
61851
+ if (!fp.ok) {
61852
+ throw new CliError("TARGET_UNREACHABLE", `\u76EE\u6807\u65E0\u6CD5\u8FDE\u901A: ${baseUrl}`, {
61853
+ hint: "\u786E\u8BA4\u7AEF\u53E3\u6B63\u786E\u4E14 local-router \u6B63\u5728\u76D1\u542C"
61854
+ });
61855
+ }
61856
+ return { baseUrl, host: h, port: p, version: fp.version, source };
61857
+ }
61858
+ return null;
61859
+ }
61860
+ async function promptPort() {
61861
+ const rl = createInterface4({ input: process.stdin, output: process.stdout });
61862
+ try {
61863
+ const answer = (await rl.question(`\u672A\u53D1\u73B0 local-router\uFF0C\u8BF7\u8F93\u5165\u7AEF\u53E3 [${DEFAULT_PORT}]: `)).trim();
61864
+ const port = answer === "" ? DEFAULT_PORT : Number.parseInt(answer, 10);
61865
+ if (!Number.isFinite(port) || port <= 0 || port > 65535) {
61866
+ throw new CliError("USAGE_ERROR", `\u65E0\u6548\u7AEF\u53E3: ${answer}`, { hint: "\u7AEF\u53E3\u8303\u56F4 1-65535" });
61867
+ }
61868
+ return port;
61869
+ } finally {
61870
+ rl.close();
61871
+ }
61872
+ }
61873
+ async function promptSelectPort(found) {
61874
+ const rl = createInterface4({ input: process.stdin, output: process.stdout });
61875
+ try {
61876
+ console.log("\u53D1\u73B0\u591A\u4E2A local-router\uFF1A");
61877
+ found.forEach((f, i) => {
61878
+ console.log(` ${i + 1}) 127.0.0.1:${f.port}${f.version ? ` (v${f.version})` : ""}`);
61879
+ });
61880
+ const answer = (await rl.question("\u8BF7\u8F93\u5165\u5E8F\u53F7: ")).trim();
61881
+ const idx = Number.parseInt(answer, 10) - 1;
61882
+ if (!Number.isFinite(idx) || idx < 0 || idx >= found.length) {
61883
+ throw new CliError("USAGE_ERROR", `\u65E0\u6548\u5E8F\u53F7: ${answer}`);
61884
+ }
61885
+ return found[idx].port;
61886
+ } finally {
61887
+ rl.close();
61888
+ }
61889
+ }
61890
+ function discoveredTarget(found) {
61891
+ return {
61892
+ baseUrl: buildBaseUrl(DEFAULT_HOST, found.port),
61893
+ host: DEFAULT_HOST,
61894
+ port: found.port,
61895
+ version: found.version,
61896
+ source: "discovered"
61897
+ };
61898
+ }
61899
+ async function resolveTarget(flags) {
61900
+ const explicit = await fromExplicit(flags.target?.url, flags.target?.host, flags.target?.port, "flag");
61901
+ if (explicit)
61902
+ return explicit;
61903
+ const envUrl = process.env.LOCAL_ROUTER_URL?.trim() || undefined;
61904
+ const envPortRaw = process.env.LOCAL_ROUTER_PORT?.trim();
61905
+ let envPort;
61906
+ if (envPortRaw) {
61907
+ envPort = Number.parseInt(envPortRaw, 10);
61908
+ if (!Number.isFinite(envPort)) {
61909
+ throw new CliError("USAGE_ERROR", `\u65E0\u6548 LOCAL_ROUTER_PORT: ${envPortRaw}`);
61910
+ }
61911
+ }
61912
+ const env = await fromExplicit(envUrl, undefined, envPort, "env");
61913
+ if (env)
61914
+ return env;
61915
+ const state = readRuntimeState();
61916
+ if (state && isProcessAlive(state.pid)) {
61917
+ const fp = await fingerprint(state.baseUrl);
61918
+ if (fp.ok) {
61919
+ return {
61920
+ baseUrl: state.baseUrl,
61921
+ host: resolveLocalAccessHost(state.host),
61922
+ port: state.port,
61923
+ version: fp.version,
61924
+ source: "runtime"
61925
+ };
61926
+ }
61927
+ }
61928
+ const defaultUrl = buildBaseUrl(DEFAULT_HOST, DEFAULT_PORT);
61929
+ const defFp = await fingerprint(defaultUrl);
61930
+ if (defFp.ok) {
61931
+ return {
61932
+ baseUrl: defaultUrl,
61933
+ host: DEFAULT_HOST,
61934
+ port: DEFAULT_PORT,
61935
+ version: defFp.version,
61936
+ source: "default"
61937
+ };
61938
+ }
61939
+ const found = await discoverLocalRouters();
61940
+ if (found.length === 1) {
61941
+ return discoveredTarget(found[0]);
61942
+ }
61943
+ if (found.length > 1) {
61944
+ if (flags.yes)
61945
+ return discoveredTarget(found[0]);
61946
+ if (flags.noInteractive || !process.stdin.isTTY) {
61947
+ throw new CliError("TARGET_NOT_FOUND", "\u53D1\u73B0\u591A\u4E2A local-router\uFF0C\u8BF7\u7528 --port \u6307\u5B9A", {
61948
+ hint: `\u5019\u9009\u7AEF\u53E3: ${found.map((f) => f.port).join(", ")}`,
61949
+ details: { candidates: found }
61950
+ });
61951
+ }
61952
+ const picked = await promptSelectPort(found);
61953
+ return discoveredTarget(found.find((f) => f.port === picked) ?? { port: picked });
61954
+ }
61955
+ if (!flags.noInteractive && process.stdin.isTTY) {
61956
+ const port = await promptPort();
61957
+ const baseUrl = buildBaseUrl(DEFAULT_HOST, port);
61958
+ const fp = await fingerprint(baseUrl);
61959
+ if (!fp.ok) {
61960
+ throw new CliError("TARGET_UNREACHABLE", `\u7AEF\u53E3 ${port} \u4E0A\u6CA1\u6709 local-router`);
61961
+ }
61962
+ return { baseUrl, host: DEFAULT_HOST, port, version: fp.version, source: "prompt" };
61963
+ }
61964
+ throw new CliError("TARGET_NOT_FOUND", "\u627E\u4E0D\u5230\u53EF\u8FDE\u63A5\u7684 local-router", {
61965
+ hint: "`local-router start` \u542F\u52A8\uFF1B\u6216\u7528 --port <port> / --url <url> \u6307\u5B9A\u76EE\u6807"
61966
+ });
61967
+ }
61968
+ function guessTargetUrl(flags) {
61969
+ if (flags.target?.url) {
61970
+ try {
61971
+ const u = new URL(flags.target.url);
61972
+ const port = u.port ? Number.parseInt(u.port, 10) : u.protocol === "https:" ? 443 : 80;
61973
+ return { baseUrl: u.origin, host: u.hostname, port, running: false };
61974
+ } catch {}
61975
+ }
61976
+ if (flags.target?.port !== undefined || flags.target?.host !== undefined) {
61977
+ const host = flags.target.host ?? DEFAULT_HOST;
61978
+ const port = flags.target.port ?? DEFAULT_PORT;
61979
+ return { baseUrl: buildBaseUrl(host, port), host, port, running: false };
61980
+ }
61981
+ const state = readRuntimeState();
61982
+ if (state && isProcessAlive(state.pid)) {
61983
+ return {
61984
+ baseUrl: state.baseUrl,
61985
+ host: resolveLocalAccessHost(state.host),
61986
+ port: state.port,
61987
+ running: true
61988
+ };
61989
+ }
61990
+ return {
61991
+ baseUrl: buildBaseUrl(DEFAULT_HOST, DEFAULT_PORT),
61992
+ host: DEFAULT_HOST,
61993
+ port: DEFAULT_PORT,
61994
+ running: false
61995
+ };
61996
+ }
61997
+ function targetMetaLine(t) {
61998
+ return `\u2192 ${t.host}:${t.port}${t.version ? ` (v${t.version})` : ""} \xB7 ${t.source}`;
61999
+ }
62000
+ async function requireTarget(ctx) {
62001
+ const t = await resolveTarget(ctx.flags);
62002
+ emitDiagnostic(ctx, targetMetaLine(t));
62003
+ return t;
62004
+ }
62005
+
62006
+ // src/cli/config-command.ts
61576
62007
  function readConfig(configArg) {
61577
62008
  const path = resolveConfigPath(configArg);
61578
62009
  return { path, config: loadConfig(path) };
@@ -61640,7 +62071,7 @@ async function selectFromList(title, items) {
61640
62071
  items.forEach((item, i) => {
61641
62072
  console.log(` ${i + 1}) ${item}`);
61642
62073
  });
61643
- const rl = createInterface4({ input: process.stdin, output: process.stdout });
62074
+ const rl = createInterface5({ input: process.stdin, output: process.stdout });
61644
62075
  try {
61645
62076
  const answer = await rl.question("\u8BF7\u8F93\u5165\u5E8F\u53F7: ");
61646
62077
  const idx = Number.parseInt(answer, 10) - 1;
@@ -62511,32 +62942,26 @@ async function handleApply(args, flags) {
62511
62942
  command: "config.apply",
62512
62943
  flags,
62513
62944
  fn: async (ctx) => {
62514
- await cleanupIfStale();
62515
- const state = readRuntimeState();
62516
- if (!state) {
62517
- throw new CliError("SERVICE_NOT_RUNNING", "\u670D\u52A1\u672A\u8FD0\u884C\uFF0C\u65E0\u6CD5 apply", {
62518
- hint: "\u542F\u52A8: `local-router start --daemon`"
62519
- });
62520
- }
62521
- const res = await fetch(`${state.baseUrl}/api/config/apply`, { method: "POST" });
62945
+ const target = await requireTarget(ctx);
62946
+ const res = await fetch(`${target.baseUrl}/api/config/apply`, { method: "POST" });
62522
62947
  if (!res.ok) {
62523
62948
  const text2 = await res.text();
62524
62949
  throw new CliError("APPLY_FAILED", `apply \u5931\u8D25: ${res.status} ${text2}`, {
62525
- details: { status: res.status, baseUrl: state.baseUrl }
62950
+ details: { status: res.status, baseUrl: target.baseUrl }
62526
62951
  });
62527
62952
  }
62528
- const healthy = await checkHealth(state.baseUrl);
62953
+ const healthy = await checkHealth(target.baseUrl);
62529
62954
  if (!healthy) {
62530
- throw new CliError("HEALTH_FAILED", `apply \u540E\u5065\u5EB7\u68C0\u67E5\u5931\u8D25: ${state.baseUrl}`);
62955
+ throw new CliError("HEALTH_FAILED", `apply \u540E\u5065\u5EB7\u68C0\u67E5\u5931\u8D25: ${target.baseUrl}`);
62531
62956
  }
62532
62957
  emitResult(ctx, {
62533
62958
  command: "config.apply",
62534
- data: { ok: true, baseUrl: state.baseUrl },
62959
+ data: { ok: true, baseUrl: target.baseUrl },
62535
62960
  md: {
62536
62961
  heading: "config.apply \xB7 \u2713",
62537
- data: `\u5DF2\u5E94\u7528: \`${state.baseUrl}\``
62962
+ data: `\u5DF2\u5E94\u7528: \`${target.baseUrl}\``
62538
62963
  },
62539
- text: `\u914D\u7F6E\u5DF2\u5E94\u7528: ${state.baseUrl}`
62964
+ text: `\u914D\u7F6E\u5DF2\u5E94\u7528: ${target.baseUrl}`
62540
62965
  });
62541
62966
  }
62542
62967
  });
@@ -63109,10 +63534,8 @@ defineSchemaCommand({
63109
63534
  // src/cli/handlers/chat.ts
63110
63535
  init_errors();
63111
63536
  init_output();
63112
- init_process();
63113
63537
  init_registry();
63114
- init_runtime();
63115
- import { createInterface as createInterface5 } from "readline/promises";
63538
+ import { createInterface as createInterface6 } from "readline/promises";
63116
63539
  defineSchemaCommand({
63117
63540
  name: "chat",
63118
63541
  summary: "\u4EA4\u4E92\u5F0F REPL\uFF08\u9ED8\u8BA4\u8D70 openai-completions\uFF0C\u6D41\u5F0F\uFF09",
@@ -63131,25 +63554,19 @@ defineSchemaCommand({
63131
63554
  { name: "no-stream", type: "boolean", description: "\u7981\u7528\u6D41\u5F0F\uFF08\u4E00\u6B21\u8FD4\u56DE\uFF09" }
63132
63555
  ],
63133
63556
  fn: async ({ values, ctx }) => {
63134
- await cleanupIfStale();
63135
- const state = readRuntimeState();
63136
- if (!state)
63137
- throw new CliError("SERVICE_NOT_RUNNING", "\u670D\u52A1\u672A\u8FD0\u884C");
63138
- if (!await checkHealth(state.baseUrl)) {
63139
- throw new CliError("HEALTH_FAILED", `\u670D\u52A1\u5065\u5EB7\u68C0\u67E5\u5931\u8D25: ${state.baseUrl}`);
63140
- }
63141
63557
  if (!process.stdin.isTTY) {
63142
63558
  throw new CliError("INTERACTIVE_REQUIRED", "chat \u9700\u8981 TTY", {
63143
63559
  hint: "\u7BA1\u9053\u573A\u666F\u8BF7\u7528 `local-router try`"
63144
63560
  });
63145
63561
  }
63146
- const baseUrl = state.baseUrl.replace(/\/+$/, "");
63562
+ const target = await requireTarget(ctx);
63563
+ const baseUrl = target.baseUrl.replace(/\/+$/, "");
63147
63564
  const url2 = `${baseUrl}/openai-completions/v1/chat/completions`;
63148
63565
  const messages = [];
63149
63566
  if (values.system)
63150
63567
  messages.push({ role: "system", content: values.system });
63151
63568
  emitDiagnostic(ctx, `chat \u2192 ${values.entry}/${values.model} \xB7 \u8F93\u5165 \`/exit\` \u6216 Ctrl+C \u9000\u51FA \xB7 /reset \u6E05\u7A7A\u4E0A\u4E0B\u6587`);
63152
- const rl = createInterface5({ input: process.stdin, output: process.stdout });
63569
+ const rl = createInterface6({ input: process.stdin, output: process.stdout });
63153
63570
  try {
63154
63571
  while (true) {
63155
63572
  const input = (await rl.question("\u203A ")).trim();
@@ -63599,7 +64016,6 @@ init_config_apply();
63599
64016
  init_errors();
63600
64017
  init_output();
63601
64018
  init_registry();
63602
- init_runtime();
63603
64019
  import { spawnSync } from "child_process";
63604
64020
  import { copyFileSync, existsSync as existsSync13, mkdtempSync, rmSync as rmSync4 } from "fs";
63605
64021
  import { tmpdir as tmpdir2 } from "os";
@@ -63679,7 +64095,10 @@ function platformOpen(target) {
63679
64095
  args = [target];
63680
64096
  }
63681
64097
  const r = spawnSync(cmd, args, { stdio: "ignore" });
63682
- return { ok: !r.error && (typeof r.status !== "number" || r.status === 0), cmd: `${cmd} ${args.join(" ")}` };
64098
+ return {
64099
+ ok: !r.error && (typeof r.status !== "number" || r.status === 0),
64100
+ cmd: `${cmd} ${args.join(" ")}`
64101
+ };
63683
64102
  }
63684
64103
  defineSchemaCommand({
63685
64104
  name: "open",
@@ -63693,7 +64112,7 @@ defineSchemaCommand({
63693
64112
  }
63694
64113
  ],
63695
64114
  flags: [{ name: "config", type: "string", description: "\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84" }],
63696
- fn: ({ positionals, values, ctx }) => {
64115
+ fn: async ({ positionals, values, ctx }) => {
63697
64116
  const target = positionals[0];
63698
64117
  if (!target) {
63699
64118
  throw new CliError("USAGE_ERROR", "\u7528\u6CD5: open <admin|docs|logs-dir|config>");
@@ -63701,13 +64120,8 @@ defineSchemaCommand({
63701
64120
  let url2;
63702
64121
  let label;
63703
64122
  if (target === "admin") {
63704
- const state = readRuntimeState();
63705
- if (!state) {
63706
- throw new CliError("SERVICE_NOT_RUNNING", "\u670D\u52A1\u672A\u8FD0\u884C\uFF0C\u65E0\u6CD5\u6253\u5F00 admin", {
63707
- hint: "`local-router start --daemon`"
63708
- });
63709
- }
63710
- url2 = `${state.baseUrl}/admin/`;
64123
+ const resolved = await resolveTarget(ctx.flags);
64124
+ url2 = `${resolved.baseUrl}/admin/`;
63711
64125
  label = "Web Admin";
63712
64126
  } else if (target === "docs") {
63713
64127
  url2 = "https://github.com/lakphy/local-router#readme";
@@ -63759,8 +64173,9 @@ defineSchemaCommand({
63759
64173
  { name: "config", type: "string", description: "\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84" }
63760
64174
  ],
63761
64175
  fn: ({ values, ctx }) => {
63762
- const state = readRuntimeState();
63763
- const baseUrl = state?.baseUrl ?? "http://127.0.0.1:4099";
64176
+ const guess = guessTargetUrl(ctx.flags);
64177
+ const baseUrl = guess.baseUrl;
64178
+ const running = guess.running;
63764
64179
  const entries = [
63765
64180
  {
63766
64181
  key: "OPENAI_BASE_URL",
@@ -63804,12 +64219,12 @@ defineSchemaCommand({
63804
64219
  data: {
63805
64220
  baseUrl,
63806
64221
  shell: values.shell,
63807
- running: !!state,
64222
+ running,
63808
64223
  entries,
63809
64224
  script
63810
64225
  },
63811
64226
  md: {
63812
- heading: `env \xB7 ${state ? "\u2713 \u8FD0\u884C\u4E2D" : "\u2717 \u672A\u8FD0\u884C\uFF08\u4F7F\u7528\u9ED8\u8BA4 4099\uFF09"}`,
64227
+ heading: `env \xB7 ${running ? "\u2713 \u8FD0\u884C\u4E2D" : "\u2717 \u672A\u8FD0\u884C\uFF08\u4F7F\u7528\u9ED8\u8BA4 4099\uFF09"}`,
63813
64228
  meta: [`baseUrl: \`${baseUrl}\``],
63814
64229
  data: [
63815
64230
  renderKv(entries.map((e) => ({ key: e.key, value: e.value }))),
@@ -64109,14 +64524,8 @@ defineSchemaCommand({
64109
64524
  ],
64110
64525
  fn: async ({ values, ctx }) => {
64111
64526
  const { entry, model, prompt, stream: streamMode, timeout: timeoutSec } = values;
64112
- await cleanupIfStale();
64113
- const state = readRuntimeState();
64114
- if (!state) {
64115
- throw new CliError("SERVICE_NOT_RUNNING", "\u670D\u52A1\u672A\u8FD0\u884C", {
64116
- hint: "`local-router start --daemon`"
64117
- });
64118
- }
64119
- const payload = buildTryPayload(entry, model, prompt, state.baseUrl);
64527
+ const target = await requireTarget(ctx);
64528
+ const payload = buildTryPayload(entry, model, prompt, target.baseUrl);
64120
64529
  const controller = new AbortController;
64121
64530
  const timer = setTimeout(() => controller.abort(), timeoutSec * 1000);
64122
64531
  const startedAt = Date.now();
@@ -64254,35 +64663,6 @@ defineSchemaCommand({
64254
64663
  import { readFileSync as readFileSync11 } from "fs";
64255
64664
  import { fileURLToPath as fileURLToPath2 } from "url";
64256
64665
 
64257
- // src/cli/asset-paths.ts
64258
- async function findExisting(...candidates) {
64259
- for (const url2 of candidates) {
64260
- try {
64261
- const exists = await Bun.file(url2).exists();
64262
- if (exists)
64263
- return url2;
64264
- } catch {}
64265
- }
64266
- return null;
64267
- }
64268
- async function readPackageJson() {
64269
- const url2 = await findExisting(new URL("../../package.json", import.meta.url), new URL("../package.json", import.meta.url), new URL("../../../package.json", import.meta.url));
64270
- if (!url2)
64271
- return null;
64272
- try {
64273
- return await Bun.file(url2).json();
64274
- } catch {
64275
- return null;
64276
- }
64277
- }
64278
- async function findConfigSchemaUrl() {
64279
- return findExisting(new URL("../../config.schema.json", import.meta.url), new URL("../config.schema.json", import.meta.url), new URL("../../../config.schema.json", import.meta.url));
64280
- }
64281
- async function readVersionString() {
64282
- const pkg = await readPackageJson();
64283
- return typeof pkg?.version === "string" ? pkg.version : "unknown";
64284
- }
64285
-
64286
64666
  // src/cli/error-docs.ts
64287
64667
  var ERROR_DOCS = {
64288
64668
  USAGE_ERROR: {
@@ -64375,6 +64755,16 @@ var ERROR_DOCS = {
64375
64755
  cause: "\u547D\u4EE4\u7F3A\u5C11\u5FC5\u586B flag\uFF0C\u672C\u5E94\u5F39\u51FA\u9009\u62E9\u5668\u4F46\u5F53\u524D stdin \u4E0D\u662F TTY \u6216\u663E\u5F0F `--no-interactive`\u3002",
64376
64756
  fix: "\u5728\u9519\u8BEF\u7684 `details` \u91CC\u67E5\u770B\u5019\u9009\u9879\uFF0C\u663E\u5F0F\u8865 `--provider/--model` \u7B49\u3002"
64377
64757
  },
64758
+ TARGET_NOT_FOUND: {
64759
+ summary: "\u627E\u4E0D\u5230\u53EF\u8FDE\u63A5\u7684 local-router",
64760
+ cause: "\u9ED8\u8BA4\u7AEF\u53E3 4099 \u4E0A\u6CA1\u6709 local-router\uFF0COS \u8FDB\u7A0B\u679A\u4E3E\u4E5F\u672A\u53D1\u73B0\u5176\u5B83\u5B9E\u4F8B\u3002",
64761
+ fix: "`local-router start` \u542F\u52A8\uFF1B\u6216\u7528 `--port <port>` / `--url <url>` \u6307\u5B9A\u76EE\u6807\u3002"
64762
+ },
64763
+ TARGET_UNREACHABLE: {
64764
+ summary: "\u6307\u5B9A\u7684\u76EE\u6807\u65E0\u6CD5\u8FDE\u901A",
64765
+ cause: "`--port` / `--url`\uFF08\u6216\u73AF\u5883\u53D8\u91CF\uFF09\u6307\u5411\u7684\u5730\u5740\u6CA1\u6709\u54CD\u5E94 `/api/health` \u6216\u4E0D\u662F local-router\u3002",
64766
+ fix: "\u786E\u8BA4\u76EE\u6807\u7AEF\u53E3/\u5730\u5740\u6B63\u786E\u4E14 local-router \u6B63\u5728\u8BE5\u5730\u5740\u76D1\u542C\u3002"
64767
+ },
64378
64768
  UNKNOWN_ERROR: {
64379
64769
  summary: "\u672A\u77E5\u9519\u8BEF",
64380
64770
  cause: "\u6CA1\u6709\u5339\u914D\u5230\u4EFB\u4F55 CliError \u7C7B\u578B\u3002",
@@ -64821,13 +65211,13 @@ defineSchemaCommand({
64821
65211
 
64822
65212
  // src/cli/handlers/lifecycle.ts
64823
65213
  init_config();
64824
- import { existsSync as existsSync15, readFileSync as readFileSync12 } from "fs";
64825
- import { setTimeout as sleep3 } from "timers/promises";
64826
65214
  init_errors();
64827
65215
  init_output();
64828
65216
  init_process();
64829
65217
  init_registry();
64830
65218
  init_runtime();
65219
+ import { existsSync as existsSync15, readFileSync as readFileSync12 } from "fs";
65220
+ import { setTimeout as sleep3 } from "timers/promises";
64831
65221
 
64832
65222
  // src/cli/wait.ts
64833
65223
  init_errors();
@@ -65046,34 +65436,34 @@ defineSchemaCommand({
65046
65436
  fn: async ({ values, ctx }) => {
65047
65437
  const retry = Math.max(1, values.retry || 1);
65048
65438
  const interval = Math.max(0, values["retry-interval"] || 1);
65049
- await cleanupIfStale();
65050
- const state = readRuntimeState();
65051
- if (!state) {
65052
- throw new CliError("SERVICE_NOT_RUNNING", "\u670D\u52A1\u672A\u8FD0\u884C", {
65053
- hint: "\u542F\u52A8: `local-router start --daemon`"
65054
- });
65055
- }
65439
+ const target = guessTargetUrl(ctx.flags);
65440
+ const baseUrl = target.baseUrl;
65056
65441
  let ok = false;
65057
65442
  for (let i = 0;i < retry; i++) {
65058
- ok = await checkHealth(state.baseUrl);
65443
+ ok = await checkHealth(baseUrl);
65059
65444
  if (ok)
65060
65445
  break;
65061
65446
  if (i < retry - 1)
65062
65447
  await sleep3(interval * 1000);
65063
65448
  }
65064
65449
  if (!ok) {
65065
- throw new CliError("HEALTH_FAILED", `\u5065\u5EB7\u68C0\u67E5\u5931\u8D25: ${state.baseUrl}/api/health`, {
65066
- details: { baseUrl: state.baseUrl, retries: retry }
65450
+ if (!target.running && !ctx.flags.target) {
65451
+ throw new CliError("SERVICE_NOT_RUNNING", "\u670D\u52A1\u672A\u8FD0\u884C", {
65452
+ hint: "\u542F\u52A8: `local-router start --daemon`"
65453
+ });
65454
+ }
65455
+ throw new CliError("HEALTH_FAILED", `\u5065\u5EB7\u68C0\u67E5\u5931\u8D25: ${baseUrl}/api/health`, {
65456
+ details: { baseUrl, retries: retry }
65067
65457
  });
65068
65458
  }
65069
65459
  emitResult(ctx, {
65070
65460
  command: "health",
65071
- data: { ok: true, baseUrl: state.baseUrl },
65461
+ data: { ok: true, baseUrl },
65072
65462
  md: {
65073
65463
  heading: "health \xB7 \u2713 ok",
65074
- meta: [`\u5730\u5740 ${state.baseUrl}`]
65464
+ meta: [`\u5730\u5740 ${baseUrl}`]
65075
65465
  },
65076
- text: `\u5065\u5EB7\u68C0\u67E5\u901A\u8FC7: ${state.baseUrl}`
65466
+ text: `\u5065\u5EB7\u68C0\u67E5\u901A\u8FC7: ${baseUrl}`
65077
65467
  });
65078
65468
  }
65079
65469
  });
@@ -65187,22 +65577,10 @@ defineSchemaCommand({
65187
65577
  // src/cli/handlers/logs.ts
65188
65578
  init_errors();
65189
65579
  init_output();
65190
- init_process();
65191
65580
  init_registry();
65192
- init_runtime();
65193
- async function requireRunning() {
65194
- await cleanupIfStale();
65195
- const state = readRuntimeState();
65196
- if (!state) {
65197
- throw new CliError("SERVICE_NOT_RUNNING", "\u65E5\u5FD7\u67E5\u8BE2\u9700\u8981\u670D\u52A1\u8FD0\u884C", {
65198
- hint: "`local-router start --daemon`"
65199
- });
65200
- }
65201
- const ok = await checkHealth(state.baseUrl);
65202
- if (!ok) {
65203
- throw new CliError("HEALTH_FAILED", `\u670D\u52A1\u5065\u5EB7\u68C0\u67E5\u5931\u8D25: ${state.baseUrl}`);
65204
- }
65205
- return { baseUrl: state.baseUrl };
65581
+ async function requireRunning(ctx) {
65582
+ const t = await requireTarget(ctx);
65583
+ return { baseUrl: t.baseUrl };
65206
65584
  }
65207
65585
  async function fetchJson(url2, init) {
65208
65586
  const res = await fetch(url2, init);
@@ -65250,7 +65628,7 @@ defineSchemaCommand({
65250
65628
  { name: "cursor", type: "string", description: "\u5206\u9875\u6E38\u6807" }
65251
65629
  ],
65252
65630
  fn: async ({ values, ctx }) => {
65253
- const { baseUrl } = await requireRunning();
65631
+ const { baseUrl } = await requireRunning(ctx);
65254
65632
  const params = new URLSearchParams;
65255
65633
  const set2 = (k, v) => {
65256
65634
  if (v !== undefined && v !== null && v !== "" && v !== false) {
@@ -65315,7 +65693,7 @@ defineSchemaCommand({
65315
65693
  const id = positionals[0];
65316
65694
  if (!id)
65317
65695
  throw new CliError("USAGE_ERROR", "\u7528\u6CD5: logs event <id>");
65318
- const { baseUrl } = await requireRunning();
65696
+ const { baseUrl } = await requireRunning(ctx);
65319
65697
  const params = new URLSearchParams;
65320
65698
  if (values["include-stream"])
65321
65699
  params.set("includeStream", "true");
@@ -65361,7 +65739,7 @@ defineSchemaCommand({
65361
65739
  }
65362
65740
  ],
65363
65741
  fn: async ({ values, ctx }) => {
65364
- const { baseUrl } = await requireRunning();
65742
+ const { baseUrl } = await requireRunning(ctx);
65365
65743
  const w = values.window ?? "24h";
65366
65744
  const { status, json: json3 } = await fetchJson(`${baseUrl}/api/logs/events?window=${w}&hasError=true&limit=1&sort=time_desc`);
65367
65745
  if (status !== 200)
@@ -65413,7 +65791,7 @@ defineSchemaCommand({
65413
65791
  }
65414
65792
  ],
65415
65793
  fn: async ({ values, ctx }) => {
65416
- const { baseUrl } = await requireRunning();
65794
+ const { baseUrl } = await requireRunning(ctx);
65417
65795
  const w = values.window ?? "24h";
65418
65796
  const { status, json: json3 } = await fetchJson(`${baseUrl}/api/metrics/logs?window=${w}`);
65419
65797
  if (status !== 200) {
@@ -65458,7 +65836,7 @@ defineSchemaCommand({
65458
65836
  supportsJson: true,
65459
65837
  requiresRunning: true,
65460
65838
  fn: async ({ ctx }) => {
65461
- const { baseUrl } = await requireRunning();
65839
+ const { baseUrl } = await requireRunning(ctx);
65462
65840
  const { status, json: json3 } = await fetchJson(`${baseUrl}/api/logs/storage`);
65463
65841
  if (status !== 200) {
65464
65842
  throw new CliError("UNKNOWN_ERROR", `\u83B7\u53D6 storage \u5931\u8D25: ${status}`, { details: json3 });
@@ -65487,7 +65865,7 @@ defineSchemaCommand({
65487
65865
  { name: "limit", type: "number", default: 50, description: "\u6700\u5927 session \u6570" }
65488
65866
  ],
65489
65867
  fn: async ({ values, ctx }) => {
65490
- const { baseUrl } = await requireRunning();
65868
+ const { baseUrl } = await requireRunning(ctx);
65491
65869
  const params = new URLSearchParams;
65492
65870
  params.set("window", values.window ?? "24h");
65493
65871
  if (values.user)
@@ -65501,7 +65879,10 @@ defineSchemaCommand({
65501
65879
  emitResult(ctx, {
65502
65880
  command: "logs.sessions",
65503
65881
  data: json3,
65504
- md: { heading: "logs.sessions", data: renderCodeBlock(JSON.stringify(json3, null, 2), "json") }
65882
+ md: {
65883
+ heading: "logs.sessions",
65884
+ data: renderCodeBlock(JSON.stringify(json3, null, 2), "json")
65885
+ }
65505
65886
  });
65506
65887
  }
65507
65888
  });
@@ -65511,7 +65892,7 @@ defineSchemaCommand({
65511
65892
  supportsJson: true,
65512
65893
  requiresRunning: true,
65513
65894
  fn: async ({ ctx }) => {
65514
- const { baseUrl } = await requireRunning();
65895
+ const { baseUrl } = await requireRunning(ctx);
65515
65896
  const stream = startStream(ctx, "logs.tail");
65516
65897
  const res = await fetch(`${baseUrl}/api/logs/tail`, {
65517
65898
  headers: { accept: "text/event-stream" }
@@ -65573,8 +65954,8 @@ defineSchemaCommand({
65573
65954
  { name: "from", type: "string", description: "ISO \u8D77\u70B9" },
65574
65955
  { name: "to", type: "string", description: "ISO \u7EC8\u70B9" }
65575
65956
  ],
65576
- fn: async ({ values }) => {
65577
- const { baseUrl } = await requireRunning();
65957
+ fn: async ({ values, ctx }) => {
65958
+ const { baseUrl } = await requireRunning(ctx);
65578
65959
  const params = new URLSearchParams;
65579
65960
  params.set("format", values.format ?? "jsonl");
65580
65961
  params.set("window", values.window ?? "24h");
@@ -65605,7 +65986,7 @@ defineSchemaCommand({
65605
65986
  const id = positionals[0];
65606
65987
  if (!id)
65607
65988
  throw new CliError("USAGE_ERROR", "\u7528\u6CD5: logs replay <event-id>");
65608
- const { baseUrl } = await requireRunning();
65989
+ const { baseUrl } = await requireRunning(ctx);
65609
65990
  const detailRes = await fetchJson(`${baseUrl}/api/logs/events/${encodeURIComponent(id)}`);
65610
65991
  if (detailRes.status === 404)
65611
65992
  throw new CliError("ROUTE_NOT_FOUND", `\u4E8B\u4EF6\u4E0D\u5B58\u5728: ${id}`);
@@ -65658,7 +66039,6 @@ defineSchemaCommand({
65658
66039
  init_config();
65659
66040
  init_errors();
65660
66041
  init_output();
65661
- init_process();
65662
66042
  init_registry();
65663
66043
  init_runtime();
65664
66044
  import { existsSync as existsSync16, rmSync as rmSync5 } from "fs";
@@ -65673,15 +66053,9 @@ async function fetchJson2(url2) {
65673
66053
  } catch {}
65674
66054
  return { status: res.status, json: json3 };
65675
66055
  }
65676
- async function requireBaseUrl() {
65677
- await cleanupIfStale();
65678
- const state = readRuntimeState();
65679
- if (!state)
65680
- throw new CliError("SERVICE_NOT_RUNNING", "\u670D\u52A1\u672A\u8FD0\u884C");
65681
- if (!await checkHealth(state.baseUrl)) {
65682
- throw new CliError("HEALTH_FAILED", `\u670D\u52A1\u5065\u5EB7\u68C0\u67E5\u5931\u8D25: ${state.baseUrl}`);
65683
- }
65684
- return state.baseUrl;
66056
+ async function requireBaseUrl(ctx) {
66057
+ const t = await requireTarget(ctx);
66058
+ return t.baseUrl;
65685
66059
  }
65686
66060
  defineSchemaCommand({
65687
66061
  name: "logs tokens",
@@ -65698,7 +66072,7 @@ defineSchemaCommand({
65698
66072
  }
65699
66073
  ],
65700
66074
  fn: async ({ values, ctx }) => {
65701
- const baseUrl = await requireBaseUrl();
66075
+ const baseUrl = await requireBaseUrl(ctx);
65702
66076
  const { status, json: json3 } = await fetchJson2(`${baseUrl}/api/logs/events?window=${values.window}&limit=1`);
65703
66077
  if (status !== 200) {
65704
66078
  throw new CliError("UNKNOWN_ERROR", `\u67E5\u8BE2\u5931\u8D25: ${status}`, { details: json3 });
@@ -65763,7 +66137,7 @@ defineSchemaCommand({
65763
66137
  }
65764
66138
  ],
65765
66139
  fn: async ({ values, ctx }) => {
65766
- const baseUrl = await requireBaseUrl();
66140
+ const baseUrl = await requireBaseUrl(ctx);
65767
66141
  let rateTable = {};
65768
66142
  if (values["rate-table"]) {
65769
66143
  try {
@@ -66253,6 +66627,59 @@ defineSchemaCommand({
66253
66627
  }
66254
66628
  });
66255
66629
 
66630
+ // src/cli/handlers/target.ts
66631
+ init_output();
66632
+ init_registry();
66633
+ var SOURCE_LABEL = {
66634
+ flag: "\u547D\u4EE4\u884C --port/--url/--host",
66635
+ env: "\u73AF\u5883\u53D8\u91CF LOCAL_ROUTER_URL/PORT",
66636
+ runtime: "\u672C\u673A daemon (status.json)",
66637
+ default: "\u9ED8\u8BA4\u7AEF\u53E3 4099",
66638
+ discovered: "OS \u8FDB\u7A0B\u679A\u4E3E\u53D1\u73B0",
66639
+ prompt: "\u4EA4\u4E92\u8F93\u5165"
66640
+ };
66641
+ defineSchemaCommand({
66642
+ name: "target",
66643
+ summary: "\u663E\u793A\u5F53\u524D\u89E3\u6790\u5230\u7684 local-router \u76EE\u6807\uFF08\u7AEF\u53E3/\u6765\u6E90/\u7248\u672C\uFF09",
66644
+ supportsJson: true,
66645
+ requiresRunning: false,
66646
+ flags: [],
66647
+ fn: async ({ ctx }) => {
66648
+ const t = await resolveTarget(ctx.flags);
66649
+ const candidates = ctx.flags.verbose ? await discoverLocalRouters() : [];
66650
+ emitResult(ctx, {
66651
+ command: "target",
66652
+ data: {
66653
+ baseUrl: t.baseUrl,
66654
+ host: t.host,
66655
+ port: t.port,
66656
+ version: t.version ?? null,
66657
+ source: t.source,
66658
+ candidates: ctx.flags.verbose ? candidates.map((c) => ({ port: c.port, version: c.version ?? null })) : undefined
66659
+ },
66660
+ md: {
66661
+ heading: `target \xB7 ${t.host}:${t.port}${t.version ? ` (v${t.version})` : ""}`,
66662
+ meta: [`\u6765\u6E90: ${SOURCE_LABEL[t.source] ?? t.source}`],
66663
+ data: [
66664
+ renderKv([
66665
+ { key: "baseUrl", value: t.baseUrl },
66666
+ { key: "host", value: t.host },
66667
+ { key: "port", value: t.port },
66668
+ { key: "version", value: t.version ?? "unknown" },
66669
+ { key: "source", value: t.source }
66670
+ ]),
66671
+ ctx.flags.verbose ? `**\u53D1\u73B0\u7684\u5B9E\u4F8B**
66672
+
66673
+ ${candidates.length > 0 ? renderTable(["port", "version"], candidates.map((c) => [String(c.port), c.version ?? "?"])) : "\uFF08\u672A\u53D1\u73B0\u5176\u5B83\u5B9E\u4F8B\uFF09"}` : ""
66674
+ ].filter(Boolean).join(`
66675
+
66676
+ `)
66677
+ },
66678
+ text: t.baseUrl
66679
+ });
66680
+ }
66681
+ });
66682
+
66256
66683
  // src/cli.ts
66257
66684
  init_output();
66258
66685
  init_parse_args();