@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 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
- - **Multi-Profile Branches**: Manage Production, Staging, and Development references using Git-like branches.
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 [commit]`
151
+ - **Usage**: `gitb diff [commit1] [commit2] [options]`
125
152
  - **Arguments**:
126
- | Argument | Description |
153
+ | Argument / Flag | Description |
127
154
  | :--- | :--- |
128
- | `commit` | Optional. The commit hash to compare against the Live DB (defaults to local HEAD). |
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
- Synchronizes configuration and history from another branch profile.
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**: `add`, `list`, `push`, `pull`.
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
 
@@ -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
- export async function runQuery(projectRef, sql) {
30
- // 1. Get Connection String from Config
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
- // 2. Connect
59
- // Use connection pooling URL provided by user
60
- const client = new Client({
61
- connectionString,
62
- ssl: { rejectUnauthorized: false }
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
- const res = await client.query(sql);
67
- return res.rows; // Array of objects
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
- // --- STORAGE API ---
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, path, content) {
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}/${path}`;
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, path) {
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}/${path}`;
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
- // --- RBAC / PERMISSIONS ---
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
  }
@@ -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
- const ref = await ask('Project Ref: ');
58
- const connString = await ask('Connection URI: ');
59
- if (!ref || !connString) {
60
- console.error(chalk.red('Project Ref and Connection URI are required.'));
61
- rl.close();
62
- return;
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
- // Switch to target branch profile
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
- console.log(chalk.yellow('Note: Your local files are unchanged. Run `gitb reset` to sync files with this branch\'s HEAD.'));
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
  }