ardent-cli 0.0.34 → 0.0.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +989 -12
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1510,11 +1510,24 @@ async function createAction2(type, url, options) {
1510
1510
  console.error(` Supported: ${supportedTypes.join(", ")}`);
1511
1511
  process.exit(1);
1512
1512
  }
1513
- const legacyByocNeonProvider = typeof options.byoc === "string" ? options.byoc : void 0;
1513
+ let selectedEnvironmentId = options.environmentId;
1514
+ let legacyByocNeonProvider;
1515
+ let useByocEnvironment = options.byoc === true;
1516
+ if (typeof options.byoc === "string") {
1517
+ if (options.byoc === "neon") {
1518
+ legacyByocNeonProvider = options.byoc;
1519
+ } else {
1520
+ useByocEnvironment = true;
1521
+ if (selectedEnvironmentId && selectedEnvironmentId !== options.byoc) {
1522
+ console.error("\u2717 Conflicting BYOC environment IDs provided");
1523
+ process.exit(1);
1524
+ }
1525
+ selectedEnvironmentId = options.byoc;
1526
+ }
1527
+ }
1514
1528
  const byocNeonProvider = options.byocNeon ?? legacyByocNeonProvider;
1515
- const useByocEnvironment = options.byoc === true;
1516
1529
  const isByocNeon = Boolean(byocNeonProvider);
