Skip to content

Commit efccd99

Browse files
committed
refactor: delegate mux api CLI to running server via HTTP
Instead of initializing services locally, the api subcommand now proxies oRPC procedure calls to a running mux server. This avoids the heavyweight service initialization when using the CLI and ensures consistent behavior with the server. Key changes: - Add proxifyOrpc utility that wraps an oRPC router to forward calls via HTTPRPCLink to a remote server - Lazily import trpc-cli only when api command is invoked - Remove inline service context creation from CLI entry point - Enhance Zod 4 schema descriptions for better CLI help output
1 parent d78b7d8 commit efccd99

File tree

3 files changed

+405
-82
lines changed

3 files changed

+405
-82
lines changed

src/cli/api.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* API CLI subcommand - delegates to a running mux server via HTTP.
3+
*
4+
* This module is loaded lazily to avoid pulling in ESM-only dependencies
5+
* (trpc-cli) when running other commands like the desktop app.
6+
*/
7+
8+
import { createCli } from "trpc-cli";
9+
import { router } from "@/node/orpc/router";
10+
import { proxifyOrpc } from "./proxifyOrpc";
11+
import { Command } from "commander";
12+
13+
export async function runApiCli(parent: Command): Promise<void> {
14+
const baseUrl = process.env.MUX_SERVER_URL ?? "http://localhost:3000";
15+
const authToken = process.env.MUX_AUTH_TOKEN;
16+
17+
const proxiedRouter = proxifyOrpc(router(), { baseUrl, authToken });
18+
const cli = createCli({ router: proxiedRouter }).buildProgram() as Command;
19+
20+
cli.name("api");
21+
cli.description("Interact with the oRPC API via a running server");
22+
cli.parent = parent;
23+
cli.parse();
24+
}

src/cli/index.ts

Lines changed: 42 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -2,91 +2,51 @@
22

33
import { Command } from "commander";
44
import { VERSION } from "../version";
5-
import { createCli } from "trpc-cli";
6-
import { router } from "@/node/orpc/router";
7-
import { Config } from "@/node/config";
8-
import { ServiceContainer } from "@/node/services/serviceContainer";
9-
import { migrateLegacyMuxHome } from "@/common/constants/paths";
10-
import type { BrowserWindow } from "electron";
11-
import type { ORPCContext } from "@/node/orpc/context";
125

13-
// Minimal BrowserWindow stub for services that expect one (same as server.ts)
14-
const mockWindow: BrowserWindow = {
15-
isDestroyed: () => false,
16-
setTitle: () => undefined,
17-
webContents: {
18-
send: () => undefined,
19-
openDevTools: () => undefined,
20-
},
21-
} as unknown as BrowserWindow;
22-
23-
async function createServiceContext(): Promise<ORPCContext> {
24-
migrateLegacyMuxHome();
25-
26-
const config = new Config();
27-
const serviceContainer = new ServiceContainer(config);
28-
await serviceContainer.initialize();
29-
serviceContainer.windowService.setMainWindow(mockWindow);
30-
31-
return {
32-
projectService: serviceContainer.projectService,
33-
workspaceService: serviceContainer.workspaceService,
34-
providerService: serviceContainer.providerService,
35-
terminalService: serviceContainer.terminalService,
36-
windowService: serviceContainer.windowService,
37-
updateService: serviceContainer.updateService,
38-
tokenizerService: serviceContainer.tokenizerService,
39-
serverService: serviceContainer.serverService,
40-
menuEventService: serviceContainer.menuEventService,
41-
};
42-
}
43-
44-
async function main() {
45-
const program = new Command();
46-
47-
program
48-
.name("mux")
49-
.description("mux - coder multiplexer")
50-
.version(`mux ${VERSION.git_describe} (${VERSION.git_commit})`, "-v, --version");
51-
52-
program
53-
.command("server")
54-
.description("Start the HTTP/WebSocket oRPC server")
55-
.allowUnknownOption() // server.ts handles its own options via commander
56-
.action(() => {
57-
// Remove 'server' from args since server.ts has its own commander instance
58-
process.argv.splice(2, 1);
59-
// eslint-disable-next-line @typescript-eslint/no-require-imports
60-
require("./server");
61-
});
62-
63-
program
64-
.command("version")
65-
.description("Show version information")
66-
.action(() => {
67-
console.log(`mux ${VERSION.git_describe} (${VERSION.git_commit})`);
68-
});
69-
70-
// Only initialize services if the 'api' subcommand is being used
71-
if (process.argv[2] === "api") {
72-
const context = await createServiceContext();
73-
program.addCommand(
74-
(createCli({ router: router(), context }).buildProgram() as Command)
75-
.name("api")
76-
.description("Interact with the oRPC API directly")
77-
);
78-
}
79-
80-
// Default action: launch desktop app when no subcommand given
81-
program.action(() => {
6+
const program = new Command();
7+
8+
program
9+
.name("mux")
10+
.description("mux - coder multiplexer")
11+
.version(`mux ${VERSION.git_describe} (${VERSION.git_commit})`, "-v, --version");
12+
13+
// Subcommands with their own CLI parsers - disable help interception so --help passes through
14+
program
15+
.command("server")
16+
.description("Start the HTTP/WebSocket oRPC server")
17+
.helpOption(false)
18+
.allowUnknownOption()
19+
.allowExcessArguments()
20+
.action(() => {
21+
process.argv.splice(2, 1);
8222
// eslint-disable-next-line @typescript-eslint/no-require-imports
83-
require("../desktop/main");
23+
require("./server");
24+
});
25+
26+
program
27+
.command("api")
28+
.description("Interact with the oRPC API via a running server")
29+
.helpOption(false)
30+
.allowUnknownOption()
31+
.allowExcessArguments()
32+
.action(async () => {
33+
process.argv.splice(2, 1);
34+
// eslint-disable-next-line no-restricted-syntax -- dynamic import needed for ESM-only trpc-cli
35+
const { runApiCli } = await import("./api");
36+
await runApiCli(program);
8437
});
8538

86-
program.parse();
87-
}
39+
program
40+
.command("version")
41+
.description("Show version information")
42+
.action(() => {
43+
console.log(`mux ${VERSION.git_describe} (${VERSION.git_commit})`);
44+
});
8845

89-
main().catch((error) => {
90-
console.error("CLI initialization failed:", error);
91-
process.exit(1);
46+
// Default action: launch desktop app when no subcommand given
47+
program.action(() => {
48+
// eslint-disable-next-line @typescript-eslint/no-require-imports
49+
require("../desktop/main");
9250
});
51+
52+
program.parse();

0 commit comments

Comments
 (0)