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.
- package/dist/index.js +971 -7
- 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
|
|
3064
|
-
import { join as
|
|
3065
|
-
import { homedir as
|
|
3066
|
-
var UPDATE_CHECK_FILE =
|
|
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 (
|
|
3072
|
-
return JSON.parse(
|
|
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
|
-
|
|
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
|