UNPKG

@electric-sql/cli

Version:
1,615 lines (1,570 loc) 51 kB
#!/usr/bin/env node // src/index.ts import { Command as Command30 } from "commander"; // src/config.ts import { readFile, writeFile, mkdir, unlink } from "fs/promises"; import { join } from "path"; import { homedir } from "os"; function getConfigDir() { return process.env.XDG_CONFIG_HOME ? join(process.env.XDG_CONFIG_HOME, `electric`) : join(homedir(), `.config`, `electric`); } function getAuthFile() { return join(getConfigDir(), `auth.json`); } async function loadStoredAuth() { try { const raw = await readFile(getAuthFile(), `utf-8`); return JSON.parse(raw); } catch { return null; } } async function saveStoredAuth(auth2) { const configDir = getConfigDir(); await mkdir(configDir, { recursive: true }); await writeFile(getAuthFile(), JSON.stringify(auth2, null, 2), { mode: 384 }); } async function clearStoredAuth() { try { await unlink(getAuthFile()); } catch { } } // src/auth.ts var explicitToken; function setExplicitToken(token2) { explicitToken = token2; } var EXPIRY_WARNING_MS = 24 * 60 * 60 * 1e3; async function resolveAuth() { if (explicitToken) { return { token: explicitToken, type: `api_token` }; } const apiToken = process.env.ELECTRIC_API_TOKEN; if (apiToken) { return { token: apiToken, type: `api_token` }; } const stored = await loadStoredAuth(); if (stored) { const expiresAt = new Date(stored.expiresAt); const now = /* @__PURE__ */ new Date(); if (expiresAt < now) { await clearStoredAuth(); throw new AuthError( `Session expired. Run 'electric auth login' to re-authenticate.` ); } if (expiresAt.getTime() - now.getTime() < EXPIRY_WARNING_MS) { const hours = Math.ceil( (expiresAt.getTime() - now.getTime()) / (60 * 60 * 1e3) ); process.stderr.write( `Warning: Session expires in ${hours} hours. Run 'electric auth login' to refresh. ` ); } return { token: stored.token, type: `jwt` }; } throw new AuthError( `Not authenticated. Pass --token, set ELECTRIC_API_TOKEN, or run 'electric auth login'.` ); } var AuthError = class extends Error { constructor(message) { super(message); this.name = `AuthError`; } }; // src/lib/confirm.ts import { createInterface } from "readline"; var _noInput = false; function setNoInput(value) { _noInput = value; } function isInteractive() { if (_noInput) return false; if (process.env.ELECTRIC_API_TOKEN) return false; return true; } function confirm(prompt) { if (!isInteractive()) { return Promise.resolve(false); } return new Promise((resolve) => { const rl = createInterface({ input: process.stdin, output: process.stderr }); rl.question(prompt, (answer) => { rl.close(); resolve(answer.toLowerCase() === `y` || answer.toLowerCase() === `yes`); }); }); } // src/client.ts import { createORPCClient } from "@orpc/client"; import { RPCLink } from "@orpc/client/fetch"; var CLI_VERSION = true ? "0.0.3" : `0.0.1`; var _verbose = false; function setVerbose(value) { _verbose = value; } function getApiBaseUrl() { return process.env.ELECTRIC_API_URL ?? `https://dashboard.electric-sql.cloud`; } function getDashboardUrl() { return process.env.ELECTRIC_DASHBOARD_URL ?? `https://dashboard.electric-sql.cloud`; } function createClient() { const link = new RPCLink({ url: `${getApiBaseUrl()}/api/rpc`, headers: async () => { const headers = { "User-Agent": `electric-cli/${CLI_VERSION}` }; const auth2 = await resolveAuth(); headers[`Authorization`] = `Bearer ${auth2.token}`; return headers; }, fetch: _verbose ? verboseFetch : void 0 }); return createORPCClient(link); } function createPublicClient() { const link = new RPCLink({ url: `${getApiBaseUrl()}/api/rpc`, headers: () => ({ "User-Agent": `electric-cli/${CLI_VERSION}` }), fetch: _verbose ? verboseFetch : void 0 }); return createORPCClient(link); } async function verboseFetch(request, init) { const method = request.method; const url = request.url; const start = performance.now(); process.stderr.write(`${method} ${url} `); const response = await fetch(request, init); const elapsed = Math.round(performance.now() - start); process.stderr.write( `${response.status} ${response.statusText} (${elapsed}ms) ` ); const requestId = response.headers.get(`x-request-id`); if (requestId) { process.stderr.write(`X-Request-Id: ${requestId} `); } return response; } // src/output.ts var _quiet = false; function setQuiet(value) { _quiet = value; } function isQuiet() { return _quiet; } function printQuiet(value) { process.stdout.write(value + ` `); } function printJson(data) { process.stdout.write(JSON.stringify(data, null, 2) + ` `); } function printTable(headers, rows) { if (rows.length === 0) { process.stdout.write(`No results found. `); return; } const widths = headers.map( (h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? ``).length)) ); const headerLine = headers.map((h, i) => h.toUpperCase().padEnd(widths[i])).join(` `); process.stdout.write(headerLine + ` `); for (const row of rows) { const line = row.map((cell, i) => (cell ?? ``).padEnd(widths[i])).join(` `); process.stdout.write(line + ` `); } } function printSuccess(message) { process.stdout.write(message + ` `); } function printKeyValue(pairs) { const maxKeyLen = Math.max(...pairs.map(([k]) => k.length)); for (const [key, value] of pairs) { process.stdout.write(`${key.padEnd(maxKeyLen)} ${value} `); } } // src/commands/auth/login.ts import { Command } from "commander"; import { randomUUID } from "crypto"; import { URL as URL2 } from "url"; // src/lib/errors.ts import { ORPCError } from "@orpc/client"; var EXIT_GENERAL_ERROR = 1; var EXIT_AUTH_ERROR = 2; var EXIT_NOT_FOUND = 3; var EXIT_VALIDATION_ERROR = 4; var EXIT_CONFLICT = 5; function errorNameForCode(code) { if (code === EXIT_AUTH_ERROR) return `AUTH_ERROR`; if (code === EXIT_NOT_FOUND) return `NOT_FOUND`; if (code === EXIT_VALIDATION_ERROR) return `VALIDATION_ERROR`; if (code === EXIT_CONFLICT) return `CONFLICT`; return `ERROR`; } function exitWithError(message, code, json) { if (json) { process.stderr.write( JSON.stringify({ error: errorNameForCode(code), message, exitCode: code }) + ` ` ); } else { process.stderr.write(`Error: ${message} `); } process.exit(code); } function handleError(error, json) { const { code, message } = classifyError(error); exitWithError(message, code, json); } function classifyByStatus(status) { if (status === 401 || status === 403) return EXIT_AUTH_ERROR; if (status === 404) return EXIT_NOT_FOUND; if (status === 400 || status === 422) return EXIT_VALIDATION_ERROR; return EXIT_GENERAL_ERROR; } function classifyError(error) { if (error instanceof AuthError) { return { code: EXIT_AUTH_ERROR, message: error.message }; } if (error instanceof ORPCError) { return { code: classifyByStatus(error.status), message: error.message }; } if (error instanceof Error) { return { code: EXIT_GENERAL_ERROR, message: error.message }; } return { code: EXIT_GENERAL_ERROR, message: String(error) }; } // src/lib/local-server.ts import { createServer } from "http"; import { URL } from "url"; function startLocalServer(options) { const { callbackPath, parseParams, successHtml = `<html><body><p>Success. You can close this tab.</p><script>window.close()</script></body></html>`, errorHtml = `<html><body><p>Something went wrong. Please try again.</p></body></html>` } = options; return new Promise((resolveStart) => { let resolveResult; let rejectResult; const resultPromise = new Promise((resolve, reject) => { resolveResult = resolve; rejectResult = reject; }); const server = createServer((req, res) => { const url = new URL(req.url ?? `/`, `http://localhost`); if (url.pathname === callbackPath) { try { const result = parseParams(url.searchParams); res.writeHead(200, { "Content-Type": `text/html` }); res.end(successHtml); resolveResult(result); } catch (err) { res.writeHead(400, { "Content-Type": `text/html` }); res.end(errorHtml); rejectResult(err instanceof Error ? err : new Error(String(err))); } } else { res.writeHead(404); res.end(); } }); const shutdown = () => { server.close(); }; server.listen(0, `127.0.0.1`, () => { const addr = server.address(); const port = typeof addr === `object` && addr ? addr.port : 0; resolveStart({ port, resultPromise, shutdown }); }); }); } // src/commands/auth/login.ts var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3; var loginCommand = new Command(`login`).description(`Log in to Electric Cloud via browser`).action(async () => { const json = loginCommand.parent?.parent?.opts().json ?? false; try { await runLogin(json); } catch (error) { handleError(error, json); } }); async function runLogin(json) { const state = randomUUID(); const { port, resultPromise, shutdown } = await startLocalServer({ callbackPath: `/callback`, parseParams: (params) => { const receivedState = params.get(`state`); if (receivedState !== state) { throw new Error(`State mismatch in OAuth callback`); } const token2 = params.get(`token`); if (!token2) { throw new Error(`No token received in OAuth callback`); } return { token: token2, email: params.get(`email`) ?? void 0, expiresAt: params.get(`expiresAt`) ?? void 0 }; }, successHtml: `<html><body><p>Authentication successful. You can close this tab.</p><script>window.close()</script></body></html>`, errorHtml: `<html><body><p>Authentication failed. Please try again.</p></body></html>` }); const loginUrl = new URL2(`/`, getDashboardUrl()); loginUrl.searchParams.set(`cli_port`, String(port)); loginUrl.searchParams.set(`cli_state`, state); try { const open = (await import("open")).default; await open(loginUrl.toString()); if (!json) { process.stderr.write( `Opening browser for login... If the browser doesn't open, visit: ${loginUrl.toString()} ` ); } } catch { process.stderr.write( `Could not open browser. Visit this URL to log in: ${loginUrl.toString()} ` ); } let timeoutId; const timeoutPromise = new Promise((_, reject) => { timeoutId = setTimeout(() => { shutdown(); reject( new Error( `Authentication timed out after ${LOGIN_TIMEOUT_MS / 6e4} minutes. Run 'electric auth login' to try again.` ) ); }, LOGIN_TIMEOUT_MS); }); try { const result = await Promise.race([resultPromise, timeoutPromise]); clearTimeout(timeoutId); shutdown(); await saveStoredAuth({ token: result.token, email: result.email ?? `unknown`, expiresAt: result.expiresAt ?? new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3).toISOString() }); if (json) { process.stdout.write( JSON.stringify({ success: true, email: result.email }) + ` ` ); } else { printSuccess(`Logged in successfully.`); } } catch (error) { shutdown(); throw error; } } // src/commands/auth/logout.ts import { Command as Command2 } from "commander"; var logoutCommand = new Command2(`logout`).description(`Log out and clear stored credentials`).action(async () => { const json = logoutCommand.parent?.parent?.opts().json ?? false; try { await clearStoredAuth(); if (json) { printJson({ success: true }); } else { printSuccess(`Logged out successfully.`); } } catch (error) { handleError(error, json); } }); // src/commands/auth/whoami.ts import { Command as Command3 } from "commander"; var whoamiCommand = new Command3(`whoami`).description(`Show the currently authenticated user`).action(async () => { const json = whoamiCommand.parent?.parent?.opts().json ?? false; try { const client = createClient(); const result = await client.auth.whoami({}); if (isQuiet()) { printQuiet(result.type === `user` ? result.userId : result.tokenId); return; } if (json) { printJson(result); } else if (result.type === `user`) { printKeyValue([ [`Type:`, `User`], [`User ID:`, result.userId], [`Email:`, result.email] ]); } else { printKeyValue([ [`Type:`, `API Token`], [`Token ID:`, result.tokenId], [`Workspace:`, result.workspaceId], [`Scopes:`, result.scopes.join(`, `)] ]); } } catch (error) { handleError(error, json); } }); // src/commands/auth/token/create.ts import { Command as Command4 } from "commander"; // src/lib/workspace-resolver.ts async function resolveWorkspace(client, explicitWorkspace) { if (explicitWorkspace) { return explicitWorkspace; } const envWorkspace = process.env.ELECTRIC_WORKSPACE_ID; if (envWorkspace) { return envWorkspace; } const whoami = await client.auth.whoami({}); if (whoami.type === `token`) { return whoami.workspaceId; } const { workspaces: workspaces2 } = await client.workspaces.list({}); if (workspaces2.length === 0) { throw new Error( `No workspaces found. Create one at https://dashboard.electric-sql.cloud` ); } if (workspaces2.length === 1) { return workspaces2[0].id; } const list = workspaces2.map((w) => ` ${w.id} ${w.name}`).join(` `); throw new Error( `Multiple workspaces found. Specify one with --workspace or ELECTRIC_WORKSPACE_ID. Available workspaces: ${list} Set a default: export ELECTRIC_WORKSPACE_ID=<id>` ); } // src/commands/auth/token/create.ts var tokenCreateCommand = new Command4(`create`).usage(`--name <name> --scopes <scopes> [options]`).description(`Create a new API token`).requiredOption(`--name <name>`, `Token name`).requiredOption(`--scopes <scopes>`, `Comma-separated scope list`).option(`--workspace <id>`, `Workspace ID`).option(`--project-ids <ids>`, `Comma-separated project ID list`).option(`--expires-at <date>`, `Expiration date (ISO 8601)`).addHelpText( `after`, ` Available scopes: v2:projects:read Read project details v2:projects:write Create, update, and delete projects v2:environments:read Read environment details v2:environments:write Create, update, and delete environments v2:services:read Read service details v2:services:write Create, update, and delete services v2:services:secrets Read service connection credentials ` ).action(async (opts) => { const json = tokenCreateCommand.parent?.parent?.parent?.opts().json ?? false; try { const client = createClient(); const workspaceId = await resolveWorkspace(client, opts.workspace); const scopes = opts.scopes.split(`,`).map((s) => s.trim()); const projectIds = opts.projectIds ? opts.projectIds.split(`,`).map((s) => s.trim()) : void 0; const result = await client.tokens.create({ workspaceId, name: opts.name, scopes, projectIds, expiresAt: opts.expiresAt ?? void 0 }); if (isQuiet()) { printQuiet(result.id); return; } if (json) { printJson(result); } else { printSuccess(`Token created successfully. `); printKeyValue([ [`ID:`, result.id], [`Token:`, result.token] ]); process.stdout.write( ` Save this token \u2014 it cannot be retrieved again. ` ); } } catch (error) { handleError(error, json); } }); // src/commands/auth/token/list.ts import { Command as Command5 } from "commander"; var tokenListCommand = new Command5(`list`).description(`List API tokens in a workspace`).option(`--workspace <id>`, `Workspace ID`).action(async (opts) => { const json = tokenListCommand.parent?.parent?.parent?.opts().json ?? false; try { const client = createClient(); const workspaceId = await resolveWorkspace(client, opts.workspace); const result = await client.tokens.list({ workspaceId }); if (isQuiet()) { for (const t of result.tokens) { printQuiet(t.id); } return; } if (json) { printJson(result); } else { printTable( [`ID`, `Name`, `Prefix`, `Scopes`, `Created`], result.tokens.map((t) => [ t.id, t.name, t.tokenPrefix, t.scopes.join(`, `), t.createdAt.slice(0, 10) ]) ); } } catch (error) { handleError(error, json); } }); // src/commands/auth/token/revoke.ts import { Command as Command6 } from "commander"; var tokenRevokeCommand = new Command6(`revoke`).description(`Revoke an API token`).argument(`<token-id>`, `Token ID to revoke`).option(`--force`, `Skip confirmation prompt`).action(async (tokenId, opts) => { const json = tokenRevokeCommand.parent?.parent?.parent?.opts().json ?? false; try { if (!opts.force) { if (json) { process.stderr.write( JSON.stringify({ error: `VALIDATION_ERROR`, message: `--force is required with --json`, exitCode: EXIT_VALIDATION_ERROR }) + ` ` ); process.exit(EXIT_VALIDATION_ERROR); } const confirmed = await confirm(`Revoke token ${tokenId}? [y/N] `); if (!confirmed) { if (!isInteractive()) { process.stderr.write( `Use --force to skip confirmation in non-interactive mode. ` ); process.exit(EXIT_VALIDATION_ERROR); } process.stderr.write(`Aborted. `); return; } } const client = createClient(); const result = await client.tokens.revoke({ tokenId }); if (isQuiet()) { printQuiet(tokenId); return; } if (json) { printJson(result); } else { printSuccess(`Token ${tokenId} revoked.`); } } catch (error) { handleError(error, json); } }); // src/commands/workspaces/list.ts import { Command as Command7 } from "commander"; var workspacesListCommand = new Command7(`list`).description(`List accessible workspaces`).action(async () => { const json = workspacesListCommand.parent?.parent?.opts().json ?? false; try { const client = createClient(); const result = await client.workspaces.list({}); if (isQuiet()) { for (const w of result.workspaces) { printQuiet(w.id); } return; } if (json) { printJson(result); } else { printTable( [`ID`, `Name`, `Created`], result.workspaces.map((w) => [w.id, w.name, w.createdAt.slice(0, 10)]) ); } } catch (error) { handleError(error, json); } }); // src/commands/workspaces/get.ts import { Command as Command8 } from "commander"; var workspacesGetCommand = new Command8(`get`).description(`Show workspace details`).argument(`<id>`, `Workspace ID`).action(async (workspaceId) => { const json = workspacesGetCommand.parent?.parent?.opts().json ?? false; try { const client = createClient(); const result = await client.workspaces.get({ workspaceId }); if (isQuiet()) { printQuiet(result.id); return; } if (json) { printJson(result); } else { printKeyValue([ [`ID:`, result.id], [`Name:`, result.name], [`Created:`, result.createdAt] ]); } } catch (error) { handleError(error, json); } }); // src/commands/projects/list.ts import { Command as Command9 } from "commander"; var projectsListCommand = new Command9(`list`).description(`List projects in a workspace`).option(`--workspace <id>`, `Workspace ID`).action(async (opts) => { const json = projectsListCommand.parent?.parent?.opts().json ?? false; try { const client = createClient(); const workspaceId = await resolveWorkspace(client, opts.workspace); const result = await client.projects.list({ workspaceId }); if (isQuiet()) { for (const p of result.projects) { printQuiet(p.id); } return; } if (json) { printJson(result); } else { printTable( [`ID`, `Name`, `Created`], result.projects.map((p) => [p.id, p.name, p.createdAt.slice(0, 10)]) ); } } catch (error) { handleError(error, json); } }); // src/commands/projects/create.ts import { Command as Command10 } from "commander"; var projectsCreateCommand = new Command10(`create`).usage(`--name <name> [options]`).description(`Create a new project`).requiredOption(`--name <name>`, `Project name`).option(`--workspace <id>`, `Workspace ID`).option(`--default-environment <name>`, `Default environment name`, `Main`).action(async (opts) => { const json = projectsCreateCommand.parent?.parent?.opts().json ?? false; try { const client = createClient(); const workspaceId = await resolveWorkspace(client, opts.workspace); const result = await client.projects.create({ workspaceId, name: opts.name, defaultEnvName: opts.defaultEnvironment }); if (isQuiet()) { printQuiet(result.projectId); return; } if (json) { printJson(result); } else { printSuccess(`Project created successfully. `); printKeyValue([ [`Project ID:`, result.projectId], [`Environment ID:`, result.environmentId] ]); } } catch (error) { handleError(error, json); } }); // src/commands/projects/get.ts import { Command as Command11 } from "commander"; var projectsGetCommand = new Command11(`get`).description(`Show project details`).argument(`<id>`, `Project ID`).action(async (projectId) => { const json = projectsGetCommand.parent?.parent?.opts().json ?? false; try { const client = createClient(); const result = await client.projects.get({ projectId }); if (isQuiet()) { printQuiet(result.id); return; } if (json) { printJson(result); } else { printKeyValue([ [`ID:`, result.id], [`Name:`, result.name], [`Workspace:`, result.workspaceId], [`Created:`, result.createdAt] ]); } } catch (error) { handleError(error, json); } }); // src/commands/projects/update.ts import { Command as Command12 } from "commander"; var projectsUpdateCommand = new Command12(`update`).usage(`<id> --name <name>`).description(`Update a project's name`).argument(`<id>`, `Project ID`).requiredOption(`--name <name>`, `New project name`).action(async (projectId, opts) => { const json = projectsUpdateCommand.parent?.parent?.opts().json ?? false; try { const client = createClient(); const result = await client.projects.update({ projectId, name: opts.name }); if (isQuiet()) { printQuiet(projectId); return; } if (json) { printJson(result); } else { printSuccess(`Project ${projectId} updated.`); } } catch (error) { handleError(error, json); } }); // src/commands/projects/delete.ts import { Command as Command13 } from "commander"; var projectsDeleteCommand = new Command13(`delete`).description(`Delete a project and all its environments`).argument(`<id>`, `Project ID`).option(`--force`, `Skip confirmation prompt`).action(async (projectId, opts) => { const json = projectsDeleteCommand.parent?.parent?.opts().json ?? false; try { if (!opts.force) { if (json) { process.stderr.write( JSON.stringify({ error: `VALIDATION_ERROR`, message: `--force is required with --json`, exitCode: EXIT_VALIDATION_ERROR }) + ` ` ); process.exit(EXIT_VALIDATION_ERROR); } const confirmed = await confirm( `Delete project ${projectId}? This cannot be undone. [y/N] ` ); if (!confirmed) { if (!isInteractive()) { process.stderr.write( `Use --force to skip confirmation in non-interactive mode. ` ); process.exit(EXIT_VALIDATION_ERROR); } process.stderr.write(`Aborted. `); return; } } const client = createClient(); const result = await client.projects.delete({ projectId }); if (isQuiet()) { printQuiet(projectId); return; } if (json) { printJson(result); } else { printSuccess(`Project ${projectId} deleted.`); } } catch (error) { handleError(error, json); } }); // src/commands/environments/list.ts import { Command as Command14 } from "commander"; var environmentsListCommand = new Command14(`list`).usage(`--project <id>`).description(`List environments in a project`).requiredOption(`--project <id>`, `Project ID`).action(async (opts) => { const json = environmentsListCommand.parent?.parent?.opts().json ?? false; try { const client = createClient(); const result = await client.environments.list({ projectId: opts.project }); if (isQuiet()) { for (const e of result.environments) { printQuiet(e.id); } return; } if (json) { printJson(result); } else { printTable( [`ID`, `Name`, `Created`], result.environments.map((e) => [ e.id, e.name, e.createdAt.slice(0, 10) ]) ); } } catch (error) { handleError(error, json); } }); // src/commands/environments/create.ts import { Command as Command15 } from "commander"; var environmentsCreateCommand = new Command15(`create`).usage(`--project <id> --name <name>`).description(`Create a new environment`).requiredOption(`--project <id>`, `Project ID`).requiredOption(`--name <name>`, `Environment name`).action(async (opts) => { const json = environmentsCreateCommand.parent?.parent?.opts().json ?? false; try { const client = createClient(); const result = await client.projects.createEnvironment({ projectId: opts.project, name: opts.name }); if (isQuiet()) { printQuiet(result.environmentId); return; } if (json) { printJson(result); } else { printSuccess(`Environment created successfully. `); printKeyValue([[`ID:`, result.environmentId]]); } } catch (error) { handleError(error, json); } }); // src/commands/environments/get.ts import { Command as Command16 } from "commander"; var environmentsGetCommand = new Command16(`get`).description(`Show environment details`).argument(`<id>`, `Environment ID`).action(async (environmentId) => { const json = environmentsGetCommand.parent?.parent?.opts().json ?? false; try { const client = createClient(); const result = await client.environments.get({ environmentId }); if (isQuiet()) { printQuiet(result.id); return; } if (json) { printJson(result); } else { printKeyValue([ [`ID:`, result.id], [`Name:`, result.name], [`Project:`, result.projectId], [`Created:`, result.createdAt] ]); } } catch (error) { handleError(error, json); } }); // src/commands/environments/update.ts import { Command as Command17 } from "commander"; var environmentsUpdateCommand = new Command17(`update`).usage(`<id> --name <name>`).description(`Update an environment's name`).argument(`<id>`, `Environment ID`).requiredOption(`--name <name>`, `New environment name`).action(async (environmentId, opts) => { const json = environmentsUpdateCommand.parent?.parent?.opts().json ?? false; try { const client = createClient(); const result = await client.environments.update({ environmentId, name: opts.name }); if (isQuiet()) { printQuiet(environmentId); return; } if (json) { printJson(result); } else { printSuccess(`Environment ${environmentId} updated.`); } } catch (error) { handleError(error, json); } }); // src/commands/environments/delete.ts import { Command as Command18 } from "commander"; var environmentsDeleteCommand = new Command18(`delete`).description(`Delete an environment and all its services`).argument(`<id>`, `Environment ID`).option(`--force`, `Skip confirmation prompt`).action(async (environmentId, opts) => { const json = environmentsDeleteCommand.parent?.parent?.opts().json ?? false; try { if (!opts.force) { if (json) { process.stderr.write( JSON.stringify({ error: `VALIDATION_ERROR`, message: `--force is required with --json`, exitCode: EXIT_VALIDATION_ERROR }) + ` ` ); process.exit(EXIT_VALIDATION_ERROR); } const confirmed = await confirm( `Delete environment ${environmentId}? This cannot be undone. [y/N] ` ); if (!confirmed) { if (!isInteractive()) { process.stderr.write( `Use --force to skip confirmation in non-interactive mode. ` ); process.exit(EXIT_VALIDATION_ERROR); } process.stderr.write(`Aborted. `); return; } } const client = createClient(); const result = await client.environments.delete({ environmentId }); if (isQuiet()) { printQuiet(environmentId); return; } if (json) { printJson(result); } else { printSuccess(`Environment ${environmentId} deleted.`); } } catch (error) { handleError(error, json); } }); // src/commands/services/list.ts import { Command as Command19 } from "commander"; var servicesListCommand = new Command19(`list`).usage(`--environment <id>`).description(`List services in an environment`).requiredOption(`--environment <id>`, `Environment ID`).action(async (opts) => { const json = servicesListCommand.parent?.parent?.opts().json ?? false; try { const client = createClient(); const result = await client.services.list({ environmentId: opts.environment }); if (isQuiet()) { for (const s of result.services) { printQuiet(s.id); } return; } if (json) { printJson(result); } else { printTable( [`ID`, `Name`, `Type`], result.services.map((s) => [s.id, s.name, s.type]) ); } } catch (error) { handleError(error, json); } }); // src/commands/services/get.ts import { Command as Command20 } from "commander"; var servicesGetCommand = new Command20(`get`).description(`Show service details`).argument(`<id>`, `Service ID`).action(async (serviceId) => { const json = servicesGetCommand.parent?.parent?.opts().json ?? false; try { const client = createClient(); const result = await client.services.get({ serviceId }); if (isQuiet()) { printQuiet(result.id); return; } if (json) { printJson(result); } else { const pairs = [ [`ID:`, result.id], [`Name:`, result.name], [`Type:`, result.type], [`Environment:`, result.environmentId] ]; if (`status` in result && result.status) { pairs.push([`Status:`, result.status]); } if (`region` in result && result.region) { pairs.push([`Region:`, result.region]); } printKeyValue(pairs); } } catch (error) { handleError(error, json); } }); // src/commands/services/update.ts import { Command as Command21 } from "commander"; var servicesUpdateCommand = new Command21(`update`).usage(`<id> --name <name>`).description(`Update a service`).argument(`<id>`, `Service ID`).requiredOption(`--name <name>`, `New service name`).action(async (serviceId, opts) => { const json = servicesUpdateCommand.parent?.parent?.opts().json ?? false; try { const client = createClient(); const result = await client.services.update({ serviceId, name: opts.name }); if (isQuiet()) { printQuiet(serviceId); return; } if (json) { printJson(result); } else { printSuccess(`Service ${serviceId} updated.`); } } catch (error) { handleError(error, json); } }); // src/commands/services/delete.ts import { Command as Command22 } from "commander"; var servicesDeleteCommand = new Command22(`delete`).description(`Delete a service`).argument(`<id>`, `Service ID`).option(`--force`, `Skip confirmation prompt`).action(async (serviceId, opts) => { const json = servicesDeleteCommand.parent?.parent?.opts().json ?? false; try { if (!opts.force) { if (json) { process.stderr.write( JSON.stringify({ error: `VALIDATION_ERROR`, message: `--force is required with --json`, exitCode: EXIT_VALIDATION_ERROR }) + ` ` ); process.exit(EXIT_VALIDATION_ERROR); } const confirmed = await confirm( `Delete service ${serviceId}? This cannot be undone. [y/N] ` ); if (!confirmed) { if (!isInteractive()) { process.stderr.write( `Use --force to skip confirmation in non-interactive mode. ` ); process.exit(EXIT_VALIDATION_ERROR); } process.stderr.write(`Aborted. `); return; } } const client = createClient(); const result = await client.services.delete({ serviceId, forceDelete: opts.force ?? false }); if (isQuiet()) { printQuiet(serviceId); return; } if (json) { printJson(result); } else { printSuccess(`Service ${serviceId} deleted.`); } } catch (error) { handleError(error, json); } }); // src/commands/services/get-secret.ts import { Command as Command23 } from "commander"; var servicesGetSecretCommand = new Command23(`get-secret`).description(`Show service connection credentials`).argument(`<id>`, `Service ID`).action(async (serviceId) => { const json = servicesGetSecretCommand.parent?.parent?.opts().json ?? false; try { const client = createClient(); const result = await client.services.getSecret({ serviceId }); if (isQuiet()) { printQuiet(result.secret); return; } if (json) { printJson(result); } else { process.stdout.write(result.secret + ` `); } } catch (error) { handleError(error, json); } }); // src/commands/services/create/postgres.ts import { Command as Command24, Option } from "commander"; // src/lib/polling.ts async function pollUntilDone(options) { const intervalMs = options.intervalMs ?? 2e3; const timeoutMs = (options.timeoutSeconds ?? 300) * 1e3; const start = Date.now(); while (true) { const elapsed = Date.now() - start; if (elapsed >= timeoutMs) { throw new Error( `Timed out after ${Math.round(elapsed / 1e3)}s waiting for operation to complete.` ); } const result = await options.fn(); if (result.done) { return result.value; } options.onPoll?.(Math.round(elapsed / 1e3)); await new Promise((resolve) => setTimeout(resolve, intervalMs)); } } // src/commands/services/create/postgres.ts var REGION_LABELS = { "us-east-1": `US East (N. Virginia)`, "eu-west-1": `EU West (Ireland)`, "ca-west-1": `CA West (Canada)` }; var servicesCreatePostgresCommand = new Command24(`postgres`).usage(`--environment <id> --database-url <url> --region <region> [options]`).description(`Connect a Postgres database for sync`).requiredOption(`--environment <id>`, `Environment ID`).requiredOption(`--database-url <url>`, `Postgres connection string`).addOption( new Option(`--region <region>`, `Deployment region`).choices(Object.keys(REGION_LABELS)).makeOptionMandatory() ).option(`--name <name>`, `Service name`).option(`--pooled-database-url <url>`, `Pooled Postgres connection string`).option( `--db-pool-size <size>`, `Database connection pool size (1-20)`, parseInt ).option(`--manual-table-publishing`, `Opt out of automatic table publishing`).option(`--wait`, `Wait for service to become active before returning`).option( `--wait-timeout <seconds>`, `Timeout in seconds when using --wait`, `300` ).addHelpText( `after`, ` Regions: ` + Object.entries(REGION_LABELS).map(([k, v]) => ` ${k} ${v}`).join(` `) ).action(async (opts) => { const json = servicesCreatePostgresCommand.parent?.parent?.parent?.opts().json ?? false; try { const client = createClient(); const options = {}; if (opts.pooledDatabaseUrl) options.pooledDatabaseUrl = opts.pooledDatabaseUrl; if (opts.dbPoolSize) options.dbPoolSize = opts.dbPoolSize; if (opts.manualTablePublishing) options.manualTablePublishing = true; const result = await client.services.postgres.create({ environmentId: opts.environment, databaseUrl: opts.databaseUrl, region: opts.region, ...opts.name ? { serviceName: opts.name } : {}, ...Object.keys(options).length > 0 ? { options } : {} }); const serviceId = result.id; let finalStatus = result.status; if (opts.wait && finalStatus !== `active` && finalStatus !== `error`) { if (!isQuiet() && !json) { process.stderr.write(`Waiting for service to become active... `); } const polled = await pollUntilDone({ fn: async () => { const service = await client.services.postgres.get({ serviceId }); const status = service.status; if (status === `active` || status === `error`) { return { done: true, value: status }; } return { done: false, value: status ?? `pending` }; }, timeoutSeconds: isNaN(parseInt(opts.waitTimeout, 10)) ? 300 : parseInt(opts.waitTimeout, 10), onPoll: (elapsed) => { if (!isQuiet() && !json) { process.stderr.write( ` Waiting for service (${elapsed}s elapsed)... ` ); } } }); finalStatus = polled; } if (finalStatus === `error`) { if (json) { printJson({ ...result, status: finalStatus }); } else if (!isQuiet()) { process.stderr.write(`Service entered error state. `); printKeyValue([ [`ID:`, serviceId], [`Status:`, `error`] ]); } process.exit(1); } if (isQuiet()) { printQuiet(serviceId); return; } if (json) { printJson(opts.wait ? { ...result, status: finalStatus } : result); } else { printSuccess(`Service created successfully. `); printKeyValue([ [`ID:`, serviceId], [`Status:`, finalStatus], [`Secret:`, result.sourceSecret] ]); process.stderr.write( ` Save this secret \u2014 it cannot be retrieved again. ` ); } } catch (error) { handleError(error, json); } }); // src/commands/services/create/streams.ts import { Command as Command25 } from "commander"; var servicesCreateStreamsCommand = new Command25(`streams`).usage(`--environment <id> --name <name>`).description(`Create a durable streams service`).requiredOption(`--environment <id>`, `Environment ID`).requiredOption(`--name <name>`, `Service name`).action(async (opts) => { const json = servicesCreateStreamsCommand.parent?.parent?.parent?.opts().json ?? false; try { const client = createClient(); const result = await client.services.streams.create({ environmentId: opts.environment, name: opts.name }); if (isQuiet()) { printQuiet(result.serviceId); return; } if (json) { printJson(result); } else { printSuccess(`Service created successfully. `); printKeyValue([ [`ID:`, result.serviceId], [`Secret:`, result.secret] ]); process.stderr.write( ` Save this secret \u2014 it cannot be retrieved again. ` ); } } catch (error) { handleError(error, json); } }); // src/commands/services/create/proxy.ts import { Command as Command26 } from "commander"; var servicesCreateProxyCommand = new Command26(`proxy`).usage(`--environment <id> --name <name> --allowlist <patterns>`).description(`Create a proxy service`).requiredOption(`--environment <id>`, `Environment ID`).requiredOption(`--name <name>`, `Service name`).requiredOption(`--allowlist <patterns>`, `Comma-separated URL patterns`).action(async (opts) => { const json = servicesCreateProxyCommand.parent?.parent?.parent?.opts().json ?? false; try { const allowlist = opts.allowlist.split(`,`).map((s) => s.trim()); const client = createClient(); const result = await client.services.proxy.create({ environmentId: opts.environment, name: opts.name, allowlist }); if (isQuiet()) { printQuiet(result.id); return; } if (json) { printJson(result); } else { printSuccess(`Service created successfully. `); printKeyValue([ [`ID:`, result.id], [`Name:`, result.name], [`Secret:`, result.secret] ]); process.stderr.write( ` Save this secret \u2014 it cannot be retrieved again. ` ); } } catch (error) { handleError(error, json); } }); // src/commands/claim/create.ts import { Command as Command27 } from "commander"; var claimCreateCommand = new Command27(`create`).description(`Provision a Postgres database with Sync`).action(async () => { const json = claimCreateCommand.parent?.parent?.opts().json ?? false; try { const client = createPublicClient(); const result = await client.claimable.create({}); if (isQuiet()) { printQuiet(result.claimId); return; } if (json) { printJson(result); } else { printKeyValue([[`Claim ID:`, result.claimId]]); process.stderr.write( ` Check provisioning status with: electric claimable status ${result.claimId} ` ); } } catch (error) { handleError(error, json); } }); // src/commands/claim/status.ts import { Command as Command28 } from "commander"; var claimStatusCommand = new Command28(`status`).description(`Check provisioning status`).argument(`<claimId>`, `Claim ID`).action(async (claimId) => { const json = claimStatusCommand.parent?.parent?.opts().json ?? false; try { const client = createPublicClient(); const result = await client.claimable.get({ claimId }); if (isQuiet()) { printQuiet(result.state); return; } if (json) { printJson(result); } else { const pairs = [[`State:`, result.state]]; if (result.claim_link) { pairs.push([ `Claim link:`, `${getDashboardUrl()}/claim?uuid=${claimId}` ]); } if (result.service_type) { pairs.push([`Service type:`, result.service_type]); } if (result.error) { pairs.push([`Error:`, result.error]); } printKeyValue(pairs); if (result.state === `ready` && result.claim_link) { process.stderr.write( ` Ready to claim. Run: electric claimable claim ${claimId} ` ); } } } catch (error) { handleError(error, json); } }); // src/commands/claim/claim.ts import { Command as Command29 } from "commander"; import { URL as URL3 } from "url"; var CLAIM_TIMEOUT_MS = 10 * 60 * 1e3; var claimCommand = new Command29(`claim`).description(`Claim a provisioned service into your workspace`).argument(`<claimId>`, `Claim ID`).option(`--workspace <id>`, `Workspace ID`).option( `--environment <id>`, `Environment ID (optional \u2014 auto-created if omitted)` ).action(async (claimId, opts) => { const json = claimCommand.parent?.parent?.opts().json ?? false; try { await runClaim(claimId, opts, json); } catch (error) { handleError(error, json); } }); async function runClaim(claimId, opts, json) { const publicClient = createPublicClient(); const status = await publicClient.claimable.get({ claimId }); if (status.state !== `ready`) { exitWithError( `Claimable service is not ready (state: ${status.state}).`, EXIT_CONFLICT, json ); } if (!status.claim_link) { exitWithError( `Claimable service has no claim link. It may have already been claimed.`, EXIT_CONFLICT, json ); } const { port, resultPromise, shutdown } = await startLocalServer({ callbackPath: `/callback`, parseParams: (params) => { const cbClaimId = params.get(`claimId`); const claimed = params.get(`claimed`); if (!cbClaimId) { throw new Error(`Missing claimId in callback`); } return { claimId: cbClaimId, claimed: claimed === `true` }; }, successHtml: `<html><body><p>Ownership transfer complete. You can close this tab.</p><script>window.close()</script></body></html>`, errorHtml: `<html><body><p>Something went wrong during the claim. Please try again.</p></body></html>` }); const claimUrl = new URL3(`/claim`, getDashboardUrl()); claimUrl.searchParams.set(`uuid`, claimId); claimUrl.searchParams.set(`cli_port`, String(port)); const browserUrl = claimUrl.toString(); try { const open = (await import("open")).default; await open(browserUrl); if (!json) { process.stderr.write( `Opening browser for Neon ownership transfer... If the browser doesn't open, visit: ${browserUrl} ` ); } } catch { process.stderr.write( `Could not open browser. Visit this URL to complete the transfer: ${browserUrl} ` ); } let timeoutId; const timeoutPromise = new Promise((_, reject) => { timeoutId = setTimeout(() => { shutdown(); reject( new Error( `Claim timed out after ${CLAIM_TIMEOUT_MS / 6e4} minutes. Run 'electric claimable claim ${claimId}' to try again.` ) ); }, CLAIM_TIMEOUT_MS); }); let callbackResult; try { callbackResult = await Promise.race([resultPromise, timeoutPromise]); clearTimeout(timeoutId); shutdown(); } catch (error) { shutdown(); throw error; } if (!callbackResult.claimed) { exitWithError( `Ownership transfer was not completed. Run 'electric claimable claim ${claimId}' to try again.`, EXIT_CONFLICT, json ); } if (!json) { process.stderr.write(`Ownership transfer confirmed. Claiming service... `); } const client = createClient(); const workspaceId = await resolveWorkspace(client, opts.workspace); const result = await client.claimable.claim({ claimId, workspaceId, ...opts.environment ? { environmentId: opts.environment } : {} }); if (isQuiet()) { printQuiet(result.serviceId); return; } if (json) { printJson(result); } else { printSuccess(`Source claimed successfully. `); printKeyValue([ [`Service ID:`, result.serviceId], [`Service type:`, result.serviceType], [`Project ID:`, result.projectId], [`Environment ID:`, result.environmentId] ]); } } // src/index.ts var jsonMode = process.argv.includes(`--json`); var program = new Command30().name(`electric`).description(`CLI for Electric Cloud`).version(CLI_VERSION).option(`--json`, `Output as JSON`).configureOutput({ writeErr: (str) => { if (jsonMode) { const message = str.replace(/^error:\s*/i, ``).trim(); process.stderr.write( JSON.stringify({ error: `VALIDATION_ERROR`, message, exitCode: EXIT_VALIDATION_ERROR }) + ` ` ); } else { process.stderr.write(str); } }, outputError: (str, write) => write(str) }).exitOverride((err) => { if (err.exitCode === 0) { process.exit(0); } process.exit(EXIT_VALIDATION_ERROR); }).option(`-q, --quiet`, `Output only the primary resource ID`).option(`--verbose`, `Log HTTP request details to stderr`).option(`--token <token>`, `Use this API token for authentication`).option(`--no-input`, `Never prompt for input (fail instead)`).hook(`preAction`, (thisCommand) => { const opts = thisCommand.opts(); if (opts.token) { setExplicitToken(opts.token); } if (opts.input === false) { setNoInput(true); } if (opts.verbose) { setVerbose(true); } if (opts.quiet) { setQuiet(true); } }); var auth = new Command30(`auth`).description( `Manage authentication and API tokens` ); var token = new Command30(`token`).description(`Manage API tokens`); token.addCommand(tokenCreateCommand); token.addCommand(tokenListCommand); token.addCommand(tokenRevokeCommand); auth.addCommand(loginCommand); auth.addCommand(logoutCommand); auth.addCommand(whoamiCommand); auth.addCommand(token); program.addCommand(auth); var workspaces = new Command30(`workspaces`).description( `List and inspect workspaces` ); workspaces.addCommand(workspacesListCommand); workspaces.addCommand(workspacesGetCommand); program.addCommand(workspaces); var projects = new Command30(`projects`).description( `Manage projects within a workspace` ); projects.addCommand(projectsListCommand); projects.addCommand(projectsCreateCommand); projects.addCommand(projectsGetCommand); projects.addCommand(projectsUpdateCommand); projects.addCommand(projectsDeleteCommand); program.addCommand(projects); var environments = new Command30(`environments`).description( `Manage environments within a project` ); environments.addCommand(environmentsListCommand); environments.addCommand(environmentsCreateCommand); environments.addCommand(environmentsGetCommand); environments.addCommand(environmentsUpdateCommand); environments.addCommand(environmentsDeleteCommand); program.addCommand(environments); var services = new Command30(`services`).description( `Manage services (Postgres, streams, proxy)` ); var servicesCreate = new Command30(`create`).description(`Create a new service`); servicesCreate.addCommand(servicesCreatePostgresCommand); servicesCreate.addCommand(servicesCreateStreamsComm