ardent-cli 0.0.35 → 0.0.37

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 +1049 -20
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -740,11 +740,9 @@ function diffAction() {
740
740
  console.error(
741
741
  `${red}error:${reset2} \`ardent branch diff\` has been removed.
742
742
 
743
- The underlying CDC service (branching-replay-service + StarRocks) was
744
- deprecated in favour of pgstream. A pgstream-backed diff view is on the
745
- roadmap but not yet shipped.
743
+ A replacement is on the roadmap.
746
744
 
747
- ${yellow}If you need the current state of a branch:${reset2} \`ardent branch info <name>\`
745
+ ${yellow}To view the current state of a branch:${reset2} \`ardent branch info <name>\`
748
746
  `
749
747
  );
750
748
  process.exit(2);
@@ -798,6 +796,31 @@ function printDegradedWarnings(warnings) {
798
796
  console.log(" Review this connector with: ardent connector list");
799
797
  }
800
798
 
799
+ // src/lib/connector_status_display.ts
800
+ var ENGINE_STATUS_DISPLAY = {
801
+ healthy: "ready",
802
+ degraded: "ready (with warnings)",
803
+ configuration_verified: "setup pending",
804
+ configuration_failed: "configuration check failed",
805
+ validating: "validation in progress",
806
+ failed_validation: "validation failed",
807
+ unhealthy: "unhealthy",
808
+ unchecked: "not yet configured"
809
+ };
810
+ var CONNECTION_STATUS_DISPLAY = {
811
+ connected: "connected",
812
+ failed: "connection failed",
813
+ unknown: "unknown"
814
+ };
815
+ function engineStatusDisplay(status) {
816
+ if (!status) return "not ready";
817
+ return ENGINE_STATUS_DISPLAY[status] ?? "not ready";
818
+ }
819
+ function connectorStatusDisplay(status) {
820
+ if (!status) return "unknown";
821
+ return CONNECTION_STATUS_DISPLAY[status] ?? ENGINE_STATUS_DISPLAY[status] ?? "not ready";
822
+ }
823
+
801
824
  // src/lib/engine_setup_result.ts
802
825
  var SUCCESS_ENGINE_STATUSES = /* @__PURE__ */ new Set(["healthy", "degraded"]);
803
826
  var RETRYABLE_ENGINE_STATUSES = /* @__PURE__ */ new Set(["configuration_verified"]);
@@ -836,23 +859,63 @@ function assertEngineSetupCompleted(operation, connectorName) {
836
859
  throw new EngineSetupTerminalStatusError(
837
860
  "retryable_engine_status",
838
861
  engineStatus,
839
- `Engine setup needs retry (status: ${engineStatus}). Run \`ardent connector retry-setup ${connectorName}\` to retry, or \`ardent connector list\` to inspect connector state first.`
862
+ `Engine setup needs retry (${engineStatusDisplay(engineStatus)}). Run \`ardent connector retry-setup ${connectorName}\` to retry, or \`ardent connector list\` to inspect connector state first.`
840
863
  );
841
864
  }
842
865
  if (FAILED_ENGINE_STATUSES.has(engineStatus)) {
843
866
  throw new EngineSetupTerminalStatusError(
844
867
  "failed_engine_status",
845
868
  engineStatus,
846
- `Engine setup failed (status: ${engineStatus}). Run \`ardent connector list\` to inspect connector state.`
869
+ `Engine setup failed (${engineStatusDisplay(engineStatus)}). Run \`ardent connector list\` to inspect connector state.`
847
870
  );
848
871
  }
849
872
  throw new EngineSetupTerminalStatusError(
850
873
  "unexpected_engine_status",
851
874
  engineStatus,
852
- `Engine setup completed with unexpected status: ${engineStatus}.`
875
+ "Engine setup completed in an unexpected state. Contact Ardent support."
853
876
  );
854
877
  }
855
878
 
879
+ // src/lib/operation_stage_display.ts
880
+ var STAGE_DISPLAY = {
881
+ // engine_setup_worker / postgres_engine_setup
882
+ "dispatched": "Starting",
883
+ "preparing": "Preparing",
884
+ "creating-neon-project": "Provisioning the branch target",
885
+ "preparing-target-databases": "Preparing target databases",
886
+ "deploying-pgstream": "Starting replication",
887
+ "applying-rls": "Applying RLS policies",
888
+ "storing-credentials": "Storing connection credentials",
889
+ "validating": "Validating the branch target",
890
+ // reset_worker / reset_connector
891
+ "resetting": "Resetting",
892
+ "deleting-pgstream": "Stopping replication",
893
+ "rediscovering-source": "Re-checking the source database",
894
+ "resetting-neon-main": "Resetting the branch target",
895
+ "creating-target-schemas": "Recreating target schemas",
896
+ "redeploying-pgstream": "Restarting replication",
897
+ // environment deploy_worker
898
+ "loading_config": "Loading environment configuration",
899
+ "deploying_infrastructure": "Provisioning environment infrastructure",
900
+ "recording_success": "Finalizing environment",
901
+ "cleaning_failed_deploy": "Cleaning up failed environment provisioning",
902
+ // environment destroy_worker
903
+ "deleting_private_links": "Removing private network links",
904
+ "destroying_infrastructure": "Tearing down environment infrastructure",
905
+ "recording_destroy_success": "Finalizing environment teardown"
906
+ };
907
+ function humanizeRawStage(_raw) {
908
+ return "Working";
909
+ }
910
+ function operationStageDisplay(stage) {
911
+ if (!stage) return "Working";
912
+ const colonIndex = stage.indexOf(":");
913
+ const base = colonIndex === -1 ? stage : stage.slice(0, colonIndex);
914
+ const suffix = colonIndex === -1 ? "" : stage.slice(colonIndex + 1);
915
+ const label = STAGE_DISPLAY[base] ?? humanizeRawStage(base);
916
+ return suffix ? `${label} (for ${suffix})` : label;
917
+ }
918
+
856
919
  // src/lib/engine_setup.ts
