@motherbase/cli 0.2.5 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +247 -247
  3. package/bin/motherbase.js +10 -10
  4. package/package.json +35 -26
  5. package/src/api.js +233 -241
  6. package/src/commands/_util.js +106 -93
  7. package/src/commands/backups.js +16 -16
  8. package/src/commands/branches.js +113 -113
  9. package/src/commands/completion.js +90 -90
  10. package/src/commands/config.js +47 -47
  11. package/src/commands/cron.js +65 -65
  12. package/src/commands/db-catalog.js +326 -326
  13. package/src/commands/db.js +103 -103
  14. package/src/commands/domains.js +124 -124
  15. package/src/commands/functions.js +304 -297
  16. package/src/commands/gen.js +172 -172
  17. package/src/commands/import.js +55 -55
  18. package/src/commands/index.js +85 -85
  19. package/src/commands/inspect.js +178 -178
  20. package/src/commands/keys.js +72 -72
  21. package/src/commands/link.js +35 -35
  22. package/src/commands/local.js +294 -273
  23. package/src/commands/login.js +39 -39
  24. package/src/commands/logout.js +16 -16
  25. package/src/commands/mcp.js +61 -61
  26. package/src/commands/migrate.js +207 -179
  27. package/src/commands/migration.js +108 -108
  28. package/src/commands/network.js +83 -83
  29. package/src/commands/orgs.js +40 -40
  30. package/src/commands/postgres-config.js +113 -113
  31. package/src/commands/projects.js +130 -130
  32. package/src/commands/queues.js +133 -133
  33. package/src/commands/realtime.js +35 -35
  34. package/src/commands/rls.js +176 -176
  35. package/src/commands/secrets.js +66 -66
  36. package/src/commands/signing.js +130 -130
  37. package/src/commands/snippets.js +44 -44
  38. package/src/commands/status.js +42 -42
  39. package/src/commands/storage.js +261 -261
  40. package/src/commands/test.js +9 -9
  41. package/src/commands/unlink.js +15 -15
  42. package/src/commands/users.js +142 -142
  43. package/src/commands/vault.js +82 -82
  44. package/src/commands/webhooks.js +62 -62
  45. package/src/commands/whoami.js +20 -20
  46. package/src/commands/wrappers.js +150 -150
  47. package/src/config.js +98 -98
  48. package/src/context.js +106 -102
  49. package/src/docs.js +68 -0
  50. package/src/edge-templates.js +194 -194
  51. package/src/format.js +92 -92
  52. package/src/inspect/csv.js +22 -22
  53. package/src/inspect/queries.js +562 -562
  54. package/src/main.js +224 -194
  55. package/src/mcp/platform-tools.js +233 -233
  56. package/src/mcp/protocol.js +152 -149
  57. package/src/mcp/storage-tools.js +93 -93
  58. package/src/mcp/tools.js +478 -478
  59. package/src/migration/commands.js +375 -375
  60. package/src/migration/files.js +245 -245
  61. package/src/prompt.js +91 -91
  62. package/src/storage/remote.js +62 -62
  63. package/src/ui.js +111 -111
