diff --git a/src/env.test.ts b/src/env.test.ts index fb27de9..aad1b8e 100644 --- a/src/env.test.ts +++ b/src/env.test.ts @@ -48,6 +48,7 @@ describe("env subcommands", () => { "--port", "8443", "-s", + "--no-verify", ]); const config = await readConfig(); @@ -61,13 +62,30 @@ describe("env subcommands", () => { test("env add merges with existing environment", async () => { await setEnvironment("prod", { host: "old.com", user: "admin" }); - await runEnvCommand(["add", "prod", "--host", "new.com"]); + await runEnvCommand(["add", "prod", "--host", "new.com", "--no-verify"]); const config = await readConfig(); expect(config.environments.prod!.host).toBe("new.com"); expect(config.environments.prod!.user).toBe("admin"); }); + test("env add rejects environment with no host or url", async () => { + const spy = spyOn(process, "exit").mockImplementation(() => { + throw new Error("process.exit"); + }); + const errSpy = spyOn(console, "error").mockImplementation(() => {}); + + await expect( + runEnvCommand(["add", "empty", "--no-verify"]), + ).rejects.toThrow("process.exit"); + + spy.mockRestore(); + errSpy.mockRestore(); + + const config = await readConfig(); + expect(config.environments.empty).toBeUndefined(); + }); + test("env list shows environments", async () => { await setEnvironment("prod", { host: "ch.prod.com" }); await setEnvironment("dev", { host: "localhost" }); diff --git a/src/env.ts b/src/env.ts index 8a2f6ac..95e5c1c 100644 --- a/src/env.ts +++ b/src/env.ts @@ -7,6 +7,8 @@ import { getEnvironment, type Environment, } from "./config"; +import { resolveConnectionConfig } from "./client"; +import type { CliConfig } from "./cli"; export async function runEnvCommand(args: readonly string[]): Promise { const subcommand = args[0]; @@ -50,6 +52,7 @@ Add options: -d, --database Database -s, --secure Use HTTPS --url ClickHouse URL (sets host, port, secure, password) + --no-verify Skip connection verification Examples: chcli env add prod --host ch.prod.com --port 8443 --secure -u admin @@ -68,6 +71,7 @@ async function envAdd(args: readonly string[]): Promise { database: { type: "string", short: "d" }, secure: { type: "boolean", short: "s", default: false }, url: { type: "string" }, + "no-verify": { type: "boolean", default: false }, }, allowPositionals: true, strict: true, @@ -89,13 +93,55 @@ async function envAdd(args: readonly string[]): Promise { if (values.url) env.url = values.url; const existing = await getEnvironment(name); + const merged = existing ? { ...existing, ...env } : env; + + // Require at least a host or url + if (!merged.host && !merged.url) { + console.error("Error: at least --host or --url is required"); + process.exit(1); + } + + // Verify connection unless --no-verify is passed + if (!values["no-verify"]) { + const { createClient } = await import("@clickhouse/client"); + + const connConfig = resolveConnectionConfig({} as CliConfig, {},merged); + const client = createClient(connConfig); + try { + await client.query({ query: "SELECT 1", format: "JSON" }); + } catch (firstErr: any) { + // If not already secure, retry with HTTPS on port 8443 + if (!merged.secure && !merged.url) { + const secureEnv = { ...merged, secure: true, port: merged.port || "8443" }; + const secureConfig = resolveConnectionConfig({} as CliConfig, {},secureEnv); + const secureClient = createClient(secureConfig); + try { + await secureClient.query({ query: "SELECT 1", format: "JSON" }); + merged.secure = true; + if (!merged.port) merged.port = "8443"; + console.log("Detected HTTPS on port 8443 — added automatically"); + } catch (secondErr: any) { + const msg = firstErr?.message ?? String(firstErr); + console.error(`Error: connection to ClickHouse failed\n${msg}`); + process.exit(1); + } finally { + await secureClient.close(); + } + } else { + const msg = firstErr?.message ?? String(firstErr); + console.error(`Error: connection to ClickHouse failed\n${msg}`); + process.exit(1); + } + } finally { + await client.close(); + } + } + if (existing) { - // Merge with existing — only override fields that were explicitly passed - const merged = { ...existing, ...env }; await setEnvironment(name, merged); console.log(`Updated environment "${name}"`); } else { - await setEnvironment(name, env); + await setEnvironment(name, merged); console.log(`Added environment "${name}"`); } }