@coldge.com/gitbase 1.0.2 → 1.0.3
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 +65 -15
- package/dist/api/supabase.js +139 -32
- package/dist/commands/branch.js +86 -14
- package/dist/commands/diff.js +180 -94
- package/dist/commands/init.js +8 -3
- package/dist/commands/log.js +85 -11
- package/dist/commands/merge.js +226 -15
- package/dist/commands/pull.js +171 -0
- package/dist/commands/push.js +324 -71
- package/dist/commands/revert.js +247 -95
- package/dist/commands/snapshot.js +49 -0
- package/dist/commands/stash.js +211 -0
- package/dist/commands/status.js +46 -38
- package/dist/commands/verify.js +120 -0
- package/dist/index.js +90 -10
- package/dist/schema/extractor.js +183 -26
- package/dist/schema/queries.js +160 -8
- package/dist/utils/hashing.js +52 -3
- package/dist/utils/sqlDiff.js +245 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -7,11 +7,13 @@ GitBase is a professional version control system for Supabase. It synchronizes y
|
|
|
7
7
|
## 🚀 Key Features
|
|
8
8
|
|
|
9
9
|
- **Reverse Git Workflow**: Treats the Live Database as the Source of Truth.
|
|
10
|
+
- **Smart Data Preservation**: Generates non-destructive `ALTER TABLE` operations instead of DROP/CREATE during pushes.
|
|
11
|
+
- **True 3-Way Merge**: Identifies common ancestors and auto-merges branch changes with conflict markers.
|
|
12
|
+
- **Native Supabase Branching**: Deep integration with Supabase Pro Branches and generic multi-project environments.
|
|
13
|
+
- **Dry-Run Safety**: Verify your local SQL syntax against the live database instantly via aborted transactions.
|
|
14
|
+
- **Automated Snapshots**: Background daemon that automatically pulls and commits Dashboard UI changes.
|
|
10
15
|
- **Dependency-Aware Restoration**: Automatically orders SQL execution (e.g., Tables before Views) using a DAG engine.
|
|
11
|
-
- **
|
|
12
|
-
- **Smart Change Detection**: Canonicalizes SQL to eliminate "noise" from formatting or whitespace.
|
|
13
|
-
- **Remote History Backups**: Store and sync your history repository in Supabase Storage buckets.
|
|
14
|
-
- **Production Guard**: RBAC protection for high-risk operations on protected branches.
|
|
16
|
+
- **Production Guard**: RBAC protection for high-risk operations on protected branches, requiring Owner/Admin capabilities.
|
|
15
17
|
|
|
16
18
|
---
|
|
17
19
|
|
|
@@ -71,10 +73,12 @@ If someone breaks production, you can reset the database to a known good state i
|
|
|
71
73
|
#### `login`
|
|
72
74
|
Authenticate with the Supabase Management API.
|
|
73
75
|
- **Usage**: `gitb login`
|
|
76
|
+
- **Arguments**: None.
|
|
74
77
|
|
|
75
78
|
#### `whoami`
|
|
76
79
|
Displays the current logged-in user and organization access.
|
|
77
80
|
- **Usage**: `gitb whoami`
|
|
81
|
+
- **Arguments**: None.
|
|
78
82
|
|
|
79
83
|
### Project Configuration
|
|
80
84
|
#### `init`
|
|
@@ -89,23 +93,39 @@ Initializes a `.gitbase` repository and links a project.
|
|
|
89
93
|
#### `status`
|
|
90
94
|
Summarizes differences between the live database and local `.sql` files.
|
|
91
95
|
- **Usage**: `gitb status`
|
|
96
|
+
- **Arguments**: None.
|
|
92
97
|
|
|
93
98
|
#### `pull` | `add`
|
|
94
99
|
Fetches schema definitions from the database to your local `supabase/` directory.
|
|
95
|
-
- **Usage**: `gitb pull [files..]`
|
|
100
|
+
- **Usage**: `gitb pull [files..] [options]`
|
|
96
101
|
- **Arguments**:
|
|
97
102
|
| Argument | Description |
|
|
98
103
|
| :--- | :--- |
|
|
99
104
|
| `files` | Optional. Specific files or directories to pull (defaults to all). |
|
|
105
|
+
| `--auto-commit` | Automatically create a commit snapshot after pulling. |
|
|
106
|
+
| `--message`, `-m` | Optional message for auto-commit. |
|
|
107
|
+
|
|
108
|
+
#### `snapshot`
|
|
109
|
+
Runs a background daemon that periodically checks for live database changes and auto-commits them.
|
|
110
|
+
- **Usage**: `gitb snapshot [options]`
|
|
111
|
+
- **Arguments**:
|
|
112
|
+
| Flag | Description |
|
|
113
|
+
| :--- | :--- |
|
|
114
|
+
| `--interval` | The interval in minutes to check for changes (default: 60). |
|
|
100
115
|
|
|
101
116
|
#### `push`
|
|
102
|
-
Applies local `.sql` files to the live database.
|
|
117
|
+
Applies local `.sql` files to the live database using a single atomic transaction.
|
|
103
118
|
- **Usage**: `gitb push [files..]`
|
|
104
119
|
- **Arguments**:
|
|
105
120
|
| Argument | Description |
|
|
106
121
|
| :--- | :--- |
|
|
107
122
|
| `files` | Optional. Specific files to push. |
|
|
108
123
|
|
|
124
|
+
#### `verify`
|
|
125
|
+
Safely tests your local `.sql` files against the remote database for syntax errors using an aborted transaction.
|
|
126
|
+
- **Usage**: `gitb verify`
|
|
127
|
+
- **Arguments**: None.
|
|
128
|
+
|
|
109
129
|
### Versioning & Comparison
|
|
110
130
|
#### `commit`
|
|
111
131
|
Saves a snapshot of the current local schema to the history.
|
|
@@ -117,15 +137,25 @@ Saves a snapshot of the current local schema to the history.
|
|
|
117
137
|
|
|
118
138
|
#### `log`
|
|
119
139
|
Displays a chronological list of commits for the current branch.
|
|
120
|
-
- **Usage**: `gitb log`
|
|
140
|
+
- **Usage**: `gitb log [file] [options]`
|
|
141
|
+
- **Arguments**:
|
|
142
|
+
| Argument / Flag | Alias | Description |
|
|
143
|
+
| :--- | :--- | :--- |
|
|
144
|
+
| `file` | | Optional. Filter commits to only those that modified this specific file (e.g. `tables/users.sql`). |
|
|
145
|
+
| `--oneline` | | Compact one-line format. |
|
|
146
|
+
| `--since` | | Only show commits after this date (ISO format, e.g. `2026-01-01`). |
|
|
147
|
+
| `--max-count` | `-n` | Limit the output to the last N commits. |
|
|
121
148
|
|
|
122
149
|
#### `diff`
|
|
123
150
|
Shows a line-by-line comparison of SQL definitions.
|
|
124
|
-
- **Usage**: `gitb diff [
|
|
151
|
+
- **Usage**: `gitb diff [commit1] [commit2] [options]`
|
|
125
152
|
- **Arguments**:
|
|
126
|
-
| Argument | Description |
|
|
153
|
+
| Argument / Flag | Description |
|
|
127
154
|
| :--- | :--- |
|
|
128
|
-
| `commit` |
|
|
155
|
+
| `commit` | Compare a specific commit (or `HEAD~N`) against local files. |
|
|
156
|
+
| `commit1 commit2` | Compare two commits against each other. |
|
|
157
|
+
| `--live` | Compare the given commit against the Live Database instead of local files. |
|
|
158
|
+
| `--files` | Filter the diff to specific file paths (e.g. `tables/users.sql`). |
|
|
129
159
|
|
|
130
160
|
### Restoration
|
|
131
161
|
#### `revert` | `reset`
|
|
@@ -139,7 +169,7 @@ Hard-resets the live database and local files to a previous commit.
|
|
|
139
169
|
|
|
140
170
|
### Multi-Environment Profiles
|
|
141
171
|
#### `branch`
|
|
142
|
-
Manages project environmental profiles.
|
|
172
|
+
Manages project environmental profiles. If Native Supabase Branching is available, prompts to automatically provision a connected branch.
|
|
143
173
|
- **Usage**: `gitb branch [name] [options]`
|
|
144
174
|
- **Arguments**:
|
|
145
175
|
| Flag | Alias | Description |
|
|
@@ -148,18 +178,38 @@ Manages project environmental profiles.
|
|
|
148
178
|
| `--delete` | `-d` | Deletes the specified branch profile. |
|
|
149
179
|
|
|
150
180
|
#### `checkout`
|
|
151
|
-
Switches the active environment profile.
|
|
181
|
+
Switches the active environment profile and automatically swaps local SQL files to match the target branch's HEAD.
|
|
152
182
|
- **Usage**: `gitb checkout <name>`
|
|
153
183
|
|
|
154
184
|
#### `merge`
|
|
155
|
-
|
|
185
|
+
Performs a true 3-way merge from another branch into your current branch, auto-resolving non-conflicting changes and inserting conflict markers.
|
|
156
186
|
- **Usage**: `gitb merge <name>`
|
|
157
187
|
|
|
188
|
+
#### `stash`
|
|
189
|
+
Saves your uncommitted local schema changes into a temporary commit-like stashed object.
|
|
190
|
+
- **Usage**: `gitb stash [subcommand] [options]`
|
|
191
|
+
- **Subcommands**:
|
|
192
|
+
- `push` / `save` (default)
|
|
193
|
+
- `pop` (apply and drop)
|
|
194
|
+
- `apply` (apply without dropping)
|
|
195
|
+
- `list` / `ls`
|
|
196
|
+
- `drop`
|
|
197
|
+
- `clear`
|
|
198
|
+
- **Arguments**:
|
|
199
|
+
| Flag | Alias | Description |
|
|
200
|
+
| :--- | :--- | :--- |
|
|
201
|
+
| `--message` | `-m` | Optional message for the stash (used with `push`/`save`). |
|
|
202
|
+
| `--index` | `-n` | The index of the stash to apply or drop (default is 0). |
|
|
203
|
+
|
|
158
204
|
### Cloud Sync
|
|
159
205
|
#### `remote`
|
|
160
206
|
Manages history backups in Supabase Storage.
|
|
161
|
-
- **Usage**: `gitb remote <subcommand
|
|
162
|
-
- **Subcommands**:
|
|
207
|
+
- **Usage**: `gitb remote <subcommand> [args]`
|
|
208
|
+
- **Subcommands**:
|
|
209
|
+
- `add <name> <bucket>`: Link a remote Supabase storage bucket (e.g. `gitb remote add origin my-backups`).
|
|
210
|
+
- `list` / `ls`: Show configured remotes.
|
|
211
|
+
- `push <name>`: Backup your `.gitbase/objects` and `config` to the remote bucket.
|
|
212
|
+
- `pull <name>`: Restore your `.gitbase` history from the remote bucket.
|
|
163
213
|
|
|
164
214
|
---
|
|
165
215
|
|
package/dist/api/supabase.js
CHANGED
|
@@ -4,10 +4,34 @@ import fs from 'fs/promises';
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import pg from 'pg';
|
|
6
6
|
import chalk from 'chalk';
|
|
7
|
-
const { Client } = pg;
|
|
7
|
+
const { Client, Pool } = pg;
|
|
8
8
|
const API_URL = 'https://api.supabase.com/v1';
|
|
9
9
|
const GITBASE_DIR = '.gitbase';
|
|
10
10
|
const CONFIG_FILE = path.join(GITBASE_DIR, 'config');
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Per-command connection pool
|
|
13
|
+
// A short-lived pool is kept alive for the duration of a single CLI command
|
|
14
|
+
// invocation, then torn down. This collapses 6+ DB connections for a typical
|
|
15
|
+
// 'status' or 'push' into 1-3, giving 3-5x fewer TCP handshakes on
|
|
16
|
+
// high-latency connections to Supabase.
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
const pools = new Map();
|
|
19
|
+
/** Call at the start of a command to create a reusable pool for this project. */
|
|
20
|
+
export async function createPool(projectRef) {
|
|
21
|
+
if (pools.has(projectRef))
|
|
22
|
+
return;
|
|
23
|
+
const connectionString = await getConnectionString(projectRef);
|
|
24
|
+
const pool = new Pool({ connectionString, ssl: { rejectUnauthorized: false }, max: 3 });
|
|
25
|
+
pools.set(projectRef, pool);
|
|
26
|
+
}
|
|
27
|
+
/** Drain and remove the pool for this project. Call at the end of each command. */
|
|
28
|
+
export async function endPool(projectRef) {
|
|
29
|
+
const pool = pools.get(projectRef);
|
|
30
|
+
if (pool) {
|
|
31
|
+
await pool.end();
|
|
32
|
+
pools.delete(projectRef);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
11
35
|
export async function getProjects() {
|
|
12
36
|
const token = await getToken();
|
|
13
37
|
if (!token)
|
|
@@ -17,6 +41,22 @@ export async function getProjects() {
|
|
|
17
41
|
});
|
|
18
42
|
return response.data;
|
|
19
43
|
}
|
|
44
|
+
export async function createSupabaseBranch(projectRef, branchName) {
|
|
45
|
+
const token = await getToken();
|
|
46
|
+
if (!token)
|
|
47
|
+
throw new Error('Not logged in.');
|
|
48
|
+
// Creating a branch returns the branch object (we need the project_ref it gets assigned)
|
|
49
|
+
const response = await axios.post(`${API_URL}/projects/${projectRef}/branches`, { branch_name: branchName }, { headers: { Authorization: `Bearer ${token}` } });
|
|
50
|
+
return response.data; // { id, name, project_ref, parent_project_ref, status ... }
|
|
51
|
+
}
|
|
52
|
+
export async function getSupabaseBranch(projectRef, branchName) {
|
|
53
|
+
const token = await getToken();
|
|
54
|
+
if (!token)
|
|
55
|
+
throw new Error('Not logged in.');
|
|
56
|
+
const response = await axios.get(`${API_URL}/projects/${projectRef}/branches`, { headers: { Authorization: `Bearer ${token}` } });
|
|
57
|
+
const branches = response.data;
|
|
58
|
+
return branches.find(b => b.name === branchName);
|
|
59
|
+
}
|
|
20
60
|
export async function getConfig() {
|
|
21
61
|
try {
|
|
22
62
|
const content = await fs.readFile(CONFIG_FILE, 'utf-8');
|
|
@@ -26,13 +66,18 @@ export async function getConfig() {
|
|
|
26
66
|
return null;
|
|
27
67
|
}
|
|
28
68
|
}
|
|
29
|
-
|
|
30
|
-
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Internal helpers
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
/**
|
|
73
|
+
* Resolves the database connection string for a given project ref.
|
|
74
|
+
* Supports both the current multi-branch config and the legacy flat config.
|
|
75
|
+
*/
|
|
76
|
+
export async function getConnectionString(projectRef) {
|
|
31
77
|
let connectionString = '';
|
|
32
78
|
try {
|
|
33
79
|
const config = await getConfig();
|
|
34
80
|
if (config) {
|
|
35
|
-
// New format
|
|
36
81
|
if (config.branches) {
|
|
37
82
|
for (const b of Object.values(config.branches)) {
|
|
38
83
|
const branch = b;
|
|
@@ -42,7 +87,6 @@ export async function runQuery(projectRef, sql) {
|
|
|
42
87
|
}
|
|
43
88
|
}
|
|
44
89
|
}
|
|
45
|
-
// Old format compatibility
|
|
46
90
|
else if (config.projectRef === projectRef) {
|
|
47
91
|
connectionString = config.connectionString;
|
|
48
92
|
if (!connectionString && config.dbPassword) {
|
|
@@ -55,16 +99,55 @@ export async function runQuery(projectRef, sql) {
|
|
|
55
99
|
if (!connectionString) {
|
|
56
100
|
throw new Error('Database connection string not found in config. Please re-run `gitb init --force`.');
|
|
57
101
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
102
|
+
return connectionString;
|
|
103
|
+
}
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Query execution
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
/**
|
|
108
|
+
* Executes a single SQL statement against the given project's database.
|
|
109
|
+
* Accepts a plain SQL string or a { text, values } parameterized query object
|
|
110
|
+
* (the latter prevents SQL injection for dynamic queries).
|
|
111
|
+
*/
|
|
112
|
+
export async function runQuery(projectRef, sql, params = []) {
|
|
113
|
+
// Use pooled connection if available, otherwise create a one-shot client
|
|
114
|
+
const pool = pools.get(projectRef);
|
|
115
|
+
if (pool) {
|
|
116
|
+
let queryConfig;
|
|
117
|
+
if (typeof sql === 'object') {
|
|
118
|
+
queryConfig = sql;
|
|
119
|
+
}
|
|
120
|
+
else if (params.length > 0) {
|
|
121
|
+
queryConfig = { text: sql, values: params };
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
queryConfig = sql;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
const res = await pool.query(queryConfig);
|
|
128
|
+
return res.rows;
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
throw new Error(`Database Error: ${err.message}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Fallback: one-shot client (for calls outside a command lifecycle)
|
|
135
|
+
const connectionString = await getConnectionString(projectRef);
|
|
136
|
+
const client = new Client({ connectionString, ssl: { rejectUnauthorized: false } });
|
|
64
137
|
try {
|
|
65
138
|
await client.connect();
|
|
66
|
-
|
|
67
|
-
|
|
139
|
+
let queryConfig;
|
|
140
|
+
if (typeof sql === 'object') {
|
|
141
|
+
queryConfig = sql;
|
|
142
|
+
}
|
|
143
|
+
else if (params.length > 0) {
|
|
144
|
+
queryConfig = { text: sql, values: params };
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
queryConfig = sql;
|
|
148
|
+
}
|
|
149
|
+
const res = await client.query(queryConfig);
|
|
150
|
+
return res.rows;
|
|
68
151
|
}
|
|
69
152
|
catch (err) {
|
|
70
153
|
throw new Error(`Database Error: ${err.message}`);
|
|
@@ -73,10 +156,42 @@ export async function runQuery(projectRef, sql) {
|
|
|
73
156
|
await client.end();
|
|
74
157
|
}
|
|
75
158
|
}
|
|
76
|
-
|
|
159
|
+
/**
|
|
160
|
+
* Executes an ordered list of SQL statements inside a single BEGIN/COMMIT
|
|
161
|
+
* transaction. On any error, the transaction is rolled back automatically
|
|
162
|
+
* and an error is thrown — leaving the database unchanged.
|
|
163
|
+
*
|
|
164
|
+
* Use this for all push and revert operations so the database is never left
|
|
165
|
+
* in a partially-applied state.
|
|
166
|
+
*/
|
|
167
|
+
export async function runTransaction(projectRef, statements) {
|
|
168
|
+
if (statements.length === 0)
|
|
169
|
+
return;
|
|
170
|
+
// For the transaction, always use a dedicated client to ensure BEGIN/COMMIT
|
|
171
|
+
// are on the same connection (pools don't guarantee this).
|
|
172
|
+
const connectionString = await getConnectionString(projectRef);
|
|
173
|
+
const client = new Client({ connectionString, ssl: { rejectUnauthorized: false } });
|
|
174
|
+
await client.connect();
|
|
175
|
+
try {
|
|
176
|
+
await client.query('BEGIN');
|
|
177
|
+
for (const stmt of statements) {
|
|
178
|
+
await client.query(stmt);
|
|
179
|
+
}
|
|
180
|
+
await client.query('COMMIT');
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
await client.query('ROLLBACK');
|
|
184
|
+
throw new Error(`Transaction rolled back — database unchanged. Reason: ${err.message}`);
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
await client.end();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Supabase Storage API
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
77
193
|
export async function listObjects(projectRef, bucket, prefix = '') {
|
|
78
194
|
const token = await getToken();
|
|
79
|
-
// Using Supabase Storage API directly
|
|
80
195
|
const url = `https://${projectRef}.supabase.co/storage/v1/object/list/${bucket}`;
|
|
81
196
|
const response = await axios.post(url, {
|
|
82
197
|
prefix,
|
|
@@ -88,10 +203,9 @@ export async function listObjects(projectRef, bucket, prefix = '') {
|
|
|
88
203
|
});
|
|
89
204
|
return response.data;
|
|
90
205
|
}
|
|
91
|
-
export async function uploadObject(projectRef, bucket,
|
|
206
|
+
export async function uploadObject(projectRef, bucket, filePath, content) {
|
|
92
207
|
const token = await getToken();
|
|
93
|
-
const url = `https://${projectRef}.supabase.co/storage/v1/object/${bucket}/${
|
|
94
|
-
// Upload history file content
|
|
208
|
+
const url = `https://${projectRef}.supabase.co/storage/v1/object/${bucket}/${filePath}`;
|
|
95
209
|
await axios.post(url, content, {
|
|
96
210
|
headers: {
|
|
97
211
|
Authorization: `Bearer ${token}`,
|
|
@@ -100,9 +214,9 @@ export async function uploadObject(projectRef, bucket, path, content) {
|
|
|
100
214
|
}
|
|
101
215
|
});
|
|
102
216
|
}
|
|
103
|
-
export async function downloadObject(projectRef, bucket,
|
|
217
|
+
export async function downloadObject(projectRef, bucket, filePath) {
|
|
104
218
|
const token = await getToken();
|
|
105
|
-
const url = `https://${projectRef}.supabase.co/storage/v1/object/${bucket}/${
|
|
219
|
+
const url = `https://${projectRef}.supabase.co/storage/v1/object/${bucket}/${filePath}`;
|
|
106
220
|
const response = await axios.get(url, {
|
|
107
221
|
headers: { Authorization: `Bearer ${token}` },
|
|
108
222
|
responseType: 'text'
|
|
@@ -122,40 +236,33 @@ export async function ensureBucket(projectRef, bucket) {
|
|
|
122
236
|
});
|
|
123
237
|
}
|
|
124
238
|
catch (e) {
|
|
125
|
-
if (e.response?.status !== 409)
|
|
126
|
-
throw e;
|
|
127
|
-
}
|
|
239
|
+
if (e.response?.status !== 409)
|
|
240
|
+
throw e; // 409 = already exists, OK
|
|
128
241
|
}
|
|
129
242
|
}
|
|
130
|
-
//
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// RBAC / Permissions
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
131
246
|
export async function isProductionAdmin(projectRef) {
|
|
132
247
|
const token = await getToken();
|
|
133
248
|
if (!token)
|
|
134
249
|
throw new Error('Not logged in.');
|
|
135
250
|
try {
|
|
136
|
-
// 1. Get the organization for this project
|
|
137
251
|
const projects = await getProjects();
|
|
138
252
|
const project = projects.find((p) => p.id === projectRef);
|
|
139
253
|
if (!project)
|
|
140
254
|
throw new Error(`Project ${projectRef} not found in your account.`);
|
|
141
255
|
const orgId = project.organization_id;
|
|
142
|
-
// 2. Get the current user's profile to find their ID/Email
|
|
143
|
-
// Actually, the easiest way with Management API is to list organization members
|
|
144
|
-
// and find the one that matches our token's identity.
|
|
145
|
-
// However, the Management API token itself doesn't easily reveal "who am I"
|
|
146
|
-
// without an extra call.
|
|
147
256
|
const userResp = await axios.get(`${API_URL}/me`, {
|
|
148
257
|
headers: { Authorization: `Bearer ${token}` }
|
|
149
258
|
});
|
|
150
259
|
const myEmail = userResp.data.email;
|
|
151
|
-
// 3. Get org members and check role
|
|
152
260
|
const membersResp = await axios.get(`${API_URL}/organizations/${orgId}/members`, {
|
|
153
261
|
headers: { Authorization: `Bearer ${token}` }
|
|
154
262
|
});
|
|
155
263
|
const me = membersResp.data.find((m) => m.email === myEmail);
|
|
156
264
|
if (!me)
|
|
157
265
|
return false;
|
|
158
|
-
// Only Owners and Administrators can touch production
|
|
159
266
|
const allowedRoles = ['Owner', 'Administrator'];
|
|
160
267
|
return allowedRoles.includes(me.roleName);
|
|
161
268
|
}
|
package/dist/commands/branch.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'fs/promises';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import readline from 'readline';
|
|
5
|
-
import { getConfig } from '../api/supabase.js';
|
|
5
|
+
import { getConfig, createSupabaseBranch, getSupabaseBranch } from '../api/supabase.js';
|
|
6
6
|
const GITBASE_DIR = '.gitbase';
|
|
7
7
|
const CONFIG_FILE = path.join(GITBASE_DIR, 'config');
|
|
8
8
|
export async function branch(argv) {
|
|
@@ -48,18 +48,59 @@ export async function branch(argv) {
|
|
|
48
48
|
console.error(chalk.red(`Branch '${branchName}' already exists.`));
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
51
|
-
const rl = readline.createInterface({
|
|
52
|
-
input: process.stdin,
|
|
53
|
-
output: process.stdout
|
|
54
|
-
});
|
|
51
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
55
52
|
const ask = (q) => new Promise(r => rl.question(q, r));
|
|
56
53
|
console.log(chalk.blue(`Creating new branch: ${branchName}`));
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
54
|
+
console.log(`How should this branch connect?`);
|
|
55
|
+
console.log(` [1] Create new native Supabase Branch (requires Pro plan)`);
|
|
56
|
+
console.log(` [2] Link to existing separate Supabase project (Free tier compatible)`);
|
|
57
|
+
const choice = await ask('\nChoice [1/2]: ');
|
|
58
|
+
let ref = '';
|
|
59
|
+
let connString = '';
|
|
60
|
+
if (choice === '1') {
|
|
61
|
+
const parentRef = config.branches[config.currentBranch]?.projectRef || config.projectRef;
|
|
62
|
+
if (!parentRef) {
|
|
63
|
+
console.error(chalk.red('Could not determine parent project ref.'));
|
|
64
|
+
rl.close();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
console.log(chalk.blue(`\nChecking for existing native Supabase branch...`));
|
|
69
|
+
let branch = await getSupabaseBranch(parentRef, branchName);
|
|
70
|
+
if (branch) {
|
|
71
|
+
console.log(chalk.green(`Found existing native branch '${branchName}'. Linking to it...`));
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
console.log(chalk.blue(`Creating native Supabase branch from ${parentRef}...`));
|
|
75
|
+
branch = await createSupabaseBranch(parentRef, branchName);
|
|
76
|
+
}
|
|
77
|
+
ref = branch.project_ref;
|
|
78
|
+
// The connection string for a branch follows the standard Supabase format
|
|
79
|
+
// using the new branch's project_ref. We need the DB password, which we
|
|
80
|
+
// can assume is the same as the parent project for branching purposes,
|
|
81
|
+
// or we must ask the user for it if it's newly provisioned.
|
|
82
|
+
// But actually, native branches share the parent's password by default.
|
|
83
|
+
const parentConn = config.branches[config.currentBranch]?.connectionString || config.connectionString;
|
|
84
|
+
const url = new URL(parentConn);
|
|
85
|
+
connString = `postgres://${url.username}:${url.password}@aws-0-${branch.region ?? 'us-east-1'}.pooler.supabase.com:6543/postgres`;
|
|
86
|
+
console.log(chalk.green(`✓ Branch configured natively (Ref: ${ref})`));
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
console.error(chalk.red(`Failed to link/create native branch. Does the parent project have branching enabled on a Pro plan?`));
|
|
90
|
+
console.error(chalk.red(`Error: ${e.response?.data?.message || e.message}`));
|
|
91
|
+
rl.close();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
console.log('');
|
|
97
|
+
ref = await ask('Project Ref: ');
|
|
98
|
+
connString = await ask('Connection URI: ');
|
|
99
|
+
if (!ref || !connString) {
|
|
100
|
+
console.error(chalk.red('Project Ref and Connection URI are required.'));
|
|
101
|
+
rl.close();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
63
104
|
}
|
|
64
105
|
const currentHead = await fs.readFile(path.join(GITBASE_DIR, 'HEAD'), 'utf-8').catch(() => null);
|
|
65
106
|
config.branches = config.branches || {};
|
|
@@ -72,6 +113,10 @@ export async function branch(argv) {
|
|
|
72
113
|
console.log(chalk.green(`Branch '${branchName}' created.`));
|
|
73
114
|
rl.close();
|
|
74
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Switches to a branch and automatically updates local supabase/ files
|
|
118
|
+
* to match the branch's HEAD commit — no manual `gitb reset` needed.
|
|
119
|
+
*/
|
|
75
120
|
export async function checkout(argv) {
|
|
76
121
|
const config = await getConfig();
|
|
77
122
|
if (!config) {
|
|
@@ -87,10 +132,37 @@ export async function checkout(argv) {
|
|
|
87
132
|
console.error(chalk.red(`Branch '${branchName}' does not exist.`));
|
|
88
133
|
return;
|
|
89
134
|
}
|
|
90
|
-
|
|
135
|
+
if (branchName === config.currentBranch) {
|
|
136
|
+
console.log(chalk.yellow(`Already on branch '${branchName}'.`));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
// Switch config
|
|
91
140
|
config.currentBranch = branchName;
|
|
92
141
|
await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
93
|
-
// Swapping local 'supabase/' folder to match branch HEAD is recommended but not implemented yet.
|
|
94
142
|
console.log(chalk.green(`Switched to branch '${branchName}'`));
|
|
95
|
-
|
|
143
|
+
// Auto-swap local supabase/ files to match the branch's HEAD commit
|
|
144
|
+
const branchHead = config.branches[branchName].head;
|
|
145
|
+
if (!branchHead) {
|
|
146
|
+
console.log(chalk.yellow(' Branch has no commits yet — local files unchanged.'));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const { readCommit, readTree, readObject } = await import('../storage/git.js');
|
|
151
|
+
const commit = await readCommit(branchHead);
|
|
152
|
+
const tree = await readTree(commit.tree);
|
|
153
|
+
// Wipe supabase/ and rewrite from the branch's commit tree
|
|
154
|
+
await fs.rm('supabase', { recursive: true, force: true });
|
|
155
|
+
for (const [relPath, hash] of Object.entries(tree)) {
|
|
156
|
+
const fullPath = path.join('supabase', relPath);
|
|
157
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
158
|
+
const content = await readObject(hash);
|
|
159
|
+
await fs.writeFile(fullPath, content, 'utf-8');
|
|
160
|
+
}
|
|
161
|
+
// Update HEAD pointer
|
|
162
|
+
await fs.writeFile(path.join(GITBASE_DIR, 'HEAD'), branchHead, 'utf-8');
|
|
163
|
+
console.log(chalk.cyan(` Local files updated to match HEAD (${branchHead.substring(0, 7)}).`));
|
|
164
|
+
}
|
|
165
|
+
catch (e) {
|
|
166
|
+
console.error(chalk.red(` Failed to update local files: ${e.message}`));
|
|
167
|
+
}
|
|
96
168
|
}
|