@@ -1,16 +1,16 @@
1
- 'use strict';
2
-
3
- const config = require('../config');
4
- const ui = require('../ui');
5
- const { CliError } = require('../context');
6
-
7
- module.exports = {
8
- summary: 'Remove the stored token for the current (or --url) console',
9
- usage: 'motherbase logout [--url <consoleUrl>]',
10
- async run(ctx) {
11
- const url = config.normalizeUrl(ctx.flags.url || ctx.consoleUrl || '');
12
- if (!url) throw new CliError('No console to log out of.');
13
- config.clearConsoleToken(url);
14
- ui.success(`Logged out of ${url}`);
15
- },
16
- };
1
+ 'use strict';
2
+
3
+ const config = require('../config');
4
+ const ui = require('../ui');
5
+ const { CliError } = require('../context');
6
+
7
+ module.exports = {
8
+ summary: 'Remove the stored token for the current (or --url) console',
9
+ usage: 'motherbase logout [--url <consoleUrl>]',
10
+ async run(ctx) {
11
+ const url = config.normalizeUrl(ctx.flags.url || ctx.consoleUrl || '');
12
+ if (!url) throw new CliError('No console to log out of.');
13
+ config.clearConsoleToken(url);
14
+ ui.success(`Logged out of ${url}`);
15
+ },
16
+ };
@@ -1,61 +1,61 @@
1
- 'use strict';
2
-
3
- // `motherbase mcp`: expose the linked project, and the console's organizations
4
- // and projects, to an AI agent over the Model Context Protocol.
5
- //
6
- // The transport is stdio, which is how an MCP client launches a server: it spawns
7
- // this process and talks JSON-RPC over its stdin/stdout. Nothing is printed to
8
- // stdout except protocol messages, so diagnostics go to stderr.
9
-
10
- const { serve, toolServer } = require('../mcp/protocol');
11
- const { buildTools } = require('../mcp/tools');
12
- const { CliError } = require('../context');
13
-
14
- const pkg = require('../../package.json');
15
-
16
- /** Whether a `require*()` resolves, without surfacing its error. */
17
- function reaches(resolve) {
18
- try {
19
- resolve();
20
- return true;
21
- } catch {
22
- return false;
23
- }
24
- }
25
-
26
- const mcp = {
27
- summary: 'Serve the linked project and console to an AI agent over MCP (stdio)',
28
- usage: 'motherbase mcp [--write]',
29
- async run(ctx, _args, flags = {}) {
30
- // Resolve what this session reaches up front. Failing here is a clear CLI
31
- // error; failing on the first tool call is an opaque one inside someone's agent.
32
- const scope = { project: reaches(() => ctx.requireProject()), console: reaches(() => ctx.requireConsole()) };
33
- if (!scope.project && !scope.console) {
34
- throw new CliError(
35
- 'Nothing to serve. Run `motherbase login` for the organization and project tools, and `motherbase link` '
36
- + '(or set MOTHERBASE_URL + MOTHERBASE_ADMIN_TOKEN) for the database tools.',
37
- );
38
- }
39
-
40
- const write = Boolean(flags.write);
41
- const tools = buildTools(ctx, { write, ...scope });
42
-
43
- process.stderr.write(
44
- `motherbase mcp: ${tools.length} tools, ${write ? 'read and write' : 'read only'}. `
45
- + `Project: ${scope.project ? ctx.projectName || ctx.projectId || 'direct' : 'none linked'}. `
46
- + `Console: ${scope.console ? ctx.consoleUrl : 'not logged in'}\n`,
47
- );
48
- if (!write) {
49
- process.stderr.write(' Pass --write to expose apply_migration, raw SQL writes, function deploys and project lifecycle.\n');
50
- }
51
-
52
- await serve({
53
- input: process.stdin,
54
- output: process.stdout,
55
- handlers: toolServer({ name: 'motherbase', version: pkg.version, tools }),
56
- onError: err => process.stderr.write(`motherbase mcp: ${err && err.message ? err.message : err}\n`),
57
- });
58
- },
59
- };
60
-
61
- module.exports = mcp;
1
+ 'use strict';
2
+
3
+ // `motherbase mcp`: expose the linked project, and the console's organizations
4
+ // and projects, to an AI agent over the Model Context Protocol.
5
+ //
6
+ // The transport is stdio, which is how an MCP client launches a server: it spawns
7
+ // this process and talks JSON-RPC over its stdin/stdout. Nothing is printed to
8
+ // stdout except protocol messages, so diagnostics go to stderr.
9
+
10
+ const { serve, toolServer } = require('../mcp/protocol');
11
+ const { buildTools } = require('../mcp/tools');
12
+ const { CliError } = require('../context');
13
+
14
+ const pkg = require('../../package.json');
15
+
16
+ /** Whether a `require*()` resolves, without surfacing its error. */
17
+ function reaches(resolve) {
18
+ try {
19
+ resolve();
20
+ return true;
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+
26
+ const mcp = {
27
+ summary: 'Serve the linked project and console to an AI agent over MCP (stdio)',
28
+ usage: 'motherbase mcp [--write]',
29
+ async run(ctx, _args, flags = {}) {
30
+ // Resolve what this session reaches up front. Failing here is a clear CLI
31
+ // error; failing on the first tool call is an opaque one inside someone's agent.
32
+ const scope = { project: reaches(() => ctx.requireProject()), console: reaches(() => ctx.requireConsole()) };
33
+ if (!scope.project && !scope.console) {
34
+ throw new CliError(
35
+ 'Nothing to serve. Run `motherbase login` for the organization and project tools, and `motherbase link` '
36
+ + '(or set MOTHERBASE_URL + MOTHERBASE_ADMIN_TOKEN) for the database tools.',
37
+ );
38
+ }
39
+
40
+ const write = Boolean(flags.write);
41
+ const tools = buildTools(ctx, { write, ...scope });
42
+
43
+ process.stderr.write(
44
+ `motherbase mcp: ${tools.length} tools, ${write ? 'read and write' : 'read only'}. `
45
+ + `Project: ${scope.project ? ctx.projectName || ctx.projectId || 'direct' : 'none linked'}. `
46
+ + `Console: ${scope.console ? ctx.consoleUrl : 'not logged in'}\n`,
47
+ );
48
+ if (!write) {
49
+ process.stderr.write(' Pass --write to expose apply_migration, raw SQL writes, function deploys and project lifecycle.\n');
50
+ }
51
+
52
+ await serve({
53
+ input: process.stdin,
54
+ output: process.stdout,
55
+ handlers: toolServer({ name: 'motherbase', version: pkg.version, tools }),
56
+ onError: err => process.stderr.write(`motherbase mcp: ${err && err.message ? err.message : err}\n`),
57
+ });
58
+ },
59
+ };
60
+
61
+ module.exports = mcp;
@@ -1,179 +1,207 @@
1
- 'use strict';
2
-
3
- // `motherbase migrate codemod <dir>`: port a Supabase codebase's imports, and
4
- // report what this SDK does not implement.
5
- //
6
- // The rewrite is trivial and the report is the point. `@motherbase/client` is
7
- // supabase-js shaped, so most files need one import line changed; what breaks a
8
- // port is the handful of calls that do not exist here, and finding those at
9
- // build time beats finding them when a user hits that screen.
10
- //
11
- // Deliberately a text pass, not an AST pass. A codemod that needs a TypeScript
12
- // parser needs typescript as a CLI dependency, and the patterns below are
13
- // import specifiers and method names, which do not need one. The cost is that a
14
- // match inside a comment or a string is still reported: over-reporting in a
15
- // list a human reads is the acceptable direction to be wrong in.
16
-
17
- const fs = require('fs');
18
- const path = require('path');
19
- const { output } = require('./_util');
20
- const { CliError } = require('../context');
21
-
22
- const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts']);
23
- const SKIP_DIRECTORIES = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage', '.turbo']);
24
-
25
- /** Import specifiers that map one to one. */
26
- const IMPORT_REWRITES = [
27
- { from: '@supabase/supabase-js', to: '@motherbase/client' },
28
- { from: '@supabase/ssr', to: '@motherbase/ssr' },
29
- ];
30
-
31
- /**
32
- * Calls this SDK does not implement, and what to do instead.
33
- *
34
- * Kept short and kept true: every entry here is a row marked `Not supported` in
35
- * packages/client/COMPATIBILITY.md. An entry that becomes supported and is left
36
- * behind sends people to rewrite working code, so this list is part of the same
37
- * change as implementing the feature.
38
- */
39
- const UNSUPPORTED = [
40
- {
41
- pattern: /\.schema\(\s*['"`](?!public['"`])/g,
42
- what: 'schema() on a schema other than public',
43
- advice: 'The REST API exposes public only. Move the tables, or expose them through a view or an RPC.',
44
- },
45
- {
46
- pattern: /exchangeCodeForSession\s*\(/g,
47
- what: 'auth.exchangeCodeForSession()',
48
- advice: 'This install runs the implicit flow. Read the session client side with auth.getSessionFromUrl().',
49
- },
50
- {
51
- pattern: /signInAnonymously\s*\(/g,
52
- what: 'auth.signInAnonymously()',
53
- advice: 'Not implemented. Create a real user, or gate the feature behind sign-in.',
54
- },
55
- {
56
- pattern: /transform\s*:\s*\{/g,
57
- what: 'storage image transform options',
58
- advice: 'No image pipeline yet. Store the sizes you need at upload time.',
59
- },
60
- {
61
- pattern: /\.delete\(\s*\)\s*(?![\s\S]{0,200}?\.(eq|neq|in|gt|gte|lt|lte|like|ilike|is|filter|match|or|not)\()/g,
62
- what: 'delete() with no filter',
63
- advice: 'Refused with 400 on purpose, so a missing filter cannot empty a table. Add a filter.',
64
- },
65
- ];
66
-
67
- function walk(dir, files = []) {
68
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
69
- if (entry.name.startsWith('.') && entry.name !== '.') continue;
70
- const full = path.join(dir, entry.name);
71
- if (entry.isDirectory()) {
72
- if (SKIP_DIRECTORIES.has(entry.name)) continue;
73
- walk(full, files);
74
- } else if (SOURCE_EXTENSIONS.has(path.extname(entry.name))) {
75
- files.push(full);
76
- }
77
- }
78
- return files;
79
- }
80
-
81
- /** Line number of an offset, 1-based, for a report a human can act on. */
82
- function lineAt(text, index) {
83
- let line = 1;
84
- for (let i = 0; i < index && i < text.length; i++) if (text[i] === '\n') line++;
85
- return line;
86
- }
87
-
88
- function rewriteImports(text) {
89
- let out = text;
90
- let changed = 0;
91
- for (const { from, to } of IMPORT_REWRITES) {
92
- // Only inside a quoted specifier, so a mention in prose is left alone.
93
- const pattern = new RegExp(`(['"\`])${from.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}\\1`, 'g');
94
- out = out.replace(pattern, match => {
95
- changed++;
96
- return match.replace(from, to);
97
- });
98
- }
99
- return { text: out, changed };
100
- }
101
-
102
- function scanUnsupported(text, file) {
103
- const findings = [];
104
- for (const rule of UNSUPPORTED) {
105
- rule.pattern.lastIndex = 0;
106
- let match;
107
- while ((match = rule.pattern.exec(text)) !== null) {
108
- findings.push({ file, line: lineAt(text, match.index), what: rule.what, advice: rule.advice });
109
- if (match.index === rule.pattern.lastIndex) rule.pattern.lastIndex++;
110
- }
111
- }
112
- return findings;
113
- }
114
-
115
- const codemod = {
116
- summary: 'Rewrite Supabase imports and report what this SDK does not support',
117
- usage: 'motherbase migrate codemod <dir> [--write]',
118
- async run(ctx, args, flags) {
119
- const target = path.resolve(ctx.cwd, args[0] || '.');
120
- if (!fs.existsSync(target) || !fs.statSync(target).isDirectory()) {
121
- throw new CliError(`${target} is not a directory.`);
122
- }
123
-
124
- const files = walk(target);
125
- const rewritten = [];
126
- const findings = [];
127
-
128
- for (const file of files) {
129
- const original = fs.readFileSync(file, 'utf8');
130
- const { text, changed } = rewriteImports(original);
131
- if (changed) {
132
- rewritten.push({ file: path.relative(ctx.cwd, file), imports: changed });
133
- // Scan the rewritten text so a line number matches what the file will
134
- // hold after --write, not what it held before.
135
- if (flags.write) fs.writeFileSync(file, text, 'utf8');
136
- }
137
- findings.push(...scanUnsupported(text, path.relative(ctx.cwd, file)));
138
- }
139
-
140
- const result = {
141
- root: target,
142
- scanned: files.length,
143
- rewritten,
144
- written: Boolean(flags.write),
145
- findings,
146
- };
147
-
148
- output(ctx, result, (data, ui) => {
149
- const verb = data.written ? 'Rewrote' : 'Would rewrite';
150
- ui.info(`Scanned ${data.scanned} files under ${data.root}`);
151
- if (data.rewritten.length) {
152
- ui.info(`${verb} imports in ${data.rewritten.length} file(s):`);
153
- for (const entry of data.rewritten) ui.info(` ${entry.file} (${entry.imports})`);
154
- } else {
155
- ui.info('No Supabase imports found.');
156
- }
157
- if (!data.written && data.rewritten.length) ui.info('Re-run with --write to apply.');
158
-
159
- if (!data.findings.length) {
160
- ui.success('No unsupported calls found.');
161
- return;
162
- }
163
- ui.info('');
164
- ui.info(`${data.findings.length} call(s) this SDK does not support:`);
165
- for (const finding of data.findings) {
166
- ui.info(` ${finding.file}:${finding.line} ${finding.what}`);
167
- ui.info(` ${finding.advice}`);
168
- }
169
- });
170
- },
171
- };
172
-
173
- module.exports = {
174
- summary: 'Port an application from Supabase',
175
- usage: 'motherbase migrate codemod <dir> [--write]',
176
- subcommands: { codemod },
177
- // Exported for the unit test: the scanner is the part worth pinning.
178
- _internals: { rewriteImports, scanUnsupported },
179
- };
1
+ 'use strict';
2
+
3
+ // `motherbase migrate codemod <dir>`: port a Supabase codebase's imports, and
4
+ // report what this SDK does not implement.
5
+ //
6
+ // The rewrite is trivial and the report is the point. `@motherbase/client` is
7
+ // supabase-js shaped (auth aside: it is Better Auth's client), so most files
8
+ // need one import line changed; what breaks a port is the calls that do not
9
+ // exist here, and finding those at build time beats finding them when a user
10
+ // hits that screen.
11
+ //
12
+ // Deliberately a text pass, not an AST pass. A codemod that needs a TypeScript
13
+ // parser needs typescript as a CLI dependency, and the patterns below are
14
+ // import specifiers and method names, which do not need one. The cost is that a
15
+ // match inside a comment or a string is still reported: over-reporting in a
16
+ // list a human reads is the acceptable direction to be wrong in.
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const { output } = require('./_util');
21
+ const { CliError } = require('../context');
22
+
23
+ const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts']);
24
+ const SKIP_DIRECTORIES = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage', '.turbo']);
25
+
26
+ /** Import specifiers that map one to one. */
27
+ const IMPORT_REWRITES = [
28
+ { from: '@supabase/supabase-js', to: '@motherbase/client' },
29
+ { from: '@supabase/ssr', to: '@motherbase/ssr' },
30
+ ];
31
+
32
+ /**
33
+ * supabase-js auth calls, with the Better Auth call that replaces each one.
34
+ * `client.auth` is Better Auth's own client, and it turns an unknown method
35
+ * into a request that answers 404, so these fail at runtime, not at import.
36
+ */
37
+ function authRenames(renames) {
38
+ return Object.entries(renames).map(([method, replacement]) => ({
39
+ pattern: new RegExp(`\\.auth\\.${method}\\s*\\(`, 'g'),
40
+ what: `auth.${method}()`,
41
+ advice: `Use ${replacement}.`,
42
+ }));
43
+ }
44
+
45
+ /**
46
+ * Calls this SDK does not implement, and what to do instead.
47
+ *
48
+ * Kept short and kept true: every entry here is a row marked `Not supported` in
49
+ * packages/client/COMPATIBILITY.md. An entry that becomes supported and is left
50
+ * behind sends people to rewrite working code, so this list is part of the same
51
+ * change as implementing the feature.
52
+ */
53
+ const UNSUPPORTED = [
54
+ {
55
+ pattern: /\.schema\(\s*['"`](?!public['"`])/g,
56
+ what: 'schema() on a schema other than public',
57
+ advice: 'The REST API exposes public only. Move the tables, or expose them through a view or an RPC.',
58
+ },
59
+ ...authRenames({
60
+ signInWithPassword: 'auth.signIn.email({ email, password })',
61
+ signUp: 'auth.signUp.email({ email, password, name })',
62
+ signInWithOAuth: 'auth.signIn.social({ provider, callbackURL })',
63
+ signInWithIdToken: 'auth.signIn.social({ provider, idToken: { token } })',
64
+ signInWithOtp: "auth.emailOtp.sendVerificationOtp({ email, type: 'sign-in' }), or auth.phoneNumber.sendOtp({ phoneNumber })",
65
+ verifyOtp: 'auth.signIn.emailOtp({ email, otp }), or auth.phoneNumber.verify({ phoneNumber, code })',
66
+ signInAnonymously: 'auth.signIn.anonymous()',
67
+ signInWithSSO: 'auth.signIn.sso({ email, callbackURL })',
68
+ getUser: 'auth.getSession(), which asks the server and returns { session, user }',
69
+ refreshSession: 'auth.getSession(): Better Auth sessions are revalidated, not rotated',
70
+ setSession: 'a storage adapter holding the session, or auth.getSessionFromUrl() on an OAuth return',
71
+ resetPasswordForEmail: 'auth.emailOtp.requestPasswordReset({ email }), then auth.emailOtp.resetPassword({ email, otp, password })',
72
+ getUserIdentities: 'auth.listAccounts()',
73
+ linkIdentity: 'auth.linkSocial({ provider, callbackURL })',
74
+ unlinkIdentity: 'auth.unlinkAccount({ providerId })',
75
+ getClaims: 'auth.token(), which returns a JWT for the session',
76
+ startAutoRefresh: 'nothing: the client revalidates on a 401',
77
+ }),
78
+ {
79
+ pattern: /\.auth\.mfa\./g,
80
+ what: 'auth.mfa.*',
81
+ advice: 'Use auth.twoFactor: enable({ password }), verifyTotp({ code }), disable({ password }).',
82
+ },
83
+ {
84
+ pattern: /transform\s*:\s*\{/g,
85
+ what: 'storage image transform options',
86
+ advice: 'No image pipeline yet. Store the sizes you need at upload time.',
87
+ },
88
+ {
89
+ pattern: /\.delete\(\s*\)\s*(?![\s\S]{0,200}?\.(eq|neq|in|gt|gte|lt|lte|like|ilike|is|filter|match|or|not)\()/g,
90
+ what: 'delete() with no filter',
91
+ advice: 'Refused with 400 on purpose, so a missing filter cannot empty a table. Add a filter.',
92
+ },
93
+ ];
94
+
95
+ function walk(dir, files = []) {
96
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
97
+ if (entry.name.startsWith('.') && entry.name !== '.') continue;
98
+ const full = path.join(dir, entry.name);
99
+ if (entry.isDirectory()) {
100
+ if (SKIP_DIRECTORIES.has(entry.name)) continue;
101
+ walk(full, files);
102
+ } else if (SOURCE_EXTENSIONS.has(path.extname(entry.name))) {
103
+ files.push(full);
104
+ }
105
+ }
106
+ return files;
107
+ }
108
+
109
+ /** Line number of an offset, 1-based, for a report a human can act on. */
110
+ function lineAt(text, index) {
111
+ let line = 1;
112
+ for (let i = 0; i < index && i < text.length; i++) if (text[i] === '\n') line++;
113
+ return line;
114
+ }
115
+
116
+ function rewriteImports(text) {
117
+ let out = text;
118
+ let changed = 0;
119
+ for (const { from, to } of IMPORT_REWRITES) {
120
+ // Only inside a quoted specifier, so a mention in prose is left alone.
121
+ const pattern = new RegExp(`(['"\`])${from.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}\\1`, 'g');
122
+ out = out.replace(pattern, match => {
123
+ changed++;
124
+ return match.replace(from, to);
125
+ });
126
+ }
127
+ return { text: out, changed };
128
+ }
129
+
130
+ function scanUnsupported(text, file) {
131
+ const findings = [];
132
+ for (const rule of UNSUPPORTED) {
133
+ rule.pattern.lastIndex = 0;
134
+ let match;
135
+ while ((match = rule.pattern.exec(text)) !== null) {
136
+ findings.push({ file, line: lineAt(text, match.index), what: rule.what, advice: rule.advice });
137
+ if (match.index === rule.pattern.lastIndex) rule.pattern.lastIndex++;
138
+ }
139
+ }
140
+ return findings;
141
+ }
142
+
143
+ const codemod = {
144
+ summary: 'Rewrite Supabase imports and report what this SDK does not support',
145
+ usage: 'motherbase migrate codemod <dir> [--write]',
146
+ async run(ctx, args, flags) {
147
+ const target = path.resolve(ctx.cwd, args[0] || '.');
148
+ if (!fs.existsSync(target) || !fs.statSync(target).isDirectory()) {
149
+ throw new CliError(`${target} is not a directory.`);
150
+ }
151
+
152
+ const files = walk(target);
153
+ const rewritten = [];
154
+ const findings = [];
155
+
156
+ for (const file of files) {
157
+ const original = fs.readFileSync(file, 'utf8');
158
+ const { text, changed } = rewriteImports(original);
159
+ if (changed) {
160
+ rewritten.push({ file: path.relative(ctx.cwd, file), imports: changed });
161
+ // Scan the rewritten text so a line number matches what the file will
162
+ // hold after --write, not what it held before.
163
+ if (flags.write) fs.writeFileSync(file, text, 'utf8');
164
+ }
165
+ findings.push(...scanUnsupported(text, path.relative(ctx.cwd, file)));
166
+ }
167
+
168
+ const result = {
169
+ root: target,
170
+ scanned: files.length,
171
+ rewritten,
172
+ written: Boolean(flags.write),
173
+ findings,
174
+ };
175
+
176
+ output(ctx, result, (data, ui) => {
177
+ const verb = data.written ? 'Rewrote' : 'Would rewrite';
178
+ ui.info(`Scanned ${data.scanned} files under ${data.root}`);
179
+ if (data.rewritten.length) {
180
+ ui.info(`${verb} imports in ${data.rewritten.length} file(s):`);
181
+ for (const entry of data.rewritten) ui.info(` ${entry.file} (${entry.imports})`);
182
+ } else {
183
+ ui.info('No Supabase imports found.');
184
+ }
185
+ if (!data.written && data.rewritten.length) ui.info('Re-run with --write to apply.');
186
+
187
+ if (!data.findings.length) {
188
+ ui.success('No unsupported calls found.');
189
+ return;
190
+ }
191
+ ui.info('');
192
+ ui.info(`${data.findings.length} call(s) this SDK does not support:`);
193
+ for (const finding of data.findings) {
194
+ ui.info(` ${finding.file}:${finding.line} ${finding.what}`);
195
+ ui.info(` ${finding.advice}`);
196
+ }
197
+ });
198
+ },
199
+ };
200
+
201
+ module.exports = {
202
+ summary: 'Port an application from Supabase',
203
+ usage: 'motherbase migrate codemod <dir> [--write]',
204
+ subcommands: { codemod },
205
+ // Exported for the unit test: the scanner is the part worth pinning.
206
+ _internals: { rewriteImports, scanUnsupported },
207
+ };