@electric-sql/cli
Version:
CLI for Electric Cloud
2,382 lines • 76.9 kB
JavaScript
#!/usr/bin/env node
// src/index.ts
import { Command as Command33 } from "commander";
import { existsSync as existsSync2 } from "fs";
import { join as join5 } from "path";
// 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((resolve4) => {
const rl = createInterface({
input: process.stdin,
output: process.stderr
});
rl.question(prompt, (answer) => {
rl.close();
resolve4(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.1.0" : `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/telemetry.ts
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
import { join as join2 } from "path";
import { randomUUID } from "crypto";
var POSTHOG_HOST = `https://us.i.posthog.com`;
var POSTHOG_KEY = "" ? "" : ``;
function isTelemetryEnabled() {
if (process.env.ELECTRIC_TELEMETRY?.toLowerCase() === `off`) return false;
if (!POSTHOG_KEY) return false;
return true;
}
function getAnonymousIdFile() {
return join2(getConfigDir(), `anonymous-id`);
}
var cachedAnonymousId;
async function getOrCreateAnonymousId() {
if (cachedAnonymousId) return cachedAnonymousId;
const idFile = getAnonymousIdFile();
try {
const id2 = (await readFile2(idFile, `utf-8`)).trim();
if (id2) {
cachedAnonymousId = id2;
return id2;
}
} catch {
}
const id = randomUUID();
try {
const configDir = getConfigDir();
await mkdir2(configDir, { recursive: true });
await writeFile2(idFile, id, { mode: 384 });
} catch {
}
cachedAnonymousId = id;
return id;
}
function capture(telemetryEvent) {
if (!isTelemetryEnabled()) return;
const promise = captureAsync(telemetryEvent);
pendingRequests.push(promise);
promise.finally(() => {
const idx = pendingRequests.indexOf(promise);
if (idx >= 0) pendingRequests.splice(idx, 1);
});
}
async function captureAsync(telemetryEvent) {
try {
const distinctId = telemetryEvent.distinctId ?? await getOrCreateAnonymousId();
const payload = {
api_key: POSTHOG_KEY,
event: telemetryEvent.event,
properties: {
...telemetryEvent.properties,
$lib: `electric-cli`
},
distinct_id: distinctId,
timestamp: (/* @__PURE__ */ new Date()).toISOString()
};
await fetch(`${POSTHOG_HOST}/i/v0/e/`, {
method: `POST`,
headers: { "Content-Type": `application/json` },
body: JSON.stringify(payload),
signal: AbortSignal.timeout(5e3)
});
} catch {
}
}
var pendingRequests = [];
async function flush() {
await Promise.allSettled(pendingRequests);
pendingRequests.length = 0;
}
// src/commands/auth/login.ts
import { Command } from "commander";
import { randomUUID as randomUUID2 } from "crypto";
import { URL as URL3 } from "url";
// src/lib/errors.ts
import { existsSync } from "fs";
import { join as join3 } from "path";
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 detectAuthType() {
if (process.env.ELECTRIC_API_TOKEN) return `api_token`;
try {
if (existsSync(join3(getConfigDir(), `auth.json`))) return `jwt`;
} catch {
}
return `none`;
}
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);
}
async function handleError(error, json, command) {
const { code, message } = classifyError(error);
capture({
event: `cli_error`,
properties: {
command: command ?? `unknown`,
error_type: errorNameForCode(code),
cli_version: CLI_VERSION,
auth_type: detectAuthType()
}
});
await flush();
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 as URL2 } 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((resolve4, reject) => {
resolveResult = resolve4;
rejectResult = reject;
});
const server = createServer((req, res) => {
const url = new URL2(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) {
await handleError(error, json);
}
});
async function runLogin(json) {
const state = randomUUID2();
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 URL3(`/`, getDashboardUrl());
loginUrl.searchParams.set(`cli_port`, String(port));
loginUrl.searchParams.set(`cli_state`, state);
try {
const open2 = (await import("open")).default;
await open2(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()
});
capture({
event: `cli_auth_login`,
properties: { cli_version: CLI_VERSION }
});
if (json) {
process.stdout.write(
JSON.stringify({ success: true, email: result.email }) + `
`
);
} else {
printSuccess(`Logged in successfully.`);
}
} catch (error) {
const reason = error instanceof Error && error.message.includes(`timed out`) ? `timeout` : error instanceof Error && error.message.includes(`State mismatch`) ? `state_mismatch` : error instanceof Error && error.message.includes(`No token`) ? `no_token` : `unknown`;
capture({
event: `cli_auth_login_failed`,
properties: { cli_version: CLI_VERSION, reason }
});
await flush();
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) {
await 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) {
await 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) {
await 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) {
await 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) {
await 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) {
await 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) {
await 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) {
await 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) {
await 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) {
await 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) {
await 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) {
await 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) {
await 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) {
await 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) {
await 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) {
await 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) {
await 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) {
await 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]);
}
if (`baseUrl` in result && result.baseUrl) {
pairs.push([`Base URL:`, result.baseUrl]);
}
printKeyValue(pairs);
}
} catch (error) {
await 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) {
await 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) {
await 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) {
await 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((resolve4) => setTimeout(resolve4, 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(
`--replication-idle-timeout <seconds>`,
`Idle replication connection timeout in seconds (0 to disable; recommended for Neon)`,
parseInt
).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;
if (opts.replicationIdleTimeout !== void 0)
options.replicationIdleTimeout = opts.replicationIdleTimeout;
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]
]);
}
} catch (error) {
await 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],
[`Name:`, result.name],
[`Base URL:`, result.baseUrl],
[`Secret:`, result.secret]
]);
}
} catch (error) {
await 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],
[`Base URL:`, result.baseUrl],
[`Secret:`, result.secret]
]);
}
} catch (error) {
await handleError(error, json);
}
});
// src/commands/services/create/yjs.ts
import { Command as Command27 } from "commander";
var servicesCreateYjsCommand = new Command27(`yjs`).usage(`--environment <id> --name <name>`).description(`Create a Yjs collaborative document service`).requiredOption(`--environment <id>`, `Environment ID`).requiredOption(`--name <name>`, `Service name`).action(async (opts) => {
const json = servicesCreateYjsCommand.parent?.parent?.parent?.opts().json ?? false;
try {
const client = createClient();
const result = await client.services.yjs.create({
environmentId: opts.environment,
name: opts.name
});
if (isQuiet()) {
printQuiet(result.id);
return;
}
if (json) {
printJson(result);
} else {
printSuccess(`Service created successfully.
`);
printKeyValue([
[`ID:`, result.id],
[`Name:`, result.name],
[`Base URL:`, result.baseUrl],
[`Secret:`, result.secret]
]);
}
} catch (error) {
await handleError(error, json);
}
});
// src/commands/durable-streams/export.ts
import { resolve as resolve2 } from "path";
import { Command as Command28 } from "commander";
// src/lib/durable-stream-archive.ts
import { createReadStream } from "fs";
import {
access,
mkdtemp,
mkdir as mkdir3,
open,
readdir,
readFile as readFile3,
rm,
stat
} from "fs/promises";
import { tmpdir } from "os";
import { basename, dirname, join as join4, posix, resolve } from "path";
import { create as createTar, extract as extractTar } from "tar";
var ARCHIVE_VERSION = 1;
var LIST_PAGE_SIZE = 1e3;
var MAX_FILENAME_BYTES = 240;
var STREAM_NEXT_OFFSET_HEADER = `Stream-Next-Offset`;
var STREAM_UP_TO_DATE_HEADER = `Stream-Up-To-Date`;
var STREAM_TTL_HEADER = `Stream-TTL`;
var STREAM_EXPIRES_AT_HEADER = `Stream-Expires-At`;
var STREAM_TOTAL_SIZE_HEADER = `Stream-Total-Size`;
var STREAM_WRITE_UNITS_HEADER = `Stream-Write-Units`;
var STREAM_CLOSED_HEADER = `Stream-Closed`;
var STREAM_METADATA_HEADER = `Electric-Stream-Metadata`;
function defaultExportDirectory(now = /* @__PURE__ */ new Date()) {
const timestamp = now.toISOString().replace(/[:.]/g, `-`);
return resolve(`durable-streams-${timestamp}`);
}
function parseHeader(value) {
const separator = value.indexOf(`:`);
if (separator <= 0) {
throw new Error(`Invalid --header value "${value}"; expected "Name: value"`);
}
const name = value.slice(0, separator).trim();
const headerValue = value.slice(separator + 1).trim();
if (!name) {
throw new Error(`Invalid --header value "${value}"; expected "Name: value"`);
}
try {
new Headers({ [name]: headerValue });
} catch {
throw new Error(`Invalid HTTP header "${name}"`);
}
return [name, headerValue];
}
function normalizeBaseUrl(value) {
let url;
try {
url = new URL(value);
} catch {
throw new Error(`Invalid URL: ${value}`);
}
if (url.protocol !== `http:` && url.protocol !== `https:`) {
throw new Error(`URL must use http or https: ${value}`);
}
if (url.search || url.hash) {
throw new Error(`URL must not contain a query string or fragment: ${value}`);
}
return url.href.replace(/\/$/, ``);
}
function encodeStreamFilename(streamPath) {
if (!streamPath) throw new Error(`Stream path must not be empty`);
const encoded = encodeURIComponent(streamPath);
if (Buffer.byteLength(`${encoded}.json`) > MAX_FILENAME_BYTES) {
throw new Error(
`URL-encoded stream path exceeds ${MAX_FILENAME_BYTES} filename bytes: ${streamPath}`
);
}
return encoded;
}
function streamUrl(baseUrl, streamPath) {
const encodedPath = streamPath.split(`/`).map((segment) => encodeURIComponent(segment)).join(`/`);
return `${normalizeBaseUrl(baseUrl)}/${encodedPath}`;
}
function controlListUrl(serviceUrl, cursor) {
const url = new URL(`${normalizeBaseUrl(serviceUrl)}/__ds/streams`);
url.searchParams.set(`limit`, String(LIST_PAGE_SIZE));
if (cursor) url.searchParams.set(`cursor`, cursor);
return url.href;
}
function isRecord(value) {
return typeof value === `object` && value !== null && !Array.isArray(value);
}
function isIsoDate(value) {
return !Number.isNaN(new Date(value).getTime());
}
function parseListedStream(value) {
if (!isRecord(value))
throw new Error(`Stream list contains a non-object item`);
const { id, path, contentType, expiresAt, metadata, createdAt } = value;
if (typeof id !== `string` || typeof path !== `string` || path.length === 0 || typeof contentType !== `string` || contentType.length === 0 || !isRecord(metadata) || typeof createdAt !== `string` || !isIsoDate(createdAt) || expiresAt !== null && (typeof expiresAt !== `string` || !isIsoDate(expiresAt))) {
throw new Error(`Stream list contains invalid metadata`);
}
return {
id,
path,
contentType,
expiresAt,
metadata,
createdAt
};
}
function parseListPage(value) {
if (!isRecord(value) || !Array.isArray(value.streams)) {
throw new Error(`Stream list response has an invalid shape`);
}
if (typeof value.hasMore !== `boolean` || value.nextCursor !== null && typeof value.nextCursor !== `string`) {
throw new Error(`Stream list response has invalid pagination metadata`);
}
const hasMore = value.hasMore;
const nextCursor = value.nextCursor;
if (hasMore !== (nextCursor !== null)) {
throw new Error(`Stream list response has inconsistent pagination metadata`);
}
return {
streams: value.streams.map(parseListedStream),
hasMore,
nextCursor
};
}
async function responseError(response, operation) {
const body = (await response.text()).slice(0, 2e3).trim();
const detail = body ? `: ${body}` : ``;
return new Error(`${operation} failed with HTTP ${response.status}${detail}`);
}
async function listAllStreams(serviceUrl, readToken, fetchClient) {
const streams = [];
const seenCursors = /* @__PURE__ */ new Set();
let cursor;
while (true) {
const response = await fetchClient(controlListUrl(serviceUrl, cursor), {
headers: { Authorization: `Bearer ${readToken}` }
});
if (!response.ok) throw await responseError(response, `Listing streams`);
const page = parseListPage(await response.json());
streams.push(...page.streams);
if (!page.nextCursor) return streams;
if (seenCursors.has(page.nextCursor)) {
throw new Error(`Stream list returned a repeated pagination cursor`);
}
seenCursors.add(page.nextCursor);
cursor = page.nextCursor;
}
}
function normalizeContentType(value) {
return value.split(`;`, 1)[0].trim().toLowerCase();
}
function jsonArrayContents(value, streamPath) {
const trimmed = value.trim();
if (!trimmed) return ``;
let parsed;
try {
parsed = JSON.parse(trimmed);
} catch {
throw new Error(`Stream ${streamPath} returned invalid JSON`);
}
if (!Array.isArray(parsed) || !trimmed.startsWith(`[`) || !trimmed.endsWith(`]`)) {
throw new Error(`Stream ${streamPath} returned a non-array JSON page`);
}
return trimmed.slice(1, -1).trim();
}
async function writeResponseBody(file, response) {
if (!response.body) return;
const reader = response.body.getReader();
try {
while (true) {
const chunk = await reader.read();
if (chunk.done) return;
await file.write(chunk.value);
}
} finally {
reader.releaseLock();
}
}
async function exportStreamData(args) {
const authHeaders = { Authorization: `Bearer ${args.readToken}` };
const head = await args.fetchClient(args.url, {
method: `HEAD`,
headers: authHeaders
});
if (!head.ok)
throw await responseError(head, `Reading metadata for ${args.streamPath}`);
const contentType = head.headers.get(`Content-Type`) ?? args.listedContentType;
const isJson = normalizeContentType(contentType) === `application/json`;
const file = await open(args.outputFile, `wx`);
let offset = `-1`;
let wroteJsonItems = false;
let tailOffset = head.headers.get(STREAM_NEXT_OFFSET_HEADER);
let closed = head.headers.get(STREAM_CLOSED_HEADER)?.toLowerCase() === `true`;
try {
if (isJson) await file.write(`[`);
while (true) {
const url = new URL(args.url);
url.searchParams.set(`offset`, offset);
const response = await args.fetchClient(url, { headers: authHeaders });
if (!response.ok) {
throw await responseError(response, `Reading ${args.streamPath}`);
}
if (isJson) {
const contents = jsonArrayContents(
await response.text(),
args.streamPath
);
if (contents) {
if (wroteJsonItems) await file.write(`,`);
await file.write(contents);
wroteJsonItems = true;
}
} else {
await writeResponseBody(file, response);
}
const upToDate = response.headers.has(STREAM_UP_TO_DATE_HEADER);
const nextOffset = response.headers.get(STREAM_NEXT_OFFSET_HEADER);
tailOffset = nextOffset ?? tailOffset;
closed = closed || response.headers.get(STREAM_CLOSED_HEADER)?.toLowerCase() === `true`;
if (upToDate) break;
if (!nextOffset || nextOffset === offset) {
throw new Error(
`Stream ${args.streamPath} did not provide a progressing next offset`
);
}
offset = nextOffset;
}
if (isJson) await file.write(`]`);
} finally {
await file.close();
}
return {
closed,
tailOffset,
ttlSeconds: head.headers.get(STREAM_TTL_HEADER),
expiresAt: head.headers.get(STREAM_EXPIRES_AT_HEADER),
totalSize: head.headers.get(STREAM_TOTAL_SIZE_HEADER),
writeUnits: head.headers.get(STREAM_WRITE_UNITS_HEADER),
archivedSize: (await stat(args.outputFile)).size,
contentType
};
}
async function ensureMissing(path) {
try {
await access(path);
} catch (error) {
if (error instanceof Error && `code` in error && error.code === `ENOENT`) {
return;
}
throw error;
}
throw new Error(`Refusing to overwrite existing path: ${path}`);
}
async function exportDurableStreams(config, options = {}) {
const fetchClient = options.fetch ?? globalThis.fetch;
const log = options.log ?? console.log;
const exportedAt = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
const directory = resolve(config.outputDirectory);
const archive = `${directory}.tar.gz`;
await ensureMissing(directory);
await ensureMissing(archive);
const streams = await listAllStreams(
config.serviceUrl,
config.readToken,
fetchClient
);
await mkdir3(dirname(directory), { recursive: true });
await mkdir3(directory);
for (const [index, stream] of streams.entries()) {
const encodedName = encodeStreamFilename(stream.path);
const dataFile = join4(directory, `${encodedName}.bin`);
const metadataFile = join4(directory, `${encodedName}.json`);
log(`[${index + 1}/${streams.length}] Exporting ${stream.path}`);
const readMetadata = await exportStreamData({
url: streamUrl(config.serviceUrl, stream.path),
readToken: config.readToken,
outputFile: dataFile,
listedContentType: stream.contentType,
streamPath: stream.path,
fetchClient
});
const metadata = {
archiveVersion: ARCHIVE_VERSION,
id: stream.id,
path: stream.path,
contentType: readMetadata.contentType,
expiresAt: readMetadata.expiresAt ?? stream.expiresAt,
metadata: stream.metadata,
createdAt: stream.createdAt,
exportedAt,
closed: readMetadata.closed,
tailOffset: readMetadata.tailOffset,
ttlSeconds: readMetadata.ttlSeconds,
totalSize: readMetadata.totalSize,
writeUnits: readMetadata.writeUnits,
archivedSize: readMetadata.archivedSize
};
await open(metadataFile, `wx`).then(async (file) => {
try {
await file.writeFile(`${JSON.stringify(metadata, null, 2)}
`);
} finally {
await file.close();
}
});
}
await createTar(
{
cwd: dirname(directory),
file: archive,
gzip: true,
portable: true
},
[basename(directory)]
);
return { directory, archive, streamCount: streams.length };
}
function requiredString(value, key) {
const item = value[key];
if (typeof item !== `string` || item.length === 0) {
throw new Error(`Archive metadata field ${key} must be a non-empty string`);
}
return item;
}
function parseArchiveMetadata(value) {
if (!isRecord(value) || value.archiveVersion !== ARCHIVE_VERSION) {
throw new Error(`Unsupported or missing archiveVersion`);
}
const expiresAt = value.expiresAt;
if (expiresAt !== null && (typeof expiresAt !== `string` || !isIsoDate(expiresAt))) {
throw new Error(`Archive metadata expiresAt must be null or an ISO date`);
}
if (!isRecord(value.metadata)) {
throw new Error(`Archive metadata field metadata must be an object`);
}
if (typeof value.closed !== `boolean`) {
throw new Error(`Archive metadata field closed must be a boolean`);
}
if (typeof value.archivedSize !== `number` || !Number.isSafeInteger(value.archivedSize) || value.archivedSize < 0) {
throw new Error(
`Archive metadata field archivedSize must be a non-negative integer`
);
}
const nullableString = (key) => {
const item = value[key];
if (item === null) return null;
if (typeof item !== `string`) {
throw new Error(`Archive metadata field ${key} must be a string or null`);
}
return item;
};
const createdAt = requiredString(value, `createdAt`);
const exportedAt = requiredString(value, `exportedAt`);
if (!isIsoDate(createdAt) || !isIsoDate(exportedAt)) {
throw new Error(`Archive creation timestamps must be ISO dates`);
}
return {
archiveVersion: ARCHIVE_VERSION,
id: requiredString(value, `id`),
path: requiredString(value, `path`),
contentType: requiredString(value, `contentType`),
expiresAt,
metadata: value.metadata,
createdAt,
exportedAt,
closed: value.closed,
tailOffset: nullableString(`tailOffset`),
ttlSeconds: nullableString(`ttlSeconds`),
totalSize: nullableString(`totalSize`),
writeUnits: nullableString(`writeUnits`),
archivedSize: value.archivedSize
};
}
function safeArchiveEntry(entryPath, type) {
const normalized = posix.normalize(entryPath);
if (posix.isAbsolute(entryPath) || normalized === `..` || normalized.startsWith(`../`) || normalized.includes(`/../`)) {
throw new Error(`Archive contains an unsafe path: ${entryPath}`);
}
if (![`File`, `OldFile`, `Directory`].includes(type)) {
throw new Error(
`Archive contains unsupported entry type ${type}: ${entryPath}`
);
}
return true;
}
async function walkFiles(directory) {
const files = [];
for (const entry of await readdir(directory, { withFileTypes: true })) {
const path = join4(directory, entry.name);
if (entry.isDirectory()) {
files.push(...await walkFiles(path));
} else if (entry.isFile()) {
files.push(path);
} else {
throw new Error(
`Archive contains an unsupported filesystem entry: ${path}`
);
}
}
return files;
}
async function readImportEntries(directory) {
const files = await walkFiles(directory);
const fileSet = new Set(files);
const metadataFiles = files.filter((file) => file.endsWith(`.json`)).sort();
const dataFiles = files.filter((file) => file.endsWith(`.bin`));
const unexpected = files.filter(
(file) => !file.endsWith(`.json`) && !file.endsWith(`.bin`)
);
if (unexpected.length > 0) {
throw new Error(`Archive contains an unexpected file: ${unexpected[0]}`);
}
const entries = [];
const streamPaths = /* @__PURE__ */ new Set();
for (const metadataFile of metadataFiles) {
const stem = metadataFile.slice(0, -`.json`.length);
const dataFile = `${stem}.bin`;
if (!fileSet.has(dataFile)) {
throw new Error(`Missing data file for ${metadataFile}`);
}
const metadata = parseArchiveMetadata(
JSON.parse(await readFile3(metadataFile, `utf8`))
);
const encodedName = basename(stem);
let decodedName;
try {
decodedName = decodeURIComponent(encodedName);
} catch {
throw new Error(`Invalid URL-encoded stream filename: ${encodedName}`);
}
if (decodedName !== metadata.path) {
throw new Error(
`Stream filename ${encodedName} does not match metadata path ${metadata.path}`
);
}
if (streamPaths.has(metadata.path)) {
throw new Error(
`Archive contains duplicate stream path: ${metadata.path}`
);
}
streamPaths.add(metadata.path);
const dataSize = (await stat(dataFile)).size;
if (dataSize !== metadata.archivedSize) {
throw new Error(
`Data size for ${metadata.path} is ${dataSize}, expected ${metadata.archivedSize}`
);
}
entries.push({ metadata, dataFile });
}
if (dataFiles.length !== entries.length) {
throw new Error(`Archive contains a data file without matching metadata`);
}
return entries;
}
function importHeaders(customHeaders, metadata) {
const headers = new Headers(customHeaders);
headers.set(`Content-Type`, metadata.contentType);
headers.set(STREAM_METADATA_HEADER, JSON.stringify(metadata.metadata));
if (metadata.expiresAt) {
headers.set(STREAM_EXPIRES_AT_HEADER, metadata.expiresAt);
} else if (metadata.ttlSeconds !== null) {
headers.set(STREAM_TTL_HEADER, metadata.ttlSeconds);
}
if (metadata.closed) headers.set(STREAM_CLOSED_HEADER, `true`);
return headers;
}
async function importEntry(config, entry, fetchClient) {
const url = streamUrl(config.serverUrl, entry.metadata.path);
const target = await fetchClient(url, {
method: `HEAD`,
headers: config.headers
});
if (target.ok) {
throw new Error(`Target stream already exists: ${entry.metadata.path}`);
}
if (target.status !== 404) {
throw await responseError(target, `Checking target ${entry.metadata.path}`);
}
const dataSize = (await stat(entry.dataFile)).size;
const request = {
method: `PUT`,
headers: importHeaders(config.headers, entry.metadata),
duplex: `half`
};
if (dataSize > 0) {
request.body = createReadStream(
entry.dataFile
);
}
const response = await fetchClient(url, request);
if (!response.ok) {
throw await responseError(response, `Creating ${entry.metadata.path}`);
}
}
async function importDurableStreams(config, options = {}) {
const fetchClient = options.fetch ?? globalThis.fetch;
const log = options.log ?? console.log;
const temporaryDirectory = await mkdtemp(
join4(tmpdir(), `durable-stream-import-`)
);
try {
await extractTar({
cwd: temporaryDirectory,
file: config.archive,
strict: true,
filter: (entryPath, entry) => {
const type = `type` in entry ? entry.type : entry.isDirectory() ? `Directory` : entry.isFile() ? `File` : `Unknown`;
return safeArchiveEntry(entryPath, type);
}
});
const entries = await readImportEntries(temporaryDirectory);
for (const [index, entry] of entries.entries()) {
log(`[${index + 1}/${entries.length}] Importing ${entry.metadata.path}`);
await importEntry(config, entry, fetchClient);
}
return { streamCount: entries.length };
} finally {
await rm(temporaryDirectory, { recursive: true, force: true });
}
}
// src/commands/durable-streams/export.ts
var durableStreamsExportCommand = new Command28(`export`).description(`Export all streams in a service to a tar.gz archive`).requiredOption(
`--service-url <url>`,
`Durable Streams service URL, ending in the service ID`
).requiredOption(`--read-token <token>`, `Service read token`).option(`--output <folder>`, `Output folder; defaults to a timestamped name`).action(async (options) => {
const json = durableStreamsExportCommand.parent?.parent?.opts().json ?? false;
try {
const result = await exportDurableStreams(
{
serviceUrl: normalizeBaseUrl(options.serviceUrl),
readToken: options.readToken,
outputDirectory: resolve2(options.output ?? defaultExportDirectory())
},
{
log: isQuiet() ? () => void 0 : (message) => process.stderr.write(`${message}
`)
}
);
if (isQuiet()) {
printQuiet(result.archive);
} else if (json) {
printJson(result);
} else {
printKeyValue([
[`Streams:`, String(result.streamCount)],
[`Directory:`, result.directory],
[`Archive:`, result.archive]
]);
}
} catch (error) {
await handleError(error, json, `durable-streams export`);
}
});
// src/commands/durable-streams/import.ts
import { resolve as resolve3 } from "path";
import { Command as Command29 } from "commander";
function collectHeader(value, previous) {
return [...previous, value];
}
var durableStreamsImportCommand = new Command29(`import`).description(`Recreate streams from a tar.gz archive`).requiredOption(`--archive <file>`, `Archive created by the export command`).requiredOption(`--server-url <url>`, `Target Durable Streams server URL`).option(
`-H, --header <header>`,
`Target request header in "Name: value" form; may be repeated`,
collectHeader,
[]
).action(async (options) => {
const json = durableStreamsImportCommand.parent?.parent?.opts().json ?? false;
try {
const headers = {};
for (const value of options.header) {
const [name, headerValue] = parseHeader(value);
headers[name] = headerValue;
}
const result = await importDurableStreams(
{
archive: resolve3(options.archive),
serverUrl: normalizeBaseUrl(options.serverUrl),
headers
},
{
log: isQuiet() ? () => void 0 : (message) => process.stderr.write(`${message}
`)
}
);
if (isQuiet()) {
printQuiet(String(result.streamCount));
} else if (json) {
printJson(result);
} else {
printKeyValue([[`Streams imported:`, String(result.streamCount)]]);
}
} catch (error) {
await handleError(error, json, `durable-streams import`);
}
});
// src/commands/claim/create.ts
import { Command as Command30 } from "commander";
var claimCreateCommand = new Command30(`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) {
await handleError(error, json);
}
});
// src/commands/claim/status.ts
import { Command as Command31 } from "commander";
var claimStatusCommand = new Command31(`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.connection_uri) {
pairs.push([`Database URL:`, result.connection_uri]);
}
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) {
await handleError(error, json);
}
});
// src/commands/claim/claim.ts
import { Command as Command32 } from "commander";
var claimCommand = new Command32(`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) {
await handleError(error, json);
}
});
async function runClaim(claimId, opts, json) {
const publicClient = createPublicClient();
const status = await publicClient.claimable.get({ claimId });
if (status.state === `claimed`) {
exitWithError(`This service has already been claimed.`, EXIT_CONFLICT, json);
}
if (status.state !== `ready`) {
exitWithError(
`Claimable service is not ready (state: ${status.state}).`,
EXIT_CONFLICT,
json
);
}
if (status.claim_link) {
const claimUrl = `${getDashboardUrl()}/claim?uuid=${claimId}`;
if (json) {
printJson({ claimId, claim_url: claimUrl, claimed: false });
} else {
process.stderr.write(`Visit the link below to claim the database:
`);
process.stdout.write(`${claimUrl}
`);
}
return;
}
if (!json) {
process.stderr.write(`Database claimed. Adding service to workspace...
`);
}
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 Command33().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);
}
;
program.__startTime = performance.now();
}).hook(`postAction`, (thisCommand, actionCommand) => {
const startTime = program.__startTime;
const durationMs = startTime ? Math.round(performance.now() - startTime) : void 0;
const rootOpts = program.opts();
const flagsUsed = collectFlags(rootOpts, actionCommand.opts());
capture({
event: `cli_command_executed`,
properties: {
command: getFullCommandName(actionCommand),
cli_version: CLI_VERSION,
auth_type: getAuthType(),
output_format: rootOpts.json ? `json` : `text`,
exit_code: 0,
duration_ms: durationMs,
flags_used: flagsUsed
}
});
});
var auth = new Command33(`auth`).description(
`Manage authentication and API tokens`
);
var token = new Command33(`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 Command33(`workspaces`).description(
`List and inspect workspaces`
);
workspaces.addCommand(workspacesListCommand);
workspaces.addCommand(workspacesGetCommand);
program.addCommand(workspaces);
var projects = new Command33(`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 Command33(`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 Command33(`services`).description(
`Manage services (Postgres, streams, proxy, yjs)`
);
var servicesCreate = new Command33(`create`).description(`Create a new service`);
servicesCreate.addCommand(servicesCreatePostgresCommand);
servicesCreate.addCommand(servicesCreateStreamsCommand);
servicesCreate.addCommand(servicesCreateProxyCommand);
servicesCreate.addCommand(servicesCreateYjsCommand);
services.addCommand(servicesListCommand);
services.addCommand(servicesGetCommand);
services.addCommand(servicesUpdateCommand);
services.addCommand(servicesDeleteCommand);
services.addCommand(servicesGetSecretCommand);
services.addCommand(servicesCreate);
program.addCommand(services);
var durableStreams = new Command33(`durable-streams`).description(
`Export and import Durable Streams archives`
);
durableStreams.addCommand(durableStreamsExportCommand);
durableStreams.addCommand(durableStreamsImportCommand);
program.addCommand(durableStreams);
var claimable = new Command33(`claimable`).description(
`Immediately provision a Postgres database with Sync (no auth required)`
);
claimable.addCommand(claimCreateCommand);
claimable.addCommand(claimStatusCommand);
claimable.addCommand(claimCommand);
program.addCommand(claimable);
function propagateErrorConfig(cmd) {
for (const sub of cmd.commands) {
sub.configureOutput(program.configureOutput());
sub.exitOverride((err) => {
if (err.exitCode === 0) {
process.exit(0);
}
process.exit(EXIT_VALIDATION_ERROR);
});
propagateErrorConfig(sub);
}
}
propagateErrorConfig(program);
function getFullCommandName(cmd) {
const parts = [];
let current = cmd;
while (current && current !== program) {
parts.unshift(current.name());
current = current.parent;
}
return parts.join(` `);
}
function getAuthType() {
const opts = program.opts();
if (opts.token) return `api_token`;
if (process.env.ELECTRIC_API_TOKEN) return `api_token`;
try {
const authFile = join5(getConfigDir(), `auth.json`);
if (existsSync2(authFile)) return `jwt`;
} catch {
}
return `none`;
}
function collectFlags(rootOpts, cmdOpts) {
const flags = [];
if (rootOpts.json) flags.push(`--json`);
if (rootOpts.quiet) flags.push(`--quiet`);
if (rootOpts.verbose) flags.push(`--verbose`);
if (rootOpts.token) flags.push(`--token`);
if (rootOpts.input === false) flags.push(`--no-input`);
if (cmdOpts.wait) flags.push(`--wait`);
if (cmdOpts.force) flags.push(`--force`);
return flags;
}
program.parseAsync().then(() => flush());
//# sourceMappingURL=index.js.map