Skip to content

Commit 272515f

Browse files
committed
refactor(cli): split executable entry from cli logic
1 parent e7d4c0f commit 272515f

3 files changed

Lines changed: 362 additions & 380 deletions

File tree

packages/cli/src/bin.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ vi.mock("./browser.js", () => ({
6161
openBrowser,
6262
}));
6363

64-
import { main } from "./bin";
64+
import { main } from "./cli";
6565
import { parseArgs, RUNTIME_CONFIG_ERROR } from "./parse-args";
6666

6767
beforeEach(() => {

packages/cli/src/bin.ts

Lines changed: 6 additions & 379 deletions
Original file line numberDiff line numberDiff line change
@@ -1,380 +1,7 @@
1-
import { existsSync, readFileSync } from "fs";
2-
import { dirname, join, resolve } from "path";
3-
import { fileURLToPath } from "url";
4-
import { clearAuthBlockByIp, listAuthBlocks } from "./auth-control.js";
5-
import { openBrowser } from "./browser.js";
6-
import { type CliConfig, readCliConfig, writeCliConfig } from "./config-store.js";
7-
import { readLogExcerpt } from "./log-excerpt.js";
8-
import { assertSupportedNodeVersion } from "./node-version.js";
9-
import { parseArgs } from "./parse-args.js";
10-
import { startManagedServer } from "./pm2-control.js";
11-
import { confirmYesNo, isInteractiveSession } from "./prompts.js";
12-
import { getServerStatus, type ServerStatus, stopRunningServer } from "./server-control.js";
13-
import { startServer } from "./server-runner.js";
14-
import { getBrowserUrl, getListenIp, getListenUrl } from "./server-url.js";
1+
import { main } from "./cli.js";
152

16-
const MANAGED_SERVER_WAIT_MS = 5000;
17-
const DEFAULT_LOG_TAIL_LINES = 40;
18-
19-
function formatConfig(config: CliConfig | null): string {
20-
return JSON.stringify(config ?? {}, null, 2);
21-
}
22-
23-
function formatStatus(status: ServerStatus): string {
24-
const listenUrl = getListenUrl(status) ?? "n/a";
25-
const browserUrl = getBrowserUrl(status) ?? "n/a";
26-
const startedAt = status.startedAt === null ? "n/a" : new Date(status.startedAt).toISOString();
27-
28-
return [
29-
`Status: ${status.status}`,
30-
`Listen host: ${status.host ?? "n/a"}`,
31-
`Listen IP: ${getListenIp(status) ?? "n/a"}`,
32-
`Port: ${status.port ?? "n/a"}`,
33-
`Listen URL: ${listenUrl}`,
34-
`Local URL: ${browserUrl}`,
35-
`PID: ${status.pid ?? "n/a"}`,
36-
`Started: ${startedAt}`,
37-
`Restarts: ${status.restartCount}`,
38-
`Out log: ${status.outFile}`,
39-
`Error log: ${status.errFile}`,
40-
].join("\n");
41-
}
42-
43-
function showLogs(
44-
status: ServerStatus,
45-
{
46-
tail = DEFAULT_LOG_TAIL_LINES,
47-
errorsOnly = false,
48-
}: { tail?: number; errorsOnly?: boolean } = {}
49-
): void {
50-
const paths = errorsOnly ? [status.errFile] : [status.outFile, status.errFile];
51-
const contents = paths
52-
.filter((path, index, paths) => paths.indexOf(path) === index)
53-
.flatMap((path) => {
54-
const content = readLogExcerpt(path, { maxLines: tail, maxChars: null });
55-
return content ? [content] : [];
56-
});
57-
58-
console.log(contents.length === 0 ? "No logs available." : contents.join("\n"));
59-
}
60-
61-
function showHelp(): void {
62-
console.log(`
63-
@spencer-kit/coder-studio - Coder Studio CLI
64-
65-
USAGE:
66-
coder-studio [COMMAND]
67-
68-
COMMANDS:
69-
serve Start the Coder Studio server in background (default)
70-
server Alias for serve
71-
open Start the server if needed and open Coder Studio in a browser
72-
auth Manage auth login blocks in local server storage
73-
config Persist CLI host/port/data-dir/password settings
74-
stop Stop the managed Coder Studio server
75-
status Show the managed server status
76-
logs Show the managed server logs
77-
help Show this help message
78-
version Show version
79-
80-
OPTIONS:
81-
--host <string> Save server host for future runs
82-
--port, -p <number> Save server port for future runs
83-
--data-dir, -d <path> Save data directory for future runs
84-
--password <string> Save auth password for future runs
85-
--restart Restart an already running managed server for serve/open
86-
--help Show help
87-
--version, -v Show version
88-
89-
EXAMPLES:
90-
coder-studio
91-
coder-studio serve
92-
coder-studio server
93-
coder-studio auth ban-list
94-
coder-studio auth unblock --ip 198.51.100.24
95-
coder-studio serve --foreground
96-
coder-studio serve --restart
97-
coder-studio open
98-
coder-studio open --restart
99-
coder-studio status
100-
coder-studio logs
101-
coder-studio stop
102-
coder-studio config --host 0.0.0.0 --port 8080
103-
`);
104-
}
105-
106-
function showConfigHelp(): void {
107-
console.log(`
108-
@spencer-kit/coder-studio - config
109-
110-
USAGE:
111-
coder-studio config [OPTIONS]
112-
coder-studio config help
113-
114-
BEHAVIOR:
115-
Without options, prints the current saved config.
116-
Bare serve reads this saved config for future runs.
117-
118-
OPTIONS:
119-
--host <string> Save server host for future runs
120-
--port, -p <number> Save server port for future runs
121-
--data-dir, -d <path> Save data directory for future runs
122-
--password <string> Save auth password for future runs
123-
--help Show config help
124-
125-
EXAMPLES:
126-
coder-studio config
127-
coder-studio config --host 0.0.0.0
128-
coder-studio config --port 8080
129-
coder-studio config --data-dir /tmp/cs-data
130-
coder-studio config --password sekrit
131-
coder-studio config --host 0.0.0.0 --port 8080
132-
`);
133-
}
134-
135-
function showVersion(): void {
136-
const manifestPath = [
137-
new URL("../package.json", import.meta.url),
138-
new URL("../../package.json", import.meta.url),
139-
].find((candidate) => existsSync(candidate));
140-
if (!manifestPath) {
141-
throw new Error("Unable to locate CLI package.json");
142-
}
143-
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as { version?: string };
144-
const version = manifest.version ?? "0.0.0";
145-
console.log(`@spencer-kit/coder-studio v${version}`);
146-
}
147-
148-
function formatAuthBlocks(blocks: Awaited<ReturnType<typeof listAuthBlocks>>): string {
149-
if (blocks.length === 0) {
150-
return "No blocked IPs.";
151-
}
152-
153-
return JSON.stringify(blocks, null, 2);
154-
}
155-
156-
function resolveManagedScriptPath(): string {
157-
const currentFile = fileURLToPath(import.meta.url);
158-
const currentDir = dirname(currentFile);
159-
const candidates = [
160-
join(currentDir, "server-runner.js"),
161-
join(currentDir, "server-runner.mjs"),
162-
join(currentDir, "../src/server-runner.ts"),
163-
];
164-
165-
const scriptPath = candidates.find((candidate) => existsSync(candidate));
166-
if (!scriptPath) {
167-
throw new Error("Unable to locate the managed server entry script");
168-
}
169-
170-
return scriptPath;
171-
}
172-
173-
function isCliEntrypoint(): boolean {
174-
if (process.argv[1] === undefined) {
175-
return false;
176-
}
177-
178-
const currentFile = fileURLToPath(import.meta.url);
179-
const currentDir = dirname(currentFile);
180-
const entryScript = resolve(process.argv[1]);
181-
const entryCandidates = new Set([
182-
currentFile,
183-
join(currentDir, "../bin.js"),
184-
join(currentDir, "../dist/bin.js"),
185-
]);
186-
187-
return entryCandidates.has(entryScript);
188-
}
189-
190-
function isRunningStatus(status: ServerStatus): boolean {
191-
return status.status === "running" || status.status === "starting";
192-
}
193-
194-
interface ManagedStartupDecision {
195-
existingStatus: ServerStatus | null;
196-
restartRequested: boolean;
197-
}
198-
199-
async function shouldRestartRunningServer(status: ServerStatus): Promise<boolean> {
200-
const currentUrl = getBrowserUrl(status) ?? getListenUrl(status) ?? "the existing server";
201-
202-
if (!isInteractiveSession()) {
203-
return false;
204-
}
205-
206-
return confirmYesNo(`Coder Studio is already running at ${currentUrl}. Restart it? [y/N] `);
207-
}
208-
209-
async function prepareManagedStartup(forceRestart = false): Promise<ManagedStartupDecision> {
210-
const status = await getServerStatus();
211-
if (!isRunningStatus(status)) {
212-
return {
213-
existingStatus: null,
214-
restartRequested: false,
215-
};
216-
}
217-
218-
const restart = forceRestart ? true : await shouldRestartRunningServer(status);
219-
if (!restart) {
220-
const currentUrl = getBrowserUrl(status) ?? getListenUrl(status) ?? "n/a";
221-
if (!isInteractiveSession()) {
222-
console.log(
223-
`Coder Studio is already running at ${currentUrl}. Service already exists and was not restarted.`
224-
);
225-
} else {
226-
console.log(`Leaving the existing Coder Studio server running at ${currentUrl}.`);
227-
}
228-
return {
229-
existingStatus: status,
230-
restartRequested: false,
231-
};
232-
}
233-
234-
console.log("Restarting the managed Coder Studio server...");
235-
return {
236-
existingStatus: null,
237-
restartRequested: true,
238-
};
239-
}
240-
241-
async function startManagedServerFlow(): Promise<void> {
242-
await startManagedServer({
243-
script: resolveManagedScriptPath(),
244-
cwd: process.cwd(),
245-
waitMs: MANAGED_SERVER_WAIT_MS,
246-
});
247-
}
248-
249-
async function openManagedServerInBrowser(existingStatus?: ServerStatus | null): Promise<void> {
250-
const status = existingStatus ?? (await getServerStatus());
251-
const browserUrl = getBrowserUrl(status);
252-
253-
if (browserUrl === null) {
254-
throw new Error("Unable to determine the running Coder Studio URL.");
255-
}
256-
257-
console.log(`Opening Coder Studio in your browser: ${browserUrl}`);
258-
await openBrowser(browserUrl);
259-
}
260-
261-
export async function main(argv = process.argv.slice(2)): Promise<void> {
262-
assertSupportedNodeVersion();
263-
const args = parseArgs(argv);
264-
265-
if (args.command === "config") {
266-
if (args.configHelp) {
267-
showConfigHelp();
268-
return;
269-
}
270-
271-
if (
272-
args.host === undefined &&
273-
args.port === undefined &&
274-
args.dataDir === undefined &&
275-
args.password === undefined
276-
) {
277-
console.log(formatConfig(readCliConfig()));
278-
return;
279-
}
280-
281-
const savedConfig = readCliConfig();
282-
const nextConfig: CliConfig = {
283-
...(savedConfig?.host !== undefined ? { host: savedConfig.host } : {}),
284-
...(savedConfig?.port !== undefined && savedConfig.port > 0
285-
? { port: savedConfig.port }
286-
: {}),
287-
...(savedConfig?.dataDir !== undefined ? { dataDir: savedConfig.dataDir } : {}),
288-
...(savedConfig?.password !== undefined ? { password: savedConfig.password } : {}),
289-
...(args.host !== undefined ? { host: args.host } : {}),
290-
...(args.port !== undefined ? { port: args.port } : {}),
291-
...(args.dataDir !== undefined ? { dataDir: args.dataDir } : {}),
292-
...(args.password !== undefined ? { password: args.password } : {}),
293-
};
294-
writeCliConfig(nextConfig);
295-
console.log(formatConfig(nextConfig));
296-
return;
297-
}
298-
299-
if (args.command === "stop") {
300-
const stopped = await stopRunningServer();
301-
console.log(stopped ? "Stopped Coder Studio server." : "No running Coder Studio server found.");
302-
return;
303-
}
304-
305-
if (args.command === "status") {
306-
console.log(formatStatus(await getServerStatus()));
307-
return;
308-
}
309-
310-
if (args.command === "logs") {
311-
showLogs(await getServerStatus(), { tail: args.tail, errorsOnly: args.errorsOnly });
312-
return;
313-
}
314-
315-
if (args.command === "help") {
316-
showHelp();
317-
return;
318-
}
319-
320-
if (args.command === "version") {
321-
showVersion();
322-
return;
323-
}
324-
325-
if (args.command === "auth") {
326-
if (args.authCommand === "ban-list") {
327-
console.log(formatAuthBlocks(await listAuthBlocks()));
328-
return;
329-
}
330-
331-
if (args.authCommand === "unblock") {
332-
const cleared = await clearAuthBlockByIp(args.ip!);
333-
console.log(cleared ? `Unblocked IP: ${args.ip}` : `No block found for IP: ${args.ip}`);
334-
return;
335-
}
336-
}
337-
338-
if (args.command === "open") {
339-
const startup = await prepareManagedStartup(args.restart);
340-
if (startup.existingStatus === null) {
341-
await startManagedServerFlow();
342-
}
343-
344-
await openManagedServerInBrowser(startup.existingStatus);
345-
return;
346-
}
347-
348-
if (args.foreground) {
349-
const startup = await prepareManagedStartup(args.restart);
350-
if (startup.existingStatus !== null) {
351-
return;
352-
}
353-
354-
if (startup.restartRequested) {
355-
await stopRunningServer();
356-
}
357-
358-
console.log("Starting Coder Studio Server in foreground...");
359-
await startServer();
360-
return;
361-
}
362-
363-
const startup = await prepareManagedStartup(args.restart);
364-
if (startup.existingStatus !== null) {
365-
return;
366-
}
367-
368-
await startManagedServerFlow();
369-
370-
console.log("Coder Studio server started in background.");
371-
console.log("Run `coder-studio status` to inspect the server.");
372-
}
373-
374-
if (isCliEntrypoint()) {
375-
main().catch((error) => {
376-
const message = error instanceof Error ? error.message : String(error);
377-
console.error("CLI error:", message);
378-
process.exit(1);
379-
});
380-
}
3+
void main().catch((error) => {
4+
const message = error instanceof Error ? error.message : String(error);
5+
console.error("CLI error:", message);
6+
process.exit(1);
7+
});

0 commit comments

Comments
 (0)