@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.
- package/README.md +48 -0
- package/dist/daemon/daemon-process.d.ts +2 -0
- package/dist/daemon/daemon-process.js +13751 -0
- package/dist/daemon/daemon-process.js.map +1 -0
- package/dist/hooks/post-checkout +327 -0
- package/dist/hooks/post-commit +137 -0
- package/dist/hooks/pre-commit +140 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7670 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -0
|
@@ -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
|
package/dist/index.d.ts
ADDED