857
920
  var EngineSetupTimeoutError = class extends Error {
858
921
  constructor(message) {
@@ -904,7 +967,7 @@ async function runEngineSetupWithPolling(connectorId, connectorName) {
904
967
  }
905
968
  if (op.stage && op.stage !== lastStage) {
906
969
  const progressLabel = op.progress != null ? ` (${op.progress}%)` : "";
907
- console.log(` ${op.stage}${progressLabel}`);
970
+ console.log(` ${operationStageDisplay(op.stage)}${progressLabel}`);
908
971
  lastStage = op.stage;
909
972
  }
910
973
  if (op.status === "completed") {
@@ -919,7 +982,7 @@ async function runEngineSetupWithPolling(connectorId, connectorName) {
919
982
  }
920
983
  }
921
984
  throw new EngineSetupTimeoutError(
922
- `Engine setup did not complete within ${maxWaitMs / 6e4} minutes. Setup may still be running server-side \u2014 do NOT delete the connector. Check status with: ardent connector list (operation ${operationId})`
985
+ `Engine setup did not complete within ${maxWaitMs / 6e4} minutes. Setup may still be running server-side \u2014 do NOT delete the connector. Check status with: ardent connector list. If you contact Ardent support, reference operation id ${operationId}.`
923
986
  );
924
987
  }
925
988
 
@@ -1258,7 +1321,9 @@ function logSubmissionDisallowed() {
1258
1321
  console.error(
1259
1322
  " Wait for the current setup attempt to finish, then retry setup if the connector still shows setup pending."
1260
1323
  );
1261
- console.error(" Contact Ardent support if the connector stays validating.");
1324
+ console.error(
1325
+ " Contact Ardent support if the connector's setup does not progress within a few minutes."
1326
+ );
1262
1327
  }
1263
1328
  function logNonInteractiveRequired(decisionsNeeded) {
1264
1329
  const noun = decisionsNeeded === 1 ? "table" : "tables";
@@ -1462,7 +1527,7 @@ async function promptForUnsupportedExtensions(connectorId, unsupported, alreadyP
1462
1527
  await api.put(`/v1/connectors/${connectorId}`, {
1463
1528
  drop_extensions: merged2
1464
1529
  });
1465
- console.log(`\u2713 Saved drop selection: ${merged2.join(", ")}`);
1530
+ console.log(`\u2713 Saved extension exclusions: ${merged2.join(", ")}`);
1466
1531
  return "applied";
1467
1532
  }
1468
1533
  console.log(
@@ -1489,7 +1554,7 @@ async function promptForUnsupportedExtensions(connectorId, unsupported, alreadyP
1489
1554
  await api.put(`/v1/connectors/${connectorId}`, {
1490
1555
  drop_extensions: merged
1491
1556
  });
1492
- console.log(`\u2713 Saved drop selection: ${merged.join(", ")}`);
1557
+ console.log(`\u2713 Saved extension exclusions: ${merged.join(", ")}`);
1493
1558
  return "applied";
1494
1559
  }
1495
1560
  function printUnloggedTablesPreflight(preflight) {
@@ -1845,7 +1910,7 @@ async function listAction2() {
1845
1910
  if (render.kind === "engine_pending") enginePendingCount += 1;
1846
1911
  const warnings = connector.warnings ?? [];
1847
1912
  const warningSuffix = warnings.length > 0 ? ` ${yellow}\u26A0 ${warnings.length}${reset2}` : "";
1848
- const statusSuffix = render.kind === "ready" ? "" : ` ${render.color}[${connector.status}]${reset2}`;
1913
+ const statusSuffix = render.kind === "ready" ? "" : ` ${render.color}[${connectorStatusDisplay(connector.status)}]${reset2}`;
1849
1914
  const nameLine = isCurrent ? `${green2}* ${render.color}${render.icon}${green2} ${connector.name}${reset2}${warningSuffix}${statusSuffix}` : ` ${render.color}${render.icon}${reset2} ${connector.name}${warningSuffix}${statusSuffix}`;
1850
1915
  console.log(nameLine);
1851
1916
  if (isCurrent) {
@@ -2862,6 +2927,964 @@ async function removeAction2(key) {
2862
2927
  }
2863
2928
  }
2864
2929
 
2930
+ // src/lib/supabase_link.ts
2931
+ import { execFileSync } from "child_process";
2932
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2, rmSync } from "fs";
2933
+ import { homedir as homedir2 } from "os";
2934
+ import { dirname, join as join2 } from "path";
2935
+ var CONFIG_DIR2 = join2(homedir2(), ".ardent");
2936
+ var STATE_DIR = join2(CONFIG_DIR2, "supabase-links");
2937
+ var ORIGINAL_SUFFIX = "-ardent-original";
2938
+ var SERVICE_SUFFIXES = ["rest", "auth", "storage", "realtime", "pg_meta"];
2939
+ var DOCKER_HOST_GATEWAY = "host.docker.internal";
2940
+ var REALTIME_LIST_CHANGES_MIGRATION_VERSION = "20230328144023";
2941
+ var SENSITIVE_DOCKER_ENV_KEYS = /* @__PURE__ */ new Set([
2942
+ "DATABASE_URL",
2943
+ "DB_PASSWORD",
2944
+ "GOTRUE_DB_DATABASE_URL",
2945
+ "PG_META_DB_PASSWORD",
2946
+ "PGRST_DB_URI",
2947
+ "PGPASSWORD"
2948
+ ]);
2949
+ function redactDockerArgs(args2) {
2950
+ return args2.map((arg) => {
2951
+ const separatorIndex = arg.indexOf("=");
2952
+ if (separatorIndex === -1) {
2953
+ return arg;
2954
+ }
2955
+ const key = arg.slice(0, separatorIndex);
2956
+ if (SENSITIVE_DOCKER_ENV_KEYS.has(key)) {
2957
+ return `${key}=***`;
2958
+ }
2959
+ return arg;
2960
+ });
2961
+ }
2962
+ function redactDockerErrorMessage(message) {
2963
+ let redactedMessage = message.replace(
2964
+ /(postgres(?:ql)?:\/\/[^:\s/]+:)[^@\s]+@/g,
2965
+ "$1***@"
2966
+ );
2967
+ for (const key of SENSITIVE_DOCKER_ENV_KEYS) {
2968
+ redactedMessage = redactedMessage.replace(
2969
+ new RegExp(`${key}=[^\\s]+`, "g"),
2970
+ `${key}=***`
2971
+ );
2972
+ }
2973
+ return redactedMessage;
2974
+ }
2975
+ function runDocker(args2, options = {}) {
2976
+ try {
2977
+ return execFileSync("docker", args2, {
2978
+ encoding: "utf-8",
2979
+ stdio: options.stdio ?? "pipe"
2980
+ }).trim();
2981
+ } catch (error) {
2982
+ if (error instanceof Error) {
2983
+ throw new Error(
2984
+ `Docker command failed: docker ${redactDockerArgs(args2).join(" ")}
2985
+ ${redactDockerErrorMessage(error.message)}`
2986
+ );
2987
+ }
2988
+ throw error;
2989
+ }
2990
+ }
2991
+ function runDockerWithInput(args2, input, options = {}) {
2992
+ try {
2993
+ return execFileSync("docker", args2, {
2994
+ encoding: "utf-8",
2995
+ input,
2996
+ stdio: ["pipe", "pipe", "pipe"],
2997
+ timeout: options.timeoutMs
2998
+ }).trim();
2999
+ } catch (error) {
3000
+ if (error instanceof Error) {
3001
+ throw new Error(
3002
+ `Docker command failed: docker ${redactDockerArgs(args2).join(" ")}
3003
+ ${redactDockerErrorMessage(error.message)}`
3004
+ );
3005
+ }
3006
+ throw error;
3007
+ }
3008
+ }
3009
+ function dockerExists(name) {
3010
+ const output = runDocker(["ps", "-a", "--format", "{{.Names}}"]);
3011
+ return output.split("\n").includes(name);
3012
+ }
3013
+ function inspectContainer(name) {
3014
+ const output = runDocker(["inspect", name]);
3015
+ const parsed = JSON.parse(output);
3016
+ if (parsed.length !== 1) {
3017
+ throw new Error(`Expected one Docker container named ${name}`);
3018
+ }
3019
+ return parsed[0];
3020
+ }
3021
+ function containerIpAddress(name, network) {
3022
+ const container = inspectContainer(name);
3023
+ const networkState = container.NetworkSettings.Networks[network];
3024
+ const ipAddress = networkState?.IPAddress;
3025
+ if (!ipAddress) {
3026
+ throw new Error(`Container ${name} is not attached to network ${network}`);
3027
+ }
3028
+ return ipAddress;
3029
+ }
3030
+ function statePath(projectId) {
3031
+ return join2(STATE_DIR, `${projectId.replace(/[^A-Za-z0-9_.-]/g, "_")}.json`);
3032
+ }
3033
+ function readState(projectId) {
3034
+ const path = statePath(projectId);
3035
+ if (!existsSync2(path)) {
3036
+ return void 0;
3037
+ }
3038
+ return JSON.parse(readFileSync5(path, "utf-8"));
3039
+ }
3040
+ function writeState(state) {
3041
+ mkdirSync2(dirname(statePath(state.projectId)), { recursive: true });
3042
+ writeFileSync2(statePath(state.projectId), JSON.stringify(state, null, 2));
3043
+ }
3044
+ function deleteState(projectId) {
3045
+ const path = statePath(projectId);
3046
+ if (existsSync2(path)) {
3047
+ rmSync(path);
3048
+ }
3049
+ }
3050
+ function detectSupabaseProject(explicitProject) {
3051
+ if (explicitProject) {
3052
+ return explicitProject;
3053
+ }
3054
+ const output = runDocker([
3055
+ "ps",
3056
+ "-a",
3057
+ "--format",
3058
+ '{{.Names}} {{.Label "com.supabase.cli.project"}}'
3059
+ ]);
3060
+ const projects = /* @__PURE__ */ new Set();
3061
+ for (const line of output.split("\n")) {
3062
+ const parts = line.split(" ");
3063
+ if (parts.length !== 2) {
3064
+ continue;
3065
+ }
3066
+ const projectId2 = parts[1]?.trim();
3067
+ if (projectId2) {
3068
+ projects.add(projectId2);
3069
+ }
3070
+ }
3071
+ if (projects.size === 0) {
3072
+ throw new Error("No local Supabase CLI containers found. Run `supabase start` first.");
3073
+ }
3074
+ if (projects.size > 1) {
3075
+ throw new Error(
3076
+ `Multiple Supabase projects found: ${Array.from(projects).join(", ")}. Pass --project <id>.`
3077
+ );
3078
+ }
3079
+ const projectId = Array.from(projects)[0];
3080
+ if (!projectId) {
3081
+ throw new Error("Could not detect local Supabase project id.");
3082
+ }
3083
+ return projectId;
3084
+ }
3085
+ function containerName(prefix, projectId) {
3086
+ return `supabase_${prefix}_${projectId}`;
3087
+ }
3088
+ function networkName(projectId) {
3089
+ return `supabase_network_${projectId}`;
3090
+ }
3091
+ async function resolveBranch(branchName) {
3092
+ const requestedBranchName = branchName || getCurrentBranch();
3093
+ if (!requestedBranchName) {
3094
+ throw new Error("No branch specified and no current branch is selected.");
3095
+ }
3096
+ const currentConnectorId = getConfig("currentConnectorId");
3097
+ if (!currentConnectorId) {
3098
+ throw new Error("No connector selected. Run `ardent connector switch` first.");
3099
+ }
3100
+ const result = await api.get(
3101
+ `/v1/cli/branches?connector_id=${encodeURIComponent(currentConnectorId)}`
3102
+ );
3103
+ if (!Array.isArray(result.branches)) {
3104
+ throw new Error("API returned invalid response: missing branches array");
3105
+ }
3106
+ setCacheEntry("branches", result.branches);
3107
+ const branch = result.branches.find((candidate) => candidate.name === requestedBranchName);
3108
+ if (!branch) {
3109
+ const cached = getCacheEntry("branches");
3110
+ const cachedBranch = cached?.data.find((candidate) => candidate.name === requestedBranchName);
3111
+ if (cachedBranch) {
3112
+ return cachedBranch;
3113
+ }
3114
+ throw new Error(`Branch "${requestedBranchName}" not found.`);
3115
+ }
3116
+ if (!branch.branch_url) {
3117
+ throw new Error(`Branch "${branch.name}" does not have a connection URL.`);
3118
+ }
3119
+ return branch;
3120
+ }
3121
+ function parseBranchUrl(branchUrl) {
3122
+ const parsed = new URL(branchUrl);
3123
+ if (!parsed.hostname || !parsed.username || !parsed.password) {
3124
+ throw new Error("Branch URL is missing host, username, or password.");
3125
+ }
3126
+ return parsed;
3127
+ }
3128
+ function branchTargetConnectHost(hostname) {
3129
+ if (hostname === "routing.localhost" || hostname.endsWith(".routing.localhost")) {
3130
+ return DOCKER_HOST_GATEWAY;
3131
+ }
3132
+ return hostname;
3133
+ }
3134
+ function shouldVerifyBranchTargetCertificate(hostname) {
3135
+ return !(hostname === "routing.localhost" || hostname.endsWith(".routing.localhost"));
3136
+ }
3137
+ function proxyScript() {
3138
+ return String.raw`
3139
+ const net = require("net");
3140
+ const tls = require("tls");
3141
+
3142
+ const listenPort = Number(process.env.LISTEN_PORT || "5432");
3143
+ const targetHost = process.env.TARGET_HOST;
3144
+ const targetConnectHost = process.env.TARGET_CONNECT_HOST || targetHost;
3145
+ const targetPort = Number(process.env.TARGET_PORT || "5432");
3146
+ const targetDatabase = process.env.TARGET_DATABASE;
3147
+ const rejectUnauthorized = process.env.TARGET_REJECT_UNAUTHORIZED !== "false";
3148
+
3149
+ if (!targetHost || !targetDatabase) {
3150
+ throw new Error("TARGET_HOST and TARGET_DATABASE are required");
3151
+ }
3152
+
3153
+ const SSL_REQUEST = 80877103;
3154
+ const AUTH_SASL = 10;
3155
+
3156
+ function readCString(buffer, offset) {
3157
+ const end = buffer.indexOf(0, offset);
3158
+ if (end === -1) {
3159
+ throw new Error("Invalid startup packet: unterminated string");
3160
+ }
3161
+ return [buffer.toString("utf8", offset, end), end + 1];
3162
+ }
3163
+
3164
+ function rewriteStartupPacket(packet) {
3165
+ const length = packet.readInt32BE(0);
3166
+ const protocol = packet.readInt32BE(4);
3167
+ const params = [];
3168
+ let offset = 8;
3169
+
3170
+ while (offset < length - 1) {
3171
+ let key;
3172
+ [key, offset] = readCString(packet, offset);
3173
+ if (!key) {
3174
+ break;
3175
+ }
3176
+ let value;
3177
+ [value, offset] = readCString(packet, offset);
3178
+ if (key === "database" && (value === "postgres" || value === "_supabase")) {
3179
+ value = targetDatabase;
3180
+ }
3181
+ params.push([key, value]);
3182
+ }
3183
+
3184
+ const chunks = [];
3185
+ const header = Buffer.alloc(4);
3186
+ header.writeInt32BE(protocol, 0);
3187
+ chunks.push(header);
3188
+ for (const [key, value] of params) {
3189
+ chunks.push(Buffer.from(key + "\0" + value + "\0", "utf8"));
3190
+ }
3191
+ chunks.push(Buffer.from([0]));
3192
+ const body = Buffer.concat(chunks);
3193
+ const output = Buffer.alloc(4 + body.length);
3194
+ output.writeInt32BE(output.length, 0);
3195
+ body.copy(output, 4);
3196
+ return output;
3197
+ }
3198
+
3199
+ function filterServerPacket(packet) {
3200
+ if (packet.length < 9 || packet[0] !== 82) {
3201
+ return packet;
3202
+ }
3203
+ const authCode = packet.readInt32BE(5);
3204
+ if (authCode !== AUTH_SASL) {
3205
+ return packet;
3206
+ }
3207
+ const length = packet.readInt32BE(1);
3208
+ const body = packet.subarray(9, 1 + length);
3209
+ const trailing = packet.subarray(1 + length);
3210
+ const mechanisms = body.toString("utf8").split("\0").filter(Boolean);
3211
+ const filtered = mechanisms.filter((mechanism) => mechanism !== "SCRAM-SHA-256-PLUS");
3212
+ if (filtered.length === mechanisms.length) {
3213
+ return packet;
3214
+ }
3215
+ filtered.push("");
3216
+ const newBody = Buffer.from(filtered.join("\0") + "\0", "utf8");
3217
+ const output = Buffer.alloc(1 + 4 + 4 + newBody.length);
3218
+ output[0] = 82;
3219
+ output.writeInt32BE(4 + 4 + newBody.length, 1);
3220
+ output.writeInt32BE(AUTH_SASL, 5);
3221
+ newBody.copy(output, 9);
3222
+ return Buffer.concat([output, trailing]);
3223
+ }
3224
+
3225
+ function connectUpstream(startupPacket, client) {
3226
+ const socket = net.connect(targetPort, targetConnectHost, () => {
3227
+ const sslRequest = Buffer.alloc(8);
3228
+ sslRequest.writeInt32BE(8, 0);
3229
+ sslRequest.writeInt32BE(SSL_REQUEST, 4);
3230
+ socket.write(sslRequest);
3231
+ });
3232
+
3233
+ socket.once("data", (response) => {
3234
+ if (response[0] !== 83) {
3235
+ client.destroy(new Error("Upstream PostgreSQL server did not accept SSL"));
3236
+ socket.destroy();
3237
+ return;
3238
+ }
3239
+
3240
+ const secureSocket = tls.connect({
3241
+ socket,
3242
+ servername: targetHost,
3243
+ rejectUnauthorized,
3244
+ }, () => {
3245
+ let rewrittenStartupPacket;
3246
+ try {
3247
+ rewrittenStartupPacket = rewriteStartupPacket(startupPacket);
3248
+ } catch {
3249
+ client.destroy();
3250
+ secureSocket.destroy();
3251
+ return;
3252
+ }
3253
+ secureSocket.write(rewrittenStartupPacket);
3254
+ });
3255
+
3256
+ secureSocket.on("data", (chunk) => {
3257
+ client.write(filterServerPacket(chunk));
3258
+ });
3259
+ client.on("data", (chunk) => secureSocket.write(chunk));
3260
+ secureSocket.on("error", () => client.destroy());
3261
+ client.on("error", () => secureSocket.destroy());
3262
+ secureSocket.on("close", () => client.destroy());
3263
+ client.on("close", () => secureSocket.destroy());
3264
+ });
3265
+
3266
+ socket.on("error", () => client.destroy());
3267
+ }
3268
+
3269
+ const server = net.createServer((client) => {
3270
+ let buffered = Buffer.alloc(0);
3271
+ let sawSslRequest = false;
3272
+
3273
+ client.on("data", function onFirstData(chunk) {
3274
+ buffered = Buffer.concat([buffered, chunk]);
3275
+ if (buffered.length < 8) {
3276
+ return;
3277
+ }
3278
+
3279
+ const length = buffered.readInt32BE(0);
3280
+ const code = buffered.readInt32BE(4);
3281
+ if (!sawSslRequest && length === 8 && code === SSL_REQUEST) {
3282
+ client.write("N");
3283
+ buffered = buffered.subarray(8);
3284
+ sawSslRequest = true;
3285
+ if (buffered.length < 8) {
3286
+ return;
3287
+ }
3288
+ }
3289
+
3290
+ const startupLength = buffered.readInt32BE(0);
3291
+ if (buffered.length < startupLength) {
3292
+ return;
3293
+ }
3294
+
3295
+ client.off("data", onFirstData);
3296
+ try {
3297
+ const startupPacket = buffered.subarray(0, startupLength);
3298
+ const remainder = buffered.subarray(startupLength);
3299
+ connectUpstream(startupPacket, client);
3300
+ if (remainder.length > 0) {
3301
+ client.unshift(remainder);
3302
+ }
3303
+ } catch {
3304
+ client.destroy();
3305
+ }
3306
+ });
3307
+ });
3308
+
3309
+ server.listen(listenPort, "0.0.0.0", () => {
3310
+ console.log(JSON.stringify({ event: "listening", listenPort, targetHost, targetConnectHost, targetDatabase }));
3311
+ });
3312
+ `;
3313
+ }
3314
+ function writeProxyScript(projectId) {
3315
+ const scriptPath = join2(STATE_DIR, projectId, "proxy.js");
3316
+ mkdirSync2(dirname(scriptPath), { recursive: true });
3317
+ writeFileSync2(scriptPath, proxyScript());
3318
+ return scriptPath;
3319
+ }
3320
+ function serviceEnvForBranch(servicePrefix, env, dbContainer, branchUser, branchPassword) {
3321
+ const localDbUri = (role) => `postgresql://${encodeURIComponent(role)}:${encodeURIComponent(branchPassword)}@${dbContainer}:5432/postgres?sslmode=disable`;
3322
+ return env.map((entry) => {
3323
+ if (servicePrefix === "rest" && entry.startsWith("PGRST_DB_URI=")) {
3324
+ return `PGRST_DB_URI=${localDbUri("authenticator")}`;
3325
+ }
3326
+ if (servicePrefix === "auth" && entry.startsWith("GOTRUE_DB_DATABASE_URL=")) {
3327
+ return `GOTRUE_DB_DATABASE_URL=${localDbUri("supabase_auth_admin")}`;
3328
+ }
3329
+ if (servicePrefix === "storage" && entry.startsWith("DATABASE_URL=")) {
3330
+ return `DATABASE_URL=${localDbUri("supabase_storage_admin")}`;
3331
+ }
3332
+ if (servicePrefix === "realtime") {
3333
+ if (entry.startsWith("DB_HOST=")) {
3334
+ return `DB_HOST=${dbContainer}`;
3335
+ }
3336
+ if (entry.startsWith("DB_NAME=")) {
3337
+ return "DB_NAME=postgres";
3338
+ }
3339
+ if (entry.startsWith("DB_USER=")) {
3340
+ return "DB_USER=supabase_admin";
3341
+ }
3342
+ if (entry.startsWith("DB_PASSWORD=")) {
3343
+ return `DB_PASSWORD=${branchPassword}`;
3344
+ }
3345
+ if (entry.startsWith("DB_PORT=")) {
3346
+ return "DB_PORT=5432";
3347
+ }
3348
+ }
3349
+ if (servicePrefix === "pg_meta") {
3350
+ if (entry.startsWith("PG_META_DB_HOST=")) {
3351
+ return `PG_META_DB_HOST=${dbContainer}`;
3352
+ }
3353
+ if (entry.startsWith("PG_META_DB_NAME=")) {
3354
+ return "PG_META_DB_NAME=postgres";
3355
+ }
3356
+ if (entry.startsWith("PG_META_DB_USER=")) {
3357
+ return `PG_META_DB_USER=${branchUser}`;
3358
+ }
3359
+ if (entry.startsWith("PG_META_DB_PASSWORD=")) {
3360
+ return `PG_META_DB_PASSWORD=${branchPassword}`;
3361
+ }
3362
+ if (entry.startsWith("PG_META_DB_PORT=")) {
3363
+ return "PG_META_DB_PORT=5432";
3364
+ }
3365
+ }
3366
+ return entry;
3367
+ });
3368
+ }
3369
+ function mountArgs(mounts) {
3370
+ const args2 = [];
3371
+ for (const mount of mounts) {
3372
+ if (!mount.Source || !mount.Target) {
3373
+ continue;
3374
+ }
3375
+ const readonly = mount.ReadOnly ? ",readonly" : "";
3376
+ args2.push(
3377
+ "--mount",
3378
+ `type=${mount.Type},source=${mount.Source},target=${mount.Target}${readonly}`
3379
+ );
3380
+ }
3381
+ return args2;
3382
+ }
3383
+ function portPublishArgs(container) {
3384
+ const args2 = [];
3385
+ const ports = container.NetworkSettings.Ports ?? {};
3386
+ for (const [containerPort, bindings] of Object.entries(ports)) {
3387
+ if (!bindings) {
3388
+ continue;
3389
+ }
3390
+ for (const binding of bindings) {
3391
+ if (!binding.HostPort) {
3392
+ continue;
3393
+ }
3394
+ const hostPrefix = binding.HostIp ? `${binding.HostIp}:` : "";
3395
+ args2.push("-p", `${hostPrefix}${binding.HostPort}:${containerPort}`);
3396
+ }
3397
+ }
3398
+ return args2;
3399
+ }
3400
+ function containerAliases(container, network, fallback) {
3401
+ const aliases = container.NetworkSettings.Networks[network]?.Aliases ?? [];
3402
+ const cleanAliases = aliases.filter((alias) => alias && alias !== container.Name.replace(/^\//, ""));
3403
+ return cleanAliases.length > 0 ? cleanAliases : [fallback];
3404
+ }
3405
+ function imageCommandArgs(image, entrypoint, command) {
3406
+ const args2 = [];
3407
+ if (entrypoint && entrypoint.length > 0) {
3408
+ args2.push("--entrypoint", entrypoint[0]);
3409
+ }
3410
+ args2.push(image);
3411
+ if (entrypoint && entrypoint.length > 1) {
3412
+ args2.push(...entrypoint.slice(1));
3413
+ }
3414
+ if (command) {
3415
+ args2.push(...command);
3416
+ }
3417
+ return args2;
3418
+ }
3419
+ function recreateServiceForBranch(servicePrefix, projectId, network, dbContainer, branchUser, branchPassword) {
3420
+ const name = containerName(servicePrefix, projectId);
3421
+ if (!dockerExists(name)) {
3422
+ return void 0;
3423
+ }
3424
+ const originalName = `${name}${ORIGINAL_SUFFIX}`;
3425
+ if (dockerExists(originalName)) {
3426
+ throw new Error(`Refusing to overwrite existing backup container ${originalName}. Run unlink first.`);
3427
+ }
3428
+ const container = inspectContainer(name);
3429
+ const aliases = containerAliases(container, network, servicePrefix.replace("pg_meta", "meta"));
3430
+ const env = serviceEnvForBranch(
3431
+ servicePrefix,
3432
+ container.Config.Env ?? [],
3433
+ dbContainer,
3434
+ branchUser,
3435
+ branchPassword
3436
+ );
3437
+ runDocker(["stop", name]);
3438
+ runDocker(["rename", name, originalName]);
3439
+ const args2 = ["run", "-d", "--name", name, "--network", network];
3440
+ for (const alias of aliases) {
3441
+ args2.push("--network-alias", alias);
3442
+ }
3443
+ for (const envValue of env) {
3444
+ args2.push("-e", envValue);
3445
+ }
3446
+ if (container.Config.User) {
3447
+ args2.push("--user", container.Config.User);
3448
+ }
3449
+ if (container.Config.WorkingDir) {
3450
+ args2.push("--workdir", container.Config.WorkingDir);
3451
+ }
3452
+ args2.push(...mountArgs(container.Mounts));
3453
+ args2.push(
3454
+ ...imageCommandArgs(container.Config.Image, container.Config.Entrypoint, container.Config.Cmd)
3455
+ );
3456
+ runDocker(args2);
3457
+ return { name, originalName };
3458
+ }
3459
+ function startProxyContainer(projectId, network, dbContainer, parsedBranchUrl, portBindings) {
3460
+ const scriptPath = writeProxyScript(projectId);
3461
+ const targetDatabase = parsedBranchUrl.pathname.replace(/^\//, "") || "postgres";
3462
+ const targetPort = parsedBranchUrl.port || "5432";
3463
+ const targetConnectHost = branchTargetConnectHost(parsedBranchUrl.hostname);
3464
+ const rejectUnauthorized = shouldVerifyBranchTargetCertificate(parsedBranchUrl.hostname);
3465
+ const args2 = [
3466
+ "run",
3467
+ "-d",
3468
+ "--name",
3469
+ dbContainer,
3470
+ "--network",
3471
+ network,
3472
+ "--network-alias",
3473
+ "db",
3474
+ "--network-alias",
3475
+ "db.supabase.internal"
3476
+ ];
3477
+ if (targetConnectHost === DOCKER_HOST_GATEWAY) {
3478
+ args2.push("--add-host", `${DOCKER_HOST_GATEWAY}:host-gateway`);
3479
+ }
3480
+ args2.push(...portBindings);
3481
+ args2.push(
3482
+ "-v",
3483
+ `${scriptPath}:/proxy.js:ro`,
3484
+ "-e",
3485
+ `TARGET_HOST=${parsedBranchUrl.hostname}`,
3486
+ "-e",
3487
+ `TARGET_CONNECT_HOST=${targetConnectHost}`,
3488
+ "-e",
3489
+ `TARGET_PORT=${targetPort}`,
3490
+ "-e",
3491
+ `TARGET_DATABASE=${targetDatabase}`,
3492
+ "-e",
3493
+ `TARGET_REJECT_UNAUTHORIZED=${String(rejectUnauthorized)}`,
3494
+ "-e",
3495
+ "LISTEN_PORT=5432",
3496
+ "node:22-alpine",
3497
+ "node",
3498
+ "/proxy.js"
3499
+ );
3500
+ runDocker(args2);
3501
+ }
3502
+ function dumpLocalStorageMigrations(dbContainer) {
3503
+ return runDocker([
3504
+ "exec",
3505
+ dbContainer,
3506
+ "pg_dump",
3507
+ "-U",
3508
+ "postgres",
3509
+ "-d",
3510
+ "postgres",
3511
+ "--data-only",
3512
+ "--table=storage.migrations",
3513
+ "--column-inserts",
3514
+ "--no-owner",
3515
+ "--no-privileges"
3516
+ ]);
3517
+ }
3518
+ function storageMigrationsImportSql(dumpSql) {
3519
+ const tempTable = "pg_temp.ardent_storage_migrations_import";
3520
+ const tempDumpSql = dumpSql.replaceAll("INSERT INTO storage.migrations", `INSERT INTO ${tempTable}`);
3521
+ return `
3522
+ BEGIN;
3523
+ CREATE TEMP TABLE ardent_storage_migrations_import (LIKE storage.migrations INCLUDING DEFAULTS);
3524
+ ${tempDumpSql}
3525
+ INSERT INTO storage.migrations
3526
+ SELECT * FROM ${tempTable}
3527
+ ON CONFLICT DO NOTHING;
3528
+ COMMIT;
3529
+ `;
3530
+ }
3531
+ function waitForOriginalDb(containerName2) {
3532
+ const deadline = Date.now() + 3e4;
3533
+ while (Date.now() < deadline) {
3534
+ try {
3535
+ runDocker(["exec", containerName2, "pg_isready", "-U", "postgres", "-d", "postgres"]);
3536
+ return;
3537
+ } catch {
3538
+ execFileSync("sleep", ["1"]);
3539
+ }
3540
+ }
3541
+ throw new Error(`Timed out waiting for ${containerName2} to accept local PostgreSQL connections.`);
3542
+ }
3543
+ function copyLocalStorageMigrationsToBranch(dumpSql, originalDbContainer, dbContainer, network, branchUser, branchPassword) {
3544
+ runDocker(["start", originalDbContainer]);
3545
+ try {
3546
+ waitForOriginalDb(originalDbContainer);
3547
+ const dbContainerIpAddress = containerIpAddress(dbContainer, network);
3548
+ runDockerWithInput(
3549
+ [
3550
+ "exec",
3551
+ "-i",
3552
+ "-e",
3553
+ `PGPASSWORD=${branchPassword}`,
3554
+ originalDbContainer,
3555
+ "psql",
3556
+ "-h",
3557
+ dbContainerIpAddress,
3558
+ "-p",
3559
+ "5432",
3560
+ "-U",
3561
+ branchUser,
3562
+ "-d",
3563
+ "postgres",
3564
+ "-v",
3565
+ "ON_ERROR_STOP=1"
3566
+ ],
3567
+ storageMigrationsImportSql(dumpSql)
3568
+ );
3569
+ } finally {
3570
+ runDocker(["stop", originalDbContainer]);
3571
+ }
3572
+ }
3573
+ function supabaseRealtimeCompatibilitySql() {
3574
+ return String.raw`
3575
+ SET lock_timeout = '2s';
3576
+ SET statement_timeout = '5s';
3577
+
3578
+ ALTER SCHEMA realtime OWNER TO supabase_admin;
3579
+ GRANT ALL PRIVILEGES ON SCHEMA realtime TO supabase_realtime_admin;
3580
+
3581
+ CREATE OR REPLACE FUNCTION realtime.list_changes(
3582
+ publication name,
3583
+ slot_name name,
3584
+ max_changes int,
3585
+ max_record_bytes int
3586
+ )
3587
+ RETURNS SETOF realtime.wal_rls
3588
+ LANGUAGE sql
3589
+ AS $$
3590
+ WITH pub AS (
3591
+ SELECT
3592
+ concat_ws(
3593
+ ',',
3594
+ CASE WHEN bool_or(pubinsert) THEN 'insert' ELSE NULL END,
3595
+ CASE WHEN bool_or(pubupdate) THEN 'update' ELSE NULL END,
3596
+ CASE WHEN bool_or(pubdelete) THEN 'delete' ELSE NULL END
3597
+ ) AS w2j_actions,
3598
+ coalesce(
3599
+ string_agg(
3600
+ realtime.quote_wal2json(format('%I.%I', schemaname, tablename)::regclass),
3601
+ ','
3602
+ ) FILTER (WHERE ppt.tablename IS NOT NULL AND ppt.tablename NOT LIKE '% %'),
3603
+ ''
3604
+ ) AS w2j_add_tables
3605
+ FROM pg_publication pp
3606
+ LEFT JOIN pg_publication_tables ppt
3607
+ ON pp.pubname = ppt.pubname
3608
+ WHERE pp.pubname = publication
3609
+ GROUP BY pp.pubname
3610
+ LIMIT 1
3611
+ ),
3612
+ w2j AS (
3613
+ SELECT
3614
+ x.*,
3615
+ pub.w2j_add_tables
3616
+ FROM pub,
3617
+ pg_logical_slot_get_changes(
3618
+ slot_name,
3619
+ NULL,
3620
+ max_changes,
3621
+ 'include-pk', 'true',
3622
+ 'include-transaction', 'false',
3623
+ 'include-timestamp', 'true',
3624
+ 'include-type-oids', 'true',
3625
+ 'format-version', '2',
3626
+ 'actions', pub.w2j_actions,
3627
+ 'add-tables', pub.w2j_add_tables
3628
+ ) x
3629
+ )
3630
+ SELECT
3631
+ xyz.wal,
3632
+ xyz.is_rls_enabled,
3633
+ xyz.subscription_ids,
3634
+ xyz.errors
3635
+ FROM w2j,
3636
+ realtime.apply_rls(
3637
+ wal := w2j.data::jsonb,
3638
+ max_record_bytes := max_record_bytes
3639
+ ) xyz(wal, is_rls_enabled, subscription_ids, errors)
3640
+ WHERE w2j.w2j_add_tables <> ''
3641
+ AND xyz.subscription_ids[1] IS NOT NULL
3642
+ $$;
3643
+
3644
+ ALTER FUNCTION realtime.list_changes(name, name, int, int) OWNER TO supabase_realtime_admin;
3645
+ GRANT EXECUTE ON FUNCTION realtime.list_changes(name, name, int, int)
3646
+ TO supabase_admin, supabase_realtime_admin;
3647
+
3648
+ INSERT INTO realtime.schema_migrations (version, inserted_at)
3649
+ VALUES (${REALTIME_LIST_CHANGES_MIGRATION_VERSION}, now())
3650
+ ON CONFLICT (version) DO NOTHING;
3651
+ `;
3652
+ }
3653
+ function applySupabaseRealtimeCompatibility(projectId, originalDbContainer, dbContainer, network, branchPassword) {
3654
+ const realtimeContainer = containerName("realtime", projectId);
3655
+ if (!dockerExists(realtimeContainer)) {
3656
+ return;
3657
+ }
3658
+ runDocker(["start", originalDbContainer]);
3659
+ try {
3660
+ waitForOriginalDb(originalDbContainer);
3661
+ const dbContainerIpAddress = containerIpAddress(dbContainer, network);
3662
+ const deadline = Date.now() + 6e4;
3663
+ let lastError = "";
3664
+ while (Date.now() < deadline) {
3665
+ try {
3666
+ runDockerWithInput(
3667
+ [
3668
+ "exec",
3669
+ "-i",
3670
+ "-e",
3671
+ `PGPASSWORD=${branchPassword}`,
3672
+ originalDbContainer,
3673
+ "psql",
3674
+ "-h",
3675
+ dbContainerIpAddress,
3676
+ "-p",
3677
+ "5432",
3678
+ "-U",
3679
+ "supabase_admin",
3680
+ "-d",
3681
+ "postgres",
3682
+ "-v",
3683
+ "ON_ERROR_STOP=1"
3684
+ ],
3685
+ supabaseRealtimeCompatibilitySql(),
3686
+ { timeoutMs: 7e3 }
3687
+ );
3688
+ runDocker(["restart", realtimeContainer]);
3689
+ return;
3690
+ } catch (error) {
3691
+ lastError = error instanceof Error ? error.message : String(error);
3692
+ execFileSync("sleep", ["1"]);
3693
+ }
3694
+ }
3695
+ throw new Error(`Timed out applying Supabase Realtime compatibility: ${lastError}`);
3696
+ } finally {
3697
+ runDocker(["stop", originalDbContainer]);
3698
+ }
3699
+ }
3700
+ function kongApiUrl(projectId) {
3701
+ const kong = containerName("kong", projectId);
3702
+ if (!dockerExists(kong)) {
3703
+ return void 0;
3704
+ }
3705
+ const output = runDocker(["port", kong, "8000/tcp"]);
3706
+ const first = output.split("\n")[0];
3707
+ const port = first?.split(":").pop();
3708
+ return port ? `http://127.0.0.1:${port}` : void 0;
3709
+ }
3710
+ async function waitForHealth(projectId) {
3711
+ const apiUrl = kongApiUrl(projectId);
3712
+ if (!apiUrl) {
3713
+ return;
3714
+ }
3715
+ const deadline = Date.now() + 6e4;
3716
+ let lastError = "";
3717
+ while (Date.now() < deadline) {
3718
+ try {
3719
+ const authResponse = await fetch(`${apiUrl}/auth/v1/health`);
3720
+ const restResponse = await fetch(`${apiUrl}/rest/v1/`);
3721
+ if (authResponse.ok && restResponse.ok) {
3722
+ return;
3723
+ }
3724
+ lastError = `auth=${authResponse.status}, rest=${restResponse.status}`;
3725
+ } catch (error) {
3726
+ lastError = error instanceof Error ? error.message : String(error);
3727
+ }
3728
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
3729
+ }
3730
+ throw new Error(`Supabase health checks did not pass: ${lastError}`);
3731
+ }
3732
+ function restoreOriginalContainer(name, originalName) {
3733
+ if (dockerExists(name)) {
3734
+ runDocker(["rm", "-f", name]);
3735
+ }
3736
+ if (dockerExists(originalName)) {
3737
+ runDocker(["rename", originalName, name]);
3738
+ runDocker(["start", name]);
3739
+ }
3740
+ }
3741
+ function rollbackPartialLink(projectId, dbContainer, originalDbContainer, services) {
3742
+ for (const service of services.reverse()) {
3743
+ restoreOriginalContainer(service.name, service.originalName);
3744
+ }
3745
+ restoreOriginalContainer(dbContainer, originalDbContainer);
3746
+ deleteState(projectId);
3747
+ }
3748
+ async function statusSupabaseAction(options = {}) {
3749
+ try {
3750
+ const projectId = detectSupabaseProject(options.project);
3751
+ const state = readState(projectId);
3752
+ console.log(`Supabase project: ${projectId}`);
3753
+ if (!state) {
3754
+ console.log("Status: not linked");
3755
+ return;
3756
+ }
3757
+ console.log("Status: linked");
3758
+ console.log(`Branch: ${state.branchName}`);
3759
+ console.log(`Linked at: ${new Date(state.linkedAt).toLocaleString()}`);
3760
+ console.log(`DB container: ${state.dbContainer}`);
3761
+ } catch (error) {
3762
+ console.error("\u2717 Failed:", error instanceof Error ? error.message : error);
3763
+ process.exit(1);
3764
+ }
3765
+ }
3766
+ async function linkSupabaseAction(branchName, options = {}) {
3767
+ let projectId = "";
3768
+ let dbContainer = "";
3769
+ let originalDbContainer = "";
3770
+ const createdServices = [];
3771
+ try {
3772
+ projectId = detectSupabaseProject(options.project);
3773
+ if (readState(projectId)) {
3774
+ throw new Error(`Supabase project ${projectId} is already linked. Run unlink first.`);
3775
+ }
3776
+ const branch = await resolveBranch(branchName);
3777
+ const parsedBranchUrl = parseBranchUrl(branch.branch_url);
3778
+ const branchUser = decodeURIComponent(parsedBranchUrl.username);
3779
+ const branchPassword = decodeURIComponent(parsedBranchUrl.password);
3780
+ dbContainer = containerName("db", projectId);
3781
+ originalDbContainer = `${dbContainer}${ORIGINAL_SUFFIX}`;
3782
+ const network = networkName(projectId);
3783
+ if (!dockerExists(dbContainer)) {
3784
+ throw new Error(`Local Supabase DB container not found: ${dbContainer}`);
3785
+ }
3786
+ if (dockerExists(originalDbContainer)) {
3787
+ throw new Error(`Backup DB container already exists: ${originalDbContainer}. Run unlink first.`);
3788
+ }
3789
+ console.log(`Linking Supabase project ${projectId} to branch ${branch.name}...`);
3790
+ const storageMigrationsDump = dumpLocalStorageMigrations(dbContainer);
3791
+ const originalDbContainerState = inspectContainer(dbContainer);
3792
+ const dbPortBindings = portPublishArgs(originalDbContainerState);
3793
+ runDocker(["stop", dbContainer]);
3794
+ runDocker(["rename", dbContainer, originalDbContainer]);
3795
+ startProxyContainer(projectId, network, dbContainer, parsedBranchUrl, []);
3796
+ copyLocalStorageMigrationsToBranch(
3797
+ storageMigrationsDump,
3798
+ originalDbContainer,
3799
+ dbContainer,
3800
+ network,
3801
+ branchUser,
3802
+ branchPassword
3803
+ );
3804
+ for (const servicePrefix of SERVICE_SUFFIXES) {
3805
+ const service = recreateServiceForBranch(
3806
+ servicePrefix,
3807
+ projectId,
3808
+ network,
3809
+ dbContainer,
3810
+ branchUser,
3811
+ branchPassword
3812
+ );
3813
+ if (service) {
3814
+ createdServices.push(service);
3815
+ }
3816
+ }
3817
+ applySupabaseRealtimeCompatibility(
3818
+ projectId,
3819
+ originalDbContainer,
3820
+ dbContainer,
3821
+ network,
3822
+ branchPassword
3823
+ );
3824
+ runDocker(["rm", "-f", dbContainer]);
3825
+ startProxyContainer(projectId, network, dbContainer, parsedBranchUrl, dbPortBindings);
3826
+ const kong = containerName("kong", projectId);
3827
+ if (dockerExists(kong)) {
3828
+ runDocker(["restart", kong]);
3829
+ }
3830
+ const state = {
3831
+ version: 1,
3832
+ projectId,
3833
+ network,
3834
+ branchName: branch.name,
3835
+ linkedAt: (/* @__PURE__ */ new Date()).toISOString(),
3836
+ dbContainer,
3837
+ originalDbContainer,
3838
+ services: createdServices
3839
+ };
3840
+ if (!options.skipHealthCheck) {
3841
+ await waitForHealth(projectId);
3842
+ }
3843
+ writeState(state);
3844
+ console.log("\u2713 Local Supabase is linked to the branch");
3845
+ trackEvent("CLI: settings supabase link succeeded");
3846
+ } catch (error) {
3847
+ if (projectId) {
3848
+ const state = readState(projectId);
3849
+ if (!state && dbContainer && originalDbContainer) {
3850
+ rollbackPartialLink(projectId, dbContainer, originalDbContainer, createdServices);
3851
+ }
3852
+ }
3853
+ trackEvent("CLI: settings supabase link failed");
3854
+ console.error("\u2717 Failed:", error instanceof Error ? error.message : error);
3855
+ process.exit(1);
3856
+ }
3857
+ }
3858
+ async function unlinkSupabaseAction(options = {}) {
3859
+ try {
3860
+ const projectId = detectSupabaseProject(options.project);
3861
+ const state = readState(projectId);
3862
+ if (!state) {
3863
+ console.log(`Supabase project ${projectId} is not linked`);
3864
+ return;
3865
+ }
3866
+ console.log(`Unlinking Supabase project ${projectId}...`);
3867
+ restoreOriginalContainer(state.dbContainer, state.originalDbContainer);
3868
+ for (const service of state.services) {
3869
+ restoreOriginalContainer(service.name, service.originalName);
3870
+ }
3871
+ const kong = containerName("kong", projectId);
3872
+ if (dockerExists(kong)) {
3873
+ runDocker(["restart", kong]);
3874
+ }
3875
+ deleteState(projectId);
3876
+ if (!options.skipHealthCheck) {
3877
+ await waitForHealth(projectId);
3878
+ }
3879
+ console.log("\u2713 Local Supabase restored to its local database");
3880
+ trackEvent("CLI: settings supabase unlink succeeded");
3881
+ } catch (error) {
3882
+ trackEvent("CLI: settings supabase unlink failed");
3883
+ console.error("\u2717 Failed:", error instanceof Error ? error.message : error);
3884
+ process.exit(1);
3885
+ }
3886
+ }
3887
+
2865
3888
  // src/commands/settings/index.ts
2866
3889
  var settingsCommand = new Command6("settings").description("Manage settings for the current connector").showHelpAfterError("(run 'ardent settings --help' for valid keys)");
2867
3890
  settingsCommand.command("list").description("List current settings for the connector").action(listAction5);
@@ -2869,6 +3892,11 @@ settingsCommand.command("set <key> <value>").description(
2869
3892
  "Set a setting. Keys: default_db, branch_sql. For branch_sql, prefix value with @ to read from file."
2870
3893
  ).action(setAction);
2871
3894
  settingsCommand.command("remove <key>").description("Remove a setting. Keys: default_db, branch_sql.").action(removeAction2);
3895
+ var supabaseSettingsCommand = new Command6("supabase").description("Link local Supabase to an Ardent branch");
3896
+ supabaseSettingsCommand.command("status").description("Show local Supabase link status").option("--project <project>", "Supabase CLI project id").action(statusSupabaseAction);
3897
+ 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);
3898
+ 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);
3899
+ settingsCommand.addCommand(supabaseSettingsCommand);
2872
3900
 
2873
3901
  // src/commands/auth/index.ts
2874
3902
  import { Command as Command7 } from "commander";
@@ -3060,16 +4088,16 @@ var logoutCommand = new Command7("logout").description("Logout from Ardent").act
3060
4088
  var statusCommand = new Command7("status").description("Show status").action(statusAction);
3061
4089
 
3062
4090
  // 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");
4091
+ import { existsSync as existsSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
4092
+ import { join as join3 } from "path";
4093
+ import { homedir as homedir3 } from "os";
4094
+ var UPDATE_CHECK_FILE = join3(homedir3(), ".ardent", "update-check.json");
3067
4095
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
3068
4096
  var PACKAGE_NAME = "ardent-cli";
3069
4097
  function loadCache() {
3070
4098
  try {
3071
- if (existsSync2(UPDATE_CHECK_FILE)) {
3072
- return JSON.parse(readFileSync5(UPDATE_CHECK_FILE, "utf-8"));
4099
+ if (existsSync3(UPDATE_CHECK_FILE)) {
4100
+ return JSON.parse(readFileSync6(UPDATE_CHECK_FILE, "utf-8"));
3073
4101
  }
3074
4102
  } catch {
3075
4103
  }
@@ -3081,7 +4109,7 @@ function saveCache(latestVersion) {
3081
4109
  latest_version: latestVersion,
3082
4110
  checked_at: (/* @__PURE__ */ new Date()).toISOString()
3083
4111
  };
3084
- writeFileSync2(UPDATE_CHECK_FILE, JSON.stringify(data));
4112
+ writeFileSync3(UPDATE_CHECK_FILE, JSON.stringify(data));
3085
4113
  } catch {
3086
4114
  }
3087
4115
  }
@@ -3168,6 +4196,7 @@ SETTINGS
3168
4196
  settings list List current settings for the connector
3169
4197
  settings set Set a setting (e.g. default_db testdb)
3170
4198
  settings remove Remove a setting
4199
+ settings supabase Link local Supabase to the current branch
3171
4200
 
3172
4201
  TEAM
3173
4202
  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.37",
4
4
  "description": "Git for Data infrastructure",
5
5
  "type": "module",
6
6
  "bin": {