@remodex/rmx 1.0.3 → 1.0.5

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.
@@ -16,8 +16,8 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-Cy432rMC.js"></script>
20
- <link rel="stylesheet" crossorigin href="/assets/index-CZqebSPQ.css">
19
+ <script type="module" crossorigin src="/assets/index-B-jlbgno.js"></script>
20
+ <link rel="stylesheet" crossorigin href="/assets/index-1SDbgh2-.css">
21
21
  </head>
22
22
  <body>
23
23
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remodex/rmx",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Remodex universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
5
5
  "displayName": "Remodex",
6
6
  "type": "module",
@@ -13,10 +13,10 @@
13
13
  "./package.json": "./package.json"
14
14
  },
15
15
  "bin": {
16
- "rmx": "./bin/ocx.mjs",
17
- "remodex": "./bin/ocx.mjs",
18
- "opencodex": "./bin/ocx.mjs",
19
- "ocx": "./bin/ocx.mjs"
16
+ "rmx": "bin/ocx.mjs",
17
+ "remodex": "bin/ocx.mjs",
18
+ "opencodex": "bin/ocx.mjs",
19
+ "ocx": "bin/ocx.mjs"
20
20
  },
21
21
  "files": [
22
22
  "bin",
@@ -8,7 +8,23 @@ import { resolveCloudflared, type CloudflaredExecutable } from "./cloudflared";
8
8
  const QUICK_TUNNEL_URL = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/iu;
9
9
  const QUICK_URL_TIMEOUT_MS = 30_000;
10
10
  const VERIFY_TIMEOUT_MS = 5_000;
11
- const RETRY_DELAYS_MS = [1_000, 2_500, 5_000, 10_000, 30_000] as const;
11
+ const PROCESS_RETRY_DELAYS_MS = [1_000, 2_500, 5_000, 10_000, 30_000] as const;
12
+ // A freshly-created Quick Tunnel URL and a newly-written Named Tunnel DNS route
13
+ // can be visible in cloudflared output before Cloudflare's edge can serve it.
14
+ // Keep that expected propagation neutral in the UI for almost two minutes. If
15
+ // it still is not reachable, expose the failure but continue probing the same
16
+ // child and URL so late propagation can recover without generating a new URL.
17
+ const VERIFY_PROPAGATION_DELAYS_MS = [
18
+ 1_000,
19
+ 2_000,
20
+ 3_000,
21
+ 5_000,
22
+ 8_000,
23
+ 13_000,
24
+ 20_000,
25
+ 30_000,
26
+ 30_000,
27
+ ] as const;
12
28
 
13
29
  export type AndroidRemoteCloudflareFailure =
14
30
  | "cloudflared_unavailable"
@@ -175,8 +191,8 @@ async function verifiedPublicGateway(
175
191
  try {
176
192
  const signal = AbortSignal.timeout(VERIFY_TIMEOUT_MS);
177
193
  const [healthResponse, descriptorResponse] = await Promise.all([
178
- fetchImpl(new URL("/healthz", baseUrl), { signal }),
179
- fetchImpl(new URL("/.well-known/t3/environment", baseUrl), { signal }),
194
+ fetchImpl(new URL("/healthz", baseUrl), { signal, cache: "no-store" }),
195
+ fetchImpl(new URL("/.well-known/t3/environment", baseUrl), { signal, cache: "no-store" }),
180
196
  ]);
181
197
  if (!healthResponse.ok || !descriptorResponse.ok) return false;
182
198
  const health = await healthResponse.json() as Record<string, unknown>;
@@ -218,7 +234,9 @@ export class ManagedAndroidRemoteCloudflareTunnel implements AndroidRemoteCloudf
218
234
  }
219
235
  return {
220
236
  mode: settings.tunnelMode,
221
- ...(settings.namedTunnelHostname ? { namedHostname: settings.namedTunnelHostname } : {}),
237
+ ...(settings.tunnelMode === "named" && settings.namedTunnelHostname
238
+ ? { namedHostname: settings.namedTunnelHostname }
239
+ : {}),
222
240
  hasNamedTunnelToken: this.hasNamedTunnelToken,
223
241
  };
224
242
  }
@@ -322,7 +340,7 @@ export class ManagedAndroidRemoteCloudflareTunnel implements AndroidRemoteCloudf
322
340
  publicUrl: null,
323
341
  error: result,
324
342
  });
