agentlink-sh 0.26.1 → 0.30.0
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/README.md +90 -94
- package/dist/{chunk-DM6KG5YU.js → chunk-KECTTRCF.js} +59 -3
- package/dist/{chunk-4CW46BPC.js → chunk-KRE7FEMO.js} +446 -83
- package/dist/{cloud-ZXVJMV5Q.js → cloud-OZQOOVJO.js} +16 -4
- package/dist/index.js +8255 -2090
- package/dist/{oauth-JGWRORJM.js → oauth-52Q6XDJL.js} +3 -5
- package/package.json +3 -2
- package/dist/chunk-7NV5CYOF.js +0 -1064
- package/dist/chunk-IV5ZSOKF.js +0 -194
- package/dist/chunk-MHI6VJ75.js +0 -27
- package/dist/constants-PWT7TUWD.js +0 -27
- package/dist/db-DNK3TD5Y.js +0 -31
- package/dist/utils-7LT4QSYL.js +0 -27
package/dist/chunk-7NV5CYOF.js
DELETED
|
@@ -1,1064 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
authenticatedFetch,
|
|
4
|
-
ensureAccessToken,
|
|
5
|
-
loadProjectCredential,
|
|
6
|
-
resolvePoolerDbUrl,
|
|
7
|
-
saveProjectCredential
|
|
8
|
-
} from "./chunk-4CW46BPC.js";
|
|
9
|
-
import {
|
|
10
|
-
ensureGitignorePattern,
|
|
11
|
-
runCommand
|
|
12
|
-
} from "./chunk-IV5ZSOKF.js";
|
|
13
|
-
import {
|
|
14
|
-
amber,
|
|
15
|
-
blue,
|
|
16
|
-
bold,
|
|
17
|
-
dim,
|
|
18
|
-
red
|
|
19
|
-
} from "./chunk-MHI6VJ75.js";
|
|
20
|
-
import {
|
|
21
|
-
pgd,
|
|
22
|
-
sb
|
|
23
|
-
} from "./chunk-DM6KG5YU.js";
|
|
24
|
-
|
|
25
|
-
// src/db.ts
|
|
26
|
-
import fs3 from "fs";
|
|
27
|
-
import os from "os";
|
|
28
|
-
import path3 from "path";
|
|
29
|
-
import { input } from "@inquirer/prompts";
|
|
30
|
-
|
|
31
|
-
// src/manifest.ts
|
|
32
|
-
import fs from "fs";
|
|
33
|
-
import path from "path";
|
|
34
|
-
var ALLOWED_CLOUD_ENV_NAMES = ["dev", "prod"];
|
|
35
|
-
var ALLOWED_ACTIVE_ENV_NAMES = ["local", "dev", "prod"];
|
|
36
|
-
function assertAllowedEnvNameForAdd(name) {
|
|
37
|
-
if (ALLOWED_CLOUD_ENV_NAMES.includes(name)) return;
|
|
38
|
-
throw new Error(
|
|
39
|
-
`Environment "${name}" is not supported. Only "dev" and "prod" can be added.
|
|
40
|
-
Agent Link enforces a fixed local \u2192 dev \u2192 prod model \u2014 no staging, no custom names.`
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
function assertAllowedEnvNameForUse(name) {
|
|
44
|
-
if (ALLOWED_ACTIVE_ENV_NAMES.includes(name)) return;
|
|
45
|
-
throw new Error(
|
|
46
|
-
`Environment "${name}" is not supported. Only "local", "dev", and "prod" are valid env use targets.
|
|
47
|
-
Agent Link enforces a fixed local \u2192 dev \u2192 prod model.`
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
function assertManifestEnvNames(manifest) {
|
|
51
|
-
if (!manifest.cloud?.environments) return;
|
|
52
|
-
const invalid = Object.keys(manifest.cloud.environments).filter(
|
|
53
|
-
(n) => !ALLOWED_CLOUD_ENV_NAMES.includes(n)
|
|
54
|
-
);
|
|
55
|
-
if (invalid.length === 0) return;
|
|
56
|
-
const list = invalid.map((n) => `"${n}"`).join(", ");
|
|
57
|
-
const removeHints = invalid.map((n) => ` agentlink env remove ${n}`).join("\n");
|
|
58
|
-
throw new Error(
|
|
59
|
-
`Unsupported environments in agentlink.json: ${list}.
|
|
60
|
-
Agent Link enforces a fixed local \u2192 dev \u2192 prod model.
|
|
61
|
-
Remove the offending entries:
|
|
62
|
-
${removeHints}`
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
function isBareManifest(manifest) {
|
|
66
|
-
return manifest?.bare === true;
|
|
67
|
-
}
|
|
68
|
-
function isLegacyCloud(cloud) {
|
|
69
|
-
if (!cloud || typeof cloud !== "object") return false;
|
|
70
|
-
const obj = cloud;
|
|
71
|
-
return typeof obj.projectRef === "string" && !obj.environments;
|
|
72
|
-
}
|
|
73
|
-
function normalizeLegacyCloud(cloud) {
|
|
74
|
-
return {
|
|
75
|
-
default: "dev",
|
|
76
|
-
environments: {
|
|
77
|
-
dev: {
|
|
78
|
-
projectRef: cloud.projectRef,
|
|
79
|
-
region: cloud.region,
|
|
80
|
-
apiUrl: cloud.apiUrl
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
function readManifest(cwd) {
|
|
86
|
-
try {
|
|
87
|
-
const raw = fs.readFileSync(path.join(cwd, "agentlink.json"), "utf-8");
|
|
88
|
-
const manifest = JSON.parse(raw);
|
|
89
|
-
if (manifest.cloud && isLegacyCloud(manifest.cloud)) {
|
|
90
|
-
manifest.cloud = normalizeLegacyCloud(manifest.cloud);
|
|
91
|
-
}
|
|
92
|
-
return manifest;
|
|
93
|
-
} catch {
|
|
94
|
-
return void 0;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
function writeManifest(cwd, manifest) {
|
|
98
|
-
const filePath = path.join(cwd, "agentlink.json");
|
|
99
|
-
fs.writeFileSync(filePath, JSON.stringify(manifest, null, 2) + "\n");
|
|
100
|
-
}
|
|
101
|
-
function resolveCloudEnv(cloud, envFlag) {
|
|
102
|
-
const envName = envFlag ?? cloud.default;
|
|
103
|
-
if (envName === "local") {
|
|
104
|
-
throw new Error(
|
|
105
|
-
`Environment "local" is a local Docker environment \u2014 not a cloud project.`
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
const env = cloud.environments[envName];
|
|
109
|
-
if (!env) {
|
|
110
|
-
const available = Object.keys(cloud.environments).join(", ");
|
|
111
|
-
throw new Error(
|
|
112
|
-
`Cloud environment "${envName}" not found. Available: ${available}`
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
return env;
|
|
116
|
-
}
|
|
117
|
-
function addCloudEnvironment(cwd, name, env) {
|
|
118
|
-
const manifest = readManifest(cwd);
|
|
119
|
-
if (!manifest) {
|
|
120
|
-
throw new Error("No agentlink.json found. Run `agentlink` to scaffold a project first.");
|
|
121
|
-
}
|
|
122
|
-
if (!manifest.cloud) {
|
|
123
|
-
manifest.cloud = { default: "local", environments: {} };
|
|
124
|
-
}
|
|
125
|
-
if (manifest.cloud.environments[name]) {
|
|
126
|
-
throw new Error(
|
|
127
|
-
`Environment "${name}" already exists. Remove it first with \`agentlink env remove ${name}\`.`
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
manifest.cloud.environments[name] = env;
|
|
131
|
-
writeManifest(cwd, manifest);
|
|
132
|
-
}
|
|
133
|
-
function removeCloudEnvironment(cwd, name) {
|
|
134
|
-
const manifest = readManifest(cwd);
|
|
135
|
-
if (!manifest?.cloud) {
|
|
136
|
-
throw new Error("No cloud environments configured.");
|
|
137
|
-
}
|
|
138
|
-
if (name === "local") {
|
|
139
|
-
throw new Error("Cannot remove the local environment.");
|
|
140
|
-
}
|
|
141
|
-
if (!manifest.cloud.environments[name]) {
|
|
142
|
-
throw new Error(`Environment "${name}" not found.`);
|
|
143
|
-
}
|
|
144
|
-
if (manifest.cloud.default === name) {
|
|
145
|
-
throw new Error(
|
|
146
|
-
`Cannot remove the active environment "${name}". Switch first with \`agentlink env use <other>\`.`
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
delete manifest.cloud.environments[name];
|
|
150
|
-
writeManifest(cwd, manifest);
|
|
151
|
-
}
|
|
152
|
-
function setCloudEnvOrgId(cwd, name, orgId) {
|
|
153
|
-
if (name === "local") return;
|
|
154
|
-
const manifest = readManifest(cwd);
|
|
155
|
-
if (!manifest?.cloud?.environments[name]) return;
|
|
156
|
-
const current = manifest.cloud.environments[name].orgId;
|
|
157
|
-
if (current === orgId) return;
|
|
158
|
-
if (current && current !== orgId) {
|
|
159
|
-
throw new Error(
|
|
160
|
-
`Refusing to overwrite orgId for env "${name}": manifest has "${current}", attempted "${orgId}".`
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
manifest.cloud.environments[name].orgId = orgId;
|
|
164
|
-
writeManifest(cwd, manifest);
|
|
165
|
-
}
|
|
166
|
-
function setDefaultEnvironment(cwd, name) {
|
|
167
|
-
const manifest = readManifest(cwd);
|
|
168
|
-
if (!manifest) {
|
|
169
|
-
throw new Error("No agentlink.json found.");
|
|
170
|
-
}
|
|
171
|
-
if (!manifest.cloud) {
|
|
172
|
-
manifest.cloud = { default: "local", environments: {} };
|
|
173
|
-
}
|
|
174
|
-
if (name !== "local" && !manifest.cloud.environments[name]) {
|
|
175
|
-
const available = ["local", ...Object.keys(manifest.cloud.environments)].join(", ");
|
|
176
|
-
throw new Error(`Environment "${name}" not found. Available: ${available}`);
|
|
177
|
-
}
|
|
178
|
-
manifest.cloud.default = name;
|
|
179
|
-
if (name === "local") {
|
|
180
|
-
manifest.mode = manifest.mode === "cloud" ? "existing" : manifest.mode;
|
|
181
|
-
} else {
|
|
182
|
-
manifest.mode = "cloud";
|
|
183
|
-
}
|
|
184
|
-
writeManifest(cwd, manifest);
|
|
185
|
-
}
|
|
186
|
-
function listEnvironments(cwd) {
|
|
187
|
-
const manifest = readManifest(cwd);
|
|
188
|
-
if (!manifest) return [];
|
|
189
|
-
const defaultName = manifest.cloud?.default ?? (manifest.mode === "cloud" ? "dev" : "local");
|
|
190
|
-
const entries = [];
|
|
191
|
-
entries.push({
|
|
192
|
-
name: "local",
|
|
193
|
-
isDefault: defaultName === "local",
|
|
194
|
-
isLocal: true
|
|
195
|
-
});
|
|
196
|
-
if (manifest.cloud?.environments) {
|
|
197
|
-
for (const [name, env] of Object.entries(manifest.cloud.environments)) {
|
|
198
|
-
entries.push({
|
|
199
|
-
name,
|
|
200
|
-
env,
|
|
201
|
-
isDefault: defaultName === name,
|
|
202
|
-
isLocal: false
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
return entries;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// src/supabase.ts
|
|
210
|
-
import { spawn } from "child_process";
|
|
211
|
-
import fs2 from "fs";
|
|
212
|
-
import path2 from "path";
|
|
213
|
-
|
|
214
|
-
// src/sql/check_setup.sql
|
|
215
|
-
var check_setup_default = "SELECT jsonb_build_object(\n 'extensions', jsonb_build_object(\n 'pg_net', EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_net'),\n 'pg_cron', EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_cron'),\n 'vault', EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'supabase_vault'),\n 'pgmq', EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pgmq'),\n -- pg_graphql must be dropped to enforce schema isolation (reported true when absent)\n 'pg_graphql_dropped', NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_graphql')\n ),\n 'queues', jsonb_build_object(\n 'agentlink_tasks', EXISTS (\n SELECT 1 FROM information_schema.tables\n WHERE table_schema = 'pgmq' AND table_name = 'q_agentlink_tasks'\n )\n ),\n 'tables', jsonb_build_object(\n 'profiles', EXISTS (\n SELECT 1 FROM information_schema.tables\n WHERE table_schema = 'public' AND table_name = 'profiles'\n ),\n 'tenants', EXISTS (\n SELECT 1 FROM information_schema.tables\n WHERE table_schema = 'public' AND table_name = 'tenants'\n ),\n 'memberships', EXISTS (\n SELECT 1 FROM information_schema.tables\n WHERE table_schema = 'public' AND table_name = 'memberships'\n ),\n 'invitations', EXISTS (\n SELECT 1 FROM information_schema.tables\n WHERE table_schema = 'public' AND table_name = 'invitations'\n )\n ),\n 'functions', jsonb_build_object(\n '_internal_admin_get_secret',\n EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'public'\n AND routine_name = '_internal_admin_get_secret'\n ),\n '_internal_admin_call_edge_function',\n EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'public'\n AND routine_name = '_internal_admin_call_edge_function'\n ),\n 'api._admin_enqueue_task',\n EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'api'\n AND routine_name = '_admin_enqueue_task'\n ),\n 'api._admin_queue_read',\n EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'api'\n AND routine_name = '_admin_queue_read'\n ),\n 'api._admin_queue_delete',\n EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'api'\n AND routine_name = '_admin_queue_delete'\n ),\n 'api._admin_queue_archive',\n EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'api'\n AND routine_name = '_admin_queue_archive'\n ),\n 'set_updated_at',\n EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'public'\n AND routine_name = 'set_updated_at'\n ),\n '_internal_admin_handle_new_user',\n EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'public'\n AND routine_name = '_internal_admin_handle_new_user'\n ),\n 'api.profile_get',\n EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'api'\n AND routine_name = 'profile_get'\n ),\n 'api.profile_update',\n EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'api'\n AND routine_name = 'profile_update'\n ),\n '_hook_before_user_created',\n EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'public'\n AND routine_name = '_hook_before_user_created'\n ),\n '_hook_send_email',\n EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'public'\n AND routine_name = '_hook_send_email'\n ),\n '_auth_tenant_id',\n EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'public'\n AND routine_name = '_auth_tenant_id'\n ),\n '_auth_tenant_role',\n EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'public'\n AND routine_name = '_auth_tenant_role'\n ),\n '_auth_has_role',\n EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'public'\n AND routine_name = '_auth_has_role'\n ),\n '_auth_is_tenant_member',\n EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'public'\n AND routine_name = '_auth_is_tenant_member'\n ),\n 'api.tenant_select',\n EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'api'\n AND routine_name = 'tenant_select'\n ),\n 'api.tenant_list',\n EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'api'\n AND routine_name = 'tenant_list'\n ),\n 'api.tenant_create',\n EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'api'\n AND routine_name = 'tenant_create'\n ),\n 'api.invitation_create',\n EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'api'\n AND routine_name = 'invitation_create'\n ),\n 'api.invitation_accept',\n EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'api'\n AND routine_name = 'invitation_accept'\n ),\n 'api.membership_list',\n EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'api'\n AND routine_name = 'membership_list'\n )\n ),\n 'triggers', jsonb_build_object(\n 'trg_profiles_updated_at',\n EXISTS (\n SELECT 1 FROM pg_trigger WHERE tgname = 'trg_profiles_updated_at'\n ),\n 'trg_auth_users_new_user',\n EXISTS (\n SELECT 1 FROM pg_trigger WHERE tgname = 'trg_auth_users_new_user'\n ),\n 'trg_tenants_updated_at',\n EXISTS (\n SELECT 1 FROM pg_trigger WHERE tgname = 'trg_tenants_updated_at'\n )\n ),\n 'secrets', jsonb_build_object(\n 'SUPABASE_URL',\n EXISTS (SELECT 1 FROM vault.secrets WHERE name = 'SUPABASE_URL'),\n 'SUPABASE_PUBLISHABLE_KEY',\n EXISTS (SELECT 1 FROM vault.secrets WHERE name = 'SUPABASE_PUBLISHABLE_KEY'),\n 'SUPABASE_SECRET_KEY',\n EXISTS (SELECT 1 FROM vault.secrets WHERE name = 'SUPABASE_SECRET_KEY')\n ),\n 'api_schema', EXISTS (\n SELECT 1 FROM information_schema.schemata WHERE schema_name = 'api'\n ),\n 'api_schema_anon_usage',\n has_schema_privilege('anon', 'api', 'USAGE'),\n 'ready', (\n EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_net')\n AND EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_cron')\n AND EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'supabase_vault')\n AND EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pgmq')\n AND NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_graphql')\n AND EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'api')\n AND has_schema_privilege('anon', 'api', 'USAGE')\n AND EXISTS (\n SELECT 1 FROM information_schema.tables\n WHERE table_schema = 'pgmq' AND table_name = 'q_agentlink_tasks'\n )\n AND EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'public' AND routine_name = '_internal_admin_get_secret'\n )\n AND EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'public' AND routine_name = '_internal_admin_call_edge_function'\n )\n AND EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'api' AND routine_name = '_admin_enqueue_task'\n )\n AND EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'api' AND routine_name = '_admin_queue_read'\n )\n AND EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'api' AND routine_name = '_admin_queue_delete'\n )\n AND EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'api' AND routine_name = '_admin_queue_archive'\n )\n AND EXISTS (\n SELECT 1 FROM information_schema.tables\n WHERE table_schema = 'public' AND table_name = 'profiles'\n )\n AND EXISTS (\n SELECT 1 FROM information_schema.tables\n WHERE table_schema = 'public' AND table_name = 'tenants'\n )\n AND EXISTS (\n SELECT 1 FROM information_schema.tables\n WHERE table_schema = 'public' AND table_name = 'memberships'\n )\n AND EXISTS (\n SELECT 1 FROM information_schema.tables\n WHERE table_schema = 'public' AND table_name = 'invitations'\n )\n AND EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'public' AND routine_name = 'set_updated_at'\n )\n AND EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'public' AND routine_name = '_internal_admin_handle_new_user'\n )\n AND EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'api' AND routine_name = 'profile_get'\n )\n AND EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'api' AND routine_name = 'profile_update'\n )\n AND EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'public' AND routine_name = '_hook_before_user_created'\n )\n AND EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'public' AND routine_name = '_hook_send_email'\n )\n AND EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'public' AND routine_name = '_auth_tenant_id'\n )\n AND EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'public' AND routine_name = '_auth_tenant_role'\n )\n AND EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'public' AND routine_name = '_auth_has_role'\n )\n AND EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'public' AND routine_name = '_auth_is_tenant_member'\n )\n AND EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'api' AND routine_name = 'tenant_select'\n )\n AND EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'api' AND routine_name = 'tenant_list'\n )\n AND EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'api' AND routine_name = 'tenant_create'\n )\n AND EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'api' AND routine_name = 'invitation_create'\n )\n AND EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'api' AND routine_name = 'invitation_accept'\n )\n AND EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'api' AND routine_name = 'membership_list'\n )\n AND EXISTS (\n SELECT 1 FROM pg_trigger WHERE tgname = 'trg_profiles_updated_at'\n )\n AND EXISTS (\n SELECT 1 FROM pg_trigger WHERE tgname = 'trg_auth_users_new_user'\n )\n AND EXISTS (\n SELECT 1 FROM pg_trigger WHERE tgname = 'trg_tenants_updated_at'\n )\n AND EXISTS (SELECT 1 FROM vault.secrets WHERE name = 'SUPABASE_URL')\n AND EXISTS (SELECT 1 FROM vault.secrets WHERE name = 'SUPABASE_PUBLISHABLE_KEY')\n AND EXISTS (SELECT 1 FROM vault.secrets WHERE name = 'SUPABASE_SECRET_KEY')\n )\n) AS setup_status;\n";
|
|
216
|
-
|
|
217
|
-
// src/sql.ts
|
|
218
|
-
function upsertSecret(value, name) {
|
|
219
|
-
const safeValue = value.replace(/'/g, "''");
|
|
220
|
-
return `DO $$ DECLARE
|
|
221
|
-
_id uuid;
|
|
222
|
-
BEGIN
|
|
223
|
-
SELECT id INTO _id FROM vault.secrets WHERE name = '${name}';
|
|
224
|
-
IF _id IS NOT NULL THEN
|
|
225
|
-
PERFORM vault.update_secret(_id, '${safeValue}', '${name}');
|
|
226
|
-
ELSE
|
|
227
|
-
PERFORM vault.create_secret('${safeValue}', '${name}');
|
|
228
|
-
END IF;
|
|
229
|
-
END $$;`;
|
|
230
|
-
}
|
|
231
|
-
function seedSQL(publishableKey, secretKey) {
|
|
232
|
-
return [
|
|
233
|
-
upsertSecret("http://host.docker.internal:54321", "SUPABASE_URL"),
|
|
234
|
-
upsertSecret(publishableKey, "SUPABASE_PUBLISHABLE_KEY"),
|
|
235
|
-
upsertSecret(secretKey, "SUPABASE_SECRET_KEY"),
|
|
236
|
-
upsertSecret("", "ALLOWED_SIGNUP_DOMAINS")
|
|
237
|
-
].join("\n");
|
|
238
|
-
}
|
|
239
|
-
function cloudSeedSQL(apiUrl, publishableKey, secretKey) {
|
|
240
|
-
return [
|
|
241
|
-
upsertSecret(apiUrl, "SUPABASE_URL"),
|
|
242
|
-
upsertSecret(publishableKey, "SUPABASE_PUBLISHABLE_KEY"),
|
|
243
|
-
upsertSecret(secretKey, "SUPABASE_SECRET_KEY"),
|
|
244
|
-
upsertSecret("", "ALLOWED_SIGNUP_DOMAINS")
|
|
245
|
-
].join("\n");
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// src/supabase.ts
|
|
249
|
-
async function parseSupabaseStatus(cwd) {
|
|
250
|
-
try {
|
|
251
|
-
const jsonOut = await runCommand(`${sb()} status -o json`, cwd);
|
|
252
|
-
const data = JSON.parse(jsonOut);
|
|
253
|
-
const status = {
|
|
254
|
-
dbUrl: data.DB_URL ?? data.db_url ?? "",
|
|
255
|
-
apiUrl: data.API_URL ?? data.api_url ?? "",
|
|
256
|
-
publishableKey: data.PUBLISHABLE_KEY ?? data.publishable_key ?? "",
|
|
257
|
-
secretKey: data.SECRET_KEY ?? data.secret_key ?? ""
|
|
258
|
-
};
|
|
259
|
-
if (status.dbUrl && status.publishableKey && status.secretKey) {
|
|
260
|
-
return status;
|
|
261
|
-
}
|
|
262
|
-
} catch {
|
|
263
|
-
}
|
|
264
|
-
const out = await runCommand(`${sb()} status`, cwd);
|
|
265
|
-
const extract = (pattern) => {
|
|
266
|
-
const match = out.match(pattern);
|
|
267
|
-
return match?.[1]?.trim() ?? "";
|
|
268
|
-
};
|
|
269
|
-
return {
|
|
270
|
-
dbUrl: extract(/DB URL\s*[:=]\s*(.+)/i),
|
|
271
|
-
apiUrl: extract(/API URL\s*[:=]\s*(.+)/i),
|
|
272
|
-
publishableKey: extract(/(?:anon|publishable)\s*key\s*[:=]\s*(.+)/i),
|
|
273
|
-
secretKey: extract(/(?:service.role|secret)\s*key\s*[:=]\s*(.+)/i)
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
function runSQL(sql, dbUrl) {
|
|
277
|
-
return new Promise((resolve, reject) => {
|
|
278
|
-
const child = spawn("psql", [dbUrl, "-t", "-A"], {
|
|
279
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
280
|
-
});
|
|
281
|
-
const stdout = [];
|
|
282
|
-
const stderr = [];
|
|
283
|
-
child.stdout.on("data", (data) => stdout.push(data.toString()));
|
|
284
|
-
child.stderr.on("data", (data) => stderr.push(data.toString()));
|
|
285
|
-
child.on("close", (code) => {
|
|
286
|
-
const out = stdout.join("").trim();
|
|
287
|
-
if (code !== 0) {
|
|
288
|
-
reject(new Error(`psql failed (exit ${code}): ${stderr.join("").trim()}`));
|
|
289
|
-
} else {
|
|
290
|
-
resolve(out);
|
|
291
|
-
}
|
|
292
|
-
});
|
|
293
|
-
child.on("error", (err) => reject(err));
|
|
294
|
-
child.stdin.write(sql);
|
|
295
|
-
child.stdin.end();
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
function writeMigrationTemplates(projectDir, templates, baseTimestamp, options) {
|
|
299
|
-
const dryRun = options?.dryRun ?? false;
|
|
300
|
-
const migrationsDir = path2.join(projectDir, "supabase", "migrations");
|
|
301
|
-
if (!dryRun) fs2.mkdirSync(migrationsDir, { recursive: true });
|
|
302
|
-
const createdVersions = [];
|
|
303
|
-
const createdFilenames = [];
|
|
304
|
-
const base = baseTimestamp ?? /* @__PURE__ */ new Date();
|
|
305
|
-
let offsetSeconds = 0;
|
|
306
|
-
const existingFiles = fs2.existsSync(migrationsDir) ? fs2.readdirSync(migrationsDir) : [];
|
|
307
|
-
for (const [name, content] of Object.entries(templates)) {
|
|
308
|
-
const existing = existingFiles.find((f) => f.endsWith(`_agentlink_${name}.sql`));
|
|
309
|
-
if (existing) continue;
|
|
310
|
-
const ts = new Date(base.getTime() + offsetSeconds * 1e3);
|
|
311
|
-
const version = ts.toISOString().replace(/\D/g, "").slice(0, 14);
|
|
312
|
-
const filename = `${version}_agentlink_${name}.sql`;
|
|
313
|
-
if (!dryRun) fs2.writeFileSync(path2.join(migrationsDir, filename), content);
|
|
314
|
-
createdVersions.push(version);
|
|
315
|
-
createdFilenames.push(filename);
|
|
316
|
-
offsetSeconds++;
|
|
317
|
-
}
|
|
318
|
-
return { createdVersions, createdFilenames };
|
|
319
|
-
}
|
|
320
|
-
async function repairMigrations(projectDir, versions, local = true, env) {
|
|
321
|
-
const localFlag = local ? " --local" : "";
|
|
322
|
-
for (const version of versions) {
|
|
323
|
-
await runCommand(
|
|
324
|
-
`${sb()} migration repair ${version} --status applied${localFlag}`,
|
|
325
|
-
projectDir,
|
|
326
|
-
void 0,
|
|
327
|
-
env
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
function updateConfigToml(projectDir, options) {
|
|
332
|
-
const dryRun = options?.dryRun ?? false;
|
|
333
|
-
const configPath = path2.join(projectDir, "supabase", "config.toml");
|
|
334
|
-
const original = fs2.readFileSync(configPath, "utf-8");
|
|
335
|
-
let content = original;
|
|
336
|
-
const patches = [];
|
|
337
|
-
content = content.replace(
|
|
338
|
-
/^(schemas\s*=\s*\[)([^\]]*)\]/m,
|
|
339
|
-
(match, prefix, inner) => {
|
|
340
|
-
const items = inner.split(",").map((s) => s.trim()).filter(Boolean);
|
|
341
|
-
if (items.some((item) => item === '"api"' || item === "'api'")) {
|
|
342
|
-
return match;
|
|
343
|
-
}
|
|
344
|
-
items.push('"api"');
|
|
345
|
-
patches.push('api.schemas: add "api"');
|
|
346
|
-
return `# TODO: After migration, only "api" should remain in schemas
|
|
347
|
-
${prefix}${items.join(", ")}]`;
|
|
348
|
-
}
|
|
349
|
-
);
|
|
350
|
-
const seedPath = '"./seeds/local-secrets.seed.sql"';
|
|
351
|
-
if (content.includes("[db.seed]")) {
|
|
352
|
-
content = content.replace(
|
|
353
|
-
/(\[db\.seed\][^\[]*sql_paths\s*=\s*\[)([^\]]*)\]/m,
|
|
354
|
-
(match, prefix, inner) => {
|
|
355
|
-
if (inner.includes("local-secrets.seed.sql")) return match;
|
|
356
|
-
const items = inner.split(",").map((s) => s.trim()).filter(Boolean);
|
|
357
|
-
items.push(seedPath);
|
|
358
|
-
patches.push("db.seed.sql_paths: add local-secrets.seed.sql");
|
|
359
|
-
return `${prefix}${items.join(", ")}]`;
|
|
360
|
-
}
|
|
361
|
-
);
|
|
362
|
-
} else {
|
|
363
|
-
content = content.trimEnd() + `
|
|
364
|
-
|
|
365
|
-
[db.seed]
|
|
366
|
-
sql_paths = [${seedPath}]
|
|
367
|
-
`;
|
|
368
|
-
patches.push("db.seed: add section with local-secrets.seed.sql");
|
|
369
|
-
}
|
|
370
|
-
if (content !== original && !dryRun) {
|
|
371
|
-
fs2.writeFileSync(configPath, content);
|
|
372
|
-
}
|
|
373
|
-
patches.push(...updateConfigTomlSendEmailHook(projectDir, { dryRun }));
|
|
374
|
-
patches.push(...updateConfigTomlBeforeUserCreatedHook(projectDir, { dryRun }));
|
|
375
|
-
patches.push(...updateConfigTomlAuthEmailConfirmations(projectDir, { dryRun }));
|
|
376
|
-
patches.push(...renameInternalEdgeFunctionSections(projectDir, { dryRun }));
|
|
377
|
-
return patches;
|
|
378
|
-
}
|
|
379
|
-
function renameInternalEdgeFunctionSections(projectDir, options) {
|
|
380
|
-
const dryRun = options?.dryRun ?? false;
|
|
381
|
-
const configPath = path2.join(projectDir, "supabase", "config.toml");
|
|
382
|
-
if (!fs2.existsSync(configPath)) return [];
|
|
383
|
-
let content = fs2.readFileSync(configPath, "utf-8");
|
|
384
|
-
const original = content;
|
|
385
|
-
const patches = [];
|
|
386
|
-
const renames = [
|
|
387
|
-
["send-email", "internal-send-auth-email"],
|
|
388
|
-
["invite-member", "internal-invite-member"],
|
|
389
|
-
["queue-worker", "internal-queue-worker"]
|
|
390
|
-
];
|
|
391
|
-
for (const [oldName, newName] of renames) {
|
|
392
|
-
if (content.includes(`[functions.${newName}]`)) continue;
|
|
393
|
-
const oldHeader = `[functions.${oldName}]`;
|
|
394
|
-
if (content.includes(oldHeader)) {
|
|
395
|
-
content = content.replace(oldHeader, `[functions.${newName}]`);
|
|
396
|
-
patches.push(`functions.${oldName} \u2192 functions.${newName}`);
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
if (content !== original && !dryRun) {
|
|
400
|
-
fs2.writeFileSync(configPath, content);
|
|
401
|
-
}
|
|
402
|
-
return patches;
|
|
403
|
-
}
|
|
404
|
-
function migrateRenamedEdgeFunctionDirs(projectDir) {
|
|
405
|
-
const fnDir = path2.join(projectDir, "supabase", "functions");
|
|
406
|
-
if (!fs2.existsSync(fnDir)) return [];
|
|
407
|
-
const renames = [
|
|
408
|
-
["send-email", "internal-send-auth-email"],
|
|
409
|
-
["invite-member", "internal-invite-member"],
|
|
410
|
-
["queue-worker", "internal-queue-worker"]
|
|
411
|
-
];
|
|
412
|
-
const migrated = [];
|
|
413
|
-
for (const [oldName, newName] of renames) {
|
|
414
|
-
const oldPath = path2.join(fnDir, oldName);
|
|
415
|
-
const newPath = path2.join(fnDir, newName);
|
|
416
|
-
const oldExists = fs2.existsSync(oldPath);
|
|
417
|
-
const newExists = fs2.existsSync(newPath);
|
|
418
|
-
if (oldExists && !newExists) {
|
|
419
|
-
fs2.renameSync(oldPath, newPath);
|
|
420
|
-
migrated.push(`${oldName} \u2192 ${newName}`);
|
|
421
|
-
} else if (oldExists && newExists) {
|
|
422
|
-
fs2.rmSync(oldPath, { recursive: true, force: true });
|
|
423
|
-
migrated.push(`${oldName} (removed orphan; ${newName} already present)`);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
return migrated;
|
|
427
|
-
}
|
|
428
|
-
function writeConfigToml(projectDir, projectId) {
|
|
429
|
-
const configPath = path2.join(projectDir, "supabase", "config.toml");
|
|
430
|
-
const content = `project_id = "${projectId}"
|
|
431
|
-
|
|
432
|
-
[api]
|
|
433
|
-
enabled = true
|
|
434
|
-
port = 54321
|
|
435
|
-
schemas = ["api"]
|
|
436
|
-
extra_search_path = ["public", "extensions"]
|
|
437
|
-
max_rows = 1000
|
|
438
|
-
|
|
439
|
-
[db]
|
|
440
|
-
port = 54322
|
|
441
|
-
shadow_port = 54320
|
|
442
|
-
major_version = 17
|
|
443
|
-
|
|
444
|
-
[db.migrations]
|
|
445
|
-
schema_paths = ["./schemas/_schemas.sql", "./schemas/_extensions.sql", "./schemas/**/*.sql"]
|
|
446
|
-
|
|
447
|
-
[db.seed]
|
|
448
|
-
sql_paths = ["./seed.sql", "./seeds/local-secrets.seed.sql"]
|
|
449
|
-
|
|
450
|
-
[auth]
|
|
451
|
-
enabled = true
|
|
452
|
-
site_url = "http://127.0.0.1:3000"
|
|
453
|
-
additional_redirect_urls = ["https://127.0.0.1:3000"]
|
|
454
|
-
jwt_expiry = 3600
|
|
455
|
-
enable_refresh_token_rotation = true
|
|
456
|
-
refresh_token_reuse_interval = 10
|
|
457
|
-
enable_signup = true
|
|
458
|
-
enable_anonymous_sign_ins = false
|
|
459
|
-
minimum_password_length = 6
|
|
460
|
-
|
|
461
|
-
[auth.email]
|
|
462
|
-
enable_signup = true
|
|
463
|
-
double_confirm_changes = true
|
|
464
|
-
# Disabled on scaffold so the first local sign-up lands in the app without
|
|
465
|
-
# needing SMTP. Set to true before shipping to production.
|
|
466
|
-
enable_confirmations = false
|
|
467
|
-
|
|
468
|
-
[edge_runtime]
|
|
469
|
-
enabled = true
|
|
470
|
-
policy = "per_worker"
|
|
471
|
-
inspector_port = 8083
|
|
472
|
-
deno_version = 2
|
|
473
|
-
|
|
474
|
-
[auth.hook.send_email]
|
|
475
|
-
enabled = true
|
|
476
|
-
uri = "pg-functions://postgres/public/_hook_send_email"
|
|
477
|
-
|
|
478
|
-
[auth.hook.before_user_created]
|
|
479
|
-
enabled = true
|
|
480
|
-
uri = "pg-functions://postgres/public/_hook_before_user_created"
|
|
481
|
-
|
|
482
|
-
[functions.internal-queue-worker]
|
|
483
|
-
enabled = true
|
|
484
|
-
verify_jwt = false
|
|
485
|
-
|
|
486
|
-
[functions.internal-send-auth-email]
|
|
487
|
-
enabled = true
|
|
488
|
-
verify_jwt = false
|
|
489
|
-
|
|
490
|
-
[functions.internal-invite-member]
|
|
491
|
-
enabled = true
|
|
492
|
-
verify_jwt = false
|
|
493
|
-
`;
|
|
494
|
-
fs2.writeFileSync(configPath, content);
|
|
495
|
-
}
|
|
496
|
-
function updateConfigTomlSendEmailHook(projectDir, options) {
|
|
497
|
-
const dryRun = options?.dryRun ?? false;
|
|
498
|
-
const configPath = path2.join(projectDir, "supabase", "config.toml");
|
|
499
|
-
let content = fs2.readFileSync(configPath, "utf-8");
|
|
500
|
-
if (!content.includes("[auth.hook.send_email]")) {
|
|
501
|
-
content = content.trimEnd() + `
|
|
502
|
-
|
|
503
|
-
[auth.hook.send_email]
|
|
504
|
-
enabled = true
|
|
505
|
-
uri = "pg-functions://postgres/public/_hook_send_email"
|
|
506
|
-
|
|
507
|
-
[functions.internal-send-auth-email]
|
|
508
|
-
enabled = true
|
|
509
|
-
verify_jwt = false
|
|
510
|
-
`;
|
|
511
|
-
if (!dryRun) fs2.writeFileSync(configPath, content);
|
|
512
|
-
return ["auth.hook.send_email: add section", "functions.internal-send-auth-email: add section"];
|
|
513
|
-
}
|
|
514
|
-
return [];
|
|
515
|
-
}
|
|
516
|
-
function updateConfigTomlBeforeUserCreatedHook(projectDir, options) {
|
|
517
|
-
const dryRun = options?.dryRun ?? false;
|
|
518
|
-
const configPath = path2.join(projectDir, "supabase", "config.toml");
|
|
519
|
-
let content = fs2.readFileSync(configPath, "utf-8");
|
|
520
|
-
if (!content.includes("[auth.hook.before_user_created]")) {
|
|
521
|
-
content = content.trimEnd() + `
|
|
522
|
-
|
|
523
|
-
[auth.hook.before_user_created]
|
|
524
|
-
enabled = true
|
|
525
|
-
uri = "pg-functions://postgres/public/_hook_before_user_created"
|
|
526
|
-
`;
|
|
527
|
-
if (!dryRun) fs2.writeFileSync(configPath, content);
|
|
528
|
-
return ["auth.hook.before_user_created: add section"];
|
|
529
|
-
}
|
|
530
|
-
return [];
|
|
531
|
-
}
|
|
532
|
-
function updateConfigTomlAuthEmailConfirmations(projectDir, options) {
|
|
533
|
-
const dryRun = options?.dryRun ?? false;
|
|
534
|
-
const configPath = path2.join(projectDir, "supabase", "config.toml");
|
|
535
|
-
const original = fs2.readFileSync(configPath, "utf-8");
|
|
536
|
-
let content = original;
|
|
537
|
-
if (!content.includes("[auth.email]")) {
|
|
538
|
-
content = content.trimEnd() + `
|
|
539
|
-
|
|
540
|
-
[auth.email]
|
|
541
|
-
enable_signup = true
|
|
542
|
-
double_confirm_changes = true
|
|
543
|
-
# Disabled on scaffold so the first local sign-up lands in the app without
|
|
544
|
-
# needing SMTP. Set to true before shipping to production.
|
|
545
|
-
enable_confirmations = false
|
|
546
|
-
`;
|
|
547
|
-
if (!dryRun) fs2.writeFileSync(configPath, content);
|
|
548
|
-
return ["auth.email: add section with enable_confirmations=false"];
|
|
549
|
-
}
|
|
550
|
-
if (/^\s*enable_confirmations\s*=/m.test(content)) {
|
|
551
|
-
return [];
|
|
552
|
-
}
|
|
553
|
-
content = content.replace(
|
|
554
|
-
/^\[auth\.email\][^\n]*\n/m,
|
|
555
|
-
(match) => `${match}# Disabled on scaffold so the first local sign-up lands in the app without
|
|
556
|
-
# needing SMTP. Set to true before shipping to production.
|
|
557
|
-
enable_confirmations = false
|
|
558
|
-
`
|
|
559
|
-
);
|
|
560
|
-
if (!dryRun) fs2.writeFileSync(configPath, content);
|
|
561
|
-
return ["auth.email.enable_confirmations: add with dev default (false)"];
|
|
562
|
-
}
|
|
563
|
-
function writeGitkeepFiles(projectDir) {
|
|
564
|
-
const schemasDir = path2.join(projectDir, "supabase", "schemas");
|
|
565
|
-
for (const sub of ["public", "api"]) {
|
|
566
|
-
const gitkeep = path2.join(schemasDir, sub, ".gitkeep");
|
|
567
|
-
fs2.mkdirSync(path2.dirname(gitkeep), { recursive: true });
|
|
568
|
-
if (!fs2.existsSync(gitkeep)) {
|
|
569
|
-
fs2.writeFileSync(gitkeep, "");
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
function writeDotEnv(projectDir, frontend, isCloud) {
|
|
574
|
-
const lines = [];
|
|
575
|
-
if (frontend === "nextjs") {
|
|
576
|
-
lines.push("NEXT_PUBLIC_SUPABASE_URL=", "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=");
|
|
577
|
-
} else if (frontend === "vite") {
|
|
578
|
-
lines.push("VITE_SUPABASE_URL=", "VITE_SUPABASE_PUBLISHABLE_KEY=");
|
|
579
|
-
}
|
|
580
|
-
lines.push("SUPABASE_DB_URL=");
|
|
581
|
-
lines.push("RESEND_API_KEY=re_your_api_key_here");
|
|
582
|
-
lines.push("RESEND_FROM_EMAIL=Your App <noreply@yourdomain.com>");
|
|
583
|
-
if (!isCloud) {
|
|
584
|
-
lines.push("RESEND_BASE_URL=http://host.docker.internal:4657");
|
|
585
|
-
}
|
|
586
|
-
lines.push("APP_NAME=YourApp");
|
|
587
|
-
lines.push("");
|
|
588
|
-
fs2.writeFileSync(path2.join(projectDir, ".env.local"), lines.join("\n"));
|
|
589
|
-
}
|
|
590
|
-
function updateDotEnvKeys(projectDir, apiUrl, publishableKey, secretKey) {
|
|
591
|
-
const envPath = path2.join(projectDir, ".env.local");
|
|
592
|
-
let content = fs2.readFileSync(envPath, "utf-8");
|
|
593
|
-
content = content.replace(/^NEXT_PUBLIC_SUPABASE_URL=.*$/m, `NEXT_PUBLIC_SUPABASE_URL=${apiUrl}`).replace(/^NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=.*$/m, `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=${publishableKey}`).replace(/^VITE_SUPABASE_URL=.*$/m, `VITE_SUPABASE_URL=${apiUrl}`).replace(/^VITE_SUPABASE_PUBLISHABLE_KEY=.*$/m, `VITE_SUPABASE_PUBLISHABLE_KEY=${publishableKey}`);
|
|
594
|
-
fs2.writeFileSync(envPath, content);
|
|
595
|
-
}
|
|
596
|
-
function writeEnvExamples(projectDir, frontend) {
|
|
597
|
-
const lines = ["# Supabase"];
|
|
598
|
-
if (frontend === "nextjs") {
|
|
599
|
-
lines.push(
|
|
600
|
-
"NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321",
|
|
601
|
-
"NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=your-publishable-key"
|
|
602
|
-
);
|
|
603
|
-
} else if (frontend === "vite") {
|
|
604
|
-
lines.push(
|
|
605
|
-
"VITE_SUPABASE_URL=http://127.0.0.1:54321",
|
|
606
|
-
"VITE_SUPABASE_PUBLISHABLE_KEY=your-publishable-key"
|
|
607
|
-
);
|
|
608
|
-
}
|
|
609
|
-
lines.push(
|
|
610
|
-
"",
|
|
611
|
-
"# Resend (https://resend.com)",
|
|
612
|
-
"RESEND_API_KEY=re_your_api_key_here",
|
|
613
|
-
"RESEND_FROM_EMAIL=Your App <noreply@yourdomain.com>",
|
|
614
|
-
"",
|
|
615
|
-
"# Resend Base URL (points to local resend-box sandbox in dev; remove for production)",
|
|
616
|
-
"RESEND_BASE_URL=http://host.docker.internal:4657",
|
|
617
|
-
"",
|
|
618
|
-
"APP_NAME=YourApp",
|
|
619
|
-
""
|
|
620
|
-
);
|
|
621
|
-
fs2.writeFileSync(path2.join(projectDir, ".env.example"), lines.join("\n"));
|
|
622
|
-
}
|
|
623
|
-
function writeLocalSecretsSeed(projectDir, publishableKey, secretKey) {
|
|
624
|
-
const seedsDir = path2.join(projectDir, "supabase", "seeds");
|
|
625
|
-
fs2.mkdirSync(seedsDir, { recursive: true });
|
|
626
|
-
const seedPath = path2.join(seedsDir, "local-secrets.seed.sql");
|
|
627
|
-
const content = [
|
|
628
|
-
"-- AgentLink: Vault secrets for local development",
|
|
629
|
-
"-- This file is managed by AgentLink CLI. Do not edit manually.",
|
|
630
|
-
"",
|
|
631
|
-
seedSQL(publishableKey, secretKey),
|
|
632
|
-
""
|
|
633
|
-
].join("\n");
|
|
634
|
-
fs2.writeFileSync(seedPath, content);
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
// src/ui.ts
|
|
638
|
-
import ora from "ora";
|
|
639
|
-
async function step(title, fn) {
|
|
640
|
-
const spinner = ora({ text: title, color: "yellow" }).start();
|
|
641
|
-
try {
|
|
642
|
-
const result = await fn(spinner);
|
|
643
|
-
spinner.suffixText = "";
|
|
644
|
-
spinner.stopAndPersist({ symbol: blue("\u2714"), text: title });
|
|
645
|
-
return result;
|
|
646
|
-
} catch (err) {
|
|
647
|
-
spinner.suffixText = "";
|
|
648
|
-
spinner.stopAndPersist({ symbol: red("\u2716"), text: title });
|
|
649
|
-
throw err;
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
function skipped(title, reason) {
|
|
653
|
-
console.log(`${dim("\u25CB")} ${dim(title)} ${dim(`[${reason}]`)}`);
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
// src/db.ts
|
|
657
|
-
function stripQuotes(val) {
|
|
658
|
-
if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
|
|
659
|
-
return val.slice(1, -1);
|
|
660
|
-
}
|
|
661
|
-
return val;
|
|
662
|
-
}
|
|
663
|
-
function readEnvValue(projectDir, key) {
|
|
664
|
-
const envPath = path3.join(projectDir, ".env.local");
|
|
665
|
-
if (!fs3.existsSync(envPath)) return void 0;
|
|
666
|
-
const content = fs3.readFileSync(envPath, "utf-8");
|
|
667
|
-
const match = content.match(new RegExp(`^${key}=(.+)$`, "m"));
|
|
668
|
-
if (!match?.[1]) return void 0;
|
|
669
|
-
return stripQuotes(match[1].trim()) || void 0;
|
|
670
|
-
}
|
|
671
|
-
async function resolveDbMode(cwd, flagUrl, envFlag) {
|
|
672
|
-
if (flagUrl) return { kind: "local", dbUrl: flagUrl };
|
|
673
|
-
const manifest = readManifest(cwd);
|
|
674
|
-
if (manifest?.mode === "cloud" && manifest.cloud?.environments) {
|
|
675
|
-
const env = resolveCloudEnv(manifest.cloud, envFlag);
|
|
676
|
-
await ensureAccessToken({ orgId: env.orgId, nonInteractive: true, projectDir: cwd });
|
|
677
|
-
return { kind: "cloud", projectRef: env.projectRef };
|
|
678
|
-
}
|
|
679
|
-
return { kind: "local" };
|
|
680
|
-
}
|
|
681
|
-
async function resolveDbUrl(cwd, flagUrl) {
|
|
682
|
-
if (flagUrl) return flagUrl;
|
|
683
|
-
const envUrl = readEnvValue(cwd, "SUPABASE_DB_URL");
|
|
684
|
-
if (envUrl) return envUrl;
|
|
685
|
-
try {
|
|
686
|
-
const status = await parseSupabaseStatus(cwd);
|
|
687
|
-
if (status.dbUrl) return status.dbUrl;
|
|
688
|
-
} catch {
|
|
689
|
-
}
|
|
690
|
-
throw new Error(
|
|
691
|
-
"Could not detect DB URL. Ensure .env.local has SUPABASE_DB_URL, or pass --db-url, or run supabase start."
|
|
692
|
-
);
|
|
693
|
-
}
|
|
694
|
-
function resolveTypesOutputPath(cwd, outputFlag) {
|
|
695
|
-
if (outputFlag) return path3.resolve(cwd, outputFlag);
|
|
696
|
-
const m = readManifest(cwd);
|
|
697
|
-
const frontend = m?.frontend;
|
|
698
|
-
if (frontend === "nextjs") {
|
|
699
|
-
return path3.join(cwd, "types", "database.ts");
|
|
700
|
-
}
|
|
701
|
-
return path3.join(cwd, "src", "types", "database.ts");
|
|
702
|
-
}
|
|
703
|
-
function formatTable(rows) {
|
|
704
|
-
if (rows.length === 0) return "(0 rows)";
|
|
705
|
-
const columns = Object.keys(rows[0]);
|
|
706
|
-
const values = rows.map(
|
|
707
|
-
(row) => columns.map((col) => {
|
|
708
|
-
const v = row[col];
|
|
709
|
-
if (v === null || v === void 0) return "";
|
|
710
|
-
if (typeof v === "object") return JSON.stringify(v);
|
|
711
|
-
return String(v);
|
|
712
|
-
})
|
|
713
|
-
);
|
|
714
|
-
const widths = columns.map(
|
|
715
|
-
(col, i) => Math.max(col.length, ...values.map((row) => row[i].length))
|
|
716
|
-
);
|
|
717
|
-
const header = columns.map((col, i) => col.padEnd(widths[i])).join(" | ");
|
|
718
|
-
const separator = widths.map((w) => "-".repeat(w)).join("-+-");
|
|
719
|
-
const body = values.map((row) => row.map((val, i) => val.padEnd(widths[i])).join(" | ")).join("\n");
|
|
720
|
-
return `${header}
|
|
721
|
-
${separator}
|
|
722
|
-
${body}
|
|
723
|
-
(${rows.length} ${rows.length === 1 ? "row" : "rows"})`;
|
|
724
|
-
}
|
|
725
|
-
async function dbSql(query, options) {
|
|
726
|
-
const mode = await resolveDbMode(options.cwd, options.dbUrl, options.env);
|
|
727
|
-
if (mode.kind === "cloud") {
|
|
728
|
-
const res = await authenticatedFetch(
|
|
729
|
-
`https://api.supabase.com/v1/projects/${mode.projectRef}/database/query`,
|
|
730
|
-
{
|
|
731
|
-
method: "POST",
|
|
732
|
-
headers: { "Content-Type": "application/json" },
|
|
733
|
-
body: JSON.stringify({ query })
|
|
734
|
-
}
|
|
735
|
-
);
|
|
736
|
-
if (!res.ok) {
|
|
737
|
-
const body = await res.text();
|
|
738
|
-
throw new Error(`Cloud SQL query failed (${res.status}): ${body}`);
|
|
739
|
-
}
|
|
740
|
-
const rows = await res.json();
|
|
741
|
-
if (options.json) {
|
|
742
|
-
console.log(JSON.stringify(rows, null, 2));
|
|
743
|
-
} else {
|
|
744
|
-
console.log(formatTable(rows));
|
|
745
|
-
}
|
|
746
|
-
} else {
|
|
747
|
-
const dbUrl = await resolveDbUrl(options.cwd, options.dbUrl);
|
|
748
|
-
const output = await runCommand(
|
|
749
|
-
`psql ${JSON.stringify(dbUrl)} -c ${JSON.stringify(query)}`,
|
|
750
|
-
options.cwd,
|
|
751
|
-
(line) => console.log(line)
|
|
752
|
-
);
|
|
753
|
-
if (output && !output.includes("\n")) console.log(output);
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
async function dbTypes(options) {
|
|
757
|
-
const mode = await resolveDbMode(options.cwd, options.dbUrl, options.env);
|
|
758
|
-
const outputPath = resolveTypesOutputPath(options.cwd, options.output);
|
|
759
|
-
let typesContent;
|
|
760
|
-
if (mode.kind === "cloud") {
|
|
761
|
-
const res = await authenticatedFetch(
|
|
762
|
-
`https://api.supabase.com/v1/projects/${mode.projectRef}/types/typescript?included_schemas=api`
|
|
763
|
-
);
|
|
764
|
-
if (!res.ok) {
|
|
765
|
-
const body = await res.text();
|
|
766
|
-
throw new Error(`Failed to generate types (${res.status}): ${body}`);
|
|
767
|
-
}
|
|
768
|
-
const data = await res.json();
|
|
769
|
-
typesContent = data.types;
|
|
770
|
-
} else {
|
|
771
|
-
typesContent = await runCommand(
|
|
772
|
-
`${sb()} gen types typescript --local --schema api`,
|
|
773
|
-
options.cwd
|
|
774
|
-
);
|
|
775
|
-
}
|
|
776
|
-
fs3.mkdirSync(path3.dirname(outputPath), { recursive: true });
|
|
777
|
-
fs3.writeFileSync(outputPath, typesContent);
|
|
778
|
-
if (!options.quiet) {
|
|
779
|
-
const relPath = path3.relative(options.cwd, outputPath);
|
|
780
|
-
console.log(`Types written to ${relPath}`);
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
async function dbApply(options) {
|
|
784
|
-
const schemasDir = path3.join(options.cwd, "supabase", "schemas");
|
|
785
|
-
if (!fs3.existsSync(schemasDir)) {
|
|
786
|
-
console.log(` ${dim("Skipping schema apply \u2014 supabase/schemas/ not found.")}`);
|
|
787
|
-
return;
|
|
788
|
-
}
|
|
789
|
-
const dbUrl = await resolveDbUrl(options.cwd, options.dbUrl);
|
|
790
|
-
if (!options.quiet) console.log("Applying schemas...");
|
|
791
|
-
await runCommand(
|
|
792
|
-
`${pgd()} declarative apply --path ./supabase/schemas/ --target ${JSON.stringify(dbUrl)}`,
|
|
793
|
-
options.cwd,
|
|
794
|
-
options.quiet ? void 0 : (line) => console.log(line)
|
|
795
|
-
);
|
|
796
|
-
if (!options.skipTypes) {
|
|
797
|
-
if (!options.quiet) console.log("\nGenerating types...");
|
|
798
|
-
try {
|
|
799
|
-
await dbTypes({ cwd: options.cwd, dbUrl: options.dbUrl, env: options.env, quiet: options.quiet });
|
|
800
|
-
} catch (err) {
|
|
801
|
-
console.warn(`Warning: type generation failed \u2014 ${err.message}`);
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
if (!options.quiet) {
|
|
805
|
-
console.log("\nDone. Run 'agentlink check' to verify.");
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
async function dbMigrate(name, options) {
|
|
809
|
-
const dbUrl = await resolveDbUrl(options.cwd, options.dbUrl);
|
|
810
|
-
const tmpDir = path3.join(os.tmpdir(), `agentlink-migrate-${Date.now()}`);
|
|
811
|
-
fs3.mkdirSync(tmpDir, { recursive: true });
|
|
812
|
-
const baselineCatalog = path3.join(tmpDir, "baseline.json");
|
|
813
|
-
const desiredCatalog = path3.join(tmpDir, "desired.json");
|
|
814
|
-
try {
|
|
815
|
-
console.log("=== Step 1/4: Exporting baseline catalog...");
|
|
816
|
-
await runCommand(
|
|
817
|
-
`${pgd()} catalog-export --target ${JSON.stringify(dbUrl)} --output ${JSON.stringify(baselineCatalog)}`,
|
|
818
|
-
options.cwd
|
|
819
|
-
);
|
|
820
|
-
console.log("=== Step 2/4: Applying schemas to get desired state...");
|
|
821
|
-
await runCommand(
|
|
822
|
-
`${pgd()} declarative apply --path ./supabase/schemas/ --target ${JSON.stringify(dbUrl)}`,
|
|
823
|
-
options.cwd
|
|
824
|
-
);
|
|
825
|
-
console.log("=== Step 3/4: Exporting desired state catalog...");
|
|
826
|
-
await runCommand(
|
|
827
|
-
`${pgd()} catalog-export --target ${JSON.stringify(dbUrl)} --output ${JSON.stringify(desiredCatalog)}`,
|
|
828
|
-
options.cwd
|
|
829
|
-
);
|
|
830
|
-
console.log("=== Step 4/4: Generating migration diff...");
|
|
831
|
-
const diff = await runCommand(
|
|
832
|
-
`${pgd()} plan --source ${JSON.stringify(baselineCatalog)} --target ${JSON.stringify(desiredCatalog)} --format sql --integration supabase --sql-format`,
|
|
833
|
-
options.cwd
|
|
834
|
-
).catch(() => "");
|
|
835
|
-
if (!diff.trim()) {
|
|
836
|
-
console.log("No changes detected. Nothing to migrate.");
|
|
837
|
-
return;
|
|
838
|
-
}
|
|
839
|
-
const migrationsDir = path3.join(options.cwd, "supabase", "migrations");
|
|
840
|
-
fs3.mkdirSync(migrationsDir, { recursive: true });
|
|
841
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\D/g, "").slice(0, 14);
|
|
842
|
-
const filename = `${timestamp}_${name}.sql`;
|
|
843
|
-
const migrationPath = path3.join(migrationsDir, filename);
|
|
844
|
-
fs3.writeFileSync(migrationPath, diff);
|
|
845
|
-
const lineCount = diff.split("\n").length;
|
|
846
|
-
console.log(`
|
|
847
|
-
Migration created: supabase/migrations/${filename}`);
|
|
848
|
-
console.log(`Lines: ${lineCount}`);
|
|
849
|
-
console.log("\nNOTE: pg-delta filters out cron + storage schemas.");
|
|
850
|
-
console.log("If your change includes cron.schedule() or storage policies,");
|
|
851
|
-
console.log("append them manually to the migration file.");
|
|
852
|
-
} finally {
|
|
853
|
-
fs3.rmSync(tmpDir, { recursive: true, force: true });
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
async function dbRebuild(options) {
|
|
857
|
-
const cwd = options.cwd;
|
|
858
|
-
const migrationsDir = path3.join(cwd, "supabase", "migrations");
|
|
859
|
-
if (fs3.existsSync(migrationsDir)) {
|
|
860
|
-
const files = fs3.readdirSync(migrationsDir).filter((f) => f.endsWith(".sql"));
|
|
861
|
-
for (const file of files) {
|
|
862
|
-
fs3.unlinkSync(path3.join(migrationsDir, file));
|
|
863
|
-
}
|
|
864
|
-
console.log(`Deleted ${files.length} migration file(s)`);
|
|
865
|
-
}
|
|
866
|
-
const progressPath = path3.join(cwd, ".agentlink-progress.json");
|
|
867
|
-
if (fs3.existsSync(progressPath)) {
|
|
868
|
-
fs3.unlinkSync(progressPath);
|
|
869
|
-
console.log("Cleared scaffold progress");
|
|
870
|
-
}
|
|
871
|
-
console.log("\nRe-applying schemas...");
|
|
872
|
-
await dbApply({ cwd, dbUrl: options.dbUrl, env: options.env, skipTypes: true, quiet: true });
|
|
873
|
-
console.log("\nRegenerating migration...");
|
|
874
|
-
await dbMigrate("agentlink_setup", { cwd, dbUrl: options.dbUrl, env: options.env });
|
|
875
|
-
console.log("\nGenerating types...");
|
|
876
|
-
try {
|
|
877
|
-
await dbTypes({ cwd, dbUrl: options.dbUrl, env: options.env });
|
|
878
|
-
} catch (err) {
|
|
879
|
-
console.warn(`Warning: type generation failed \u2014 ${err.message}`);
|
|
880
|
-
}
|
|
881
|
-
console.log("\nDone. Database rebuilt from schema files.");
|
|
882
|
-
}
|
|
883
|
-
async function dbUrlCheck(options) {
|
|
884
|
-
const mode = await resolveDbMode(options.cwd, options.dbUrl, options.env);
|
|
885
|
-
if (mode.kind !== "cloud" || !mode.projectRef) {
|
|
886
|
-
const dbUrl = await resolveDbUrl(options.cwd, options.dbUrl);
|
|
887
|
-
console.log(dbUrl);
|
|
888
|
-
return;
|
|
889
|
-
}
|
|
890
|
-
await ensureAccessToken({ nonInteractive: true, projectDir: options.cwd });
|
|
891
|
-
const password = loadProjectCredential(mode.projectRef);
|
|
892
|
-
if (!password) {
|
|
893
|
-
throw new Error(
|
|
894
|
-
`No stored password for project ${mode.projectRef}.
|
|
895
|
-
Run: agentlink env add dev`
|
|
896
|
-
);
|
|
897
|
-
}
|
|
898
|
-
const poolerUrl = await resolvePoolerDbUrl(mode.projectRef, password);
|
|
899
|
-
console.log(`Pooler URL: ${poolerUrl}`);
|
|
900
|
-
const currentUrl = readEnvValue(options.cwd, "SUPABASE_DB_URL");
|
|
901
|
-
if (currentUrl === poolerUrl) {
|
|
902
|
-
console.log("\u2714 .env.local is up to date");
|
|
903
|
-
return;
|
|
904
|
-
}
|
|
905
|
-
if (currentUrl) {
|
|
906
|
-
console.log(`
|
|
907
|
-
Current: ${currentUrl}`);
|
|
908
|
-
console.log(`Expected: ${poolerUrl}`);
|
|
909
|
-
}
|
|
910
|
-
if (options.fix) {
|
|
911
|
-
const envPath = path3.join(options.cwd, ".env.local");
|
|
912
|
-
if (fs3.existsSync(envPath)) {
|
|
913
|
-
let content = fs3.readFileSync(envPath, "utf-8");
|
|
914
|
-
if (/^SUPABASE_DB_URL=/m.test(content)) {
|
|
915
|
-
content = content.replace(/^SUPABASE_DB_URL=.*$/m, `SUPABASE_DB_URL=${poolerUrl}`);
|
|
916
|
-
} else {
|
|
917
|
-
content = content.trimEnd() + `
|
|
918
|
-
SUPABASE_DB_URL=${poolerUrl}
|
|
919
|
-
`;
|
|
920
|
-
}
|
|
921
|
-
fs3.writeFileSync(envPath, content);
|
|
922
|
-
console.log("\n\u2714 .env.local updated with correct pooler URL");
|
|
923
|
-
}
|
|
924
|
-
} else if (currentUrl !== poolerUrl) {
|
|
925
|
-
console.log("\nRun with --fix to update .env.local");
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
var passwordTheme = {
|
|
929
|
-
prefix: { idle: blue("?"), done: blue("\u2714") },
|
|
930
|
-
style: {
|
|
931
|
-
answer: (text) => amber(text),
|
|
932
|
-
message: (text) => bold(text),
|
|
933
|
-
help: (text) => dim(text)
|
|
934
|
-
}
|
|
935
|
-
};
|
|
936
|
-
async function dbPassword(cwd, value, opts = {}) {
|
|
937
|
-
const manifest = readManifest(cwd);
|
|
938
|
-
if (!manifest) {
|
|
939
|
-
throw new Error("No agentlink.json found. Run `agentlink` to scaffold a project first.");
|
|
940
|
-
}
|
|
941
|
-
if (!manifest.cloud?.environments) {
|
|
942
|
-
throw new Error("No cloud environment configured. This command is for cloud projects.");
|
|
943
|
-
}
|
|
944
|
-
const env = resolveCloudEnv(manifest.cloud, opts.env);
|
|
945
|
-
const projectRef = env.projectRef;
|
|
946
|
-
if (value) {
|
|
947
|
-
saveProjectCredential(projectRef, value);
|
|
948
|
-
console.log(`Password saved for project ${dim(projectRef)}`);
|
|
949
|
-
} else {
|
|
950
|
-
const current = loadProjectCredential(projectRef);
|
|
951
|
-
if (current) {
|
|
952
|
-
const masked = current.length > 8 ? current.slice(0, 4) + "\u25CF".repeat(current.length - 8) + current.slice(-4) : "\u25CF".repeat(current.length);
|
|
953
|
-
console.log(`Current password: ${dim(masked)} (project: ${projectRef})`);
|
|
954
|
-
console.log();
|
|
955
|
-
}
|
|
956
|
-
const resetUrl = `https://supabase.com/dashboard/project/${projectRef}/settings/database`;
|
|
957
|
-
console.log(` ${dim("Reset your password at:")}`);
|
|
958
|
-
console.log(` ${dim(resetUrl)}`);
|
|
959
|
-
console.log();
|
|
960
|
-
const newPassword = await input({
|
|
961
|
-
message: "Database password:",
|
|
962
|
-
theme: passwordTheme,
|
|
963
|
-
transformer: (v, { isFinal }) => {
|
|
964
|
-
if (isFinal) return "\u25CF".repeat(Math.min(v.length, 8));
|
|
965
|
-
return "\u25CF".repeat(v.length);
|
|
966
|
-
},
|
|
967
|
-
validate: (val) => val.trim().length > 0 || "Password is required"
|
|
968
|
-
});
|
|
969
|
-
saveProjectCredential(projectRef, newPassword.trim());
|
|
970
|
-
console.log(`Password saved for project ${dim(projectRef)}`);
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
async function dbBackup(options) {
|
|
974
|
-
const dbUrl = await resolveDbUrl(options.cwd, options.dbUrl);
|
|
975
|
-
const manifest = readManifest(options.cwd);
|
|
976
|
-
const envLabel = options.env ?? manifest?.cloud?.default ?? "local";
|
|
977
|
-
const now = /* @__PURE__ */ new Date();
|
|
978
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
979
|
-
const timestamp = `${now.getUTCFullYear()}-${pad(now.getUTCMonth() + 1)}-${pad(now.getUTCDate())}T${pad(now.getUTCHours())}-${pad(now.getUTCMinutes())}-${pad(now.getUTCSeconds())}`;
|
|
980
|
-
const outDir = path3.join(options.cwd, "supabase", "backups", envLabel, timestamp);
|
|
981
|
-
fs3.mkdirSync(outDir, { recursive: true });
|
|
982
|
-
ensureGitignorePattern(options.cwd, "supabase/backups/");
|
|
983
|
-
const rolesFile = path3.join(outDir, "roles.sql");
|
|
984
|
-
const schemaFile = path3.join(outDir, "schema.sql");
|
|
985
|
-
const dataFile = path3.join(outDir, "data.sql");
|
|
986
|
-
const urlArg = JSON.stringify(dbUrl);
|
|
987
|
-
console.log();
|
|
988
|
-
console.log(` ${blue("\u25CF")} Backing up ${bold(envLabel)} \u2192 ${dim(path3.relative(options.cwd, outDir))}`);
|
|
989
|
-
console.log();
|
|
990
|
-
await step("Dumping roles", async () => {
|
|
991
|
-
await runCommand(
|
|
992
|
-
`${sb()} db dump --db-url ${urlArg} -f ${JSON.stringify(rolesFile)} --role-only`,
|
|
993
|
-
options.cwd
|
|
994
|
-
);
|
|
995
|
-
});
|
|
996
|
-
await step("Dumping schema", async () => {
|
|
997
|
-
await runCommand(
|
|
998
|
-
`${sb()} db dump --db-url ${urlArg} -f ${JSON.stringify(schemaFile)}`,
|
|
999
|
-
options.cwd
|
|
1000
|
-
);
|
|
1001
|
-
});
|
|
1002
|
-
await step("Dumping data", async () => {
|
|
1003
|
-
await runCommand(
|
|
1004
|
-
`${sb()} db dump --db-url ${urlArg} -f ${JSON.stringify(dataFile)} --use-copy --data-only -x "storage.buckets_vectors" -x "storage.vector_indexes"`,
|
|
1005
|
-
options.cwd
|
|
1006
|
-
);
|
|
1007
|
-
});
|
|
1008
|
-
console.log();
|
|
1009
|
-
console.log(` ${blue("Done.")} Snapshot written:`);
|
|
1010
|
-
for (const f of [rolesFile, schemaFile, dataFile]) {
|
|
1011
|
-
const sz = fs3.statSync(f).size;
|
|
1012
|
-
console.log(` ${dim(path3.relative(options.cwd, f))} ${dim(formatBytes(sz))}`);
|
|
1013
|
-
}
|
|
1014
|
-
console.log();
|
|
1015
|
-
}
|
|
1016
|
-
function formatBytes(n) {
|
|
1017
|
-
if (n < 1024) return `${n} B`;
|
|
1018
|
-
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
1019
|
-
return `${(n / 1024 / 1024).toFixed(1)} MB`;
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
export {
|
|
1023
|
-
ALLOWED_CLOUD_ENV_NAMES,
|
|
1024
|
-
assertAllowedEnvNameForAdd,
|
|
1025
|
-
assertAllowedEnvNameForUse,
|
|
1026
|
-
assertManifestEnvNames,
|
|
1027
|
-
isBareManifest,
|
|
1028
|
-
readManifest,
|
|
1029
|
-
writeManifest,
|
|
1030
|
-
resolveCloudEnv,
|
|
1031
|
-
addCloudEnvironment,
|
|
1032
|
-
removeCloudEnvironment,
|
|
1033
|
-
setCloudEnvOrgId,
|
|
1034
|
-
setDefaultEnvironment,
|
|
1035
|
-
listEnvironments,
|
|
1036
|
-
check_setup_default,
|
|
1037
|
-
seedSQL,
|
|
1038
|
-
cloudSeedSQL,
|
|
1039
|
-
parseSupabaseStatus,
|
|
1040
|
-
runSQL,
|
|
1041
|
-
writeMigrationTemplates,
|
|
1042
|
-
repairMigrations,
|
|
1043
|
-
updateConfigToml,
|
|
1044
|
-
migrateRenamedEdgeFunctionDirs,
|
|
1045
|
-
writeConfigToml,
|
|
1046
|
-
writeGitkeepFiles,
|
|
1047
|
-
writeDotEnv,
|
|
1048
|
-
updateDotEnvKeys,
|
|
1049
|
-
writeEnvExamples,
|
|
1050
|
-
writeLocalSecretsSeed,
|
|
1051
|
-
step,
|
|
1052
|
-
skipped,
|
|
1053
|
-
readEnvValue,
|
|
1054
|
-
resolveDbMode,
|
|
1055
|
-
resolveDbUrl,
|
|
1056
|
-
dbSql,
|
|
1057
|
-
dbTypes,
|
|
1058
|
-
dbApply,
|
|
1059
|
-
dbMigrate,
|
|
1060
|
-
dbRebuild,
|
|
1061
|
-
dbUrlCheck,
|
|
1062
|
-
dbPassword,
|
|
1063
|
-
dbBackup
|
|
1064
|
-
};
|