@electric-sql/cli
Version:
CLI for Electric Cloud
1,615 lines (1,570 loc) • 51 kB
JavaScript
#!/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