325
- const delay = RETRY_DELAYS_MS[Math.min(attempt, RETRY_DELAYS_MS.length - 1)]!;
343
+ const delay = PROCESS_RETRY_DELAYS_MS[Math.min(attempt, PROCESS_RETRY_DELAYS_MS.length - 1)]!;
326
344
  attempt += 1;
327
345
  await this.deps.sleep(delay);
328
346
  }
@@ -444,7 +462,7 @@ export class ManagedAndroidRemoteCloudflareTunnel implements AndroidRemoteCloudf
444
462
  return "tunnel_failed";
445
463
  }
446
464
  verificationFailures += 1;
447
- if (verificationFailures >= 5) {
465
+ if (verificationFailures > VERIFY_PROPAGATION_DELAYS_MS.length) {
448
466
  this.publish({
449
467
  mode,
450
468
  status: "error",
@@ -452,7 +470,9 @@ export class ManagedAndroidRemoteCloudflareTunnel implements AndroidRemoteCloudf
452
470
  error: "verification_failed",
453
471
  });
454
472
  }
455
- const delay = RETRY_DELAYS_MS[Math.min(verificationFailures - 1, RETRY_DELAYS_MS.length - 1)]!;
473
+ const delay = VERIFY_PROPAGATION_DELAYS_MS[
474
+ Math.min(verificationFailures - 1, VERIFY_PROPAGATION_DELAYS_MS.length - 1)
475
+ ]!;
456
476
  const wait = await Promise.race([
457
477
  this.deps.sleep(delay).then(() => "retry" as const),
458
478
  child.exited.then(() => "exit" as const),
@@ -473,7 +493,9 @@ export class DisabledAndroidRemoteCloudflareTunnel implements AndroidRemoteCloud
473
493
  async configuration(settings: AndroidRemoteSettings): Promise<AndroidRemoteCloudflareConfiguration> {
474
494
  return {
475
495
  mode: settings.tunnelMode,
476
- ...(settings.namedTunnelHostname ? { namedHostname: settings.namedTunnelHostname } : {}),
496
+ ...(settings.tunnelMode === "named" && settings.namedTunnelHostname
497
+ ? { namedHostname: settings.namedTunnelHostname }
498
+ : {}),
477
499
  hasNamedTunnelToken: false,
478
500
  };
479
501
  }
@@ -6,6 +6,8 @@ import { resolveCodexRuntime } from "../codex/runtime";
6
6
  const DEFAULT_CODEX_APP_SERVER_PORT = 10106;
7
7
  const START_TIMEOUT_MS = 12_000;
8
8
  const REQUEST_TIMEOUT_MS = 30_000;
9
+ const OWNED_PROCESS_TERM_GRACE_MS = 1_000;
10
+ const OWNED_PROCESS_KILL_GRACE_MS = 1_000;
9
11
 
10
12
  const APP_SERVER_PROFILE_KEYS = new Set([
11
13
  "model_provider",
@@ -353,6 +355,45 @@ async function waitUntilReady(port: number, child: Bun.Subprocess): Promise<void
353
355
  throw new Error("Codex task server did not become ready");
354
356
  }
355
357
 
358
+ type KillableSubprocess = Pick<Bun.Subprocess, "kill" | "exited">;
359
+
360
+ function waitForSubprocessExit(child: KillableSubprocess, timeoutMs: number): Promise<boolean> {
361
+ return new Promise(resolve => {
362
+ let settled = false;
363
+ let timer: ReturnType<typeof setTimeout> | undefined;
364
+ const finish = (exited: boolean): void => {
365
+ if (settled) return;
366
+ settled = true;
367
+ if (timer !== undefined) clearTimeout(timer);
368
+ resolve(exited);
369
+ };
370
+ timer = setTimeout(() => finish(false), Math.max(0, timeoutMs));
371
+ // A failed exit promise still means the child is no longer running. The
372
+ // rejection is consumed here so shutdown never creates an unhandled error.
373
+ void child.exited.then(() => finish(true), () => finish(true));
374
+ });
375
+ }
376
+
377
+ /**
378
+ * Stop a process that the current Remodex runtime explicitly spawned.
379
+ *
380
+ * Codex normally honors SIGTERM, but an app-server can be wedged while its
381
+ * listener is being torn down. Keep the wait bounded and escalate only for
382
+ * this caller-owned handle; callers must never pass a process they did not
383
+ * create and positively mark as owned.
384
+ */
385
+ export async function terminateOwnedCodexProcess(
386
+ child: KillableSubprocess,
387
+ termGraceMs = OWNED_PROCESS_TERM_GRACE_MS,
388
+ killGraceMs = OWNED_PROCESS_KILL_GRACE_MS,
389
+ ): Promise<void> {
390
+ try { child.kill("SIGTERM"); } catch { /* already exited */ }
391
+ if (await waitForSubprocessExit(child, termGraceMs)) return;
392
+
393
+ try { child.kill("SIGKILL"); } catch { /* already exited */ }
394
+ await waitForSubprocessExit(child, killGraceMs);
395
+ }
396
+
356
397
  export type AndroidCodexRuntimeStatus = {
357
398
  connected: boolean;
358
399
  port: number;
@@ -553,13 +594,8 @@ export class AndroidCodexRuntime {
553
594
  private async stopOwnedProcess(): Promise<void> {
554
595
  const child = this.child;
555
596
  this.child = null;
556
- if (child && this.ownedProcess) {
557
- try { child.kill(); } catch { /* already exited */ }
558
- await Promise.race([
559
- child.exited.catch(() => -1),
560
- new Promise(resolve => setTimeout(resolve, 1_000)),
561
- ]);
562
- }
597
+ const owned = child !== null && this.ownedProcess;
563
598
  this.ownedProcess = false;
599
+ if (owned) await terminateOwnedCodexProcess(child);
564
600
  }
565
601
  }
@@ -5,6 +5,7 @@ import { homedir, networkInterfaces, hostname as readHostname } from "node:os";
5
5
  import { basename, dirname, isAbsolute, join, parse, resolve } from "node:path";
6
6
  import type { Server, ServerWebSocket } from "bun";
7
7
  import { getConfigDir } from "../config";
8
+ import { redactSecretString } from "../lib/redact";
8
9
  import { commandInvocation } from "../lib/win-exec";
9
10
  import type { ManagementModelRow } from "../server/management/model-rows";
10
11
  import { modelSourceDisplayName } from "../model-sources";
@@ -351,6 +352,58 @@ function stringValue(value: unknown, maximum = 4096): string {
351
352
  return typeof value === "string" ? value.trim().slice(0, maximum) : "";
352
353
  }
353
354
 
355
+ /**
356
+ * Keep the lifecycle error actionable without echoing credentials or an
357
+ * unbounded exception string through the management API. Startup failures
358
+ * are otherwise indistinguishable from a transient tunnel failure, which
359
+ * makes `rmx onboard --verbose` misleading.
360
+ */
361
+ function startupErrorText(error: unknown): string {
362
+ const parts: string[] = [];
363
+ const seen = new Set<object>();
364
+ let current: unknown = error;
365
+ for (let depth = 0; depth < 4 && current !== undefined && current !== null; depth += 1) {
366
+ if ((typeof current === "object" || typeof current === "function") && current !== null) {
367
+ if (seen.has(current)) break;
368
+ seen.add(current);
369
+ }
370
+ if (current instanceof Error) {
371
+ if (current.message.trim()) parts.push(current.message.trim());
372
+ current = current.cause;
373
+ continue;
374
+ }
375
+ const object = record(current);
376
+ if (object) {
377
+ if (typeof object.message === "string" && object.message.trim()) {
378
+ parts.push(object.message.trim());
379
+ }
380
+ current = object.cause;
381
+ continue;
382
+ }
383
+ const text = String(current).trim();
384
+ if (text) parts.push(text);
385
+ break;
386
+ }
387
+ return parts.filter((part, index) => index === 0 || part !== parts[index - 1]).join(": ");
388
+ }
389
+
390
+ function gatewayStartupError(error: unknown): string {
391
+ const raw = startupErrorText(error);
392
+ const safe = redactSecretString(raw)
393
+ .replace(/[\u0000-\u001f\u007f]/gu, " ")
394
+ .replace(/\s+/gu, " ")
395
+ .trim()
396
+ .slice(0, 240);
397
+ const prefix = "Could not start the Android Remote gateway";
398
+ // Runtime adapters may already use the public prefix. Strip any repeated
399
+ // wrapper before adding one canonical, actionable message.
400
+ let detail = safe;
401
+ while (new RegExp(`^${prefix}(?::|\\s|$)`, "iu").test(detail)) {
402
+ detail = detail.slice(prefix.length).replace(/^:\s*/u, "").trim();
403
+ }
404
+ return detail ? `${prefix}: ${detail}` : prefix;
405
+ }
406
+
354
407
  function desktopConversationIsActive(value: unknown): boolean | null {
355
408
  const state = record(value);
356
409
  if (!state) return null;
@@ -1978,6 +2031,8 @@ export class AndroidRemoteGatewayController {
1978
2031
  private server: Server<GatewayWsData> | null = null;
1979
2032
  private codex: AndroidCodexClient | null = null;
1980
2033
  private unsubscribeCodex: (() => void) | null = null;
2034
+ /** All callers must await one physical gateway startup. */
2035
+ private startFlight: Promise<void> | null = null;
1981
2036
  private transition: Promise<void> = Promise.resolve();
1982
2037
  private readonly sockets = new Set<ServerWebSocket<GatewayWsData>>();
1983
2038
  private readonly socketsByClient = new Map<string, Set<ServerWebSocket<GatewayWsData>>>();
@@ -2371,7 +2426,19 @@ export class AndroidRemoteGatewayController {
2371
2426
  }
2372
2427
 
2373
2428
  async start(): Promise<void> {
2374
- if (this.gatewayStatus === "ready" || this.gatewayStatus === "starting") return;
2429
+ if (this.gatewayStatus === "ready") return;
2430
+ if (this.startFlight) return this.startFlight;
2431
+
2432
+ const flight = this.startInternal();
2433
+ this.startFlight = flight;
2434
+ try {
2435
+ await flight;
2436
+ } finally {
2437
+ if (this.startFlight === flight) this.startFlight = null;
2438
+ }
2439
+ }
2440
+
2441
+ private async startInternal(): Promise<void> {
2375
2442
  this.gatewayStatus = "starting";
2376
2443
  this.statusError = undefined;
2377
2444
  try {
@@ -2396,15 +2463,17 @@ export class AndroidRemoteGatewayController {
2396
2463
  for (const remoteThreadId of this.queuedTurns.keys()) {
2397
2464
  void this.startNextQueuedTurn(remoteThreadId).catch(() => undefined);
2398
2465
  }
2399
- } catch {
2466
+ } catch (error) {
2400
2467
  this.gatewayStatus = "error";
2401
- this.statusError = "Could not start the Android Remote gateway";
2468
+ this.statusError = gatewayStartupError(error);
2402
2469
  await this.stopResources();
2403
- throw new Error(this.statusError);
2470
+ throw new Error(this.statusError, { cause: error });
2404
2471
  }
2405
2472
  }
2406
2473
 
2407
2474
  async stop(): Promise<void> {
2475
+ const starting = this.startFlight;
2476
+ if (starting) await starting.catch(() => undefined);
2408
2477
  await this.stopResources();
2409
2478
  this.gatewayStatus = "stopped";
2410
2479
  this.statusError = undefined;
package/src/cli/help.ts CHANGED
@@ -20,6 +20,15 @@ function canonicalCommand(value: string): string {
20
20
  }
21
21
 
22
22
  const helpEntries: Record<string, HelpEntry> = {
23
+ onboard: {
24
+ usage: "rmx onboard [--verbose] [--json] [--no-open]",
25
+ summary: "Configure Codex, Android Remote, the background service, and a verified phone link.",
26
+ details: [
27
+ "Safe to rerun: existing providers and custom-domain tunnels are preserved.",
28
+ "Fresh users get Quick Tunnel; Windows also installs the status tray.",
29
+ "--verbose shows inner command diagnostics; --json implies --no-open.",
30
+ ],
31
+ },
23
32
  init: { usage: "rmx init", summary: "Interactive setup for providers and Codex config injection." },
24
33
  setup: { usage: "rmx setup", summary: "Interactive setup for providers and Codex config injection (alias of init)." },
25
34
  start: { usage: "rmx start [--port <port>]", summary: "Start the proxy server and sync models to Codex." },
@@ -104,7 +113,10 @@ const helpEntries: Record<string, HelpEntry> = {
104
113
  },
105
114
  login: { usage: "rmx login <provider>", summary: "OAuth or API-key login for a provider." },
106
115
  logout: { usage: "rmx logout <provider>", summary: "Remove a stored provider login." },
107
- gui: { usage: "rmx gui", summary: "Open the Remodex dashboard." },
116
+ gui: {
117
+ usage: "rmx gui [--update]",
118
+ summary: "Open the Remodex dashboard; --update opens the npm package updater.",
119
+ },
108
120
  update: {
109
121
  usage: "rmx update [--tag latest|preview]",
110
122
  summary: "Update Remodex. Preview installs stay on the preview tag unless overridden.",
@@ -198,16 +210,15 @@ const helpEntries: Record<string, HelpEntry> = {
198
210
  grok: { usage: "rmx grok <status|exclude|include|set|clear|apply> ...", summary: "Manage and apply the Grok Build model fence." },
199
211
  integration: { usage: "rmx integration <claude|grok|client> ...", summary: "Manage supported client integrations." },
200
212
  system: {
201
- usage: "rmx system <status|settings|startup|diagnostics|sync|update|desktop-update> ...",
213
+ usage: "rmx system <status|settings|startup|diagnostics|sync|update> ...",
202
214
  summary: "Manage headless runtime settings, startup, sync, diagnostics, and updates.",
203
215
  details: [
216
+ "update check [--channel latest|preview] Check the @remodex/rmx package on npm.",
217
+ "update run [--channel latest|preview] --yes Install the npm package update.",
204
218
  "update auto on [--channel latest|preview] Enable the daily unattended package updater (enabled by default for global installs).",
205
219
  "update auto off Disable unattended package updates.",
206
220
  "update auto status Show scheduler, last result, and rollback information.",
207
- "desktop-update check [--channel latest|preview] Check the native desktop release manifest.",
208
- "desktop-update run [--channel latest|preview] Download and verify a native desktop update.",
209
- "desktop-update status Show shared tray/dashboard update state.",
210
- "Headless commands never install a native package; installation requires the desktop shell.",
221
+ "Changelogs come from the matching GitHub Release (v<version>).",
211
222
  ],
212
223
  },
213
224
  config: {
@@ -303,6 +314,7 @@ export function printUsage(): void {
303
314
 
304
315
  Usage:
305
316
  rmx Install/update and start the background service
317
+ rmx onboard Complete first-time setup and open the phone pairing QR
306
318
  rmx setup Interactive setup (alias: init)
307
319
  rmx start [--port <port>] Start the proxy server (auto-syncs models to Codex)
308
320
  rmx stop Stop the proxy AND restore native Codex (plain codex works again)
@@ -349,6 +361,7 @@ Usage:
349
361
 
350
362
  Examples:
351
363
  rmx Install/update and start the background service
364
+ rmx onboard Set up Codex, the service, and Android Remote
352
365
  rmx init Set up provider and inject into Codex
353
366
  rmx start Start on default port (10100)
354
367
  rmx start --port 8080 Start on custom port
package/src/cli/index.ts CHANGED
@@ -59,15 +59,6 @@ import { maybeShowUpdatePrompt } from "../update/notify";
59
59
  import { syncModelsToCodex } from "../codex/sync";
60
60
  import { setIntegrationEnabled, shouldSyncCodexOnStart, shouldSyncGrokOnStart, syncCodexOnStartIfEnabled } from "../codex/desired-state";
61
61
  import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job";
62
- import { currentVersion } from "../update/index";
63
- import {
64
- desktopUpdateInstallAction,
65
- readDesktopUpdateState,
66
- runAutomaticDesktopUpdateCheck,
67
- runDesktopUpdateWorker,
68
- startDesktopUpdateJob,
69
- type DesktopReleaseChannel,
70
- } from "../update/desktop-release";
71
62
  import { collectOrcaCodexHomeDiagnostic } from "../codex/home";
72
63
  import { removeOwnedConfigState } from "../lib/config-ownership";
73
64
  import { withProcessRuntimeProvenance } from "../lib/bun-runtime";
@@ -663,56 +654,6 @@ async function handleTrayProxyRestart(): Promise<void> {
663
654
  await handleProxyRestart(() => handleTrayProxyStart(false));
664
655
  }
665
656
 
666
- /**
667
- * Fixed desktop-shell action used by the tray. A first click checks and
668
- * downloads a verified artifact; once the tray shows "ready", the next click
669
- * hands it to the platform installer. No caller-selected command or path is
670
- * accepted here.
671
- */
672
- async function handleDesktopUpdateRequest(requestArgs: string[] = []): Promise<void> {
673
- let expectedReady: { id: string; channel: DesktopReleaseChannel } | undefined;
674
- if (requestArgs.length > 0) {
675
- const [mode, id, channel] = requestArgs;
676
- if (
677
- requestArgs.length !== 3
678
- || mode !== "--install-ready"
679
- || !id
680
- || !/^desktop-\d+-[a-z0-9]{1,32}$/.test(id)
681
- || (channel !== "latest" && channel !== "preview")
682
- ) {
683
- console.error("Invalid desktop update request.");
684
- process.exitCode = 64;
685
- return;
686
- }
687
- expectedReady = { id, channel };
688
- }
689
- const previous = readDesktopUpdateState();
690
- const install = expectedReady !== undefined || desktopUpdateInstallAction(previous);
691
- const channel: DesktopReleaseChannel = expectedReady?.channel
692
- ?? previous?.channel
693
- ?? (currentVersion().includes("-") ? "preview" : "latest");
694
- try {
695
- const job = startDesktopUpdateJob(channel, {
696
- install,
697
- ...(expectedReady ? { expectedReady } : {}),
698
- });
699
- console.log(
700
- install
701
- ? `Desktop update installation started (${job.latestVersion ?? "latest"}).`
702
- : "Desktop update check started.",
703
- );
704
- } catch (error) {
705
- console.error(error instanceof Error ? error.message : "Desktop update could not start.");
706
- process.exitCode = 1;
707
- }
708
- }
709
-
710
- async function handleAutomaticDesktopUpdateCheck(): Promise<void> {
711
- const channel: DesktopReleaseChannel = currentVersion().includes("-") ? "preview" : "latest";
712
- const result = await runAutomaticDesktopUpdateCheck(channel);
713
- console.log(JSON.stringify(result));
714
- }
715
-
716
657
  async function handleRestartStartWhenStopped(): Promise<boolean | "skipped"> {
717
658
  if (!codexAutoStartEnabled(loadConfig())) {
718
659
  console.log("Codex autostart is disabled; no proxy was started.");
@@ -1080,6 +1021,11 @@ async function handleReady(args: ReadyArgs): Promise<never> {
1080
1021
  }
1081
1022
 
1082
1023
  switch (command) {
1024
+ case "onboard": {
1025
+ const { runOnboardCommand } = await import("./onboard");
1026
+ process.exitCode = await runOnboardCommand(args.slice(1));
1027
+ break;
1028
+ }
1083
1029
  case "init":
1084
1030
  case "setup": {
1085
1031
  const { runInit } = await import("./init");
@@ -1272,6 +1218,13 @@ switch (command) {
1272
1218
  break;
1273
1219
  }
1274
1220
  case "gui": {
1221
+ const guiArgs = args.slice(1);
1222
+ const openUpdate = guiArgs.length === 1 && guiArgs[0] === "--update";
1223
+ if (guiArgs.length > (openUpdate ? 1 : 0)) {
1224
+ console.error("Usage: rmx gui [--update]");
1225
+ process.exitCode = 64;
1226
+ break;
1227
+ }
1275
1228
  const cfg = await import("../config");
1276
1229
  const config = cfg.loadConfig();
1277
1230
  // Identity-checked liveness (not the pid file + a fixed sleep): finds a fallback-port
@@ -1295,7 +1248,7 @@ switch (command) {
1295
1248
  // Open the host the proxy actually binds — `localhost` only answers for
1296
1249
  // loopback/wildcard binds, not a concrete LAN/IPv6 hostname.
1297
1250
  const guiHost = probeHostname(live?.hostname ?? config.hostname);
1298
- const guiUrl = `http://${guiHost === "127.0.0.1" ? "localhost" : guiHost}:${live?.port ?? config.port}`;
1251
+ const guiUrl = `http://${guiHost === "127.0.0.1" ? "localhost" : guiHost}:${live?.port ?? config.port}${openUpdate ? "/#dashboard/update" : ""}`;
1299
1252
  console.log(`Opening ${guiUrl}`);
1300
1253
  const { openUrl } = await import("../lib/open-url");
1301
1254
  openUrl(guiUrl);
@@ -1370,19 +1323,6 @@ switch (command) {
1370
1323
  if (!result.ok) process.exitCode = 1;
1371
1324
  break;
1372
1325
  }
1373
- case "__desktop-update-worker": {
1374
- const id = args[1];
1375
- const mode = args[2];
1376
- if (!id || !/^desktop-\d+-[a-z0-9]+$/.test(id) || (mode !== "download" && mode !== "install")) {
1377
- console.error("Invalid desktop update worker arguments.");
1378
- process.exitCode = 64;
1379
- break;
1380
- }
1381
- await runDesktopUpdateWorker(id, mode === "install");
1382
- break;
1383
- }
1384
- case "__desktop-update":
1385
- case "__desktop-auto-update-check":
1386
1326
  case "__tray-start":
1387
1327
  case "__tray-restart":
1388
1328
  case "__startup-health":
@@ -1391,8 +1331,6 @@ switch (command) {
1391
1331
  await dispatchInternalCliCommand(command as InternalCliCommand, {
1392
1332
  trayStart: async () => { await handleTrayProxyStart(); },
1393
1333
  trayRestart: handleTrayProxyRestart,
1394
- desktopUpdate: () => handleDesktopUpdateRequest(args.slice(1)),
1395
- desktopAutoUpdateCheck: handleAutomaticDesktopUpdateCheck,
1396
1334
  startupHealth: async () => {
1397
1335
  const { collectStartupHealth } = await import("../codex/autostart-health");
1398
1336
  console.log(JSON.stringify(collectStartupHealth(loadConfig())));
package/src/cli/init.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as readline from "node:readline";
2
2
  import { constants as fsConstants, copyFileSync, existsSync, readFileSync, unlinkSync } from "node:fs";
3
3
  import { injectCodexConfig } from "../codex/inject";
4
+ import { CODEX_CONFIG_PATH } from "../codex/paths";
4
5
  import { classifyOpenAiTierBackup, getConfigPath, getDefaultConfig, isValidProviderName, saveConfig } from "../config";
5
6
  import { enrichProviderFromCatalog } from "../oauth/key-providers";
6
7
  import { deriveInitProviders } from "../providers/derive";
@@ -189,13 +190,18 @@ export async function runInit(): Promise<void> {
189
190
  // preserved by renaming it out of the collision path (sol review 260722).
190
191
  cleanupOpenAiTierBackupAfterInit();
191
192
  console.log(`\n✅ Config saved to ${getConfigPath()}`);
193
+ console.log(` Codex config target: ${CODEX_CONFIG_PATH}`);
192
194
  if (oauthHint) console.log(`🔐 Authenticate this provider with: rmx login ${providerName}`);
193
195
 
194
196
  const injectAnswer = await prompt.ask("Inject into Codex config.toml? [Y/n]: ");
195
197
  if (injectAnswer.trim().toLowerCase() !== "n") {
196
198
  console.log("Fetching available models from provider...");
197
199
  const result = await injectCodexConfig(port, config);
198
- console.log(result.success ? `✅ ${result.message}` : `⚠️ ${result.message}`);
200
+ const routed = result.success && result.routingApplied !== false;
201
+ console.log(routed ? `✅ ${result.message}` : `⚠️ ${result.message}`);
202
+ if (!routed) {
203
+ console.log(` Remodex did not take ownership of plain Codex routing. Review the reason above, then run 'rmx sync'.`);
204
+ }
199
205
  }
200
206
 
201
207
  const shimAnswer = await prompt.ask("Install Codex autostart shim? [Y/n]: ");
@@ -2,8 +2,6 @@ export type InternalCliCommand =
2
2
  | "__tray-start"
3
3
  | "__tray-restart"
4
4
  | "__startup-health"
5
- | "__desktop-update"
6
- | "__desktop-auto-update-check"
7
5
  | "__desktop-restart-codex"
8
6
  | "__desktop-restart-client";
9
7
 
@@ -11,8 +9,6 @@ export interface InternalCliHandlers {
11
9
  trayStart: () => void | Promise<void>;
12
10
  trayRestart: () => void | Promise<void>;
13
11
  startupHealth: () => void | Promise<void>;
14
- desktopUpdate: () => void | Promise<void>;
15
- desktopAutoUpdateCheck: () => void | Promise<void>;
16
12
  desktopRestartCodex: () => void | Promise<void>;
17
13
  desktopRestartClient: () => void | Promise<void>;
18
14
  }
@@ -26,8 +22,6 @@ export async function dispatchInternalCliCommand(
26
22
  case "__tray-start": return void await handlers.trayStart();
27
23
  case "__tray-restart": return void await handlers.trayRestart();
28
24
  case "__startup-health": return void await handlers.startupHealth();
29
- case "__desktop-update": return void await handlers.desktopUpdate();
30
- case "__desktop-auto-update-check": return void await handlers.desktopAutoUpdateCheck();
31
25
  case "__desktop-restart-codex": return void await handlers.desktopRestartCodex();
32
26
  case "__desktop-restart-client": return void await handlers.desktopRestartClient();
33
27
  default: throw new Error(`Unsupported internal CLI command: ${String(command)}`);