|
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"; |
15 | 2 |
|
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