@episoda/cli 0.2.144

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.
@@ -0,0 +1,327 @@
1
+ #!/bin/bash
2
+ # Git post-checkout hook to update module.checkout_* fields for realtime badge updates
3
+ # EP534: Also checks branch locks and auto-reverts if branch is locked by another user
4
+ # EP649: Added support for cloud VM checkout tracking (Fly.io, Codespaces, Gitpod, etc.)
5
+ # EP749: Simplified to use module.checkout_* as single source of truth (removed branch_checkout table)
6
+ # This runs after every successful git checkout (from API or terminal)
7
+
8
+ # Set up logging (worktree-safe)
9
+ HOOKS_DIR=$(git rev-parse --git-path hooks 2>/dev/null || echo ".git/hooks")
10
+ mkdir -p "$HOOKS_DIR" 2>/dev/null
11
+ LOG_FILE="${HOOKS_DIR}/post-checkout.log"
12
+ log() {
13
+ echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
14
+ }
15
+
16
+ # Get hook parameters
17
+ PREV_HEAD=$1
18
+ NEW_HEAD=$2
19
+ BRANCH_CHECKOUT=$3 # 1 if branch checkout, 0 if file checkout
20
+
21
+ # Allow automation to skip this hook entirely
22
+ if [ -n "$EPISODA_SKIP_CHECKOUT_HOOK" ]; then
23
+ log "Skipping update - EPISODA_SKIP_CHECKOUT_HOOK set"
24
+ exit 0
25
+ fi
26
+
27
+ # Skip updates during rebase/detached HEAD to avoid guessing branches
28
+ if git rev-parse -q --verify REBASE_HEAD >/dev/null 2>&1; then
29
+ log "Skipping update - rebase in progress"
30
+ exit 0
31
+ fi
32
+
33
+ REBASE_MERGE_DIR=$(git rev-parse --git-path rebase-merge 2>/dev/null || echo "")
34
+ REBASE_APPLY_DIR=$(git rev-parse --git-path rebase-apply 2>/dev/null || echo "")
35
+ if [ -d "$REBASE_MERGE_DIR" ] || [ -d "$REBASE_APPLY_DIR" ]; then
36
+ log "Skipping update - rebase in progress"
37
+ exit 0
38
+ fi
39
+
40
+ # Get the old and new branch names
41
+ OLD_BRANCH=$(git name-rev --name-only $PREV_HEAD 2>/dev/null | sed 's/^remotes\/origin\///')
42
+ NEW_BRANCH=$(git rev-parse --abbrev-ref HEAD)
43
+
44
+ # If in detached HEAD state, skip update (no guessing)
45
+ if [ "$NEW_BRANCH" = "HEAD" ]; then
46
+ log "Skipping update - detached HEAD state"
47
+ exit 0
48
+ fi
49
+
50
+ log "Branch checkout: $OLD_BRANCH → $NEW_BRANCH"
51
+
52
+ # Get database connection info from .env.local
53
+ if [ -f .env.local ]; then
54
+ export $(grep -v '^#' .env.local | grep -E 'NEXT_PUBLIC_SUPABASE_URL|SUPABASE_SERVICE_ROLE_KEY' | xargs)
55
+ fi
56
+
57
+ # EP553: Atomic branch checkout using checkout_branch_atomic() function
58
+ # Combines lock check and checkout update in a single transaction to prevent race conditions
59
+ CHECKOUT_RESULT=$(node -e "
60
+ let createClient;
61
+ try {
62
+ ({ createClient } = require('@supabase/supabase-js'));
63
+ } catch (error) {
64
+ console.log('SKIP:Missing Supabase dependency');
65
+ process.exit(0);
66
+ }
67
+ const { execSync } = require('child_process');
68
+
69
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
70
+ const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
71
+
72
+ if (!supabaseUrl || !supabaseKey) {
73
+ console.log('SKIP:Missing Supabase credentials');
74
+ process.exit(0);
75
+ }
76
+
77
+ const supabase = createClient(supabaseUrl, supabaseKey);
78
+
79
+ (async () => {
80
+ try {
81
+ // EP553: Get user ID and workspace ID from git config
82
+ let userId = null;
83
+ let configuredWorkspaceId = null;
84
+
85
+ try {
86
+ userId = execSync('git config episoda.userId', { encoding: 'utf8' }).trim();
87
+ } catch (e) {
88
+ console.log('SKIP:No user configured');
89
+ process.exit(0);
90
+ }
91
+
92
+ try {
93
+ configuredWorkspaceId = execSync('git config episoda.workspaceId', { encoding: 'utf8' }).trim();
94
+ } catch (e) {
95
+ console.log('SKIP:No workspace configured');
96
+ process.exit(0);
97
+ }
98
+
99
+ if (!userId || !configuredWorkspaceId) {
100
+ console.log('SKIP:Configuration incomplete');
101
+ process.exit(0);
102
+ }
103
+
104
+ // EP553: Verify user exists and is not banned
105
+ const { data: userData, error: userError } = await supabase.auth.admin.getUserById(userId);
106
+
107
+ if (userError || !userData.user) {
108
+ console.log('ERROR:Invalid user ID');
109
+ process.exit(1);
110
+ }
111
+
112
+ if (userData.user.banned) {
113
+ console.log('ERROR:User account suspended');
114
+ process.exit(1);
115
+ }
116
+
117
+ // EP553: Get user's actual workspace via workspace_member table
118
+ const { data: membership, error: membershipError } = await supabase
119
+ .from('workspace_member')
120
+ .select('workspace_id')
121
+ .eq('user_id', userId)
122
+ .maybeSingle();
123
+
124
+ if (membershipError || !membership) {
125
+ console.log('ERROR:User not member of workspace');
126
+ process.exit(1);
127
+ }
128
+
129
+ const workspaceId = membership.workspace_id;
130
+
131
+ // EP553: Multi-user machine protection
132
+ if (configuredWorkspaceId !== workspaceId) {
133
+ console.log('ERROR:Workspace mismatch');
134
+ process.exit(1);
135
+ }
136
+
137
+ // EP725: Get project ID from git config for main/master checkout tracking
138
+ // This ensures main branch checkouts are recorded with project_id for badge display
139
+ let configuredProjectId = null;
140
+ try {
141
+ configuredProjectId = execSync('git config episoda.projectId', { encoding: 'utf8' }).trim();
142
+ } catch (e) {
143
+ // No projectId configured - will use null (backwards compatible)
144
+ }
145
+
146
+ // EP556: Extract UID from branch name and look up by UID instead of branch_name
147
+ // This eliminates race conditions during Ready→Doing transitions
148
+ // EP962: Supports both new format (EP{number}-{description}) and legacy (module/EP{number}-{description})
149
+ let moduleId = null;
150
+ let projectId = configuredProjectId || null; // EP725: Default to configured project for main/master
151
+ if ('${NEW_BRANCH}' !== 'main' && '${NEW_BRANCH}' !== 'master') {
152
+ // Match both EP123-description and module/EP123-description
153
+ const branchMatch = '${NEW_BRANCH}'.match(/^(?:module\/)?EP(\d+)-/);
154
+ if (branchMatch) {
155
+ const uid = 'EP' + branchMatch[1];
156
+ // EP970: Also fetch checkout_at to detect if orchestrator already handled this
157
+ const { data: module } = await supabase
158
+ .from('module')
159
+ .select('id, project_id, checkout_at, checkout_user_id')
160
+ .eq('uid', uid)
161
+ .maybeSingle();
162
+
163
+ if (module) {
164
+ moduleId = module.id;
165
+ projectId = module.project_id;
166
+
167
+ // EP970: Skip if orchestrator already handled checkout within last 10 seconds
168
+ // This prevents duplicate RPC calls when dragging cards on the kanban board
169
+ if (module.checkout_at && module.checkout_user_id === userId) {
170
+ const checkoutAge = Date.now() - new Date(module.checkout_at).getTime();
171
+ if (checkoutAge < 10000) {
172
+ console.log('SKIP:Orchestrator already handled checkout (' + Math.round(checkoutAge/1000) + 's ago)');
173
+ process.exit(0);
174
+ }
175
+ }
176
+ }
177
+ }
178
+ }
179
+
180
+ // EP553: Detect environment type based on where the hook is running
181
+ // EP649: Added Fly.io cloud VM detection
182
+ // Check for common cloud development environment variables
183
+ let environment = 'local';
184
+ let cloudMachineId = null;
185
+
186
+ if (process.env.FLY_MACHINE_ID) {
187
+ environment = 'cloud'; // Fly.io VM (Episoda cloud dev)
188
+ cloudMachineId = process.env.FLY_MACHINE_ID;
189
+ } else if (process.env.CODESPACES === 'true' || process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN) {
190
+ environment = 'cloud'; // GitHub Codespaces
191
+ cloudMachineId = process.env.CODESPACE_NAME || null;
192
+ } else if (process.env.GITPOD_WORKSPACE_ID) {
193
+ environment = 'cloud'; // Gitpod
194
+ cloudMachineId = process.env.GITPOD_WORKSPACE_ID;
195
+ } else if (process.env.C9_PROJECT || process.env.AWS_CLOUD9_USER) {
196
+ environment = 'cloud'; // AWS Cloud9
197
+ cloudMachineId = process.env.C9_PROJECT || null;
198
+ } else if (process.env.REPL_ID || process.env.REPLIT_DB_URL) {
199
+ environment = 'cloud'; // Replit
200
+ cloudMachineId = process.env.REPL_ID || null;
201
+ }
202
+
203
+ // EP726: Get device UUID for checkout tracking
204
+ // EP773: Tables renamed: local_device → local_machine, cloud_device → cloud_machine
205
+ // For local: read local_machine.id from git config (cached by CLI daemon)
206
+ // For cloud: read cloud_machine.id from git config (set during VM provisioning)
207
+ let localDeviceId = null;
208
+ let cloudDeviceId = null;
209
+
210
+ if (environment === 'local') {
211
+ // Local checkout: get local_machine.id from git config
212
+ try {
213
+ localDeviceId = execSync('git config episoda.deviceId', { encoding: 'utf8' }).trim();
214
+ } catch (e) {
215
+ // No deviceId configured yet - will use null (backwards compatible)
216
+ }
217
+ } else {
218
+ // Cloud checkout: get cloud_machine.id from git config
219
+ // EP773: Renamed cloud_device → cloud_machine
220
+ try {
221
+ cloudDeviceId = execSync('git config episoda.cloudDeviceId', { encoding: 'utf8' }).trim();
222
+ } catch (e) {
223
+ // No cloudDeviceId configured - try to look up by FLY_MACHINE_ID
224
+ if (cloudMachineId) {
225
+ const { data: cloudMachine } = await supabase
226
+ .from('cloud_machine')
227
+ .select('id')
228
+ .eq('machine_id', cloudMachineId)
229
+ .maybeSingle();
230
+ if (cloudMachine) {
231
+ cloudDeviceId = cloudMachine.id;
232
+ }
233
+ }
234
+ }
235
+ }
236
+
237
+ // EP726: Call atomic checkout function with unified machine IDs
238
+ // EP768: Renamed p_*_device_id → p_*_machine_id to match function signature
239
+ // - p_local_machine_id: UUID FK to local_machine (for local checkouts)
240
+ // - p_cloud_machine_id: UUID FK to cloud_machine (for cloud checkouts)
241
+ const { data: result, error: rpcError } = await supabase
242
+ .rpc('checkout_branch_atomic', {
243
+ p_branch_name: '${NEW_BRANCH}',
244
+ p_user_id: userId,
245
+ p_workspace_id: workspaceId,
246
+ p_project_id: projectId,
247
+ p_environment: environment,
248
+ p_module_id: moduleId,
249
+ p_cloud_machine_id: cloudDeviceId,
250
+ p_local_machine_id: localDeviceId
251
+ });
252
+
253
+ if (rpcError) {
254
+ console.log('ERROR:' + rpcError.message);
255
+ process.exit(1);
256
+ }
257
+
258
+ if (!result.success) {
259
+ if (result.error === 'BRANCH_LOCKED') {
260
+ console.log('LOCKED:' + result.locked_by + ':' + result.environment);
261
+ } else if (result.error === 'BRANCH_BUSY') {
262
+ console.log('BUSY:' + result.message);
263
+ } else {
264
+ console.log('ERROR:' + (result.message || 'Unknown error'));
265
+ }
266
+ process.exit(0);
267
+ }
268
+
269
+ // Success - branch checked out atomically
270
+ console.log('SUCCESS:${NEW_BRANCH}');
271
+ } catch (err) {
272
+ console.log('ERROR:' + err.message);
273
+ }
274
+ })();
275
+ ")
276
+
277
+ # Handle the checkout result
278
+ if [[ "$CHECKOUT_RESULT" == SKIP:* ]]; then
279
+ # Configuration incomplete or credentials missing - skip quietly
280
+ log "Skipping checkout update: ${CHECKOUT_RESULT#SKIP:}"
281
+ exit 0
282
+ elif [[ "$CHECKOUT_RESULT" == ERROR:* ]]; then
283
+ # Fatal error - abort
284
+ echo ""
285
+ echo "❌ ERROR: ${CHECKOUT_RESULT#ERROR:}"
286
+ echo ""
287
+ exit 1
288
+ elif [[ "$CHECKOUT_RESULT" == LOCKED:* ]]; then
289
+ # Branch is locked by another user - revert checkout
290
+ LOCKED_USER=$(echo "$CHECKOUT_RESULT" | cut -d':' -f2)
291
+ LOCK_ENV=$(echo "$CHECKOUT_RESULT" | cut -d':' -f3)
292
+
293
+ echo ""
294
+ echo "❌ ERROR: Branch '$NEW_BRANCH' is currently checked out by $LOCKED_USER in $LOCK_ENV mode."
295
+ echo ""
296
+ echo "This branch is locked to prevent conflicts. Options:"
297
+ echo " 1. Ask $LOCKED_USER to check in via GitHub Integration modal"
298
+ echo " 2. Work on a different module/branch"
299
+ echo " 3. Wait for the lock to be released"
300
+ echo ""
301
+ echo "Reverting to previous branch: $OLD_BRANCH"
302
+ echo ""
303
+
304
+ # Revert to old branch
305
+ git checkout "$OLD_BRANCH" 2>/dev/null
306
+ exit 1
307
+ elif [[ "$CHECKOUT_RESULT" == BUSY:* ]]; then
308
+ # Another user is currently checking out - suggest retry
309
+ echo ""
310
+ echo "⏳ Branch checkout in progress by another user"
311
+ echo "Please try again in a moment"
312
+ echo ""
313
+ echo "Reverting to previous branch: $OLD_BRANCH"
314
+ echo ""
315
+
316
+ # Revert to old branch
317
+ git checkout "$OLD_BRANCH" 2>/dev/null
318
+ exit 1
319
+ elif [[ "$CHECKOUT_RESULT" == SUCCESS:* ]]; then
320
+ # Success
321
+ log "Successfully checked out branch: $NEW_BRANCH"
322
+ exit 0
323
+ else
324
+ # Unexpected result
325
+ log "Unexpected checkout result: $CHECKOUT_RESULT"
326
+ exit 0
327
+ fi
@@ -0,0 +1,137 @@
1
+ #!/bin/bash
2
+ #
3
+ # EP837: Record commits made on main branch for machine-aware tracking
4
+ #
5
+ # This hook runs after every successful commit. When on main branch,
6
+ # it records the commit in the local_commit table so that main branch
7
+ # protection only triggers for commits made on THIS machine.
8
+ #
9
+ # Problem: git log origin/main..HEAD shows ALL unpushed commits including
10
+ # commits made on other machines. This causes false positives when
11
+ # origin/main is stale.
12
+ #
13
+ # Solution: Track which commits were made on which machine. Main branch
14
+ # check queries local_commit table filtered by machine_id.
15
+
16
+ # Get current branch
17
+ BRANCH=$(git branch --show-current)
18
+
19
+ # Only track commits on main or master branch
20
+ if [ "$BRANCH" != "main" ] && [ "$BRANCH" != "master" ]; then
21
+ exit 0
22
+ fi
23
+
24
+ # Get the commit that was just made
25
+ COMMIT_SHA=$(git rev-parse HEAD)
26
+ COMMIT_MESSAGE=$(git log -1 --pretty=format:%s)
27
+ COMMIT_AUTHOR=$(git log -1 --pretty=format:%an)
28
+
29
+ # Read config from ~/.episoda/config.json
30
+ CONFIG_FILE="$HOME/.episoda/config.json"
31
+
32
+ if [ ! -f "$CONFIG_FILE" ]; then
33
+ # No config file - CLI not authenticated, skip silently
34
+ exit 0
35
+ fi
36
+
37
+ # Record the commit using the API
38
+ RESULT=$(node -e "
39
+ const fs = require('fs');
40
+ const https = require('https');
41
+ const http = require('http');
42
+ const { execSync } = require('child_process');
43
+
44
+ (async () => {
45
+ try {
46
+ // Read config
47
+ const config = JSON.parse(fs.readFileSync('$CONFIG_FILE', 'utf8'));
48
+ const token = config.access_token;
49
+ const apiUrl = config.api_url || 'https://episoda.dev';
50
+ const deviceId = config.device_id;
51
+ const projectId = config.project_id;
52
+
53
+ if (!token) {
54
+ console.log('SKIP:No token');
55
+ process.exit(0);
56
+ }
57
+
58
+ if (!deviceId) {
59
+ console.log('SKIP:No device_id');
60
+ process.exit(0);
61
+ }
62
+
63
+ if (!projectId) {
64
+ console.log('SKIP:No project_id');
65
+ process.exit(0);
66
+ }
67
+
68
+ // Record the commit
69
+ const url = new URL('/api/commits/local', apiUrl);
70
+ const client = url.protocol === 'https:' ? https : http;
71
+
72
+ const postData = JSON.stringify({
73
+ sha: '$COMMIT_SHA',
74
+ machine_id: deviceId,
75
+ project_id: projectId,
76
+ branch: '$BRANCH',
77
+ message: \`$COMMIT_MESSAGE\`,
78
+ author: \`$COMMIT_AUTHOR\`
79
+ });
80
+
81
+ const req = client.request(url, {
82
+ method: 'POST',
83
+ headers: {
84
+ 'Authorization': 'Bearer ' + token,
85
+ 'Content-Type': 'application/json',
86
+ 'Content-Length': Buffer.byteLength(postData)
87
+ },
88
+ timeout: 5000
89
+ }, (res) => {
90
+ let data = '';
91
+ res.on('data', chunk => data += chunk);
92
+ res.on('end', () => {
93
+ try {
94
+ const json = JSON.parse(data);
95
+ if (json.success) {
96
+ console.log('SUCCESS:' + (json.created ? 'recorded' : 'exists'));
97
+ } else {
98
+ console.log('ERROR:' + (json.error?.message || 'Unknown error'));
99
+ }
100
+ } catch (e) {
101
+ console.log('ERROR:Parse error - ' + e.message);
102
+ }
103
+ });
104
+ });
105
+
106
+ req.on('error', (e) => {
107
+ console.log('ERROR:Network - ' + e.message);
108
+ });
109
+ req.on('timeout', () => {
110
+ req.destroy();
111
+ console.log('ERROR:Timeout');
112
+ });
113
+
114
+ req.write(postData);
115
+ req.end();
116
+ } catch (e) {
117
+ console.log('ERROR:' + e.message);
118
+ }
119
+ })();
120
+ " 2>/dev/null)
121
+
122
+ # Log result (non-blocking, don't fail the commit)
123
+ case "$RESULT" in
124
+ SUCCESS:*)
125
+ # Commit recorded successfully
126
+ ;;
127
+ SKIP:*)
128
+ # Configuration incomplete - skip silently
129
+ ;;
130
+ ERROR:*)
131
+ # Log error but don't fail the commit
132
+ echo "Warning: Failed to record commit for tracking: ${RESULT#ERROR:}" >&2
133
+ ;;
134
+ esac
135
+
136
+ # Always exit successfully - don't block commits
137
+ exit 0
@@ -0,0 +1,140 @@
1
+ #!/bin/bash
2
+ #
3
+ # EP490: Prevent commits directly to main branch
4
+ # EP647: Extended to block ALL non-module branches (except hotfix/*)
5
+ # EP962: Updated to support new branch format (EP123-description instead of module/EP123-...)
6
+ # This hook enforces the module workflow for all development work
7
+ #
8
+
9
+ BRANCH=$(git branch --show-current)
10
+
11
+ # Allow module branches - both new format (EP123-description) and legacy (module/EP123-description)
12
+ # EP647: But warn if it's a cloud module being committed locally
13
+ if [[ "$BRANCH" == EP* ]] || [[ "$BRANCH" == module/EP* ]]; then
14
+ # Extract module UID from branch name (e.g., EP647 from EP647-description or module/EP647-description)
15
+ MODULE_UID=$(echo "$BRANCH" | grep -oE 'EP[0-9]+')
16
+
17
+ if [ -n "$MODULE_UID" ]; then
18
+ # Check if this is a cloud module
19
+ # Read config from ~/.episoda/config.json
20
+ CONFIG_FILE="$HOME/.episoda/config.json"
21
+
22
+ if [ -f "$CONFIG_FILE" ]; then
23
+ # Extract access_token and api_url using node (more reliable than jq which may not be installed)
24
+ CLOUD_CHECK=$(node -e "
25
+ const fs = require('fs');
26
+ const https = require('https');
27
+ const http = require('http');
28
+
29
+ try {
30
+ const config = JSON.parse(fs.readFileSync('$CONFIG_FILE', 'utf8'));
31
+ const token = config.access_token;
32
+ const apiUrl = config.api_url || 'https://episoda.dev';
33
+
34
+ if (!token) {
35
+ console.log('SKIP:No token');
36
+ process.exit(0);
37
+ }
38
+
39
+ const url = new URL('/api/modules/$MODULE_UID', apiUrl);
40
+ const client = url.protocol === 'https:' ? https : http;
41
+
42
+ const req = client.request(url, {
43
+ method: 'GET',
44
+ headers: {
45
+ 'Authorization': 'Bearer ' + token,
46
+ 'Content-Type': 'application/json'
47
+ },
48
+ timeout: 3000
49
+ }, (res) => {
50
+ let data = '';
51
+ res.on('data', chunk => data += chunk);
52
+ res.on('end', () => {
53
+ try {
54
+ const json = JSON.parse(data);
55
+ if (json.success && json.moduleRecord && json.moduleRecord.dev_mode === 'cloud') {
56
+ console.log('CLOUD:' + json.moduleRecord.title);
57
+ } else {
58
+ console.log('LOCAL');
59
+ }
60
+ } catch (e) {
61
+ console.log('SKIP:Parse error');
62
+ }
63
+ });
64
+ });
65
+
66
+ req.on('error', () => console.log('SKIP:Network error'));
67
+ req.on('timeout', () => { req.destroy(); console.log('SKIP:Timeout'); });
68
+ req.end();
69
+ } catch (e) {
70
+ console.log('SKIP:Config error');
71
+ }
72
+ " 2>/dev/null)
73
+
74
+ if [[ "$CLOUD_CHECK" == CLOUD:* ]]; then
75
+ MODULE_TITLE="${CLOUD_CHECK#CLOUD:}"
76
+ echo ""
77
+ echo "⚠️ Warning: '$MODULE_UID' is a CLOUD module"
78
+ echo ""
79
+ echo "Module: $MODULE_TITLE"
80
+ echo ""
81
+ echo "This module is configured for cloud development (dev_mode: cloud)."
82
+ echo "Local commits may cause sync issues with the cloud environment."
83
+ echo ""
84
+ echo "Recommended workflow:"
85
+ echo " 1. Connect to cloud VM: episoda dev ssh $MODULE_UID"
86
+ echo " 2. Make changes and commit in the cloud environment"
87
+ echo ""
88
+ echo "To bypass this warning:"
89
+ echo " git commit --no-verify"
90
+ echo ""
91
+ exit 1
92
+ fi
93
+ fi
94
+ fi
95
+
96
+ # Module branch is allowed (local module or cloud check skipped/failed)
97
+ exit 0
98
+ fi
99
+
100
+ # Allow hotfix branches (hotfix/description) for emergency fixes
101
+ if [[ "$BRANCH" == hotfix/* ]]; then
102
+ exit 0
103
+ fi
104
+
105
+ # Block main branch with specific message
106
+ if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then
107
+ echo ""
108
+ echo "❌ Cannot commit directly to main branch!"
109
+ echo ""
110
+ echo "Please create a module first:"
111
+ echo " 1. Go to https://episoda.dev/workflow"
112
+ echo " 2. Create a new module (or select existing)"
113
+ echo " 3. Move it to 'Doing' state to create a branch"
114
+ echo " 4. Then switch to the module branch:"
115
+ echo " git stash && git checkout EP-XXX-... && git stash pop"
116
+ echo ""
117
+ echo "To bypass this check (not recommended):"
118
+ echo " git commit --no-verify"
119
+ echo ""
120
+ exit 1
121
+ fi
122
+
123
+ # Block all other branches
124
+ echo ""
125
+ echo "❌ Cannot commit on branch '$BRANCH'"
126
+ echo ""
127
+ echo "All work must be done on module branches (EP-XXX-...)."
128
+ echo "This ensures proper tracking and code review workflow."
129
+ echo ""
130
+ echo "Options:"
131
+ echo " 1. Create a module at https://episoda.dev/workflow"
132
+ echo " Move it to 'Doing' to create a branch, then checkout"
133
+ echo ""
134
+ echo " 2. For emergency hotfixes, use a hotfix/* branch:"
135
+ echo " git checkout -b hotfix/description"
136
+ echo ""
137
+ echo "To bypass this check (not recommended):"
138
+ echo " git commit --no-verify"
139
+ echo ""
140
+ exit 1
@@ -0,0 +1,2 @@
1
+
2
+ export { }