ardent-cli 0.0.35 → 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 +971 -7
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2862,6 +2862,964 @@ async function removeAction2(key) {
2862
2862
  }
2863
2863
  }
2864
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
+
2865
3823
  // src/commands/settings/index.ts
2866
3824
  var settingsCommand = new Command6("settings").description("Manage settings for the current connector").showHelpAfterError("(run 'ardent settings --help' for valid keys)");
2867
3825
  settingsCommand.command("list").description("List current settings for the connector").action(listAction5);
@@ -2869,6 +3827,11 @@ settingsCommand.command("set <key> <value>").description(
2869
3827
  "Set a setting. Keys: default_db, branch_sql. For branch_sql, prefix value with @ to read from file."
2870
3828
  ).action(setAction);
2871
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);
2872
3835
 
2873
3836
  // src/commands/auth/index.ts
2874
3837
  import { Command as Command7 } from "commander";
@@ -3060,16 +4023,16 @@ var logoutCommand = new Command7("logout").description("Logout from Ardent").act
3060
4023
  var statusCommand = new Command7("status").description("Show status").action(statusAction);
3061
4024
 
3062
4025
  // src/lib/update-check.ts
3063
- import { existsSync as existsSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
3064
- import { join as join2 } from "path";
3065
- import { homedir as homedir2 } from "os";
3066
- 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");
3067
4030
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
3068
4031
  var PACKAGE_NAME = "ardent-cli";
3069
4032
  function loadCache() {
3070
4033
  try {
3071
- if (existsSync2(UPDATE_CHECK_FILE)) {
3072
- 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"));
3073
4036
  }
3074
4037
  } catch {
3075
4038
  }
@@ -3081,7 +4044,7 @@ function saveCache(latestVersion) {
3081
4044
  latest_version: latestVersion,
3082
4045
  checked_at: (/* @__PURE__ */ new Date()).toISOString()
3083
4046
  };
3084
- writeFileSync2(UPDATE_CHECK_FILE, JSON.stringify(data));
4047
+ writeFileSync3(UPDATE_CHECK_FILE, JSON.stringify(data));
3085
4048
  } catch {
3086
4049
  }
3087
4050
  }
@@ -3168,6 +4131,7 @@ SETTINGS
3168
4131
  settings list List current settings for the connector
3169
4132
  settings set Set a setting (e.g. default_db testdb)
3170
4133
  settings remove Remove a setting
4134
+ settings supabase Link local Supabase to the current branch
3171
4135
 
3172
4136
  TEAM
3173
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.35",
3
+ "version": "0.0.36",
4
4
  "description": "Git for Data infrastructure",
5
5
  "type": "module",
6
6
  "bin": {