| import * as p from "@clack/prompts"; |
| import type { AuthConfig, ServerConfig } from "../config/schema.js"; |
| import { parseHostnameCsv } from "../config/hostnames.js"; |
|
|
| export async function promptServer(opts?: { |
| currentServer?: Partial<ServerConfig>; |
| currentAuth?: Partial<AuthConfig>; |
| }): Promise<{ server: ServerConfig; auth: AuthConfig }> { |
| const currentServer = opts?.currentServer; |
| const currentAuth = opts?.currentAuth; |
|
|
| const deploymentModeSelection = await p.select({ |
| message: "Deployment mode", |
| options: [ |
| { |
| value: "local_trusted", |
| label: "Local trusted", |
| hint: "Easiest for local setup (no login, localhost-only)", |
| }, |
| { |
| value: "authenticated", |
| label: "Authenticated", |
| hint: "Login required; use for private network or public hosting", |
| }, |
| ], |
| initialValue: currentServer?.deploymentMode ?? "local_trusted", |
| }); |
|
|
| if (p.isCancel(deploymentModeSelection)) { |
| p.cancel("Setup cancelled."); |
| process.exit(0); |
| } |
| const deploymentMode = deploymentModeSelection as ServerConfig["deploymentMode"]; |
|
|
| let exposure: ServerConfig["exposure"] = "private"; |
| if (deploymentMode === "authenticated") { |
| const exposureSelection = await p.select({ |
| message: "Exposure profile", |
| options: [ |
| { |
| value: "private", |
| label: "Private network", |
| hint: "Private access (for example Tailscale), lower setup friction", |
| }, |
| { |
| value: "public", |
| label: "Public internet", |
| hint: "Internet-facing deployment with stricter requirements", |
| }, |
| ], |
| initialValue: currentServer?.exposure ?? "private", |
| }); |
| if (p.isCancel(exposureSelection)) { |
| p.cancel("Setup cancelled."); |
| process.exit(0); |
| } |
| exposure = exposureSelection as ServerConfig["exposure"]; |
| } |
|
|
| const hostDefault = deploymentMode === "local_trusted" ? "127.0.0.1" : "0.0.0.0"; |
| const hostStr = await p.text({ |
| message: "Bind host", |
| defaultValue: currentServer?.host ?? hostDefault, |
| placeholder: hostDefault, |
| validate: (val) => { |
| if (!val.trim()) return "Host is required"; |
| }, |
| }); |
|
|
| if (p.isCancel(hostStr)) { |
| p.cancel("Setup cancelled."); |
| process.exit(0); |
| } |
|
|
| const portStr = await p.text({ |
| message: "Server port", |
| defaultValue: String(currentServer?.port ?? 3100), |
| placeholder: "3100", |
| validate: (val) => { |
| const n = Number(val); |
| if (isNaN(n) || n < 1 || n > 65535 || !Number.isInteger(n)) { |
| return "Must be an integer between 1 and 65535"; |
| } |
| }, |
| }); |
|
|
| if (p.isCancel(portStr)) { |
| p.cancel("Setup cancelled."); |
| process.exit(0); |
| } |
|
|
| let allowedHostnames: string[] = []; |
| if (deploymentMode === "authenticated" && exposure === "private") { |
| const allowedHostnamesInput = await p.text({ |
| message: "Allowed hostnames (comma-separated, optional)", |
| defaultValue: (currentServer?.allowedHostnames ?? []).join(", "), |
| placeholder: "dotta-macbook-pro, your-host.tailnet.ts.net", |
| validate: (val) => { |
| try { |
| parseHostnameCsv(val); |
| return; |
| } catch (err) { |
| return err instanceof Error ? err.message : "Invalid hostname list"; |
| } |
| }, |
| }); |
|
|
| if (p.isCancel(allowedHostnamesInput)) { |
| p.cancel("Setup cancelled."); |
| process.exit(0); |
| } |
| allowedHostnames = parseHostnameCsv(allowedHostnamesInput); |
| } |
|
|
| const port = Number(portStr) || 3100; |
| let auth: AuthConfig = { baseUrlMode: "auto", disableSignUp: false }; |
| if (deploymentMode === "authenticated" && exposure === "public") { |
| const urlInput = await p.text({ |
| message: "Public base URL", |
| defaultValue: currentAuth?.publicBaseUrl ?? "", |
| placeholder: "https://paperclip.example.com", |
| validate: (val) => { |
| const candidate = val.trim(); |
| if (!candidate) return "Public base URL is required for public exposure"; |
| try { |
| const url = new URL(candidate); |
| if (url.protocol !== "http:" && url.protocol !== "https:") { |
| return "URL must start with http:// or https://"; |
| } |
| return; |
| } catch { |
| return "Enter a valid URL"; |
| } |
| }, |
| }); |
| if (p.isCancel(urlInput)) { |
| p.cancel("Setup cancelled."); |
| process.exit(0); |
| } |
| auth = { |
| baseUrlMode: "explicit", |
| disableSignUp: false, |
| publicBaseUrl: urlInput.trim().replace(/\/+$/, ""), |
| }; |
| } else if (currentAuth?.baseUrlMode === "explicit" && currentAuth.publicBaseUrl) { |
| auth = { |
| baseUrlMode: "explicit", |
| disableSignUp: false, |
| publicBaseUrl: currentAuth.publicBaseUrl, |
| }; |
| } |
|
|
| return { |
| server: { |
| deploymentMode, |
| exposure, |
| host: hostStr.trim(), |
| port, |
| allowedHostnames, |
| serveUi: currentServer?.serveUi ?? true, |
| }, |
| auth, |
| }; |
| } |
|
|