agentlink-sh 0.28.0 → 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.
@@ -1,1070 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- authenticatedFetch,
4
- ensureAccessToken,
5
- loadProjectCredential,
6
- resolvePoolerDbUrl,
7
- saveProjectCredential
8
- } from "./chunk-K5XVJEHP.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-ILLYV7U7.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 'roles', EXISTS (\n SELECT 1 FROM information_schema.tables\n WHERE table_schema = 'public' AND table_name = 'roles'\n ),\n 'permissions', EXISTS (\n SELECT 1 FROM information_schema.tables\n WHERE table_schema = 'public' AND table_name = 'permissions'\n ),\n 'role_permissions', EXISTS (\n SELECT 1 FROM information_schema.tables\n WHERE table_schema = 'public' AND table_name = 'role_permissions'\n ),\n 'session_tenants', EXISTS (\n SELECT 1 FROM information_schema.tables\n WHERE table_schema = 'public' AND table_name = 'session_tenants'\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_custom_access_token',\n EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'public'\n AND routine_name = '_hook_custom_access_token'\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_has_permission',\n EXISTS (\n SELECT 1 FROM information_schema.routines\n WHERE routine_schema = 'public'\n AND routine_name = '_auth_has_permission'\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.tables\n WHERE table_schema = 'public' AND table_name = 'roles'\n )\n AND EXISTS (\n SELECT 1 FROM information_schema.tables\n WHERE table_schema = 'public' AND table_name = 'permissions'\n )\n AND EXISTS (\n SELECT 1 FROM information_schema.tables\n WHERE table_schema = 'public' AND table_name = 'role_permissions'\n )\n AND EXISTS (\n SELECT 1 FROM information_schema.tables\n WHERE table_schema = 'public' AND table_name = 'session_tenants'\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_custom_access_token'\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_has_permission'\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 updateConfigToml(projectDir, options) {
299
- const dryRun = options?.dryRun ?? false;
300
- const configPath = path2.join(projectDir, "supabase", "config.toml");
301
- const original = fs2.readFileSync(configPath, "utf-8");
302
- let content = original;
303
- const patches = [];
304
- content = content.replace(
305
- /^(schemas\s*=\s*\[)([^\]]*)\]/m,
306
- (match, prefix, inner) => {
307
- const items = inner.split(",").map((s) => s.trim()).filter(Boolean);
308
- if (items.some((item) => item === '"api"' || item === "'api'")) {
309
- return match;
310
- }
311
- items.push('"api"');
312
- patches.push('api.schemas: add "api"');
313
- return `# TODO: After migration, only "api" should remain in schemas
314
- ${prefix}${items.join(", ")}]`;
315
- }
316
- );
317
- const seedPath = '"./seeds/local-secrets.seed.sql"';
318
- if (content.includes("[db.seed]")) {
319
- content = content.replace(
320
- /(\[db\.seed\][^\[]*sql_paths\s*=\s*\[)([^\]]*)\]/m,
321
- (match, prefix, inner) => {
322
- if (inner.includes("local-secrets.seed.sql")) return match;
323
- const items = inner.split(",").map((s) => s.trim()).filter(Boolean);
324
- items.push(seedPath);
325
- patches.push("db.seed.sql_paths: add local-secrets.seed.sql");
326
- return `${prefix}${items.join(", ")}]`;
327
- }
328
- );
329
- } else {
330
- content = content.trimEnd() + `
331
-
332
- [db.seed]
333
- sql_paths = [${seedPath}]
334
- `;
335
- patches.push("db.seed: add section with local-secrets.seed.sql");
336
- }
337
- if (content !== original && !dryRun) {
338
- fs2.writeFileSync(configPath, content);
339
- }
340
- patches.push(...updateConfigTomlSendEmailHook(projectDir, { dryRun }));
341
- patches.push(...updateConfigTomlBeforeUserCreatedHook(projectDir, { dryRun }));
342
- patches.push(...updateConfigTomlCustomAccessTokenHook(projectDir, { dryRun }));
343
- patches.push(...updateConfigTomlAuthEmailConfirmations(projectDir, { dryRun }));
344
- patches.push(...renameInternalEdgeFunctionSections(projectDir, { dryRun }));
345
- return patches;
346
- }
347
- function renameInternalEdgeFunctionSections(projectDir, options) {
348
- const dryRun = options?.dryRun ?? false;
349
- const configPath = path2.join(projectDir, "supabase", "config.toml");
350
- if (!fs2.existsSync(configPath)) return [];
351
- let content = fs2.readFileSync(configPath, "utf-8");
352
- const original = content;
353
- const patches = [];
354
- const renames = [
355
- ["send-email", "internal-send-auth-email"],
356
- ["invite-member", "internal-invite-member"],
357
- ["queue-worker", "internal-queue-worker"]
358
- ];
359
- for (const [oldName, newName] of renames) {
360
- if (content.includes(`[functions.${newName}]`)) continue;
361
- const oldHeader = `[functions.${oldName}]`;
362
- if (content.includes(oldHeader)) {
363
- content = content.replace(oldHeader, `[functions.${newName}]`);
364
- patches.push(`functions.${oldName} \u2192 functions.${newName}`);
365
- }
366
- }
367
- if (content !== original && !dryRun) {
368
- fs2.writeFileSync(configPath, content);
369
- }
370
- return patches;
371
- }
372
- function migrateRenamedEdgeFunctionDirs(projectDir) {
373
- const fnDir = path2.join(projectDir, "supabase", "functions");
374
- if (!fs2.existsSync(fnDir)) return [];
375
- const renames = [
376
- ["send-email", "internal-send-auth-email"],
377
- ["invite-member", "internal-invite-member"],
378
- ["queue-worker", "internal-queue-worker"]
379
- ];
380
- const migrated = [];
381
- for (const [oldName, newName] of renames) {
382
- const oldPath = path2.join(fnDir, oldName);
383
- const newPath = path2.join(fnDir, newName);
384
- const oldExists = fs2.existsSync(oldPath);
385
- const newExists = fs2.existsSync(newPath);
386
- if (oldExists && !newExists) {
387
- fs2.renameSync(oldPath, newPath);
388
- migrated.push(`${oldName} \u2192 ${newName}`);
389
- } else if (oldExists && newExists) {
390
- fs2.rmSync(oldPath, { recursive: true, force: true });
391
- migrated.push(`${oldName} (removed orphan; ${newName} already present)`);
392
- }
393
- }
394
- return migrated;
395
- }
396
- function writeConfigToml(projectDir, projectId) {
397
- const configPath = path2.join(projectDir, "supabase", "config.toml");
398
- const content = `project_id = "${projectId}"
399
-
400
- [api]
401
- enabled = true
402
- port = 54321
403
- schemas = ["api"]
404
- extra_search_path = ["public", "extensions"]
405
- max_rows = 1000
406
-
407
- [db]
408
- port = 54322
409
- shadow_port = 54320
410
- major_version = 17
411
-
412
- [db.migrations]
413
- schema_paths = ["./schemas/_schemas.sql", "./schemas/_extensions.sql", "./schemas/**/*.sql"]
414
-
415
- [db.seed]
416
- sql_paths = ["./seed.sql", "./seeds/local-secrets.seed.sql"]
417
-
418
- [auth]
419
- enabled = true
420
- site_url = "http://127.0.0.1:3000"
421
- # Wildcards cover the full canonical auth flow:
422
- # /auth/confirm \u2014 single PKCE callback for signup, magiclink, recovery, email_change
423
- # /auth/update-password \u2014 recovery destination after confirm exchange
424
- # /accept-invite \u2014 workspace invitation acceptance
425
- # /auth/check-inbox \u2014 pending state (no PKCE redirect ever lands here, but allowing
426
- # the prefix prevents accidental denies during dev)
427
- additional_redirect_urls = [
428
- "http://127.0.0.1:3000/**",
429
- "http://localhost:3000/**",
430
- "http://127.0.0.1:5173/**",
431
- "http://localhost:5173/**",
432
- "https://127.0.0.1:3000/**",
433
- ]
434
- jwt_expiry = 3600
435
- enable_refresh_token_rotation = true
436
- refresh_token_reuse_interval = 10
437
- enable_signup = true
438
- enable_anonymous_sign_ins = false
439
- minimum_password_length = 6
440
-
441
- [auth.email]
442
- enable_signup = true
443
- double_confirm_changes = true
444
- # Disabled on scaffold so the first local sign-up lands in the app without
445
- # needing SMTP. Set to true before shipping to production.
446
- enable_confirmations = false
447
-
448
- [edge_runtime]
449
- enabled = true
450
- policy = "per_worker"
451
- inspector_port = 8083
452
- deno_version = 2
453
-
454
- [auth.hook.send_email]
455
- enabled = true
456
- uri = "pg-functions://postgres/public/_hook_send_email"
457
-
458
- [auth.hook.before_user_created]
459
- enabled = true
460
- uri = "pg-functions://postgres/public/_hook_before_user_created"
461
-
462
- [auth.hook.custom_access_token]
463
- enabled = true
464
- uri = "pg-functions://postgres/public/_hook_custom_access_token"
465
-
466
- [functions.internal-queue-worker]
467
- enabled = true
468
- verify_jwt = false
469
-
470
- [functions.internal-send-auth-email]
471
- enabled = true
472
- verify_jwt = false
473
-
474
- [functions.internal-invite-member]
475
- enabled = true
476
- verify_jwt = false
477
- `;
478
- fs2.writeFileSync(configPath, content);
479
- }
480
- function updateConfigTomlSendEmailHook(projectDir, options) {
481
- const dryRun = options?.dryRun ?? false;
482
- const configPath = path2.join(projectDir, "supabase", "config.toml");
483
- let content = fs2.readFileSync(configPath, "utf-8");
484
- if (!content.includes("[auth.hook.send_email]")) {
485
- content = content.trimEnd() + `
486
-
487
- [auth.hook.send_email]
488
- enabled = true
489
- uri = "pg-functions://postgres/public/_hook_send_email"
490
-
491
- [functions.internal-send-auth-email]
492
- enabled = true
493
- verify_jwt = false
494
- `;
495
- if (!dryRun) fs2.writeFileSync(configPath, content);
496
- return ["auth.hook.send_email: add section", "functions.internal-send-auth-email: add section"];
497
- }
498
- return [];
499
- }
500
- function updateConfigTomlBeforeUserCreatedHook(projectDir, options) {
501
- const dryRun = options?.dryRun ?? false;
502
- const configPath = path2.join(projectDir, "supabase", "config.toml");
503
- let content = fs2.readFileSync(configPath, "utf-8");
504
- if (!content.includes("[auth.hook.before_user_created]")) {
505
- content = content.trimEnd() + `
506
-
507
- [auth.hook.before_user_created]
508
- enabled = true
509
- uri = "pg-functions://postgres/public/_hook_before_user_created"
510
- `;
511
- if (!dryRun) fs2.writeFileSync(configPath, content);
512
- return ["auth.hook.before_user_created: add section"];
513
- }
514
- return [];
515
- }
516
- function updateConfigTomlCustomAccessTokenHook(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.custom_access_token]")) {
521
- content = content.trimEnd() + `
522
-
523
- [auth.hook.custom_access_token]
524
- enabled = true
525
- uri = "pg-functions://postgres/public/_hook_custom_access_token"
526
- `;
527
- if (!dryRun) fs2.writeFileSync(configPath, content);
528
- return ["auth.hook.custom_access_token: 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
- const applyOutput = 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 (/^Stuck after \d+ round/m.test(applyOutput)) {
797
- if (options.quiet) {
798
- console.error(applyOutput);
799
- }
800
- throw new Error(
801
- "pg-delta declarative apply did not complete cleanly \u2014 some statements failed and the database state is inconsistent with supabase/schemas/. Fix the errors listed above (typically duplicate definitions or unresolved references) and re-run."
802
- );
803
- }
804
- if (!options.skipTypes) {
805
- if (!options.quiet) console.log("\nGenerating types...");
806
- try {
807
- await dbTypes({ cwd: options.cwd, dbUrl: options.dbUrl, env: options.env, quiet: options.quiet });
808
- } catch (err) {
809
- console.warn(`Warning: type generation failed \u2014 ${err.message}`);
810
- }
811
- }
812
- if (!options.quiet) {
813
- console.log("\nDone. Run 'agentlink check' to verify.");
814
- }
815
- }
816
- async function dbMigrate(name, options) {
817
- const dbUrl = await resolveDbUrl(options.cwd, options.dbUrl);
818
- const tmpDir = path3.join(os.tmpdir(), `agentlink-migrate-${Date.now()}`);
819
- fs3.mkdirSync(tmpDir, { recursive: true });
820
- const baselineCatalog = path3.join(tmpDir, "baseline.json");
821
- const desiredCatalog = path3.join(tmpDir, "desired.json");
822
- try {
823
- console.log("=== Step 1/4: Exporting baseline catalog...");
824
- await runCommand(
825
- `${pgd()} catalog-export --target ${JSON.stringify(dbUrl)} --output ${JSON.stringify(baselineCatalog)}`,
826
- options.cwd
827
- );
828
- console.log("=== Step 2/4: Applying schemas to get desired state...");
829
- await runCommand(
830
- `${pgd()} declarative apply --path ./supabase/schemas/ --target ${JSON.stringify(dbUrl)}`,
831
- options.cwd
832
- );
833
- console.log("=== Step 3/4: Exporting desired state catalog...");
834
- await runCommand(
835
- `${pgd()} catalog-export --target ${JSON.stringify(dbUrl)} --output ${JSON.stringify(desiredCatalog)}`,
836
- options.cwd
837
- );
838
- console.log("=== Step 4/4: Generating migration diff...");
839
- const diff = await runCommand(
840
- `${pgd()} plan --source ${JSON.stringify(baselineCatalog)} --target ${JSON.stringify(desiredCatalog)} --format sql --integration supabase --sql-format`,
841
- options.cwd
842
- ).catch(() => "");
843
- if (!diff.trim()) {
844
- console.log("No changes detected. Nothing to migrate.");
845
- return;
846
- }
847
- const migrationsDir = path3.join(options.cwd, "supabase", "migrations");
848
- fs3.mkdirSync(migrationsDir, { recursive: true });
849
- const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\D/g, "").slice(0, 14);
850
- const filename = `${timestamp}_${name}.sql`;
851
- const migrationPath = path3.join(migrationsDir, filename);
852
- fs3.writeFileSync(migrationPath, diff);
853
- const lineCount = diff.split("\n").length;
854
- console.log(`
855
- Migration created: supabase/migrations/${filename}`);
856
- console.log(`Lines: ${lineCount}`);
857
- console.log("\nNOTE: pg-delta filters out cron + storage schemas.");
858
- console.log("If your change includes cron.schedule() or storage policies,");
859
- console.log("append them manually to the migration file.");
860
- } finally {
861
- fs3.rmSync(tmpDir, { recursive: true, force: true });
862
- }
863
- }
864
- async function dbRebuild(options) {
865
- const cwd = options.cwd;
866
- const migrationsDir = path3.join(cwd, "supabase", "migrations");
867
- if (fs3.existsSync(migrationsDir)) {
868
- const files = fs3.readdirSync(migrationsDir).filter((f) => f.endsWith(".sql"));
869
- for (const file of files) {
870
- fs3.unlinkSync(path3.join(migrationsDir, file));
871
- }
872
- console.log(`Deleted ${files.length} migration file(s)`);
873
- }
874
- const progressPath = path3.join(cwd, ".agentlink-progress.json");
875
- if (fs3.existsSync(progressPath)) {
876
- fs3.unlinkSync(progressPath);
877
- console.log("Cleared scaffold progress");
878
- }
879
- console.log("\nRe-applying schemas...");
880
- await dbApply({ cwd, dbUrl: options.dbUrl, env: options.env, skipTypes: true, quiet: true });
881
- console.log("\nRegenerating migration...");
882
- await dbMigrate("agentlink_setup", { cwd, dbUrl: options.dbUrl, env: options.env });
883
- console.log("\nGenerating types...");
884
- try {
885
- await dbTypes({ cwd, dbUrl: options.dbUrl, env: options.env });
886
- } catch (err) {
887
- console.warn(`Warning: type generation failed \u2014 ${err.message}`);
888
- }
889
- console.log("\nDone. Database rebuilt from schema files.");
890
- }
891
- async function dbUrlCheck(options) {
892
- const mode = await resolveDbMode(options.cwd, options.dbUrl, options.env);
893
- if (mode.kind !== "cloud" || !mode.projectRef) {
894
- const dbUrl = await resolveDbUrl(options.cwd, options.dbUrl);
895
- console.log(dbUrl);
896
- return;
897
- }
898
- await ensureAccessToken({ nonInteractive: true, projectDir: options.cwd });
899
- const password = loadProjectCredential(mode.projectRef);
900
- if (!password) {
901
- throw new Error(
902
- `No stored password for project ${mode.projectRef}.
903
- Run: agentlink env add dev`
904
- );
905
- }
906
- const poolerUrl = await resolvePoolerDbUrl(mode.projectRef, password);
907
- console.log(`Pooler URL: ${poolerUrl}`);
908
- const currentUrl = readEnvValue(options.cwd, "SUPABASE_DB_URL");
909
- if (currentUrl === poolerUrl) {
910
- console.log("\u2714 .env.local is up to date");
911
- return;
912
- }
913
- if (currentUrl) {
914
- console.log(`
915
- Current: ${currentUrl}`);
916
- console.log(`Expected: ${poolerUrl}`);
917
- }
918
- if (options.fix) {
919
- const envPath = path3.join(options.cwd, ".env.local");
920
- if (fs3.existsSync(envPath)) {
921
- let content = fs3.readFileSync(envPath, "utf-8");
922
- if (/^SUPABASE_DB_URL=/m.test(content)) {
923
- content = content.replace(/^SUPABASE_DB_URL=.*$/m, `SUPABASE_DB_URL=${poolerUrl}`);
924
- } else {
925
- content = content.trimEnd() + `
926
- SUPABASE_DB_URL=${poolerUrl}
927
- `;
928
- }
929
- fs3.writeFileSync(envPath, content);
930
- console.log("\n\u2714 .env.local updated with correct pooler URL");
931
- }
932
- } else if (currentUrl !== poolerUrl) {
933
- console.log("\nRun with --fix to update .env.local");
934
- }
935
- }
936
- var passwordTheme = {
937
- prefix: { idle: blue("?"), done: blue("\u2714") },
938
- style: {
939
- answer: (text) => amber(text),
940
- message: (text) => bold(text),
941
- help: (text) => dim(text)
942
- }
943
- };
944
- async function dbPassword(cwd, value, opts = {}) {
945
- const manifest = readManifest(cwd);
946
- if (!manifest) {
947
- throw new Error("No agentlink.json found. Run `agentlink` to scaffold a project first.");
948
- }
949
- if (!manifest.cloud?.environments) {
950
- throw new Error("No cloud environment configured. This command is for cloud projects.");
951
- }
952
- const env = resolveCloudEnv(manifest.cloud, opts.env);
953
- const projectRef = env.projectRef;
954
- if (value) {
955
- saveProjectCredential(projectRef, value);
956
- console.log(`Password saved for project ${dim(projectRef)}`);
957
- } else {
958
- const current = loadProjectCredential(projectRef);
959
- if (current) {
960
- const masked = current.length > 8 ? current.slice(0, 4) + "\u25CF".repeat(current.length - 8) + current.slice(-4) : "\u25CF".repeat(current.length);
961
- console.log(`Current password: ${dim(masked)} (project: ${projectRef})`);
962
- console.log();
963
- }
964
- const resetUrl = `https://supabase.com/dashboard/project/${projectRef}/settings/database`;
965
- console.log(` ${dim("Reset your password at:")}`);
966
- console.log(` ${dim(resetUrl)}`);
967
- console.log();
968
- const newPassword = await input({
969
- message: "Database password:",
970
- theme: passwordTheme,
971
- transformer: (v, { isFinal }) => {
972
- if (isFinal) return "\u25CF".repeat(Math.min(v.length, 8));
973
- return "\u25CF".repeat(v.length);
974
- },
975
- validate: (val) => val.trim().length > 0 || "Password is required"
976
- });
977
- saveProjectCredential(projectRef, newPassword.trim());
978
- console.log(`Password saved for project ${dim(projectRef)}`);
979
- }
980
- }
981
- async function dbBackup(options) {
982
- const dbUrl = await resolveDbUrl(options.cwd, options.dbUrl);
983
- const manifest = readManifest(options.cwd);
984
- const envLabel = options.env ?? manifest?.cloud?.default ?? "local";
985
- const now = /* @__PURE__ */ new Date();
986
- const pad = (n) => String(n).padStart(2, "0");
987
- const timestamp = `${now.getUTCFullYear()}-${pad(now.getUTCMonth() + 1)}-${pad(now.getUTCDate())}T${pad(now.getUTCHours())}-${pad(now.getUTCMinutes())}-${pad(now.getUTCSeconds())}`;
988
- const outDir = path3.join(options.cwd, "supabase", "backups", envLabel, timestamp);
989
- fs3.mkdirSync(outDir, { recursive: true });
990
- ensureGitignorePattern(options.cwd, "supabase/backups/");
991
- const rolesFile = path3.join(outDir, "roles.sql");
992
- const schemaFile = path3.join(outDir, "schema.sql");
993
- const dataFile = path3.join(outDir, "data.sql");
994
- const urlArg = JSON.stringify(dbUrl);
995
- console.log();
996
- console.log(` ${blue("\u25CF")} Backing up ${bold(envLabel)} \u2192 ${dim(path3.relative(options.cwd, outDir))}`);
997
- console.log();
998
- await step("Dumping roles", async () => {
999
- await runCommand(
1000
- `${sb()} db dump --db-url ${urlArg} -f ${JSON.stringify(rolesFile)} --role-only`,
1001
- options.cwd
1002
- );
1003
- });
1004
- await step("Dumping schema", async () => {
1005
- await runCommand(
1006
- `${sb()} db dump --db-url ${urlArg} -f ${JSON.stringify(schemaFile)}`,
1007
- options.cwd
1008
- );
1009
- });
1010
- await step("Dumping data", async () => {
1011
- await runCommand(
1012
- `${sb()} db dump --db-url ${urlArg} -f ${JSON.stringify(dataFile)} --use-copy --data-only -x "storage.buckets_vectors" -x "storage.vector_indexes"`,
1013
- options.cwd
1014
- );
1015
- });
1016
- console.log();
1017
- console.log(` ${blue("Done.")} Snapshot written:`);
1018
- for (const f of [rolesFile, schemaFile, dataFile]) {
1019
- const sz = fs3.statSync(f).size;
1020
- console.log(` ${dim(path3.relative(options.cwd, f))} ${dim(formatBytes(sz))}`);
1021
- }
1022
- console.log();
1023
- }
1024
- function formatBytes(n) {
1025
- if (n < 1024) return `${n} B`;
1026
- if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
1027
- return `${(n / 1024 / 1024).toFixed(1)} MB`;
1028
- }
1029
-
1030
- export {
1031
- ALLOWED_CLOUD_ENV_NAMES,
1032
- assertAllowedEnvNameForAdd,
1033
- assertAllowedEnvNameForUse,
1034
- assertManifestEnvNames,
1035
- isBareManifest,
1036
- readManifest,
1037
- writeManifest,
1038
- resolveCloudEnv,
1039
- addCloudEnvironment,
1040
- removeCloudEnvironment,
1041
- setCloudEnvOrgId,
1042
- setDefaultEnvironment,
1043
- listEnvironments,
1044
- check_setup_default,
1045
- seedSQL,
1046
- cloudSeedSQL,
1047
- parseSupabaseStatus,
1048
- runSQL,
1049
- updateConfigToml,
1050
- migrateRenamedEdgeFunctionDirs,
1051
- writeConfigToml,
1052
- writeGitkeepFiles,
1053
- writeDotEnv,
1054
- updateDotEnvKeys,
1055
- writeEnvExamples,
1056
- writeLocalSecretsSeed,
1057
- step,
1058
- skipped,
1059
- readEnvValue,
1060
- resolveDbMode,
1061
- resolveDbUrl,
1062
- dbSql,
1063
- dbTypes,
1064
- dbApply,
1065
- dbMigrate,
1066
- dbRebuild,
1067
- dbUrlCheck,
1068
- dbPassword,
1069
- dbBackup
1070
- };