@supacontrol/cli 0.1.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 +303 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/package.json +80 -0
package/README.md
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# @supacontrol/cli
|
|
2
|
+
|
|
3
|
+
> Safety-first CLI wrapper for Supabase with environment guards and confirmation prompts
|
|
4
|
+
|
|
5
|
+
[](https://github.com/your-org/supacontrol/actions/workflows/ci.yml)
|
|
6
|
+
[](https://www.npmjs.com/package/@supacontrol/cli)
|
|
7
|
+
|
|
8
|
+
## Why SupaControl?
|
|
9
|
+
|
|
10
|
+
The Supabase CLI is powerful but dangerous. One wrong command on the wrong branch can wipe your production database. **SupaControl adds safety guards:**
|
|
11
|
+
|
|
12
|
+
- ๐ **Environment locking** - Production locked by default
|
|
13
|
+
- โ
**Confirmation prompts** - Type environment name to confirm destructive operations
|
|
14
|
+
- ๐ฟ **Branch-based auto-detection** - Automatically targets the right environment
|
|
15
|
+
- ๐ก๏ธ **Project ref validation** - Prevents operating on wrong database
|
|
16
|
+
- ๐งน **Clean git checks** - Requires clean working directory for safety
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# npm
|
|
22
|
+
npm install -g @supacontrol/cli
|
|
23
|
+
|
|
24
|
+
# pnpm
|
|
25
|
+
pnpm add -g @supacontrol/cli
|
|
26
|
+
|
|
27
|
+
# yarn
|
|
28
|
+
yarn global add @supacontrol/cli
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Initialize in your Supabase project (requires supabase init first)
|
|
35
|
+
supacontrol init
|
|
36
|
+
|
|
37
|
+
# Check current status
|
|
38
|
+
supacontrol status
|
|
39
|
+
|
|
40
|
+
# Push migrations (with safety guards)
|
|
41
|
+
supacontrol push
|
|
42
|
+
|
|
43
|
+
# Reset database (requires confirmation)
|
|
44
|
+
supacontrol reset
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Configuration
|
|
48
|
+
|
|
49
|
+
SupaControl uses a `supacontrol.toml` file in your project root:
|
|
50
|
+
|
|
51
|
+
```toml
|
|
52
|
+
[settings]
|
|
53
|
+
# Fail on any guard warning (default: false)
|
|
54
|
+
strict_mode = false
|
|
55
|
+
|
|
56
|
+
# Require clean git working tree (default: true)
|
|
57
|
+
require_clean_git = true
|
|
58
|
+
|
|
59
|
+
# Show migration diff before push (default: true)
|
|
60
|
+
show_migration_diff = true
|
|
61
|
+
|
|
62
|
+
[environments.staging]
|
|
63
|
+
# Supabase project reference
|
|
64
|
+
project_ref = "your-staging-project-ref"
|
|
65
|
+
|
|
66
|
+
# Git branches that map to this environment
|
|
67
|
+
git_branches = ["develop", "staging"]
|
|
68
|
+
|
|
69
|
+
# Operations that require confirmation
|
|
70
|
+
protected_operations = ["reset"]
|
|
71
|
+
|
|
72
|
+
[environments.production]
|
|
73
|
+
project_ref = "your-production-project-ref"
|
|
74
|
+
git_branches = ["main", "master"]
|
|
75
|
+
protected_operations = ["push", "reset", "seed"]
|
|
76
|
+
|
|
77
|
+
# Custom confirmation word (default: environment name)
|
|
78
|
+
confirm_word = "production"
|
|
79
|
+
|
|
80
|
+
# Lock environment (blocks ALL destructive operations)
|
|
81
|
+
locked = true
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Configuration Options
|
|
85
|
+
|
|
86
|
+
#### Settings
|
|
87
|
+
|
|
88
|
+
| Option | Type | Default | Description |
|
|
89
|
+
|--------|------|---------|-------------|
|
|
90
|
+
| `strict_mode` | boolean | `false` | Fail on warnings, not just errors |
|
|
91
|
+
| `require_clean_git` | boolean | `true` | Require clean git working directory |
|
|
92
|
+
| `show_migration_diff` | boolean | `true` | Show diff before push |
|
|
93
|
+
|
|
94
|
+
#### Environment Options
|
|
95
|
+
|
|
96
|
+
| Option | Type | Default | Description |
|
|
97
|
+
|--------|------|---------|-------------|
|
|
98
|
+
| `project_ref` | string | - | Supabase project reference |
|
|
99
|
+
| `git_branches` | string[] | `[]` | Branches mapping to this environment |
|
|
100
|
+
| `protected_operations` | string[] | `[]` | Operations requiring confirmation |
|
|
101
|
+
| `confirm_word` | string | env name | Word to type for confirmation |
|
|
102
|
+
| `locked` | boolean | `true` for production | Block all destructive operations |
|
|
103
|
+
|
|
104
|
+
#### Protected Operations
|
|
105
|
+
|
|
106
|
+
- `push` - Push migrations
|
|
107
|
+
- `reset` - Reset database
|
|
108
|
+
- `pull` - Pull schema changes
|
|
109
|
+
- `seed` - Run seed files
|
|
110
|
+
- `link` - Link to project
|
|
111
|
+
- `unlink` - Unlink project
|
|
112
|
+
|
|
113
|
+
#### Branch Patterns
|
|
114
|
+
|
|
115
|
+
Use wildcards to match multiple branches:
|
|
116
|
+
|
|
117
|
+
```toml
|
|
118
|
+
[environments.preview]
|
|
119
|
+
git_branches = ["feature/*", "pr/*", "preview/*"]
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Commands
|
|
123
|
+
|
|
124
|
+
### `supacontrol init`
|
|
125
|
+
|
|
126
|
+
Interactive setup wizard to create `supacontrol.toml`.
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
supacontrol init
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### `supacontrol status`
|
|
133
|
+
|
|
134
|
+
Show current environment, linked project, and configuration.
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
supacontrol status
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### `supacontrol push`
|
|
141
|
+
|
|
142
|
+
Push local migrations to remote database.
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
# Auto-detect environment from git branch
|
|
146
|
+
supacontrol push
|
|
147
|
+
|
|
148
|
+
# Target specific environment
|
|
149
|
+
supacontrol push -e staging
|
|
150
|
+
|
|
151
|
+
# Dry run (show what would be pushed)
|
|
152
|
+
supacontrol push --dry-run
|
|
153
|
+
|
|
154
|
+
# Force (bypass guards - use with caution!)
|
|
155
|
+
supacontrol push --force
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### `supacontrol reset`
|
|
159
|
+
|
|
160
|
+
Reset remote database (destructive!).
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
supacontrol reset
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### `supacontrol pull`
|
|
167
|
+
|
|
168
|
+
Pull schema changes from remote database.
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
supacontrol pull
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### `supacontrol switch`
|
|
175
|
+
|
|
176
|
+
Switch to a different environment (relinks Supabase project).
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
supacontrol switch staging
|
|
180
|
+
supacontrol switch production
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### `supacontrol lock/unlock`
|
|
184
|
+
|
|
185
|
+
Lock or unlock an environment.
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
supacontrol lock production
|
|
189
|
+
supacontrol unlock staging
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### `supacontrol doctor`
|
|
193
|
+
|
|
194
|
+
Health check for your setup.
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
supacontrol doctor
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## CI/CD Usage
|
|
201
|
+
|
|
202
|
+
### GitHub Actions
|
|
203
|
+
|
|
204
|
+
```yaml
|
|
205
|
+
- name: Push migrations
|
|
206
|
+
run: |
|
|
207
|
+
supacontrol push -e production --ci --i-know-what-im-doing
|
|
208
|
+
env:
|
|
209
|
+
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### CI Mode Flags
|
|
213
|
+
|
|
214
|
+
| Flag | Description |
|
|
215
|
+
|------|-------------|
|
|
216
|
+
| `--ci` | Non-interactive mode |
|
|
217
|
+
| `--i-know-what-im-doing` | Required for protected operations in CI |
|
|
218
|
+
| `-e, --env <name>` | Explicit environment (required in CI) |
|
|
219
|
+
|
|
220
|
+
### Environment Variables
|
|
221
|
+
|
|
222
|
+
| Variable | Description |
|
|
223
|
+
|----------|-------------|
|
|
224
|
+
| `SUPABASE_ACCESS_TOKEN` | Supabase access token for API operations |
|
|
225
|
+
|
|
226
|
+
## Safety Features
|
|
227
|
+
|
|
228
|
+
### Lock Guard
|
|
229
|
+
|
|
230
|
+
Production environments are locked by default. Locked environments block ALL destructive operations:
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
โ Environment 'production' is locked
|
|
234
|
+
Suggestions:
|
|
235
|
+
โข Set 'locked = false' in supacontrol.toml for [environments.production]
|
|
236
|
+
โข Or use --force flag to override (not recommended for production)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Operation Guard
|
|
240
|
+
|
|
241
|
+
Protected operations require typing a confirmation word:
|
|
242
|
+
|
|
243
|
+
```
|
|
244
|
+
โ This will reset the staging database
|
|
245
|
+
Type 'staging' to confirm:
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Project Guard
|
|
249
|
+
|
|
250
|
+
Validates that the linked Supabase project matches the expected environment:
|
|
251
|
+
|
|
252
|
+
```
|
|
253
|
+
โ Project mismatch: linked to 'wrong-project' but 'production' expects 'prod-project'
|
|
254
|
+
Suggestions:
|
|
255
|
+
โข Run 'supabase link --project-ref prod-project' to switch
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Git Guard
|
|
259
|
+
|
|
260
|
+
Requires clean git working directory for destructive operations:
|
|
261
|
+
|
|
262
|
+
```
|
|
263
|
+
โ Uncommitted changes detected
|
|
264
|
+
Suggestions:
|
|
265
|
+
โข Commit or stash your changes before running this command
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Aliases
|
|
269
|
+
|
|
270
|
+
The CLI is available under three names:
|
|
271
|
+
|
|
272
|
+
- `supacontrol` - Full name
|
|
273
|
+
- `supac` - Short name
|
|
274
|
+
- `spc` - Very short name
|
|
275
|
+
|
|
276
|
+
## Contributing
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
# Clone the repo
|
|
280
|
+
git clone https://github.com/your-org/supacontrol.git
|
|
281
|
+
cd supacontrol
|
|
282
|
+
|
|
283
|
+
# Install dependencies
|
|
284
|
+
pnpm install
|
|
285
|
+
|
|
286
|
+
# Run tests
|
|
287
|
+
pnpm --filter @supacontrol/cli test
|
|
288
|
+
|
|
289
|
+
# Build
|
|
290
|
+
pnpm --filter @supacontrol/cli build
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Test Fixtures
|
|
294
|
+
|
|
295
|
+
Test fixtures in `tests/fixtures/` are protected by SHA256 checksums. If a test fails:
|
|
296
|
+
|
|
297
|
+
1. **DO NOT** modify the fixture
|
|
298
|
+
2. **DO** fix the implementation in `src/`
|
|
299
|
+
3. If the expected behavior changed, update fixtures and run `pnpm verify-fixtures --update`
|
|
300
|
+
|
|
301
|
+
## License
|
|
302
|
+
|
|
303
|
+
MIT ยฉ [Your Name]
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
|
|
3
|
+
interface GlobalOptions {
|
|
4
|
+
verbose: boolean;
|
|
5
|
+
ci: boolean;
|
|
6
|
+
env?: string;
|
|
7
|
+
}
|
|
8
|
+
declare const program: Command;
|
|
9
|
+
declare function withErrorHandling<T extends (...args: unknown[]) => Promise<void>>(fn: T): (...args: Parameters<T>) => Promise<void>;
|
|
10
|
+
|
|
11
|
+
export { type GlobalOptions, program, withErrorHandling };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{Command as pt}from"commander";import Ee from"picocolors";import{createRequire as gt}from"module";import{Command as mn}from"commander";import y from"picocolors";import{readFile as Fo}from"fs/promises";import{resolve as qo,dirname as Ct}from"path";import{parse as Uo}from"smol-toml";import ve from"picocolors";import{z as x}from"zod";var Do=x.enum(["push","reset","pull","seed","link","unlink"]),Ve=x.object({strict_mode:x.boolean().default(!1),require_clean_git:x.boolean().default(!0),show_migration_diff:x.boolean().default(!0)}),ze=x.object({project_ref:x.string().optional(),git_branches:x.array(x.string()).default([]),protected_operations:x.array(Do).default([]),confirm_word:x.string().optional(),locked:x.boolean().optional()}),Ye=x.object({settings:Ve.default({strict_mode:!1,require_clean_git:!0,show_migration_diff:!0}),environments:x.record(x.string(),ze).default({})}),Ke=x.object({settings:Ve.optional(),environments:x.record(x.string(),ze).optional()});function G(e,o){return o.locked!==void 0?o.locked:e==="production"||o.git_branches.includes("main")||o.git_branches.includes("master")}var Ho=["supacontrol.toml","config/supacontrol.toml"],oe=class extends Error{filePath;constructor(o,n,t){super(o,{cause:t}),this.name="ConfigError",this.filePath=n}};async function Wo(e){for(let o of Ho){let n=qo(e,o);try{return{content:await Fo(n,"utf-8"),path:n}}catch(t){if(t instanceof Error&&"code"in t&&t.code==="ENOENT")continue;throw new oe(`Failed to read config file: ${n}`,n,t instanceof Error?t:void 0)}}return null}function Ze(e){return e.issues.map(n=>{let t=n.path.join(".");return` ${ve.dim(t?`${t}: `:"")}${n.message}`}).join(`
|
|
3
|
+
`)}async function ce(e){let o=e??process.cwd(),n=await Wo(o);if(!n)return null;let t;try{t=Uo(n.content)}catch(s){throw new oe(`Invalid TOML syntax in ${n.path}:
|
|
4
|
+
${s instanceof Error?s.message:String(s)}`,n.path,s instanceof Error?s:void 0)}let r=Ke.safeParse(t);if(!r.success)throw new oe(`Invalid config in ${n.path}:
|
|
5
|
+
${Ze(r.error)}`,n.path);let i=Ye.safeParse(t);if(!i.success)throw new oe(`Invalid config in ${n.path}:
|
|
6
|
+
${Ze(i.error)}`,n.path);return i.data}async function F(e){try{let o=await ce(e);return o||(console.error(ve.red("\u2717"),"No supacontrol.toml found"),console.error(ve.dim(" Run `supacontrol init` to create one")),process.exit(1)),o}catch(o){throw o instanceof oe&&(console.error(ve.red("\u2717"),o.message),process.exit(1)),o}}function H(e,o){let n=o.environments[e];return n?{name:e,config:n,projectRef:n.project_ref,matchType:"exact"}:null}function ne(e){return Object.keys(e.environments)}function W(e,o){if(!e)return null;let n=Object.entries(o.environments);for(let[t,r]of n)if(r&&r.project_ref===e)return{name:t,config:r,projectRef:r.project_ref,matchType:"exact"};return null}import{execa as Je}from"execa";var le=null,te=null,pe=null;function Y(){le=null,te=null,pe=null}async function Le(e,o){let n=o?await Je("git",e,{cwd:o}):await Je("git",e);return typeof n.stdout=="string"?n.stdout:""}async function Ce(e){if(pe!==null)return pe;try{return await Le(["rev-parse","--is-inside-work-tree"],e),pe=!0,!0}catch{return pe=!1,!1}}async function V(e){if(le!==null)return le;try{return await Ce(e)?(le=(await Le(["symbolic-ref","--short","HEAD"],e)).trim(),le):null}catch{return le=null,null}}async function J(e){if(te!==null)return te;try{return await Ce(e)?(te=(await Le(["status","--porcelain"],e)).trim().length>0,te):(te=!1,!1)}catch{return te=!0,!0}}import{readFile as Vo}from"fs/promises";import{resolve as zo}from"path";var Pe=["push","reset","seed","pull","migrate"];function M(e){return{allowed:!0,...e}}function ue(e,o){return{allowed:!1,reason:e,...o}}function Te(e,o){return{allowed:!0,requiresConfirmation:!0,confirmWord:e,...o}}function Qe(e){for(let a of e)if(!a.allowed)return a;let o=["low","medium","high","critical"],n="low",t=!1,r,i=[];for(let a of e){if(a.riskLevel){let l=o.indexOf(n);o.indexOf(a.riskLevel)>l&&(n=a.riskLevel)}a.requiresConfirmation&&(t=!0,a.confirmWord&&(r=a.confirmWord)),a.suggestions&&i.push(...a.suggestions)}let s={allowed:!0,riskLevel:n,requiresConfirmation:t};return r!==void 0&&(s.confirmWord=r),i.length>0&&(s.suggestions=[...new Set(i)]),s}var Yo="supabase/.temp/project-ref",re;function q(){re=void 0}async function _(e){if(re!==void 0)return re;let o=e??process.cwd(),n=zo(o,Yo);try{return re=(await Vo(n,"utf-8")).trim(),re}catch(t){return t instanceof Error&&"code"in t&&t.code==="ENOENT",re=null,null}}async function Xe(e){let{environment:o,environmentName:n}=e;if(!o.project_ref)return M({suggestions:[`Consider adding 'project_ref' to [environments.${n}] for extra safety`]});let t=await _();return t===null?M({suggestions:["No Supabase project is currently linked",`Run 'supabase link --project-ref ${o.project_ref}' to link`]}):t!==o.project_ref?ue(`Project mismatch: linked to '${t}' but '${n}' expects '${o.project_ref}'`,{suggestions:[`Run 'supabase link --project-ref ${o.project_ref}' to switch`,`Or update [environments.${n}].project_ref in supacontrol.toml`],riskLevel:"high"}):M()}import{execa as Oe}from"execa";import Q from"picocolors";var ke;async function me(){if(ke!==void 0)return ke;try{return await Oe("supabase",["--version"]),ke=!0,!0}catch{return ke=!1,!1}}async function je(){try{let e=await Oe("supabase",["--version"]),o=typeof e.stdout=="string"?e.stdout:"",n=o.match(/(\d+\.\d+\.\d+)/);return n&&n[1]?n[1]:o.trim()||null}catch{return null}}async function E(e,o={}){let{cwd:n,stream:t=!0,env:r}=o;if(!await me())return console.error(Q.red("\u2717"),"Supabase CLI is not installed"),console.error(Q.dim(" Install it with: npm install -g supabase")),console.error(Q.dim(" Or: brew install supabase/tap/supabase")),{exitCode:1,stdout:"",stderr:"Supabase CLI not installed",success:!1};try{let s={reject:!1,...n?{cwd:n}:{},...r?{env:r}:{},...t?{stdio:"inherit"}:{},...o.input?{input:o.input}:{}},a=await Oe("supabase",e,s);return{exitCode:a.exitCode??0,stdout:typeof a.stdout=="string"?a.stdout:"",stderr:typeof a.stderr=="string"?a.stderr:"",success:a.exitCode===0}}catch(s){return{exitCode:1,stdout:"",stderr:s instanceof Error?s.message:String(s),success:!1}}}async function X(){await me()||(console.error(Q.red("\u2717"),"Supabase CLI is not installed"),console.error(),console.error(Q.dim("Install it with one of:")),console.error(Q.dim(" npm install -g supabase")),console.error(Q.dim(" brew install supabase/tap/supabase")),console.error(Q.dim(" scoop install supabase")),console.error(),process.exit(1))}import{homedir as eo}from"os";import{join as oo}from"path";import{readFile as Ko,writeFile as Zo,mkdir as Jo,chmod as Qo}from"fs/promises";import*as K from"@clack/prompts";import no from"picocolors";var Xo="SUPABASE_ACCESS_TOKEN",to=".supacontrol",en="credentials",on="https://supabase.com/dashboard/account/tokens";function Ae(){return oo(eo(),to,en)}function nn(){return oo(eo(),to)}async function ge(){let e=process.env[Xo];if(e)return e;try{let o=Ae(),t=(await Ko(o,"utf-8")).trim();if(t)return t}catch{}return null}async function tn(e){let o=nn(),n=Ae();await Jo(o,{recursive:!0}),await Zo(n,e,"utf-8");try{await Qo(n,384)}catch{}}async function rn(){console.log(),K.note(["To fetch your Supabase projects, we need an access token.","",`Generate one at: ${no.cyan(on)}`,"",'Select "Generate new token" and copy the token.'].join(`
|
|
7
|
+
`),"Authentication Required");let e=await K.password({message:"Paste your Supabase access token:",validate(o){if(!o||o.length<10)return"Please enter a valid access token"}});return K.isCancel(e)?null:e}async function ro(e){let{skipPrompt:o=!1,saveToken:n=!0}=e??{},t=await ge();if(t)return t;if(o)return null;let r=await rn();if(!r)return null;if(n){let i=await K.confirm({message:"Save token for future use?",initialValue:!0});!K.isCancel(i)&&i&&(await tn(r),console.log(no.dim(` Saved to ${Ae()}`)))}return r}import{z as v}from"zod";var sn="https://api.supabase.com/v1",an=120,cn=v.enum(["ACTIVE_HEALTHY","ACTIVE_UNHEALTHY","COMING_UP","GOING_DOWN","INACTIVE","INIT_FAILED","REMOVED","RESTORING","UNKNOWN","UPGRADING","PAUSING","PAUSED"]),ln=v.enum(["free","pro","team","enterprise","platform"]),un=v.object({id:v.string(),name:v.string(),plan:ln.optional(),allowed_release_channels:v.array(v.string()).optional(),opt_in_tags:v.array(v.string()).optional()}),so=v.object({id:v.string(),organization_id:v.string(),organization_slug:v.string().optional(),name:v.string(),region:v.string(),created_at:v.string(),database:v.object({host:v.string(),version:v.string()}).optional(),status:cn,is_branch_enabled:v.boolean().optional(),preview_branch_refs:v.array(v.string()).optional()}),io=v.object({id:v.string(),name:v.string(),project_ref:v.string(),parent_project_ref:v.string(),is_default:v.boolean(),status:v.string(),created_at:v.string().optional()}),z=class extends Error{constructor(n,t,r){super(n);this.statusCode=t;this.response=r;this.name="SupabaseAPIError"}},Ie=class{accessToken;requestCount=0;requestWindowStart=Date.now();constructor(o){this.accessToken=o}async request(o,n={}){await this.checkRateLimit();let t=`${sn}${o}`,r=await fetch(t,{...n,headers:{Authorization:`Bearer ${this.accessToken}`,"Content-Type":"application/json",...n.headers}});if(this.requestCount++,!r.ok){let i=`API request failed: ${r.status} ${r.statusText}`,s;try{s=await r.json(),typeof s=="object"&&s!==null&&"message"in s&&(i=String(s.message))}catch{}throw r.status===401?new z("Invalid or expired access token. Run `supabase login` to re-authenticate.",r.status,s):r.status===429?new z("Rate limit exceeded. Please wait a moment and try again.",r.status,s):new z(i,r.status,s)}return r.json()}async checkRateLimit(){let o=Date.now(),n=60*1e3;if(o-this.requestWindowStart>n&&(this.requestCount=0,this.requestWindowStart=o),this.requestCount>=an){let t=n-(o-this.requestWindowStart);t>0&&(await new Promise(r=>setTimeout(r,t)),this.requestCount=0,this.requestWindowStart=Date.now())}}async authenticate(){try{return await this.getProjects(),!0}catch(o){if(o instanceof z&&o.statusCode===401)return!1;throw o}}async getProjects(){let o=await this.request("/projects"),n=[];for(let t of o){let r=so.safeParse(t);r.success&&n.push(r.data)}return n}async getProject(o){try{let n=await this.request(`/projects/${o}`),t=so.safeParse(n);return t.success?t.data:null}catch(n){if(n instanceof z&&n.statusCode===404)return null;throw n}}async getProjectByName(o){return(await this.getProjects()).find(t=>t.name.toLowerCase()===o.toLowerCase())??null}async getOrganization(o){try{let n=await this.request(`/organizations/${o}`),t=un.safeParse(n);return t.success?t.data:null}catch(n){if(n instanceof z&&n.statusCode===404)return null;throw n}}async getOrganizationForProject(o){let n=o.organization_slug||o.organization_id;return this.getOrganization(n)}async getBranches(o){try{let n=await this.request(`/projects/${o}/branches`),t=[];for(let r of n){let i=io.safeParse(r);i.success&&t.push(i.data)}return t}catch(n){if(n instanceof z&&(n.statusCode===403||n.statusCode===400))return[];throw n}}async createBranch(o,n){let t=await this.request(`/projects/${o}/branches`,{method:"POST",body:JSON.stringify({branch_name:n})}),r=io.safeParse(t);return r.success?r.data:null}async deleteBranch(o,n){try{return await this.request(`/projects/${o}/branches/${n}`,{method:"DELETE"}),!0}catch(t){return t instanceof z&&t.statusCode===404}}async checkBranchingCapability(o){let n="unknown";try{let l=await this.getOrganizationForProject(o);l?.plan&&(n=l.plan)}catch{}let r=n!=="unknown"&&["pro","team","enterprise","platform"].includes(n),i=n==="free",s=[];try{s=await this.getBranches(o.id)}catch{}return i?{available:!1,plan:n,branches:s,reason:"free_plan"}:r?{available:!0,plan:n,branches:s,reason:"paid_plan"}:s.filter(l=>!l.is_default).length>0?{available:!0,plan:n,branches:s,reason:"has_non_default_branches"}:{available:!1,plan:n,branches:s,reason:"unknown_capability"}}async isBranchingAvailable(o){return(await this.checkBranchingCapability(o)).available}};function $e(e){return new Ie(e)}function ao(){return new mn("status").description("Show current environment and project status").action(async()=>{await pn()})}async function dn(e,o,n){let t=o.find(r=>r.id===e);if(t)return{ref:e,type:"project",name:t.name};for(let r of o)if(r.status==="ACTIVE_HEALTHY")try{let s=(await n.getBranches(r.id)).find(a=>a.project_ref===e);if(s)return{ref:e,type:"branch",name:s.name,parentProjectName:r.name,parentProjectRef:r.id}}catch{}return{ref:e,type:"unknown"}}async function pn(){console.log(),console.log(y.bold("SupaControl Status")),console.log(y.dim("\u2500".repeat(50)));let e=await ce();if(!e){console.log(),console.log(y.yellow("\u26A0"),"No supacontrol.toml found"),console.log(y.dim(" Run: supacontrol init")),console.log();return}let o=await _(),n=null,t=await ge(),r=!1;if(o&&t)try{let g=$e(t),p=await g.getProjects();n=await dn(o,p,g)}catch{r=!0}let i=W(o,e);if(console.log(),i){let g=G(i.name,i.config),p=g?y.red("\u{1F512}"):y.green("\u{1F513}"),h=g?y.red("LOCKED"):y.green("unlocked");console.log(y.bold(`Active Environment: ${y.cyan(i.name)} ${p}`)),n?.type==="project"?console.log(` Project: ${n.name}`):n?.type==="branch"?console.log(` Project: ${n.parentProjectName} ${y.dim(`(${n.name} branch)`)}`):o&&console.log(` Project: ${o}`),console.log(` Status: ${h}`),i.config.protected_operations.length>0&&console.log(` Protected: ${y.yellow(i.config.protected_operations.join(", "))}`)}else o?(console.log(y.bold(`Active Environment: ${y.yellow("unknown")}`)),n?.type==="project"?console.log(` Linked to: ${n.name} ${y.dim(`(${o})`)}`):n?.type==="branch"?(console.log(` Linked to: ${n.name} branch ${y.dim(`(${o})`)}`),console.log(` Parent: ${n.parentProjectName}`)):console.log(` Linked to: ${o}`),console.log(),console.log(y.yellow(" \u26A0 This project is not configured in supacontrol.toml")),console.log(y.dim(" Add it to an environment or run: supacontrol init"))):(console.log(y.bold(`Active Environment: ${y.dim("none")}`)),console.log(y.dim(" No Supabase project linked")),console.log(y.dim(" Run: supacontrol switch <environment>")));console.log();let s=await V(),a=await J();if(s){let g=a?y.yellow(" *"):"";console.log(`Git: ${y.cyan(s)}${g}`)}else console.log(`Git: ${y.dim("not a git repository")}`);let l=Object.keys(e.environments);if(l.length>0){console.log(),console.log(y.bold("Environments"));for(let g of l){let p=e.environments[g];if(!p)continue;let m=G(g,p)?y.red("\u{1F512}"):y.green("\u{1F513}"),b=i?.name===g,P=b?y.cyan("\u2192"):" ",R=b?y.cyan(" \u2190 active"):"";console.log(` ${P} ${g} ${m}${R}`)}}if(console.log(),await me()){let g=await je();console.log(y.dim(`Supabase CLI v${g}`))}else console.log(y.red("Supabase CLI not installed"));r&&(console.log(),console.log(y.dim("Note: Could not fetch project details from API")),console.log(y.dim(" Your access token may have expired")),console.log(y.dim(" Generate a new one: https://supabase.com/dashboard/account/tokens"))),console.log()}import{Command as Sn}from"commander";import $ from"picocolors";import Ne from"picocolors";function co(e){let{environmentName:o,environment:n,operation:t}=e;return Pe.includes(t)?G(o,n)?ue(`Environment '${o}' is locked`,{suggestions:[`Set 'locked = false' in supacontrol.toml for [environments.${o}]`,"Or use --force flag to override (not recommended for production)"],riskLevel:"critical"}):M():M()}var lo={diff:"low",pull:"low",push:"medium",migrate:"medium",seed:"high",reset:"critical",link:"low",unlink:"medium"};function uo(e){let{operation:o,environment:n,environmentName:t,isCI:r}=e,i=lo[o]??"medium";if(!n.protected_operations.includes(o))return M({riskLevel:i});let a=n.confirm_word??t;return r?Te(a,{reason:`Operation '${o}' on '${t}' requires confirmation`,riskLevel:i}):Te(a,{reason:`This operation is protected. Type '${a}' to confirm.`,riskLevel:i})}function Me(e){return lo[e]??"medium"}function mo(e){let{config:o,hasUncommittedChanges:n,operation:t}=e;return o.settings.require_clean_git?Pe.includes(t)?n?ue("Git working directory has uncommitted changes",{suggestions:["Run `git stash` to temporarily store changes","Or run `git commit` to commit your changes","Or set `require_clean_git = false` in supacontrol.toml"],riskLevel:"medium"}):M():M():M()}import*as N from"@clack/prompts";import T from"picocolors";var po={low:{color:T.blue,label:"Low Risk"},medium:{color:T.yellow,label:"Medium Risk"},high:{color:T.red,label:"High Risk"},critical:{color:e=>T.bold(T.red(e)),label:"CRITICAL RISK"}},gn={push:"Push local migrations to the remote database",reset:"Reset the remote database to match local migrations",pull:"Pull remote schema changes to local migrations",seed:"Run seed data on the remote database",migrate:"Run database migrations",diff:"Show differences between local and remote schemas",link:"Link to a Supabase project",unlink:"Unlink from the current Supabase project"};async function Be(e){let{environmentName:o,operation:n,riskLevel:t,confirmWord:r,isCI:i,reason:s}=e,a=po[t],l=gn[n]??n;if(i)return console.error(T.red("\u2717"),"Cannot confirm interactively in CI mode"),console.error(T.dim(" Use --i-know-what-im-doing flag to bypass confirmation")),{confirmed:!1};if(N.note([a.color(`${a.label}`),"",`Operation: ${T.bold(n)}`,`Environment: ${T.bold(o)}`,`Description: ${l}`,s?`
|
|
8
|
+
Reason: ${s}`:""].filter(Boolean).join(`
|
|
9
|
+
`),a.color("\u26A0 Confirmation Required")),t==="critical"||r){let g=r??o,p=await N.text({message:`Type '${T.bold(g)}' to confirm:`,placeholder:g,validate(h){if(h!==g)return`Please type exactly '${g}' to confirm`}});return N.isCancel(p)?(N.cancel("Operation cancelled"),{confirmed:!1,cancelled:!0}):{confirmed:p===g}}let f=await N.confirm({message:"Do you want to proceed?",initialValue:!1});return N.isCancel(f)?(N.cancel("Operation cancelled"),{confirmed:!1,cancelled:!0}):{confirmed:f}}function Ge(e,o,n,t){let r=po[t];console.log(),console.log(T.blue("\u2192"),T.bold(e),"on",r.color(o)),n&&console.log(T.dim(` Project: ${n}`)),console.log(T.dim(` Risk: ${r.label}`)),console.log()}async function de(e){let o=[],n=co(e);if(!n.allowed)return Se(n),n;o.push(n);let t=uo(e);if(!t.allowed)return Se(t),t;o.push(t);let r=await Xe(e);if(!r.allowed)return Se(r),r;o.push(r);let i=mo(e);if(!i.allowed)return Se(i),i;o.push(i);let s=Qe(o);if(s.requiresConfirmation){let a=s.riskLevel??Me(e.operation),{confirmed:l,cancelled:f}=await Be({environmentName:e.environmentName,operation:e.operation,riskLevel:a,confirmWord:s.confirmWord,isCI:e.isCI,reason:s.reason});return f?{...s,allowed:!1,cancelled:!0}:l?{...s,allowed:!0,confirmed:!0}:{...s,allowed:!1,reason:"Confirmation declined"}}return Ge(e.operation,e.environmentName,e.environment.project_ref,s.riskLevel??"low"),s}function Se(e){if(console.error(),console.error(Ne.red("\u2717"),e.reason??"Operation blocked"),e.suggestions&&e.suggestions.length>0){console.error(),console.error(Ne.dim("Suggestions:"));for(let o of e.suggestions)console.error(Ne.dim(` \u2022 ${o}`))}console.error()}import{readdir as bo,readFile as yo,writeFile as se,mkdir as fn,rename as hn}from"fs/promises";import{resolve as ie,join as he,basename as go}from"path";import{createHash as bn}from"crypto";import d from"picocolors";import*as O from"@clack/prompts";function fo(e){return bn("sha256").update(e).digest("hex").slice(0,16)}async function yn(){let e=ie(process.cwd(),"supabase","migrations");try{return(await bo(e)).filter(n=>n.endsWith(".sql")&&!n.endsWith(".remote.sql")).map(n=>n.replace(".sql","")).sort()}catch{return[]}}async function fe(){let e=ie(process.cwd(),"supabase","migrations");try{return(await bo(e)).filter(n=>n.endsWith(".sql")&&!n.endsWith(".remote.sql")).map(n=>{let t=n.match(/^(\d{14})_?(.*)\.sql$/),r=t?.[1]??n.replace(".sql",""),i=t?.[2]??"";return{timestamp:r,name:i,filename:n,fullPath:he(e,n)}}).sort((n,t)=>n.timestamp.localeCompare(t.timestamp))}catch{return[]}}async function De(){let e=await E(["migration","list"],{stream:!1});if(!e.success||!e.stdout)return[];let o=e.stdout.split(`
|
|
10
|
+
`),n=[];for(let t of o){if(t.includes("Local")||t.includes("---")||!t.trim())continue;let r=t.split("|");if(r.length>=2){let i=r[1]?.trim();if(i){let s=i.match(/^(\d{14})$/);s?.[1]&&n.push(s[1])}}}return n.sort()}async function Fe(){try{let[e,o]=await Promise.all([yn(),De()]),n=e.map(i=>i.match(/^(\d{14})/)?.[1]??i),t=o.filter(i=>!n.includes(i)),r=n.filter(i=>i!==void 0&&!o.includes(i));return{needsSync:t.length>0,remoteMissing:t,localMissing:r}}catch(e){return{needsSync:!1,remoteMissing:[],localMissing:[],error:e instanceof Error?e.message:"Unknown error"}}}async function wn(){let e=ie(process.cwd(),"supabase","migrations");try{await fn(e,{recursive:!0})}catch{}let o=await E(["migration","fetch","--linked"],{stream:!1,input:`y
|
|
11
|
+
`});return o.success?{success:!0,fetched:await fe()}:{success:!1,error:o.stderr||"Failed to fetch migrations",fetched:[]}}async function vn(e){return await yo(e.fullPath,"utf-8")}function qe(e,o){let n=new Set(e.split(`
|
|
12
|
+
`).map(s=>s.trim()).filter(Boolean)),t=new Set(o.split(`
|
|
13
|
+
`).map(s=>s.trim()).filter(Boolean)),r=[],i=[];for(let s of n)t.has(s)||r.push(s);for(let s of t)n.has(s)||i.push(s);return{additions:r,removals:i}}function Cn(e){let{additions:o,removals:n}=qe(e.localContent,e.remoteContent);if(console.log(),console.log(d.bold(` File: ${e.filename}`)),console.log(),n.length>0){console.log(d.dim(" In database (will be kept):"));for(let t of n.slice(0,8))console.log(d.red(` - ${t.slice(0,70)}`));n.length>8&&console.log(d.dim(` ... and ${n.length-8} more lines`)),console.log()}if(o.length>0){console.log(d.dim(" In your local file (NOT in database):"));for(let t of o.slice(0,8))console.log(d.green(` + ${t.slice(0,70)}`));o.length>8&&console.log(d.dim(` ... and ${o.length-8} more lines`))}}async function Pn(e){console.log(),console.log(d.yellow("\u26A0"),d.bold("Local file differs from applied migration")),console.log(),Cn(e),console.log(),O.note([d.bold(d.yellow("Your local edits are NOT in the database.")),"","It looks like this migration file was edited after it was","already applied to your database. That's a common mistake!","",d.bold("How migrations work:"),' \u2022 Each migration runs ONCE, then is marked as "done"'," \u2022 Editing the file later has no effect - it won't re-run"," \u2022 To change your schema, create a NEW migration","",d.dim("Example: To add a column, don't edit the CREATE TABLE."),d.dim("Instead, create a new migration with ALTER TABLE ... ADD COLUMN.")].join(`
|
|
14
|
+
`),"What happened?");let o=await O.select({message:"How do you want to handle this?",options:[{value:"create-migration",label:d.green("Create new migration from my edits")+" "+d.green("(Recommended)"),hint:"Applies your changes to the database properly"},{value:"keep-remote",label:"Restore file to match database",hint:"Discards your local edits"},{value:"save-both",label:"Keep both versions",hint:"Save applied version as .remote.sql for reference"},{value:"keep-local",label:d.dim("Keep local file as-is (not recommended)"),hint:"File won't match database - you'll need to fix manually"},{value:"cancel",label:d.dim("Cancel"),hint:"Abort the sync"}]});return O.isCancel(o)?"cancel":o}function kn(){let e=new Date;return[e.getUTCFullYear(),String(e.getUTCMonth()+1).padStart(2,"0"),String(e.getUTCDate()).padStart(2,"0"),String(e.getUTCHours()).padStart(2,"0"),String(e.getUTCMinutes()).padStart(2,"0"),String(e.getUTCSeconds()).padStart(2,"0")].join("")}async function jn(e,o){let n=ie(process.cwd(),"supabase","migrations"),t=kn();if(o&&t<=o){let h=parseInt(o,10)+1;t=String(h).padStart(14,"0")}let r=e.filename.match(/^\d{14}_(.+)\.sql$/),i=r?r[1]:"update",s=`${t}_${i}_changes.sql`,a=he(n,s),{additions:l}=qe(e.localContent,e.remoteContent);if(l.length===0)return{success:!1,error:"No differences found to migrate"};let f=["-- Migration generated by SupaControl",`-- Based on local edits to: ${e.filename}`,`-- Generated: ${new Date().toISOString()}`,"--","-- \u26A0\uFE0F REVIEW THIS MIGRATION BEFORE APPLYING","-- The following changes were detected in your local file:","--"],g=l.filter(h=>{let m=h.toLowerCase();return m.includes("create ")||m.includes("alter ")||m.includes("add ")||m.includes("drop ")||m.includes("index")||m.includes("column")||m.includes("constraint")||m.includes("default")||m.includes("comment on")}),p;return g.length>0?p=[...f,"","-- Detected changes (may need manual adjustment):",...g.map(h=>`-- ${h}`),"","-- TODO: Write the actual SQL to apply these changes","-- Example: ALTER TABLE ... ADD COLUMN ...;",""].join(`
|
|
15
|
+
`):p=[...f,"","-- Could not automatically extract SQL statements.","-- Please review the diff and write the appropriate migration.","","-- Local file additions:",...l.slice(0,20).map(h=>`-- ${h}`),l.length>20?`-- ... and ${l.length-20} more lines`:"",""].filter(Boolean).join(`
|
|
16
|
+
`),await se(a,p),await se(e.localPath,e.remoteContent),{success:!0,migrationPath:a}}async function $n(e,o,n){let t=ie(process.cwd(),"supabase","migrations");switch(o){case"create-migration":{let r=await jn(e,n);if(r.success&&r.migrationPath)console.log(d.green("\u2713"),`Restored ${e.filename} to match database`),console.log(d.green("\u2713"),`Created new migration: ${go(r.migrationPath)}`),console.log(),console.log(d.yellow("\u26A0"),"Please review and edit the new migration before pushing"),console.log(d.dim(` ${r.migrationPath}`));else if(r.error==="No differences found to migrate")await se(e.localPath,e.remoteContent),console.log(d.green("\u2713"),`Restored ${e.filename} to match database`),console.log(d.dim(" (No meaningful code differences found)"));else return console.log(d.red("\u2717"),"Failed to create migration:",r.error),!1;return!0}case"keep-local":return console.log(d.yellow("\u26A0"),`Keeping local version of ${e.filename}`),console.log(d.dim(" Note: This file does not match what's in your database")),!0;case"keep-remote":return await se(e.localPath,e.remoteContent),console.log(d.green("\u2713"),`Restored ${e.filename} to match database`),!0;case"save-both":{let r=he(t,e.filename.replace(".sql",".remote.sql"));return await se(r,e.remoteContent),console.log(d.yellow("\u26A0"),`Keeping local ${e.filename} (doesn't match database)`),console.log(d.green("\u2713"),`Saved database version as ${go(r)}`),!0}case"cancel":return!1}}async function ho(e,o){let n=[],t=ie(process.cwd(),"supabase","migrations"),i=[...e].sort((a,l)=>a.timestamp.localeCompare(l.timestamp)).filter(a=>a.timestamp<=o);if(i.length===0)return n;console.log(),console.log(d.blue("\u2192"),"Reordering local migrations to come after remote...");let s=parseInt(o,10)+1;for(let a of i){let l=String(s).padStart(14,"0"),f=a.filename.replace(a.timestamp,l),g=he(t,f);await hn(a.fullPath,g),n.push({original:a.filename,renamed:f}),console.log(d.dim(` ${a.filename}`)),console.log(d.green(` \u2192 ${f}`)),s++}return n}async function wo(){return console.log(d.blue("\u2192"),"Pulling migrations from remote..."),(await E(["db","pull"],{stream:!0})).success}async function vo(){let e=await Fe();if(e.error)return console.log(d.yellow("\u26A0"),"Could not check migration sync status"),console.log(d.dim(` ${e.error}`)),{success:!0};if(e.remoteMissing.length===0&&e.localMissing.length===0)return{success:!0};if(e.remoteMissing.length===0&&e.localMissing.length>0){let m=await De(),b=m.length>0?m.sort()[m.length-1]:null;if(b&&e.localMissing.some(R=>R<=b)){console.log(d.blue("\u2192"),`${e.localMissing.length} new migration(s) need timestamp adjustment`);let I=(await fe()).filter(ae=>e.localMissing.includes(ae.timestamp)),Z=await ho(I,b);Z.length>0&&console.log(d.green("\u2713"),`Reordered ${Z.length} migration(s) to come after remote`)}else console.log(d.blue("\u2192"),`${e.localMissing.length} new migration(s) ready to push`);return{success:!0}}if(console.log(),console.log(d.yellow("\u26A0"),"Migration sync needed"),console.log(),e.remoteMissing.length>0){console.log(d.dim(" Remote has migrations not saved locally:"));for(let m of e.remoteMissing)console.log(d.blue(` + ${m}`))}if(e.localMissing.length>0){console.log(d.dim(" Local has migrations not on remote:"));for(let m of e.localMissing)console.log(d.green(` + ${m}`))}console.log(),O.note([d.bold("We will:"),"",`${d.blue("1.")} Download ${e.remoteMissing.length} migration file(s) from remote`,`${d.blue("2.")} Check for any content conflicts with your local files`,`${d.blue("3.")} Let you resolve any conflicts`,e.localMissing.length>0?`${d.blue("4.")} Reorder your local migrations if needed, then push`:"","",d.dim("This preserves both remote and local work.")].filter(Boolean).join(`
|
|
17
|
+
`),"Migration Sync");let o=await O.confirm({message:"Proceed with sync?",initialValue:!0});if(O.isCancel(o)||!o)return{success:!1,cancelled:!0};let n=await fe(),t=new Map;for(let m of n)t.set(m.timestamp,m),m.content=await vn(m);let r=O.spinner();r.start("Fetching remote migrations...");let i=await wn();if(!i.success)return r.stop("Fetch failed"),console.log(d.red("\u2717"),"Failed to fetch remote migrations:",i.error),{success:!1};r.stop(`Fetched ${e.remoteMissing.length} migration(s) from remote`);let s=[];for(let[m,b]of t){if(e.remoteMissing.includes(m)||e.localMissing.includes(m))continue;let P=b.fullPath,R;try{R=await yo(P,"utf-8")}catch{continue}let I=b.content??"",Z=fo(I),ae=fo(R);if(Z!==ae){let{additions:ye,removals:we}=qe(I,R);if(ye.length===0&&we.length===0)continue;s.push({timestamp:m,filename:b.filename,localPath:P,localContent:I,remoteContent:R,localHash:Z,remoteHash:ae})}}let a=[...e.remoteMissing].sort(),l=a.length>0?a[a.length-1]:void 0;if(s.length>0){console.log(),console.log(d.yellow("\u26A0"),`Found ${s.length} file(s) with content differences`),console.log();for(let m=0;m<s.length;m++){let b=s[m],P=m+1,R=s.length;console.log(d.blue("\u2500".repeat(60))),console.log(d.blue(` Conflict ${P} of ${R}`)),console.log(d.blue("\u2500".repeat(60)));let I=await Pn(b);if(I==="cancel"){let we=O.spinner();we.start("Restoring original files...");for(let[,_e]of t)_e.content&&await se(_e.fullPath,_e.content);return we.stop("Restored original files"),{success:!1,cancelled:!0}}let Z=O.spinner(),ae=I==="create-migration"?"Creating migration from your edits...":I==="keep-remote"?"Restoring file to match database...":I==="save-both"?"Saving both versions...":"Processing...";Z.start(ae);let ye=await $n(b,I,l);if(Z.stop(ye?"Complete":"Failed"),!ye)return{success:!1,cancelled:!0};m<s.length-1&&(console.log(),console.log(d.dim(" Moving to next conflict...")),console.log())}console.log(),console.log(d.green("\u2713"),`Resolved ${s.length} conflict(s)`)}let f=ie(process.cwd(),"supabase","migrations");for(let m of e.localMissing){let b=t.get(m);if(b&&b.content){let P=he(f,b.filename);await se(P,b.content)}}if(e.localMissing.length>0&&l){let b=(await fe()).filter(R=>e.localMissing.includes(R.timestamp)),P=await ho(b,l);P.length>0&&console.log(d.green("\u2713"),`Reordered ${P.length} local migration(s)`)}console.log(),console.log(d.green("\u2713"),"Migration sync complete!");let g=await fe(),p=await De(),h=g.filter(m=>!p.includes(m.timestamp)).map(m=>m.filename);if(h.length>0){console.log(),console.log(d.blue("\u2192"),`${h.length} migration(s) ready to push:`);for(let m of h)console.log(d.dim(` ${m}`))}return{success:!0}}function Po(){return new Sn("push").description("Push local migrations to the remote database").option("--force","Bypass all safety guards (use with caution)",!1).option("--dry-run","Show what would be pushed without executing",!1).option("--i-know-what-im-doing","Required flag for production operations in CI mode",!1).action(async function(){let e=this.optsWithGlobals();await Rn(e)})}async function Rn(e){Y(),q(),await X();let o=await F(),n=await V(),t=await J(),r;if(e.env)r=H(e.env,o),r||(console.error($.red("\u2717"),`Environment '${e.env}' not found in config`),console.error($.dim(" Available environments:"),Object.keys(o.environments).join(", ")),process.exit(1));else{let s=await _();s||(console.error($.red("\u2717"),"No Supabase project linked"),console.error($.dim(" Run: supacontrol switch <environment>")),process.exit(1)),r=W(s,o),r||(console.error($.red("\u2717"),"Linked project is not configured in supacontrol.toml"),console.error($.dim(` Linked to: ${s}`)),console.error($.dim(" Run: supacontrol init or add this project to your config")),process.exit(1))}if(e.force)console.log($.yellow("\u26A0"),"Force mode: bypassing all safety guards"),console.log();else{let s={operation:"push",environmentName:r.name,environment:r.config,config:o,gitBranch:n,isCI:e.ci,hasUncommittedChanges:t},a=await de(s);a.allowed||(a.cancelled&&process.exit(0),process.exit(1)),e.ci&&a.requiresConfirmation&&!e.iKnowWhatImDoing&&(console.error($.red("\u2717"),"CI mode requires --i-know-what-im-doing flag for this operation"),process.exit(1))}if(!e.force){let s=await vo();s.success||(s.cancelled&&process.exit(0),console.log(),console.log($.dim("Use --force to push anyway (not recommended)")),process.exit(1))}if(o.settings.show_migration_diff&&!e.dryRun){console.log($.blue("\u2192"),"Checking for pending migrations...");let s=await E(["db","diff"],{stream:!1});if(s.success&&s.stdout)if(s.stdout.trim().length>0){let l=s.stdout.split(`
|
|
18
|
+
`),f=l.filter(h=>h.trim().startsWith("create ")).length,g=l.filter(h=>h.trim().startsWith("alter ")).length,p=l.filter(h=>h.trim().startsWith("drop ")).length;f>0||g>0||p>0?console.log($.dim(` Found schema differences: ${f} create, ${g} alter, ${p} drop`)):console.log($.dim(" Schema is in sync"))}else console.log($.dim(" No pending schema changes"));else console.log($.dim(" Could not check for schema changes"));console.log()}if(e.dryRun){console.log($.yellow("\u26A0"),"Dry run mode: no changes will be made"),console.log(),console.log("Would execute:"),console.log($.dim(" supabase db push")),console.log();return}console.log($.blue("\u2192"),"Pushing migrations to",$.cyan(r.name)),console.log();let i=await E(["db","push","--yes"],{stream:!0});i.success?(console.log(),console.log($.green("\u2713"),"Push completed successfully")):(console.log(),console.error($.red("\u2717"),"Push failed"),process.exit(i.exitCode))}import{Command as xn}from"commander";import C from"picocolors";import*as D from"@clack/prompts";function ko(){return new xn("reset").description("Reset database to match local migrations (DESTRUCTIVE)").option("--force","Bypass all safety guards (DANGEROUS)",!1).option("--linked","Reset the linked remote database instead of local",!1).option("--i-know-what-im-doing","Required flag for this operation in CI mode",!1).action(async function(){let e=this.optsWithGlobals();await En(e)})}async function En(e){Y(),q(),await X();let o=await F(),n=await V(),t=await J(),r;if(e.env)r=H(e.env,o),r||(console.error(C.red("\u2717"),`Environment '${e.env}' not found in config`),console.error(C.dim(" Available environments:"),Object.keys(o.environments).join(", ")),process.exit(1));else{let l=await _();l||(console.error(C.red("\u2717"),"No Supabase project linked"),console.error(C.dim(" Run: supacontrol switch <environment>")),process.exit(1)),r=W(l,o),r||(console.error(C.red("\u2717"),"Linked project is not configured in supacontrol.toml"),console.error(C.dim(` Linked to: ${l}`)),console.error(C.dim(" Run: supacontrol init or add this project to your config")),process.exit(1))}let i=G(r.name,r.config);if(e.ci&&(e.env||(console.error(C.red("\u2717"),"CI mode requires explicit --env flag for reset"),process.exit(1)),e.iKnowWhatImDoing||(console.error(C.red("\u2717"),"CI mode requires --i-know-what-im-doing flag for reset"),process.exit(1))),console.log(),D.note([C.bold(C.red("CRITICAL WARNING")),"",`This will ${C.red("DROP ALL TABLES")} and recreate the database`,"from your local migrations.","",`Environment: ${C.cyan(r.name)}`,e.linked?`Target: ${C.red("REMOTE DATABASE")}`:`Target: ${C.yellow("Local database")}`,i?`
|
|
19
|
+
${C.red("\u{1F512} Environment is LOCKED")}`:""].filter(Boolean).join(`
|
|
20
|
+
`),C.red("\u26A0\uFE0F Database Reset")),e.force){if(console.log(),console.log(C.red("\u26A0"),C.bold("FORCE MODE ENABLED")),console.log(C.red(" All safety guards are being bypassed!")),console.log(),!e.ci){let l=await D.confirm({message:C.red("Are you absolutely sure you want to proceed?"),initialValue:!1});(D.isCancel(l)||!l)&&(D.cancel("Operation cancelled"),process.exit(0))}}else{let l={operation:"reset",environmentName:r.name,environment:r.config,config:o,gitBranch:n,isCI:e.ci,hasUncommittedChanges:t},f=await de(l);f.allowed||(f.cancelled&&process.exit(0),process.exit(1))}if(!e.ci&&!e.force){let l=r.config.confirm_word??r.name;console.log();let f=await D.text({message:`Type '${C.bold(C.red(l))}' to confirm database reset:`,validate(g){if(g!==l)return`Please type exactly '${l}' to confirm`}});D.isCancel(f)&&(D.cancel("Operation cancelled"),process.exit(0)),f!==l&&(console.error(C.red("\u2717"),"Confirmation failed"),process.exit(1))}let s=["db","reset"];e.linked&&s.push("--linked"),console.log(),console.log(C.blue("\u2192"),"Resetting database for",C.cyan(r.name)),console.log();let a=await E(s,{stream:!0});a.success?(console.log(),console.log(C.green("\u2713"),"Database reset completed successfully")):(console.log(),console.error(C.red("\u2717"),"Database reset failed"),process.exit(a.exitCode))}import{Command as _n}from"commander";import B from"picocolors";function jo(){return new _n("pull").description("Pull remote schema changes to local migrations").option("--force","Bypass safety guards",!1).action(async function(){let e=this.optsWithGlobals();await Ln(e)})}async function Ln(e){Y(),q(),await X();let o=await F(),n=await V(),t=await J(),r;if(e.env)r=H(e.env,o),r||(console.error(B.red("\u2717"),`Environment '${e.env}' not found in config`),console.error(B.dim(" Available environments:"),Object.keys(o.environments).join(", ")),process.exit(1));else{let s=await _();s||(console.error(B.red("\u2717"),"No Supabase project linked"),console.error(B.dim(" Run: supacontrol switch <environment>")),process.exit(1)),r=W(s,o),r||(console.error(B.red("\u2717"),"Linked project is not configured in supacontrol.toml"),console.error(B.dim(` Linked to: ${s}`)),console.error(B.dim(" Run: supacontrol init or add this project to your config")),process.exit(1))}if(e.force)console.log(B.yellow("\u26A0"),"Force mode: bypassing safety guards"),console.log();else{let s={operation:"pull",environmentName:r.name,environment:r.config,config:o,gitBranch:n,isCI:e.ci,hasUncommittedChanges:t},a=await de(s);a.allowed||(a.cancelled&&process.exit(0),process.exit(1))}console.log(B.blue("\u2192"),"Pulling schema from",B.cyan(r.name)),console.log();let i=await E(["db","pull"],{stream:!0});i.success?(console.log(),console.log(B.green("\u2713"),"Pull completed successfully"),console.log(B.dim(" Review the generated migrations in supabase/migrations/"))):(console.log(),console.error(B.red("\u2717"),"Pull failed"),process.exit(i.exitCode))}import{Command as Tn}from"commander";import w from"picocolors";import*as Re from"@clack/prompts";function $o(){return new Tn("switch").description("Switch to a different environment (link to its project)").argument("<environment>","Target environment name").action(async e=>{await On(e)})}async function On(e){q(),await X();let o=await F(),n=H(e,o);if(!n){console.error(w.red("\u2717"),`Environment '${e}' not found in config`),console.error(),console.error(w.dim("Available environments:"));for(let i of ne(o)){let s=o.environments[i];s&&console.error(w.dim(` - ${i}${s.project_ref?` (${s.project_ref})`:""}`))}process.exit(1)}let t=await _();if(!n.projectRef){if(e==="local"){console.log(w.blue("\u2192"),"Switching to local development"),t?(console.log(w.dim(" Unlinking from remote project...")),(await E(["unlink"],{stream:!1})).success?(console.log(w.green("\u2713"),"Unlinked from remote project"),console.log(w.dim(" Now using local database"))):console.log(w.yellow("\u26A0"),"Could not unlink (may already be unlinked)")):console.log(w.green("\u2713"),"Already using local database");return}console.error(w.red("\u2717"),`Environment '${e}' has no project_ref configured`),console.error(w.dim(` Add 'project_ref' to [environments.${e}] in supacontrol.toml`)),process.exit(1)}if(t===n.projectRef){console.log(w.green("\u2713"),`Already linked to ${w.cyan(n.projectRef)}`),console.log(w.dim(` Environment: ${e}`));return}console.log(w.blue("\u2192"),`Switching to ${w.cyan(e)}`),t&&console.log(w.dim(` From: ${t}`)),console.log(w.dim(` To: ${n.projectRef}`)),console.log();let r=await E(["link","--project-ref",n.projectRef],{stream:!0});r.success?(console.log(),console.log(w.green("\u2713"),`Linked to ${w.cyan(n.projectRef)}`),console.log(w.dim(` Environment: ${e}`)),console.log(),await An()):(console.log(),console.error(w.red("\u2717"),"Failed to link to project"),console.error(w.dim(" Make sure you are logged in: supabase login")),process.exit(r.exitCode))}async function An(){let e=await Fe();if(e.needsSync&&e.remoteMissing.length>0){console.log(w.yellow("\u26A0"),`Remote has ${e.remoteMissing.length} migration(s) not in local`);for(let n of e.remoteMissing)console.log(w.dim(` - ${n}`));console.log();let o=await Re.confirm({message:"Pull remote migrations to sync local state?",initialValue:!0});if(Re.isCancel(o))return;o?await wo()?console.log(w.green("\u2713"),"Migrations synced"):console.log(w.yellow("\u26A0"),"Migration sync had issues - check output above"):(console.log(w.dim("Skipped migration sync")),console.log(w.dim(" Run `supabase db pull` manually to sync later")))}else e.localMissing.length>0?(console.log(w.blue("\u2192"),`You have ${e.localMissing.length} local migration(s) to push`),console.log(w.dim(" Run `spc push` when ready"))):console.log(w.green("\u2713"),"Migrations are in sync")}import{Command as Eo}from"commander";import S from"picocolors";import*as ee from"@clack/prompts";import{writeFile as In,access as Mn,constants as Bn}from"fs/promises";import{resolve as So}from"path";var Ro="supacontrol.toml";function Gn(e){return["[settings]","# Fail on any guard warning, not just errors",`strict_mode = ${e.strict_mode}`,"","# Require clean git working tree before destructive operations",`require_clean_git = ${e.require_clean_git}`,"","# Show migration diff before push",`show_migration_diff = ${e.show_migration_diff}`].join(`
|
|
21
|
+
`)}function Nn(e,o){let n=[`[environments.${e}]`];return o.project_ref!==void 0&&(n.push("# Supabase project reference"),n.push(`project_ref = "${o.project_ref}"`)),o.git_branches.length>0&&(n.push("# Git branches that map to this environment"),n.push(`git_branches = [${o.git_branches.map(t=>`"${t}"`).join(", ")}]`)),o.protected_operations.length>0&&(n.push("# Operations that require confirmation"),n.push(`protected_operations = [${o.protected_operations.map(t=>`"${t}"`).join(", ")}]`)),o.confirm_word!==void 0&&(n.push("# Custom confirmation word (type this to confirm)"),n.push(`confirm_word = "${o.confirm_word}"`)),o.locked!==void 0&&(n.push("# Lock environment to prevent all destructive operations"),n.push(`locked = ${o.locked}`)),n.join(`
|
|
22
|
+
`)}function Dn(e){let o=["# SupaControl Configuration","# https://github.com/your-org/supacontrol","",Gn(e.settings)],n=Object.keys(e.environments);if(n.length>0){o.push("");for(let t of n){let r=e.environments[t];r&&(o.push(Nn(t,r)),o.push(""))}}return o.join(`
|
|
23
|
+
`).trimEnd()+`
|
|
24
|
+
`}async function xo(e){let o=e??process.cwd(),n=So(o,Ro);try{return await Mn(n,Bn.F_OK),!0}catch{return!1}}async function be(e,o){let n=o??process.cwd(),t=So(n,Ro),r=Dn(e);return await In(t,r,"utf-8"),t}function _o(){return new Eo("lock").description("Lock an environment to prevent destructive operations").argument("[environment]","Environment to lock (defaults to current)").action(async function(e){let o=this.optsWithGlobals();await Fn(e,o)})}function Lo(){return new Eo("unlock").description("Unlock an environment to allow destructive operations").argument("[environment]","Environment to unlock (defaults to current)").action(async function(e){let o=this.optsWithGlobals();await qn(e,o)})}async function Fn(e,o){Y();let n=await F(),t=await To(e,n);t||process.exit(1);let r=n.environments[t];if(r||(console.error(S.red("\u2717"),`Environment '${t}' not found`),process.exit(1)),G(t,r)){console.log(S.green("\u2713"),`Environment '${t}' is already locked`);return}r.locked=!0,await be(n),console.log(S.green("\u2713"),`Locked environment '${S.cyan(t)}'`),console.log(S.dim(" Destructive operations are now blocked"))}async function qn(e,o){Y();let n=await F(),t=await To(e,n);t||process.exit(1);let r=n.environments[t];if(r||(console.error(S.red("\u2717"),`Environment '${t}' not found`),process.exit(1)),!G(t,r)){console.log(S.green("\u2713"),`Environment '${t}' is already unlocked`);return}if((t==="production"||r.git_branches.includes("main")||r.git_branches.includes("master"))&&!o.ci){console.log(),ee.note([S.yellow("Warning: Unlocking production environment"),"","This will allow destructive operations like:"," - db push"," - db reset"," - db seed"].join(`
|
|
25
|
+
`),S.yellow("\u26A0 Production Unlock"));let a=await ee.confirm({message:`Are you sure you want to unlock '${t}'?`,initialValue:!1});(ee.isCancel(a)||!a)&&(ee.cancel("Operation cancelled"),process.exit(0))}r.locked=!1,await be(n),console.log(S.yellow("\u26A0"),`Unlocked environment '${S.cyan(t)}'`),console.log(S.dim(" Destructive operations are now allowed")),console.log(S.dim(` Run 'supacontrol lock ${t}' to re-lock`))}async function To(e,o){if(!o)return null;if(e){if(!H(e,o)){console.error(S.red("\u2717"),`Environment '${e}' not found in config`),console.error(),console.error(S.dim("Available environments:"));for(let i of ne(o))console.error(S.dim(` - ${i}`));return null}return e}q();let n=await _();if(!n)return console.error(S.red("\u2717"),"No Supabase project linked"),console.error(S.dim(" Specify environment: supacontrol lock <environment>")),console.error(S.dim(" Or run: supacontrol switch <environment>")),null;let t=W(n,o);return t?t.name:(console.error(S.red("\u2717"),"Linked project is not configured in supacontrol.toml"),console.error(S.dim(" Specify environment: supacontrol lock <environment>")),null)}import{Command as Un}from"commander";import L from"picocolors";import{access as Oo,constants as Ao}from"fs/promises";import{resolve as Io}from"path";function Mo(){return new Un("doctor").description("Check for common issues and misconfigurations").option("--verbose","Show detailed output",!1).option("--report","Show summary only",!1).action(async e=>{await Hn(e)})}async function Hn(e){console.log(),console.log(L.bold("SupaControl Doctor")),console.log(L.dim("Checking your project setup...")),console.log();let o=[];o.push(await Vn()),o.push(await zn()),o.push(await Yn()),o.push(await Kn()),o.push(await Zn()),o.push(await Jn()),o.push(await Qn());let n=o.filter(i=>i.status==="pass").length,t=o.filter(i=>i.status==="warn").length,r=o.filter(i=>i.status==="fail").length;if(!e.report){for(let i of o)Wn(i,e.verbose);console.log()}console.log(L.bold("Summary")),console.log(L.dim("\u2500".repeat(40))),console.log(` ${L.green("\u2713")} ${n} passed`),t>0&&console.log(` ${L.yellow("\u26A0")} ${t} warnings`),r>0&&console.log(` ${L.red("\u2717")} ${r} failed`),console.log(),(t>0||r>0)&&(console.log(L.dim("Fix the issues above to improve your setup.")),console.log()),r>0&&process.exit(1)}function Wn(e,o){let n=e.status==="pass"?L.green("\u2713"):e.status==="warn"?L.yellow("\u26A0"):e.status==="fail"?L.red("\u2717"):L.blue("\u2139");if(console.log(`${n} ${e.name}`),console.log(L.dim(` ${e.message}`)),o&&e.details)for(let t of e.details)console.log(L.dim(` - ${t}`));e.fix&&(e.status==="warn"||e.status==="fail")&&console.log(L.dim(` Fix: ${e.fix}`)),console.log()}async function Vn(){if(!await me())return{name:"Supabase CLI",status:"fail",message:"Supabase CLI is not installed",fix:"npm install -g supabase"};let o=await je();return{name:"Supabase CLI",status:"pass",message:`Installed${o?` (v${o})`:""}`}}async function zn(){if(!await Ce())return{name:"Git Repository",status:"warn",message:"Not a git repository",details:["Git branch detection will not work","Auto-environment switching disabled"],fix:"git init"};let o=await V();return{name:"Git Repository",status:"pass",message:o?`On branch '${o}'`:"Repository detected"}}async function Yn(){let e=await ce();if(!e)return{name:"SupaControl Config",status:"warn",message:"No supacontrol.toml found",details:["Create a config to enable environment protection"],fix:"supacontrol init"};let o=ne(e).length;return{name:"SupaControl Config",status:"pass",message:`Loaded with ${o} environment${o!==1?"s":""}`}}async function Kn(){let e=Io(process.cwd(),"supabase");try{return await Oo(e,Ao.F_OK),{name:"Supabase Project",status:"pass",message:"supabase/ directory found"}}catch{return{name:"Supabase Project",status:"warn",message:"No supabase/ directory found",details:["Initialize a Supabase project to use database features"],fix:"supabase init"}}}async function Zn(){let e=await _();return e?{name:"Linked Project",status:"pass",message:`Linked to ${e}`}:{name:"Linked Project",status:"info",message:"No project linked (using local database)",details:["Link a project to push migrations to remote"]}}async function Jn(){let e=await ce();if(!e)return{name:"Environment Safety",status:"info",message:"No config to check"};let o=ne(e),n=[],t=[];for(let r of o){let i=e.environments[r];if(!i)continue;G(r,i)?n.push(r):(r==="production"||i.git_branches.includes("main")||i.git_branches.includes("master"))&&t.push(r)}return t.length>0?{name:"Environment Safety",status:"warn",message:`Production environment(s) unlocked: ${t.join(", ")}`,details:["Consider locking production to prevent accidental changes"],fix:`supacontrol lock ${t[0]}`}:n.length>0?{name:"Environment Safety",status:"pass",message:`${n.length} environment${n.length!==1?"s":""} locked`,details:n.map(r=>`${r} is locked`)}:{name:"Environment Safety",status:"info",message:"No locked environments"}}async function Qn(){let e=Io(process.cwd(),"supabase/migrations");try{return await Oo(e,Ao.F_OK),{name:"Migrations",status:"pass",message:"Migrations directory found"}}catch{return{name:"Migrations",status:"info",message:"No migrations directory",details:["Create migrations with `supabase migration new <name>`"]}}}import{Command as et}from"commander";import{access as ot,constants as nt}from"fs/promises";import{resolve as tt}from"path";import*as u from"@clack/prompts";import c from"picocolors";import*as xe from"@clack/prompts";import U from"picocolors";function Xn(e){return new Date(e).toLocaleDateString("en-US",{year:"numeric",month:"short",day:"numeric"})}function Ue(e){let o=e.status==="ACTIVE_HEALTHY"?U.green("Active"):e.status==="PAUSED"?U.yellow("Paused"):U.red(e.status);console.log(),console.log(U.bold("Selected project:")),console.log(` ${U.cyan("Name:")} ${e.name}`),console.log(` ${U.cyan("Ref:")} ${e.id}`),console.log(` ${U.cyan("Region:")} ${e.region}`),console.log(` ${U.cyan("Status:")} ${o}`),console.log(` ${U.cyan("Created:")} ${Xn(e.created_at)}`),e.database&&console.log(` ${U.cyan("DB Host:")} ${U.dim(e.database.host)}`),console.log()}async function rt(){let e=tt(process.cwd(),"supabase","config.toml");try{return await ot(e,nt.F_OK),!0}catch{return!1}}var He={local:{label:"Local only",hint:"Just local development with Supabase CLI"},"local-staging":{label:"Local + Staging",hint:"Local dev + one remote staging environment"},"local-staging-production":{label:"Local + Staging + Production",hint:"Full setup with staging and production"}};function st(e,o){let n={};return(e==="local-staging"||e==="local-staging-production")&&(n.staging={project_ref:o.staging,git_branches:["develop","staging"],protected_operations:["reset"],confirm_word:void 0,locked:void 0}),e==="local-staging-production"&&(n.production={project_ref:o.production,git_branches:["main","master"],protected_operations:["push","reset","seed"],confirm_word:"production",locked:!0}),n}function it(){u.note([`${c.bold("Supabase Branching")} is now available!`,"","With branching, you can have isolated database environments","for each Git branch or PR, without managing multiple projects.","",`Learn more: ${c.cyan("https://supabase.com/docs/guides/platform/branching")}`].join(`
|
|
26
|
+
`),"Pro Tip")}async function at(e,o){if(o.length===0){u.note([`${c.red("No Supabase projects found in your account.")}`,"","To set up staging + production environments, you need either:","",`${c.cyan("1.")} Two separate Supabase projects`,` ${c.dim("Create projects at: https://supabase.com/dashboard/projects")}`,"",`${c.cyan("2.")} One project with Supabase Branching enabled (Pro plan)`,` ${c.dim("Your main project = production")}`,` ${c.dim("A branch = staging")}`,` ${c.dim("Learn more: https://supabase.com/docs/guides/platform/branching")}`].join(`
|
|
27
|
+
`),`${c.yellow("\u26A0")} Cannot set up staging + production`);let p=await u.select({message:"What would you like to do?",options:[{value:"local-staging",label:"Continue with Local + Staging only",hint:"Single remote environment"},{value:"local",label:"Continue with Local only",hint:"No remote environments"},{value:"cancel",label:"Cancel setup",hint:"Create projects first"}]});return(u.isCancel(p)||p==="cancel")&&(u.cancel("Setup cancelled"),process.exit(0)),{canProceed:!1}}let n=u.spinner();n.start("Checking branching capability...");let t=[],r=o.filter(p=>p.status==="ACTIVE_HEALTHY"),i=10,s=500;for(let p=0;p<r.length;p+=i){let h=r.slice(p,p+i),m=await Promise.all(h.map(async b=>{let P=await e.checkBranchingCapability(b);return{proj:b,capability:P}}));for(let{proj:b,capability:P}of m)P.available&&t.push({project:b,branches:P.branches});p+i<r.length&&await new Promise(b=>setTimeout(b,s))}n.stop(t.length>0?`Found ${t.length} project${t.length>1?"s":""} with branching`:"No projects with branching capability"),console.log();let a=o.length>=2,l=t.length>0,f=[];if(a&&f.push({value:"separate-projects",label:`Use separate Supabase projects (${o.length} available)`,hint:"Each environment uses its own project"}),l){let p=t.length;f.push({value:"use-branching",label:`Use Supabase Branching (${p} project${p>1?"s":""} with branching)`,hint:"Main project = production, branch = staging"})}if(f.length===0){let p=o[0];u.note([`${c.yellow("\u26A0")} You have 1 project without branching enabled.`,"",`Project: ${c.cyan(p.name)}`,"","To set up staging + production, you need either:","",`${c.cyan("1.")} Enable Supabase Branching (Pro plan required)`,` ${c.dim(`https://supabase.com/dashboard/project/${p.id}/settings/general`)}`," \u2192 Main project = production"," \u2192 Branch = staging","",`${c.cyan("2.")} Create a second Supabase project`,` ${c.dim("https://supabase.com/dashboard/projects")}`].join(`
|
|
28
|
+
`),"Additional Setup Required");let h=await u.select({message:"What would you like to do?",options:[{value:"open-branching",label:"Open branching settings in browser",hint:"Enable branching, then re-run init"},{value:"open-projects",label:"Open Supabase dashboard to create project",hint:"Create second project, then re-run init"},{value:"local-staging",label:"Continue with Local + Staging only",hint:"Use single project for staging"},{value:"cancel",label:"Cancel setup"}]});if((u.isCancel(h)||h==="cancel")&&(u.cancel("Setup cancelled"),process.exit(0)),h==="open-branching"){let m=`https://supabase.com/dashboard/project/${p.id}/settings/general`;console.log(),console.log(c.cyan("\u2192"),"Open this URL to enable branching:"),console.log(c.dim(` ${m}`)),console.log(),console.log(c.dim("After enabling branching, run `supacontrol init` again.")),process.exit(0)}if(h==="open-projects"){let m="https://supabase.com/dashboard/projects";console.log(),console.log(c.cyan("\u2192"),"Open this URL to create a new project:"),console.log(c.dim(` ${m}`)),console.log(),console.log(c.dim("After creating a project, run `supacontrol init` again.")),process.exit(0)}return{canProceed:!1}}f.push({value:"cancel",label:c.dim("Cancel setup"),hint:""}),u.note(["You selected Local + Staging + Production.","",`${c.bold("Separate projects:")} Each environment uses its own Supabase project.`,`${c.dim(" Best for: Complete isolation between environments")}`,"",`${c.bold("Supabase Branching:")} Production uses main project, staging uses a branch.`,`${c.dim(" Best for: Easier schema sync, single billing, team workflows")}`].join(`
|
|
29
|
+
`),"Environment Strategy");let g=await u.select({message:"How would you like to configure your environments?",options:f});if((u.isCancel(g)||g==="cancel")&&(u.cancel("Setup cancelled"),process.exit(0)),g==="use-branching"){let p=t[0];if(t.length>1){let h=await u.select({message:"Which project should be your production environment?",options:t.map(({project:b,branches:P})=>({value:b.id,label:`${b.name} ${c.dim(`(${b.region})`)} ${c.green("[Active]")}`,hint:P.length>0?`${P.length} existing branch${P.length>1?"es":""}`:"No branches yet"}))});u.isCancel(h)&&(u.cancel("Setup cancelled"),process.exit(0));let m=t.find(b=>b.project.id===h);m&&(p=m)}return{canProceed:!0,useBranching:!0,parentProject:p.project,branches:p.branches}}return{canProceed:!0,useBranching:!1}}async function ct(e,o,n){let t=n.filter(l=>!l.is_default),r=t.find(l=>l.name.toLowerCase()==="staging");if(t.length===0)return console.log(c.dim('No existing branches found. Creating "staging" branch...')),We(e,o,n,"staging");if(t.length===1){let l=t[0],f=l.name.toLowerCase()==="staging";console.log(c.green("\u2713"),`Found existing branch: "${l.name}"`),u.note([`${c.bold("Branch details:")}`,` Name: ${c.cyan(l.name)}`,` Ref: ${c.dim(l.project_ref)}`,` Status: ${l.status}`,"",f?c.dim('This branch is named "staging" - likely intended for staging environment.'):c.yellow("This branch has a custom name. Please confirm its intended use.")].join(`
|
|
30
|
+
`),"Existing Branch Found");let g=[];g.push({value:"use",label:`Use "${l.name}" as staging environment`}),f&&(g[0].hint="Recommended"),g.push({value:"create",label:'Create a new "staging" branch instead',hint:"This branch may be for another purpose"}),g.push({value:"cancel",label:c.dim("Cancel setup"),hint:"Let me check this first"});let p=await u.select({message:"What would you like to do with this branch?",options:g});return(u.isCancel(p)||p==="cancel")&&(u.cancel("Setup cancelled"),process.exit(0)),p==="use"?l.project_ref:We(e,o,n,r?"staging-env":"staging")}console.log(c.green("\u2713"),`Found ${t.length} existing branches`),u.note([`${c.bold("Existing branches:")}`,...t.map(l=>` \u2022 ${l.name} ${c.dim(`(ref: ${l.project_ref})`)}`),"",c.yellow("Please select which branch to use for staging, or create a new one.")].join(`
|
|
31
|
+
`),"Multiple Branches Found");let i=[],s=[...t].sort((l,f)=>l.name.toLowerCase()==="staging"?-1:f.name.toLowerCase()==="staging"?1:l.name.localeCompare(f.name));for(let l of s){let f=l.name.toLowerCase()==="staging";i.push({value:l.project_ref,label:`"${l.name}"`,hint:f?'Recommended - named "staging"':`ref: ${l.project_ref}`})}i.push({value:"__create__",label:c.cyan("+ Create a new branch"),hint:"Creates a new branch for staging"}),i.push({value:"__cancel__",label:c.dim("Cancel setup"),hint:"Let me check these branches first"});let a=await u.select({message:"Select a branch for staging environment:",options:i});return(u.isCancel(a)||a==="__cancel__")&&(u.cancel("Setup cancelled"),process.exit(0)),a==="__create__"?We(e,o,n,r?"staging-env":"staging"):a}async function We(e,o,n,t){let r=await u.text({message:"Enter a name for the new branch:",placeholder:t,defaultValue:t,validate:s=>{if(!s||s.trim().length===0)return"Branch name is required";if(!/^[a-zA-Z0-9_-]+$/.test(s))return"Branch name can only contain letters, numbers, hyphens, and underscores";if(n.some(a=>a.name.toLowerCase()===s.toLowerCase()))return"A branch with this name already exists"}});u.isCancel(r)&&(u.cancel("Setup cancelled"),process.exit(0));let i=u.spinner();i.start(`Creating branch "${r}"...`);try{let s=await e.createBranch(o.id,r);return s||(i.stop("Failed to create branch"),u.cancel("Could not parse branch response. Please try again."),process.exit(1)),i.stop(`Branch "${r}" created`),console.log(c.green("\u2713"),`Branch ref: ${c.dim(s.project_ref)}`),await lt(s.project_ref),s.project_ref}catch(s){i.stop("Failed to create branch");let a=s instanceof Error?s.message:"Unknown error";console.error(c.red("\u2717"),a),(a.includes("branch limit")||a.includes("limit"))&&(console.log(),console.log(c.yellow("This may be a plan limitation:")),console.log(c.dim(" \u2022 Check your branch limit at:")),console.log(c.dim(` https://supabase.com/dashboard/project/${o.id}/settings/general`))),console.log(),u.cancel("Could not create branch."),process.exit(1)}}async function lt(e){let o=u.spinner();if(o.start("Linking to branch..."),!(await E(["link","--project-ref",e],{stream:!1})).success){o.stop("Failed to link"),console.log(c.yellow("\u26A0"),"Could not link to branch automatically"),console.log(c.dim(` Run manually: supabase link --project-ref ${e}`));return}o.stop("Linked to branch"),o.start("Syncing migrations from remote..."),(await E(["db","pull"],{stream:!1})).success?(o.stop("Migrations synced"),console.log(c.green("\u2713"),"Remote migrations pulled to local")):(o.stop("Migration sync skipped"),console.log(c.dim(" No remote migrations to sync (this is normal for new branches)")))}function ut(e){let o=[`${c.cyan("1.")} Review ${c.bold("supacontrol.toml")} and adjust as needed`];e!=="local"?(o.push(`${c.cyan("2.")} Run ${c.bold("supacontrol switch <env>")} to link a project`),o.push(`${c.cyan("3.")} Run ${c.bold("supacontrol status")} to verify setup`)):o.push(`${c.cyan("2.")} Run ${c.bold("supacontrol status")} to verify setup`),o.push(`${c.cyan(o.length+1+".")} Run ${c.bold("supacontrol push")} to push migrations`),console.log(),console.log(c.bold("Next steps:"));for(let n of o)console.log(` ${n}`);console.log()}async function mt(){let e=A.opts();if(u.intro(c.bgCyan(c.black(" SupaControl Setup "))),await rt()||(u.cancel("Supabase not initialized"),console.error(c.red("\u2717"),"No supabase/config.toml found"),console.error(c.dim(" Run `supabase init` first to initialize Supabase")),process.exit(1)),console.log(c.green("\u2713"),"Supabase project detected"),await xo()){let a=await u.confirm({message:"supacontrol.toml already exists. Overwrite?",initialValue:!1});(u.isCancel(a)||!a)&&(u.cancel("Setup cancelled"),process.exit(0))}let t=await u.select({message:"How many environments do you need?",options:[{value:"local",...He.local},{value:"local-staging",...He["local-staging"]},{value:"local-staging-production",...He["local-staging-production"]}]});u.isCancel(t)&&(u.cancel("Setup cancelled"),process.exit(0));let r={},i=null;if(t!=="local"){let a=await ge();a?console.log(c.green("\u2713"),"Using saved access token"):(e.ci&&(u.cancel("No access token found"),console.error(c.red("\u2717"),"SUPABASE_ACCESS_TOKEN not set"),console.error(c.dim(" Set the environment variable or run interactively")),process.exit(1)),a=await ro({saveToken:!0}),a||(u.cancel("Setup cancelled"),process.exit(0)));let l=$e(a),f=u.spinner();f.start("Validating access token..."),await l.authenticate()||(f.stop("Invalid token"),u.cancel("Invalid or expired access token"),console.error(c.dim(" Generate a new token at https://supabase.com/dashboard/account/tokens")),process.exit(1)),f.stop("Token validated");let p=await l.getProjects();if(t==="local-staging-production"){let h=await at(l,p);if(h.canProceed)h.useBranching&&(i={parentProject:h.parentProject,branches:h.branches});else{let m=await u.select({message:"Continue with a different setup?",options:[{value:"local-staging",label:"Local + Staging",hint:"Single remote environment"},{value:"local",label:"Local only",hint:"No remote environments"},{value:"cancel",label:"Cancel setup"}]});(u.isCancel(m)||m==="cancel")&&(u.cancel("Setup cancelled"),process.exit(0)),t=m}}if(t==="local-staging-production"&&i){console.log(),console.log(c.bold(`Configure ${c.cyan("production")} environment:`)),console.log(c.green("\u2713"),`Using main project: ${c.cyan(i.parentProject.name)}`),r.production=i.parentProject.id,Ue(i.parentProject),console.log(),console.log(c.bold(`Configure ${c.cyan("staging")} environment:`));let h=await ct(l,i.parentProject,i.branches);r.staging=h}else if(t!=="local"){let h=t==="local-staging"?["staging"]:["production","staging"];for(let m of h){if(console.log(),console.log(c.bold(`Configure ${c.cyan(m)} environment:`)),p.length===0){console.log(c.yellow("\u26A0"),"No projects found in your account"),console.log(c.dim(" You can add the project_ref manually to supacontrol.toml"));continue}let b=Object.values(r).filter(R=>R!==void 0),P=await dt(p,m,b);if(P){r[m]=P;let R=p.find(I=>I.id===P);R&&Ue(R)}else console.log(c.dim(` Skipped - configure ${m}.project_ref in supacontrol.toml`))}}i||it()}let s={settings:{strict_mode:!1,require_clean_git:!1,show_migration_diff:!0},environments:st(t,r)};await be(s),console.log(c.green("\u2713"),`Created ${c.bold("supacontrol.toml")}`),ut(t),u.outro(c.green("Setup complete!"))}async function dt(e,o,n=[]){let t=e.filter(a=>!n.includes(a.id));if(t.length===0&&n.length>0){u.note([`${c.red("All your Supabase projects are already assigned to other environments.")}`,"","Each environment MUST have a unique project_ref to prevent","accidentally running operations on the wrong database.","",`${c.bold("Options:")}`,"",`${c.cyan("1.")} Create a new Supabase project for ${o}:`,` ${c.dim("https://supabase.com/dashboard/projects")}`,"",`${c.cyan("2.")} Use Supabase Branching (recommended for teams):`,` ${c.dim("https://supabase.com/docs/guides/platform/branching")}`," Branching creates isolated environments from a single project.","",`${c.cyan("3.")} Skip ${o} for now and configure manually later.`].join(`
|
|
32
|
+
`),`${c.yellow("\u26A0")} No available projects for ${o}`);let a=await u.confirm({message:`Skip ${o} configuration for now?`,initialValue:!0});if(u.isCancel(a)&&(u.cancel("Setup cancelled"),process.exit(0)),a)return null;u.cancel("Setup cancelled - create additional projects and try again"),process.exit(0)}let i=[...[...t].sort((a,l)=>a.status==="ACTIVE_HEALTHY"&&l.status!=="ACTIVE_HEALTHY"?-1:l.status==="ACTIVE_HEALTHY"&&a.status!=="ACTIVE_HEALTHY"?1:a.name.localeCompare(l.name)).map(a=>{let l=a.status==="ACTIVE_HEALTHY"?c.green("[Active]"):a.status==="PAUSED"?c.yellow("[Paused]"):c.red(`[${a.status}]`);return{value:a.id,label:`${a.name} ${c.dim(`(${a.region})`)} ${l}`,hint:`ref: ${a.id}`}}),{value:"__skip__",label:c.dim("Skip (configure manually later)"),hint:`Set ${o}.project_ref in supacontrol.toml`}],s=await u.select({message:`Select project for ${o}:`,options:i});return u.isCancel(s)&&(u.cancel("Setup cancelled"),process.exit(0)),s==="__skip__"?null:s}function Bo(){return new et("init").description("Initialize SupaControl in your project").action(Go(mt))}var ft=gt(import.meta.url),No=ft("../package.json"),A=new pt;A.name("supacontrol").description(No.description).version(No.version,"-v, --version","Show version number").option("--verbose","Enable verbose output",!1).option("--ci","Run in CI mode (non-interactive, strict)",!1).option("-e, --env <environment>","Target environment").configureHelp({sortSubcommands:!0,sortOptions:!0});function Go(e){return async(...o)=>{try{await e(...o)}catch(n){let t=A.opts();n instanceof Error?(console.error(Ee.red("\u2717"),n.message),t.verbose&&n.stack&&console.error(Ee.dim(n.stack))):(console.error(Ee.red("\u2717"),"An unexpected error occurred"),t.verbose&&console.error(Ee.dim(String(n)))),process.exit(1)}}}A.addCommand(Bo());A.addCommand(ao());A.addCommand(Po());A.addCommand(ko());A.addCommand(jo());A.addCommand($o());A.addCommand(_o());A.addCommand(Lo());A.addCommand(Mo());A.parse();export{A as program,Go as withErrorHandling};
|
|
33
|
+
//# sourceMappingURL=index.js.map
|