1517
- if (options.environmentId && !useByocEnvironment) {
1530
+ if (selectedEnvironmentId && !useByocEnvironment) {
1518
1531
  console.error("\u2717 --environment-id requires --byoc");
1519
1532
  process.exit(1);
1520
1533
  }
@@ -1584,8 +1597,8 @@ async function createAction2(type, url, options) {
1584
1597
  }
1585
1598
  if (useByocEnvironment) {
1586
1599
  createPayload.use_environment = true;
1587
- if (options.environmentId) {
1588
- createPayload.environment_id = options.environmentId;
1600
+ if (selectedEnvironmentId) {
1601
+ createPayload.environment_id = selectedEnvironmentId;
1589
1602
  }
1590
1603
  if (options.privateLinkId) {
1591
1604
  createPayload.private_link_id = options.privateLinkId;
@@ -2849,6 +2862,964 @@ async function removeAction2(key) {
2849
2862
  }
2850
2863
  }
2851
2864
 
2865
+ // src/lib/supabase_link.ts
2866
+ import { execFileSync } from "child_process";
2867
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2, rmSync } from "fs";
2868
+ import { homedir as homedir2 } from "os";
2869
+ import { dirname, join as join2 } from "path";
2870
+ var CONFIG_DIR2 = join2(homedir2(), ".ardent");
2871
+ var STATE_DIR = join2(CONFIG_DIR2, "supabase-links");
2872
+ var ORIGINAL_SUFFIX = "-ardent-original";
2873
+ var SERVICE_SUFFIXES = ["rest", "auth", "storage", "realtime", "pg_meta"];
2874
+ var DOCKER_HOST_GATEWAY = "host.docker.internal";
2875
+ var REALTIME_LIST_CHANGES_MIGRATION_VERSION = "20230328144023";
2876
+ var SENSITIVE_DOCKER_ENV_KEYS = /* @__PURE__ */ new Set([
2877
+ "DATABASE_URL",
2878
+ "DB_PASSWORD",
2879
+ "GOTRUE_DB_DATABASE_URL",
2880
+ "PG_META_DB_PASSWORD",
2881
+ "PGRST_DB_URI",
2882
+ "PGPASSWORD"
2883
+ ]);
2884
+ function redactDockerArgs(args2) {
2885
+ return args2.map((arg) => {
2886
+ const separatorIndex = arg.indexOf("=");
2887
+ if (separatorIndex === -1) {
2888
+ return arg;
2889
+ }
2890
+ const key = arg.slice(0, separatorIndex);
2891
+ if (SENSITIVE_DOCKER_ENV_KEYS.has(key)) {
2892
+ return `${key}=***`;
2893
+ }
2894
+ return arg;
2895
+ });
2896
+ }
2897
+ function redactDockerErrorMessage(message) {
2898
+ let redactedMessage = message.replace(
2899
+ /(postgres(?:ql)?:\/\/[^:\s/]+:)[^@\s]+@/g,
2900
+ "$1***@"
2901
+ );
2902
+ for (const key of SENSITIVE_DOCKER_ENV_KEYS) {
2903
+ redactedMessage = redactedMessage.replace(
2904
+ new RegExp(`${key}=[^\\s]+`, "g"),
2905
+ `${key}=***`
2906
+ );
2907
+ }
2908
+ return redactedMessage;
2909
+ }
2910
+ function runDocker(args2, options = {}) {
2911
+ try {
2912
+ return execFileSync("docker", args2, {
2913
+ encoding: "utf-8",
2914
+ stdio: options.stdio ?? "pipe"
2915
+ }).trim();
2916
+ } catch (error) {
2917
+ if (error instanceof Error) {
2918
+ throw new Error(
2919
+ `Docker command failed: docker ${redactDockerArgs(args2).join(" ")}
2920
+ ${redactDockerErrorMessage(error.message)}`
2921
+ );
2922
+ }
2923
+ throw error;
2924
+ }
2925
+ }
2926
+ function runDockerWithInput(args2, input, options = {}) {
2927
+ try {
2928
+ return execFileSync("docker", args2, {
2929
+ encoding: "utf-8",
2930
+ input,
2931
+ stdio: ["pipe", "pipe", "pipe"],
2932
+ timeout: options.timeoutMs
2933
+ }).trim();
2934
+ } catch (error) {
2935
+ if (error instanceof Error) {
2936
+ throw new Error(
2937
+ `Docker command failed: docker ${redactDockerArgs(args2).join(" ")}
2938
+ ${redactDockerErrorMessage(error.message)}`
2939
+ );
2940
+ }
2941
+ throw error;
2942
+ }
2943
+ }
2944
+ function dockerExists(name) {
2945
+ const output = runDocker(["ps", "-a", "--format", "{{.Names}}"]);
2946
+ return output.split("\n").includes(name);
2947
+ }
2948
+ function inspectContainer(name) {
2949
+ const output = runDocker(["inspect", name]);
2950
+ const parsed = JSON.parse(output);
2951
+ if (parsed.length !== 1) {
2952
+ throw new Error(`Expected one Docker container named ${name}`);
2953
+ }
2954
+ return parsed[0];
2955
+ }
2956
+ function containerIpAddress(name, network) {
2957
+ const container = inspectContainer(name);
2958
+ const networkState = container.NetworkSettings.Networks[network];
2959
+ const ipAddress = networkState?.IPAddress;
2960
+ if (!ipAddress) {
2961
+ throw new Error(`Container ${name} is not attached to network ${network}`);
2962
+ }
2963
+ return ipAddress;
2964
+ }
2965
+ function statePath(projectId) {
2966
+ return join2(STATE_DIR, `${projectId.replace(/[^A-Za-z0-9_.-]/g, "_")}.json`);
2967
+ }
2968
+ function readState(projectId) {
2969
+ const path = statePath(projectId);
2970
+ if (!existsSync2(path)) {
2971
+ return void 0;
2972
+ }
2973
+ return JSON.parse(readFileSync5(path, "utf-8"));
2974
+ }
2975
+ function writeState(state) {
2976
+ mkdirSync2(dirname(statePath(state.projectId)), { recursive: true });
2977
+ writeFileSync2(statePath(state.projectId), JSON.stringify(state, null, 2));
2978
+ }
2979
+ function deleteState(projectId) {
2980
+ const path = statePath(projectId);
2981
+ if (existsSync2(path)) {
2982
+ rmSync(path);
2983
+ }
2984
+ }
2985
+ function detectSupabaseProject(explicitProject) {
2986
+ if (explicitProject) {
2987
+ return explicitProject;
2988
+ }
2989
+ const output = runDocker([
2990
+ "ps",
2991
+ "-a",
2992
+ "--format",
2993
+ '{{.Names}} {{.Label "com.supabase.cli.project"}}'
2994
+ ]);
2995
+ const projects = /* @__PURE__ */ new Set();
2996
+ for (const line of output.split("\n")) {
2997
+ const parts = line.split(" ");
2998
+ if (parts.length !== 2) {
2999
+ continue;
3000
+ }
3001
+ const projectId2 = parts[1]?.trim();
3002
+ if (projectId2) {
3003
+ projects.add(projectId2);
3004
+ }
3005
+ }
3006
+ if (projects.size === 0) {
3007
+ throw new Error("No local Supabase CLI containers found. Run `supabase start` first.");
3008
+ }
3009
+ if (projects.size > 1) {
3010
+ throw new Error(
3011
+ `Multiple Supabase projects found: ${Array.from(projects).join(", ")}. Pass --project <id>.`
3012
+ );
3013
+ }
3014
+ const projectId = Array.from(projects)[0];
3015
+ if (!projectId) {
3016
+ throw new Error("Could not detect local Supabase project id.");
3017
+ }
3018
+ return projectId;
3019
+ }
3020
+ function containerName(prefix, projectId) {
3021
+ return `supabase_${prefix}_${projectId}`;
3022
+ }
3023
+ function networkName(projectId) {
3024
+ return `supabase_network_${projectId}`;
3025
+ }
3026
+ async function resolveBranch(branchName) {
3027
+ const requestedBranchName = branchName || getCurrentBranch();
3028
+ if (!requestedBranchName) {
3029
+ throw new Error("No branch specified and no current branch is selected.");
3030
+ }
3031
+ const currentConnectorId = getConfig("currentConnectorId");
3032
+ if (!currentConnectorId) {
3033
+ throw new Error("No connector selected. Run `ardent connector switch` first.");
3034
+ }
3035
+ const result = await api.get(
3036
+ `/v1/cli/branches?connector_id=${encodeURIComponent(currentConnectorId)}`
3037
+ );
3038
+ if (!Array.isArray(result.branches)) {
3039
+ throw new Error("API returned invalid response: missing branches array");
3040
+ }
3041
+ setCacheEntry("branches", result.branches);
3042
+ const branch = result.branches.find((candidate) => candidate.name === requestedBranchName);
3043
+ if (!branch) {
3044
+ const cached = getCacheEntry("branches");
3045
+ const cachedBranch = cached?.data.find((candidate) => candidate.name === requestedBranchName);
3046
+ if (cachedBranch) {
3047
+ return cachedBranch;
3048
+ }
3049
+ throw new Error(`Branch "${requestedBranchName}" not found.`);
3050
+ }
3051
+ if (!branch.branch_url) {
3052
+ throw new Error(`Branch "${branch.name}" does not have a connection URL.`);
3053
+ }
3054
+ return branch;
3055
+ }
3056
+ function parseBranchUrl(branchUrl) {
3057
+ const parsed = new URL(branchUrl);
3058
+ if (!parsed.hostname || !parsed.username || !parsed.password) {
3059
+ throw new Error("Branch URL is missing host, username, or password.");
3060
+ }
3061
+ return parsed;
3062
+ }
3063
+ function branchTargetConnectHost(hostname) {
3064
+ if (hostname === "routing.localhost" || hostname.endsWith(".routing.localhost")) {
3065
+ return DOCKER_HOST_GATEWAY;
3066
+ }
3067
+ return hostname;
3068
+ }
3069
+ function shouldVerifyBranchTargetCertificate(hostname) {
3070
+ return !(hostname === "routing.localhost" || hostname.endsWith(".routing.localhost"));
3071
+ }
3072
+ function proxyScript() {
3073
+ return String.raw`
3074
+ const net = require("net");
3075
+ const tls = require("tls");
3076
+
3077
+ const listenPort = Number(process.env.LISTEN_PORT || "5432");
3078
+ const targetHost = process.env.TARGET_HOST;
3079
+ const targetConnectHost = process.env.TARGET_CONNECT_HOST || targetHost;
3080
+ const targetPort = Number(process.env.TARGET_PORT || "5432");
3081
+ const targetDatabase = process.env.TARGET_DATABASE;
3082
+ const rejectUnauthorized = process.env.TARGET_REJECT_UNAUTHORIZED !== "false";
3083
+
3084
+ if (!targetHost || !targetDatabase) {
3085
+ throw new Error("TARGET_HOST and TARGET_DATABASE are required");
3086
+ }
3087
+
3088
+ const SSL_REQUEST = 80877103;
3089
+ const AUTH_SASL = 10;
3090
+
3091
+ function readCString(buffer, offset) {
3092
+ const end = buffer.indexOf(0, offset);
3093
+ if (end === -1) {
3094
+ throw new Error("Invalid startup packet: unterminated string");
3095
+ }
3096
+ return [buffer.toString("utf8", offset, end), end + 1];
3097
+ }
3098
+
3099
+ function rewriteStartupPacket(packet) {
3100
+ const length = packet.readInt32BE(0);
3101
+ const protocol = packet.readInt32BE(4);
3102
+ const params = [];
3103
+ let offset = 8;
3104
+
3105
+ while (offset < length - 1) {
3106
+ let key;
3107
+ [key, offset] = readCString(packet, offset);
3108
+ if (!key) {
3109
+ break;
3110
+ }
3111
+ let value;
3112
+ [value, offset] = readCString(packet, offset);
3113
+ if (key === "database" && (value === "postgres" || value === "_supabase")) {
3114
+ value = targetDatabase;
3115
+ }
3116
+ params.push([key, value]);
3117
+ }
3118
+
3119
+ const chunks = [];
3120
+ const header = Buffer.alloc(4);
3121
+ header.writeInt32BE(protocol, 0);
3122
+ chunks.push(header);
3123
+ for (const [key, value] of params) {
3124
+ chunks.push(Buffer.from(key + "\0" + value + "\0", "utf8"));
3125
+ }
3126
+ chunks.push(Buffer.from([0]));
3127
+ const body = Buffer.concat(chunks);
3128
+ const output = Buffer.alloc(4 + body.length);
3129
+ output.writeInt32BE(output.length, 0);
3130
+ body.copy(output, 4);
3131
+ return output;
3132
+ }
3133
+
3134
+ function filterServerPacket(packet) {
3135
+ if (packet.length < 9 || packet[0] !== 82) {
3136
+ return packet;
3137
+ }
3138
+ const authCode = packet.readInt32BE(5);
3139
+ if (authCode !== AUTH_SASL) {
3140
+ return packet;
3141
+ }
3142
+ const length = packet.readInt32BE(1);
3143
+ const body = packet.subarray(9, 1 + length);
3144
+ const trailing = packet.subarray(1 + length);
3145
+ const mechanisms = body.toString("utf8").split("\0").filter(Boolean);
3146
+ const filtered = mechanisms.filter((mechanism) => mechanism !== "SCRAM-SHA-256-PLUS");
3147
+ if (filtered.length === mechanisms.length) {
3148
+ return packet;
3149
+ }
3150
+ filtered.push("");
3151
+ const newBody = Buffer.from(filtered.join("\0") + "\0", "utf8");
3152
+ const output = Buffer.alloc(1 + 4 + 4 + newBody.length);
3153
+ output[0] = 82;
3154
+ output.writeInt32BE(4 + 4 + newBody.length, 1);
3155
+ output.writeInt32BE(AUTH_SASL, 5);
3156
+ newBody.copy(output, 9);
3157
+ return Buffer.concat([output, trailing]);
3158
+ }
3159
+
3160
+ function connectUpstream(startupPacket, client) {
3161
+ const socket = net.connect(targetPort, targetConnectHost, () => {
3162
+ const sslRequest = Buffer.alloc(8);
3163
+ sslRequest.writeInt32BE(8, 0);
3164
+ sslRequest.writeInt32BE(SSL_REQUEST, 4);
3165
+ socket.write(sslRequest);
3166
+ });
3167
+
3168
+ socket.once("data", (response) => {
3169
+ if (response[0] !== 83) {
3170
+ client.destroy(new Error("Upstream PostgreSQL server did not accept SSL"));
3171
+ socket.destroy();
3172
+ return;
3173
+ }
3174
+
3175
+ const secureSocket = tls.connect({
3176
+ socket,
3177
+ servername: targetHost,
3178
+ rejectUnauthorized,
3179
+ }, () => {
3180
+ let rewrittenStartupPacket;
3181
+ try {
3182
+ rewrittenStartupPacket = rewriteStartupPacket(startupPacket);
3183
+ } catch {
3184
+ client.destroy();
3185
+ secureSocket.destroy();
3186
+ return;
3187
+ }
3188
+ secureSocket.write(rewrittenStartupPacket);
3189
+ });
3190
+
3191
+ secureSocket.on("data", (chunk) => {
3192
+ client.write(filterServerPacket(chunk));
3193
+ });
3194
+ client.on("data", (chunk) => secureSocket.write(chunk));
3195
+ secureSocket.on("error", () => client.destroy());
3196
+ client.on("error", () => secureSocket.destroy());
3197
+ secureSocket.on("close", () => client.destroy());
3198
+ client.on("close", () => secureSocket.destroy());
3199
+ });
3200
+
3201
+ socket.on("error", () => client.destroy());
3202
+ }
3203
+
3204
+ const server = net.createServer((client) => {
3205
+ let buffered = Buffer.alloc(0);
3206
+ let sawSslRequest = false;
3207
+
3208
+ client.on("data", function onFirstData(chunk) {
3209
+ buffered = Buffer.concat([buffered, chunk]);
3210
+ if (buffered.length < 8) {
3211
+ return;
3212
+ }
3213
+
3214
+ const length = buffered.readInt32BE(0);
3215
+ const code = buffered.readInt32BE(4);
3216
+ if (!sawSslRequest && length === 8 && code === SSL_REQUEST) {
3217
+ client.write("N");
3218
+ buffered = buffered.subarray(8);
3219
+ sawSslRequest = true;
3220
+ if (buffered.length < 8) {
3221
+ return;
3222
+ }
3223
+ }
3224
+
3225
+ const startupLength = buffered.readInt32BE(0);
3226
+ if (buffered.length < startupLength) {
3227
+ return;
3228
+ }
3229
+
3230
+ client.off("data", onFirstData);
3231
+ try {
3232
+ const startupPacket = buffered.subarray(0, startupLength);
3233
+ const remainder = buffered.subarray(startupLength);
3234
+ connectUpstream(startupPacket, client);
3235
+ if (remainder.length > 0) {
3236
+ client.unshift(remainder);
3237
+ }
3238
+ } catch {
3239
+ client.destroy();
3240
+ }
3241
+ });
3242
+ });
3243
+
3244
+ server.listen(listenPort, "0.0.0.0", () => {
3245
+ console.log(JSON.stringify({ event: "listening", listenPort, targetHost, targetConnectHost, targetDatabase }));
3246
+ });
3247
+ `;
3248
+ }
3249
+ function writeProxyScript(projectId) {
3250
+ const scriptPath = join2(STATE_DIR, projectId, "proxy.js");
3251
+ mkdirSync2(dirname(scriptPath), { recursive: true });
3252
+ writeFileSync2(scriptPath, proxyScript());
3253
+ return scriptPath;
3254
+ }
3255
+ function serviceEnvForBranch(servicePrefix, env, dbContainer, branchUser, branchPassword) {
3256
+ const localDbUri = (role) => `postgresql://${encodeURIComponent(role)}:${encodeURIComponent(branchPassword)}@${dbContainer}:5432/postgres?sslmode=disable`;
3257
+ return env.map((entry) => {
3258
+ if (servicePrefix === "rest" && entry.startsWith("PGRST_DB_URI=")) {
3259
+ return `PGRST_DB_URI=${localDbUri("authenticator")}`;
3260
+ }
3261
+ if (servicePrefix === "auth" && entry.startsWith("GOTRUE_DB_DATABASE_URL=")) {
3262
+ return `GOTRUE_DB_DATABASE_URL=${localDbUri("supabase_auth_admin")}`;
3263
+ }
3264
+ if (servicePrefix === "storage" && entry.startsWith("DATABASE_URL=")) {
3265
+ return `DATABASE_URL=${localDbUri("supabase_storage_admin")}`;
3266
+ }
3267
+ if (servicePrefix === "realtime") {
3268
+ if (entry.startsWith("DB_HOST=")) {
3269
+ return `DB_HOST=${dbContainer}`;
3270
+ }
3271
+ if (entry.startsWith("DB_NAME=")) {
3272
+ return "DB_NAME=postgres";
3273
+ }
3274
+ if (entry.startsWith("DB_USER=")) {
3275
+ return "DB_USER=supabase_admin";
3276
+ }
3277
+ if (entry.startsWith("DB_PASSWORD=")) {
3278
+ return `DB_PASSWORD=${branchPassword}`;
3279
+ }
3280
+ if (entry.startsWith("DB_PORT=")) {
3281
+ return "DB_PORT=5432";
3282
+ }
3283
+ }
3284
+ if (servicePrefix === "pg_meta") {
3285
+ if (entry.startsWith("PG_META_DB_HOST=")) {
3286
+ return `PG_META_DB_HOST=${dbContainer}`;
3287
+ }
3288
+ if (entry.startsWith("PG_META_DB_NAME=")) {
3289
+ return "PG_META_DB_NAME=postgres";
3290
+ }
3291
+ if (entry.startsWith("PG_META_DB_USER=")) {
3292
+ return `PG_META_DB_USER=${branchUser}`;
3293
+ }
3294
+ if (entry.startsWith("PG_META_DB_PASSWORD=")) {
3295
+ return `PG_META_DB_PASSWORD=${branchPassword}`;
3296
+ }
3297
+ if (entry.startsWith("PG_META_DB_PORT=")) {
3298
+ return "PG_META_DB_PORT=5432";
3299
+ }
3300
+ }
3301
+ return entry;
3302
+ });
3303
+ }
3304
+ function mountArgs(mounts) {
3305
+ const args2 = [];
3306
+ for (const mount of mounts) {
3307
+ if (!mount.Source || !mount.Target) {
3308
+ continue;
3309
+ }
3310
+ const readonly = mount.ReadOnly ? ",readonly" : "";
3311
+ args2.push(
3312
+ "--mount",
3313
+ `type=${mount.Type},source=${mount.Source},target=${mount.Target}${readonly}`
3314
+ );
3315
+ }
3316
+ return args2;
3317
+ }
3318
+ function portPublishArgs(container) {
3319
+ const args2 = [];
3320
+ const ports = container.NetworkSettings.Ports ?? {};
3321
+ for (const [containerPort, bindings] of Object.entries(ports)) {
3322
+ if (!bindings) {
3323
+ continue;
3324
+ }
3325
+ for (const binding of bindings) {
3326
+ if (!binding.HostPort) {
3327
+ continue;
3328
+ }
3329
+ const hostPrefix = binding.HostIp ? `${binding.HostIp}:` : "";
3330
+ args2.push("-p", `${hostPrefix}${binding.HostPort}:${containerPort}`);
3331
+ }
3332
+ }
3333
+ return args2;
3334
+ }
3335
+ function containerAliases(container, network, fallback) {
3336
+ const aliases = container.NetworkSettings.Networks[network]?.Aliases ?? [];
3337
+ const cleanAliases = aliases.filter((alias) => alias && alias !== container.Name.replace(/^\//, ""));
3338
+ return cleanAliases.length > 0 ? cleanAliases : [fallback];
3339
+ }
3340
+ function imageCommandArgs(image, entrypoint, command) {
3341
+ const args2 = [];
3342
+ if (entrypoint && entrypoint.length > 0) {
3343
+ args2.push("--entrypoint", entrypoint[0]);
3344
+ }
3345
+ args2.push(image);
3346
+ if (entrypoint && entrypoint.length > 1) {
3347
+ args2.push(...entrypoint.slice(1));
3348
+ }
3349
+ if (command) {
3350
+ args2.push(...command);
3351
+ }
3352
+ return args2;
3353
+ }
3354
+ function recreateServiceForBranch(servicePrefix, projectId, network, dbContainer, branchUser, branchPassword) {
3355
+ const name = containerName(servicePrefix, projectId);
3356
+ if (!dockerExists(name)) {
3357
+ return void 0;
3358
+ }
3359
+ const originalName = `${name}${ORIGINAL_SUFFIX}`;
3360
+ if (dockerExists(originalName)) {
3361
+ throw new Error(`Refusing to overwrite existing backup container ${originalName}. Run unlink first.`);
3362
+ }
3363
+ const container = inspectContainer(name);
3364
+ const aliases = containerAliases(container, network, servicePrefix.replace("pg_meta", "meta"));
3365
+ const env = serviceEnvForBranch(
3366
+ servicePrefix,
3367
+ container.Config.Env ?? [],
3368
+ dbContainer,
3369
+ branchUser,
3370
+ branchPassword
3371
+ );
3372
+ runDocker(["stop", name]);
3373
+ runDocker(["rename", name, originalName]);
3374
+ const args2 = ["run", "-d", "--name", name, "--network", network];
3375
+ for (const alias of aliases) {
3376
+ args2.push("--network-alias", alias);
3377
+ }
3378
+ for (const envValue of env) {
3379
+ args2.push("-e", envValue);
3380
+ }
3381
+ if (container.Config.User) {
3382
+ args2.push("--user", container.Config.User);
3383
+ }
3384
+ if (container.Config.WorkingDir) {
3385
+ args2.push("--workdir", container.Config.WorkingDir);
3386
+ }
3387
+ args2.push(...mountArgs(container.Mounts));
3388
+ args2.push(
3389
+ ...imageCommandArgs(container.Config.Image, container.Config.Entrypoint, container.Config.Cmd)
3390
+ );
3391
+ runDocker(args2);
3392
+ return { name, originalName };
3393
+ }
3394
+ function startProxyContainer(projectId, network, dbContainer, parsedBranchUrl, portBindings) {
3395
+ const scriptPath = writeProxyScript(projectId);
3396
+ const targetDatabase = parsedBranchUrl.pathname.replace(/^\//, "") || "postgres";
3397
+ const targetPort = parsedBranchUrl.port || "5432";
3398
+ const targetConnectHost = branchTargetConnectHost(parsedBranchUrl.hostname);
3399
+ const rejectUnauthorized = shouldVerifyBranchTargetCertificate(parsedBranchUrl.hostname);
3400
+ const args2 = [
3401
+ "run",
3402
+ "-d",
3403
+ "--name",
3404
+ dbContainer,
3405
+ "--network",
3406
+ network,
3407
+ "--network-alias",
3408
+ "db",
3409
+ "--network-alias",
3410
+ "db.supabase.internal"
3411
+ ];
3412
+ if (targetConnectHost === DOCKER_HOST_GATEWAY) {
3413
+ args2.push("--add-host", `${DOCKER_HOST_GATEWAY}:host-gateway`);
3414
+ }
3415
+ args2.push(...portBindings);
3416
+ args2.push(
3417
+ "-v",
3418
+ `${scriptPath}:/proxy.js:ro`,
3419
+ "-e",
3420
+ `TARGET_HOST=${parsedBranchUrl.hostname}`,
3421
+ "-e",
3422
+ `TARGET_CONNECT_HOST=${targetConnectHost}`,
3423
+ "-e",
3424
+ `TARGET_PORT=${targetPort}`,
3425
+ "-e",
3426
+ `TARGET_DATABASE=${targetDatabase}`,
3427
+ "-e",
3428
+ `TARGET_REJECT_UNAUTHORIZED=${String(rejectUnauthorized)}`,
3429
+ "-e",
3430
+ "LISTEN_PORT=5432",
3431
+ "node:22-alpine",
3432
+ "node",
3433
+ "/proxy.js"
3434
+ );
3435
+ runDocker(args2);
3436
+ }
3437
+ function dumpLocalStorageMigrations(dbContainer) {
3438
+ return runDocker([
3439
+ "exec",
3440
+ dbContainer,
3441
+ "pg_dump",
3442
+ "-U",
3443
+ "postgres",
3444
+ "-d",
3445
+ "postgres",
3446
+ "--data-only",
3447
+ "--table=storage.migrations",
3448
+ "--column-inserts",
3449
+ "--no-owner",
3450
+ "--no-privileges"
3451
+ ]);
3452
+ }
3453
+ function storageMigrationsImportSql(dumpSql) {
3454
+ const tempTable = "pg_temp.ardent_storage_migrations_import";
3455
+ const tempDumpSql = dumpSql.replaceAll("INSERT INTO storage.migrations", `INSERT INTO ${tempTable}`);
3456
+ return `
3457
+ BEGIN;
3458
+ CREATE TEMP TABLE ardent_storage_migrations_import (LIKE storage.migrations INCLUDING DEFAULTS);
3459
+ ${tempDumpSql}
3460
+ INSERT INTO storage.migrations
3461
+ SELECT * FROM ${tempTable}
3462
+ ON CONFLICT DO NOTHING;
3463
+ COMMIT;
3464
+ `;
3465
+ }
3466
+ function waitForOriginalDb(containerName2) {
3467
+ const deadline = Date.now() + 3e4;
3468
+ while (Date.now() < deadline) {
3469
+ try {
3470
+ runDocker(["exec", containerName2, "pg_isready", "-U", "postgres", "-d", "postgres"]);
3471
+ return;
3472
+ } catch {
3473
+ execFileSync("sleep", ["1"]);
3474
+ }
3475
+ }
3476
+ throw new Error(`Timed out waiting for ${containerName2} to accept local PostgreSQL connections.`);
3477
+ }
3478
+ function copyLocalStorageMigrationsToBranch(dumpSql, originalDbContainer, dbContainer, network, branchUser, branchPassword) {
3479
+ runDocker(["start", originalDbContainer]);
3480
+ try {
3481
+ waitForOriginalDb(originalDbContainer);
3482
+ const dbContainerIpAddress = containerIpAddress(dbContainer, network);
3483
+ runDockerWithInput(
3484
+ [
3485
+ "exec",
3486
+ "-i",
3487
+ "-e",
3488
+ `PGPASSWORD=${branchPassword}`,
3489
+ originalDbContainer,
3490
+ "psql",
3491
+ "-h",
3492
+ dbContainerIpAddress,
3493
+ "-p",
3494
+ "5432",
3495
+ "-U",
3496
+ branchUser,
3497
+ "-d",
3498
+ "postgres",
3499
+ "-v",
3500
+ "ON_ERROR_STOP=1"
3501
+ ],
3502
+ storageMigrationsImportSql(dumpSql)
3503
+ );
3504
+ } finally {
3505
+ runDocker(["stop", originalDbContainer]);
3506
+ }
3507
+ }
3508
+ function supabaseRealtimeCompatibilitySql() {
3509
+ return String.raw`
3510
+ SET lock_timeout = '2s';
3511
+ SET statement_timeout = '5s';
3512
+
3513
+ ALTER SCHEMA realtime OWNER TO supabase_admin;
3514
+ GRANT ALL PRIVILEGES ON SCHEMA realtime TO supabase_realtime_admin;
3515
+
3516
+ CREATE OR REPLACE FUNCTION realtime.list_changes(
3517
+ publication name,
3518
+ slot_name name,
3519
+ max_changes int,
3520
+ max_record_bytes int
3521
+ )
3522
+ RETURNS SETOF realtime.wal_rls
3523
+ LANGUAGE sql
3524
+ AS $$
3525
+ WITH pub AS (
3526
+ SELECT
3527
+ concat_ws(
3528
+ ',',
3529
+ CASE WHEN bool_or(pubinsert) THEN 'insert' ELSE NULL END,
3530
+ CASE WHEN bool_or(pubupdate) THEN 'update' ELSE NULL END,
3531
+ CASE WHEN bool_or(pubdelete) THEN 'delete' ELSE NULL END
3532
+ ) AS w2j_actions,
3533
+ coalesce(
3534
+ string_agg(
3535
+ realtime.quote_wal2json(format('%I.%I', schemaname, tablename)::regclass),
3536
+ ','
3537
+ ) FILTER (WHERE ppt.tablename IS NOT NULL AND ppt.tablename NOT LIKE '% %'),
3538
+ ''
3539
+ ) AS w2j_add_tables
3540
+ FROM pg_publication pp
3541
+ LEFT JOIN pg_publication_tables ppt
3542
+ ON pp.pubname = ppt.pubname
3543
+ WHERE pp.pubname = publication
3544
+ GROUP BY pp.pubname
3545
+ LIMIT 1
3546
+ ),
3547
+ w2j AS (
3548
+ SELECT
3549
+ x.*,
3550
+ pub.w2j_add_tables
3551
+ FROM pub,
3552
+ pg_logical_slot_get_changes(
3553
+ slot_name,
3554
+ NULL,
3555
+ max_changes,
3556
+ 'include-pk', 'true',
3557
+ 'include-transaction', 'false',
3558
+ 'include-timestamp', 'true',
3559
+ 'include-type-oids', 'true',
3560
+ 'format-version', '2',
3561
+ 'actions', pub.w2j_actions,
3562
+ 'add-tables', pub.w2j_add_tables
3563
+ ) x
3564
+ )
3565
+ SELECT
3566
+ xyz.wal,
3567
+ xyz.is_rls_enabled,
3568
+ xyz.subscription_ids,
3569
+ xyz.errors
3570
+ FROM w2j,
3571
+ realtime.apply_rls(
3572
+ wal := w2j.data::jsonb,
3573
+ max_record_bytes := max_record_bytes
3574
+ ) xyz(wal, is_rls_enabled, subscription_ids, errors)
3575
+ WHERE w2j.w2j_add_tables <> ''
3576
+ AND xyz.subscription_ids[1] IS NOT NULL
3577
+ $$;
3578
+
3579
+ ALTER FUNCTION realtime.list_changes(name, name, int, int) OWNER TO supabase_realtime_admin;
3580
+ GRANT EXECUTE ON FUNCTION realtime.list_changes(name, name, int, int)
3581
+ TO supabase_admin, supabase_realtime_admin;
3582
+
3583
+ INSERT INTO realtime.schema_migrations (version, inserted_at)
3584
+ VALUES (${REALTIME_LIST_CHANGES_MIGRATION_VERSION}, now())
3585
+ ON CONFLICT (version) DO NOTHING;
3586
+ `;
3587
+ }
3588
+ function applySupabaseRealtimeCompatibility(projectId, originalDbContainer, dbContainer, network, branchPassword) {
3589
+ const realtimeContainer = containerName("realtime", projectId);
3590
+ if (!dockerExists(realtimeContainer)) {
3591
+ return;
3592
+ }
3593
+ runDocker(["start", originalDbContainer]);
3594
+ try {
3595
+ waitForOriginalDb(originalDbContainer);
3596
+ const dbContainerIpAddress = containerIpAddress(dbContainer, network);
3597
+ const deadline = Date.now() + 6e4;
3598
+ let lastError = "";
3599
+ while (Date.now() < deadline) {
3600
+ try {
3601
+ runDockerWithInput(
3602
+ [
3603
+ "exec",
3604
+ "-i",
3605
+ "-e",
3606
+ `PGPASSWORD=${branchPassword}`,
3607
+ originalDbContainer,
3608
+ "psql",
3609
+ "-h",
3610
+ dbContainerIpAddress,
3611
+ "-p",
3612
+ "5432",
3613
+ "-U",
3614
+ "supabase_admin",
3615
+ "-d",
3616
+ "postgres",
3617
+ "-v",
3618
+ "ON_ERROR_STOP=1"
3619
+ ],
3620
+ supabaseRealtimeCompatibilitySql(),
3621
+ { timeoutMs: 7e3 }
3622
+ );
3623
+ runDocker(["restart", realtimeContainer]);
3624
+ return;
3625
+ } catch (error) {
3626
+ lastError = error instanceof Error ? error.message : String(error);
3627
+ execFileSync("sleep", ["1"]);
3628
+ }
3629
+ }
3630
+ throw new Error(`Timed out applying Supabase Realtime compatibility: ${lastError}`);
3631
+ } finally {
3632
+ runDocker(["stop", originalDbContainer]);
3633
+ }
3634
+ }
3635
+ function kongApiUrl(projectId) {
3636
+ const kong = containerName("kong", projectId);
3637
+ if (!dockerExists(kong)) {
3638
+ return void 0;
3639
+ }
3640
+ const output = runDocker(["port", kong, "8000/tcp"]);
3641
+ const first = output.split("\n")[0];
3642
+ const port = first?.split(":").pop();
3643
+ return port ? `http://127.0.0.1:${port}` : void 0;
3644
+ }
3645
+ async function waitForHealth(projectId) {
3646
+ const apiUrl = kongApiUrl(projectId);
3647
+ if (!apiUrl) {
3648
+ return;
3649
+ }
3650
+ const deadline = Date.now() + 6e4;
3651
+ let lastError = "";
3652
+ while (Date.now() < deadline) {
3653
+ try {
3654
+ const authResponse = await fetch(`${apiUrl}/auth/v1/health`);
3655
+ const restResponse = await fetch(`${apiUrl}/rest/v1/`);
3656
+ if (authResponse.ok && restResponse.ok) {
3657
+ return;
3658
+ }
3659
+ lastError = `auth=${authResponse.status}, rest=${restResponse.status}`;
3660
+ } catch (error) {
3661
+ lastError = error instanceof Error ? error.message : String(error);
3662
+ }
3663
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
3664
+ }
3665
+ throw new Error(`Supabase health checks did not pass: ${lastError}`);
3666
+ }
3667
+ function restoreOriginalContainer(name, originalName) {
3668
+ if (dockerExists(name)) {
3669
+ runDocker(["rm", "-f", name]);
3670
+ }
3671
+ if (dockerExists(originalName)) {
3672
+ runDocker(["rename", originalName, name]);
3673
+ runDocker(["start", name]);
3674
+ }
3675
+ }
3676
+ function rollbackPartialLink(projectId, dbContainer, originalDbContainer, services) {
3677
+ for (const service of services.reverse()) {
3678
+ restoreOriginalContainer(service.name, service.originalName);
3679
+ }
3680
+ restoreOriginalContainer(dbContainer, originalDbContainer);
3681
+ deleteState(projectId);
3682
+ }
3683
+ async function statusSupabaseAction(options = {}) {
3684
+ try {
3685
+ const projectId = detectSupabaseProject(options.project);
3686
+ const state = readState(projectId);
3687
+ console.log(`Supabase project: ${projectId}`);
3688
+ if (!state) {
3689
+ console.log("Status: not linked");
3690
+ return;
3691
+ }
3692
+ console.log("Status: linked");
3693
+ console.log(`Branch: ${state.branchName}`);
3694
+ console.log(`Linked at: ${new Date(state.linkedAt).toLocaleString()}`);
3695
+ console.log(`DB container: ${state.dbContainer}`);
3696
+ } catch (error) {
3697
+ console.error("\u2717 Failed:", error instanceof Error ? error.message : error);
3698
+ process.exit(1);
3699
+ }
3700
+ }
3701
+ async function linkSupabaseAction(branchName, options = {}) {
3702
+ let projectId = "";
3703
+ let dbContainer = "";
3704
+ let originalDbContainer = "";
3705
+ const createdServices = [];
3706
+ try {
3707
+ projectId = detectSupabaseProject(options.project);
3708
+ if (readState(projectId)) {
3709
+ throw new Error(`Supabase project ${projectId} is already linked. Run unlink first.`);
3710
+ }
3711
+ const branch = await resolveBranch(branchName);
3712
+ const parsedBranchUrl = parseBranchUrl(branch.branch_url);
3713
+ const branchUser = decodeURIComponent(parsedBranchUrl.username);
3714
+ const branchPassword = decodeURIComponent(parsedBranchUrl.password);
3715
+ dbContainer = containerName("db", projectId);
3716
+ originalDbContainer = `${dbContainer}${ORIGINAL_SUFFIX}`;
3717
+ const network = networkName(projectId);
3718
+ if (!dockerExists(dbContainer)) {
3719
+ throw new Error(`Local Supabase DB container not found: ${dbContainer}`);
3720
+ }
3721
+ if (dockerExists(originalDbContainer)) {
3722
+ throw new Error(`Backup DB container already exists: ${originalDbContainer}. Run unlink first.`);
3723
+ }
3724
+ console.log(`Linking Supabase project ${projectId} to branch ${branch.name}...`);
3725
+ const storageMigrationsDump = dumpLocalStorageMigrations(dbContainer);
3726
+ const originalDbContainerState = inspectContainer(dbContainer);
3727
+ const dbPortBindings = portPublishArgs(originalDbContainerState);
3728
+ runDocker(["stop", dbContainer]);
3729
+ runDocker(["rename", dbContainer, originalDbContainer]);
3730
+ startProxyContainer(projectId, network, dbContainer, parsedBranchUrl, []);
3731
+ copyLocalStorageMigrationsToBranch(
3732
+ storageMigrationsDump,
3733
+ originalDbContainer,
3734
+ dbContainer,
3735
+ network,
3736
+ branchUser,
3737
+ branchPassword
3738
+ );
3739
+ for (const servicePrefix of SERVICE_SUFFIXES) {
3740
+ const service = recreateServiceForBranch(
3741
+ servicePrefix,
3742
+ projectId,
3743
+ network,
3744
+ dbContainer,
3745
+ branchUser,
3746
+ branchPassword
3747
+ );
3748
+ if (service) {
3749
+ createdServices.push(service);
3750
+ }
3751
+ }
3752
+ applySupabaseRealtimeCompatibility(
3753
+ projectId,
3754
+ originalDbContainer,
3755
+ dbContainer,
3756
+ network,
3757
+ branchPassword
3758
+ );
3759
+ runDocker(["rm", "-f", dbContainer]);
3760
+ startProxyContainer(projectId, network, dbContainer, parsedBranchUrl, dbPortBindings);
3761
+ const kong = containerName("kong", projectId);
3762
+ if (dockerExists(kong)) {
3763
+ runDocker(["restart", kong]);
3764
+ }
3765
+ const state = {
3766
+ version: 1,
3767
+ projectId,
3768
+ network,
3769
+ branchName: branch.name,
3770
+ linkedAt: (/* @__PURE__ */ new Date()).toISOString(),
3771
+ dbContainer,
3772
+ originalDbContainer,
3773
+ services: createdServices
3774
+ };
3775
+ if (!options.skipHealthCheck) {
3776
+ await waitForHealth(projectId);
3777
+ }
3778
+ writeState(state);
3779
+ console.log("\u2713 Local Supabase is linked to the branch");
3780
+ trackEvent("CLI: settings supabase link succeeded");
3781
+ } catch (error) {
3782
+ if (projectId) {
3783
+ const state = readState(projectId);
3784
+ if (!state && dbContainer && originalDbContainer) {
3785
+ rollbackPartialLink(projectId, dbContainer, originalDbContainer, createdServices);
3786
+ }
3787
+ }
3788
+ trackEvent("CLI: settings supabase link failed");
3789
+ console.error("\u2717 Failed:", error instanceof Error ? error.message : error);
3790
+ process.exit(1);
3791
+ }
3792
+ }
3793
+ async function unlinkSupabaseAction(options = {}) {
3794
+ try {
3795
+ const projectId = detectSupabaseProject(options.project);
3796
+ const state = readState(projectId);
3797
+ if (!state) {
3798
+ console.log(`Supabase project ${projectId} is not linked`);
3799
+ return;
3800
+ }
3801
+ console.log(`Unlinking Supabase project ${projectId}...`);
3802
+ restoreOriginalContainer(state.dbContainer, state.originalDbContainer);
3803
+ for (const service of state.services) {
3804
+ restoreOriginalContainer(service.name, service.originalName);
3805
+ }
3806
+ const kong = containerName("kong", projectId);
3807
+ if (dockerExists(kong)) {
3808
+ runDocker(["restart", kong]);
3809
+ }
3810
+ deleteState(projectId);
3811
+ if (!options.skipHealthCheck) {
3812
+ await waitForHealth(projectId);
3813
+ }
3814
+ console.log("\u2713 Local Supabase restored to its local database");
3815
+ trackEvent("CLI: settings supabase unlink succeeded");
3816
+ } catch (error) {
3817
+ trackEvent("CLI: settings supabase unlink failed");
3818
+ console.error("\u2717 Failed:", error instanceof Error ? error.message : error);
3819
+ process.exit(1);
3820
+ }
3821
+ }
3822
+
2852
3823
  // src/commands/settings/index.ts
2853
3824
  var settingsCommand = new Command6("settings").description("Manage settings for the current connector").showHelpAfterError("(run 'ardent settings --help' for valid keys)");
2854
3825
  settingsCommand.command("list").description("List current settings for the connector").action(listAction5);
@@ -2856,6 +3827,11 @@ settingsCommand.command("set <key> <value>").description(
2856
3827
  "Set a setting. Keys: default_db, branch_sql. For branch_sql, prefix value with @ to read from file."
2857
3828
  ).action(setAction);
2858
3829
  settingsCommand.command("remove <key>").description("Remove a setting. Keys: default_db, branch_sql.").action(removeAction2);
3830
+ var supabaseSettingsCommand = new Command6("supabase").description("Link local Supabase to an Ardent branch");
3831
+ supabaseSettingsCommand.command("status").description("Show local Supabase link status").option("--project <project>", "Supabase CLI project id").action(statusSupabaseAction);
3832
+ supabaseSettingsCommand.command("link [branch]").description("Point local Supabase at an Ardent branch").option("--project <project>", "Supabase CLI project id").option("--skip-health-check", "Skip Supabase API health checks after linking").action(linkSupabaseAction);
3833
+ supabaseSettingsCommand.command("unlink").description("Restore local Supabase to its local database").option("--project <project>", "Supabase CLI project id").option("--skip-health-check", "Skip Supabase API health checks after unlinking").action(unlinkSupabaseAction);
3834
+ settingsCommand.addCommand(supabaseSettingsCommand);
2859
3835
 
2860
3836
  // src/commands/auth/index.ts
2861
3837
  import { Command as Command7 } from "commander";
@@ -3047,16 +4023,16 @@ var logoutCommand = new Command7("logout").description("Logout from Ardent").act
3047
4023
  var statusCommand = new Command7("status").description("Show status").action(statusAction);
3048
4024
 
3049
4025
  // src/lib/update-check.ts
3050
- import { existsSync as existsSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
3051
- import { join as join2 } from "path";
3052
- import { homedir as homedir2 } from "os";
3053
- var UPDATE_CHECK_FILE = join2(homedir2(), ".ardent", "update-check.json");
4026
+ import { existsSync as existsSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
4027
+ import { join as join3 } from "path";
4028
+ import { homedir as homedir3 } from "os";
4029
+ var UPDATE_CHECK_FILE = join3(homedir3(), ".ardent", "update-check.json");
3054
4030
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
3055
4031
  var PACKAGE_NAME = "ardent-cli";
3056
4032
  function loadCache() {
3057
4033
  try {
3058
- if (existsSync2(UPDATE_CHECK_FILE)) {
3059
- return JSON.parse(readFileSync5(UPDATE_CHECK_FILE, "utf-8"));
4034
+ if (existsSync3(UPDATE_CHECK_FILE)) {
4035
+ return JSON.parse(readFileSync6(UPDATE_CHECK_FILE, "utf-8"));
3060
4036
  }
3061
4037
  } catch {
3062
4038
  }
@@ -3068,7 +4044,7 @@ function saveCache(latestVersion) {
3068
4044
  latest_version: latestVersion,
3069
4045
  checked_at: (/* @__PURE__ */ new Date()).toISOString()
3070
4046
  };
3071
- writeFileSync2(UPDATE_CHECK_FILE, JSON.stringify(data));
4047
+ writeFileSync3(UPDATE_CHECK_FILE, JSON.stringify(data));
3072
4048
  } catch {
3073
4049
  }
3074
4050
  }
@@ -3155,6 +4131,7 @@ SETTINGS
3155
4131
  settings list List current settings for the connector
3156
4132
  settings set Set a setting (e.g. default_db testdb)
3157
4133
  settings remove Remove a setting
4134
+ settings supabase Link local Supabase to the current branch
3158
4135
 
3159
4136
  TEAM
3160
4137
  invite <email> Invite a user to your organization
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ardent-cli",
3
- "version": "0.0.34",
3
+ "version": "0.0.36",
4
4
  "description": "Git for Data infrastructure",
5
5
  "type": "module",
6
6
  "bin": {