commandmate 0.2.0 → 0.2.1

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.
Files changed (62) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-build-manifest.json +9 -9
  3. package/.next/app-path-routes-manifest.json +1 -1
  4. package/.next/build-manifest.json +2 -2
  5. package/.next/cache/.tsbuildinfo +1 -1
  6. package/.next/cache/config.json +3 -3
  7. package/.next/cache/webpack/client-production/0.pack +0 -0
  8. package/.next/cache/webpack/client-production/1.pack +0 -0
  9. package/.next/cache/webpack/client-production/2.pack +0 -0
  10. package/.next/cache/webpack/client-production/index.pack +0 -0
  11. package/.next/cache/webpack/client-production/index.pack.old +0 -0
  12. package/.next/cache/webpack/edge-server-production/index.pack +0 -0
  13. package/.next/cache/webpack/server-production/0.pack +0 -0
  14. package/.next/cache/webpack/server-production/index.pack +0 -0
  15. package/.next/next-server.js.nft.json +1 -1
  16. package/.next/prerender-manifest.json +1 -1
  17. package/.next/required-server-files.json +1 -1
  18. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  19. package/.next/server/app/_not-found.html +1 -1
  20. package/.next/server/app/_not-found.rsc +1 -1
  21. package/.next/server/app/api/repositories/excluded/route.js +8 -8
  22. package/.next/server/app/api/repositories/restore/route.js +7 -7
  23. package/.next/server/app/api/repositories/route.js +5 -5
  24. package/.next/server/app/api/repositories/sync/route.js +5 -5
  25. package/.next/server/app/api/slash-commands.body +1 -1
  26. package/.next/server/app/api/worktrees/[id]/auto-yes/route.js +1 -1
  27. package/.next/server/app/api/worktrees/[id]/capture/route.js +2 -2
  28. package/.next/server/app/api/worktrees/[id]/current-output/route.js +1 -1
  29. package/.next/server/app/api/worktrees/[id]/prompt-response/route.js +1 -1
  30. package/.next/server/app/api/worktrees/[id]/route.js +1 -1
  31. package/.next/server/app/api/worktrees/[id]/terminal/route.js +1 -1
  32. package/.next/server/app/api/worktrees/route.js +1 -1
  33. package/.next/server/app/index.html +1 -1
  34. package/.next/server/app/index.rsc +1 -1
  35. package/.next/server/app/page_client-reference-manifest.js +1 -1
  36. package/.next/server/app/worktrees/[id]/files/[...path]/page_client-reference-manifest.js +1 -1
  37. package/.next/server/app/worktrees/[id]/page.js +1 -1
  38. package/.next/server/app/worktrees/[id]/page_client-reference-manifest.js +1 -1
  39. package/.next/server/app/worktrees/[id]/simple-terminal/page_client-reference-manifest.js +1 -1
  40. package/.next/server/app/worktrees/[id]/terminal/page_client-reference-manifest.js +1 -1
  41. package/.next/server/app-paths-manifest.json +7 -7
  42. package/.next/server/chunks/5488.js +5 -5
  43. package/.next/server/chunks/7536.js +1 -1
  44. package/.next/server/chunks/9367.js +2 -2
  45. package/.next/server/functions-config-manifest.json +1 -1
  46. package/.next/server/pages/404.html +1 -1
  47. package/.next/server/pages/500.html +1 -1
  48. package/.next/server/server-reference-manifest.json +1 -1
  49. package/.next/static/chunks/app/worktrees/[id]/{page-d64624eb67af57c0.js → page-8bd88bdc29607413.js} +1 -1
  50. package/.next/trace +5 -5
  51. package/dist/server/server.js +25 -2
  52. package/dist/server/src/lib/auto-yes-manager.js +88 -7
  53. package/dist/server/src/lib/claude-poller.js +4 -0
  54. package/dist/server/src/lib/claude-session.js +48 -19
  55. package/dist/server/src/lib/cli-patterns.js +60 -4
  56. package/dist/server/src/lib/db-repository.js +482 -0
  57. package/dist/server/src/lib/prompt-detector.js +199 -109
  58. package/dist/server/src/lib/response-poller.js +73 -27
  59. package/dist/server/src/lib/tmux.js +48 -0
  60. package/package.json +1 -1
  61. /package/.next/static/{bdUePCj-b9Gv5okYGp49O → oUD-A998xeBoez6zsrTH3}/_buildManifest.js +0 -0
  62. /package/.next/static/{bdUePCj-b9Gv5okYGp49O → oUD-A998xeBoez6zsrTH3}/_ssgManifest.js +0 -0
@@ -0,0 +1,482 @@
1
+ "use strict";
2
+ /**
3
+ * Repository and Clone Job Database Operations
4
+ * Issue #71: Clone URL registration feature
5
+ * Issue #190: Repository exclusion on sync
6
+ */
7
+ var __importDefault = (this && this.__importDefault) || function (mod) {
8
+ return (mod && mod.__esModule) ? mod : { "default": mod };
9
+ };
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.MAX_DISABLED_REPOSITORIES = void 0;
12
+ exports.createRepository = createRepository;
13
+ exports.getRepositoryByNormalizedUrl = getRepositoryByNormalizedUrl;
14
+ exports.getRepositoryById = getRepositoryById;
15
+ exports.getRepositoryByPath = getRepositoryByPath;
16
+ exports.updateRepository = updateRepository;
17
+ exports.getAllRepositories = getAllRepositories;
18
+ exports.resolveRepositoryPath = resolveRepositoryPath;
19
+ exports.validateRepositoryPath = validateRepositoryPath;
20
+ exports.ensureEnvRepositoriesRegistered = ensureEnvRepositoriesRegistered;
21
+ exports.filterExcludedPaths = filterExcludedPaths;
22
+ exports.registerAndFilterRepositories = registerAndFilterRepositories;
23
+ exports.disableRepository = disableRepository;
24
+ exports.getExcludedRepositoryPaths = getExcludedRepositoryPaths;
25
+ exports.getExcludedRepositories = getExcludedRepositories;
26
+ exports.restoreRepository = restoreRepository;
27
+ exports.createCloneJob = createCloneJob;
28
+ exports.getCloneJob = getCloneJob;
29
+ exports.updateCloneJob = updateCloneJob;
30
+ exports.getActiveCloneJobByUrl = getActiveCloneJobByUrl;
31
+ exports.getCloneJobsByStatus = getCloneJobsByStatus;
32
+ const crypto_1 = require("crypto");
33
+ const path_1 = __importDefault(require("path"));
34
+ const system_directories_1 = require("../config/system-directories");
35
+ /**
36
+ * Map repository row to Repository model
37
+ */
38
+ function mapRepositoryRow(row) {
39
+ return {
40
+ id: row.id,
41
+ name: row.name,
42
+ path: row.path,
43
+ enabled: row.enabled === 1,
44
+ cloneUrl: row.clone_url || undefined,
45
+ normalizedCloneUrl: row.normalized_clone_url || undefined,
46
+ cloneSource: row.clone_source,
47
+ isEnvManaged: row.is_env_managed === 1,
48
+ createdAt: new Date(row.created_at),
49
+ updatedAt: new Date(row.updated_at),
50
+ };
51
+ }
52
+ /**
53
+ * Map clone job row to CloneJobDB model
54
+ */
55
+ function mapCloneJobRow(row) {
56
+ return {
57
+ id: row.id,
58
+ cloneUrl: row.clone_url,
59
+ normalizedCloneUrl: row.normalized_clone_url,
60
+ targetPath: row.target_path,
61
+ repositoryId: row.repository_id || undefined,
62
+ status: row.status,
63
+ pid: row.pid || undefined,
64
+ progress: row.progress,
65
+ errorCategory: row.error_category || undefined,
66
+ errorCode: row.error_code || undefined,
67
+ errorMessage: row.error_message || undefined,
68
+ startedAt: row.started_at ? new Date(row.started_at) : undefined,
69
+ completedAt: row.completed_at ? new Date(row.completed_at) : undefined,
70
+ createdAt: new Date(row.created_at),
71
+ };
72
+ }
73
+ // ============================================================
74
+ // Repository Operations
75
+ // ============================================================
76
+ /**
77
+ * Create a new repository
78
+ */
79
+ function createRepository(db, data) {
80
+ const id = (0, crypto_1.randomUUID)();
81
+ const now = Date.now();
82
+ const stmt = db.prepare(`
83
+ INSERT INTO repositories (
84
+ id, name, path, enabled, clone_url, normalized_clone_url,
85
+ clone_source, is_env_managed, created_at, updated_at
86
+ )
87
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
88
+ `);
89
+ stmt.run(id, data.name, data.path, data.enabled !== false ? 1 : 0, data.cloneUrl || null, data.normalizedCloneUrl || null, data.cloneSource, data.isEnvManaged ? 1 : 0, now, now);
90
+ return {
91
+ id,
92
+ name: data.name,
93
+ path: data.path,
94
+ enabled: data.enabled !== false,
95
+ cloneUrl: data.cloneUrl,
96
+ normalizedCloneUrl: data.normalizedCloneUrl,
97
+ cloneSource: data.cloneSource,
98
+ isEnvManaged: data.isEnvManaged || false,
99
+ createdAt: new Date(now),
100
+ updatedAt: new Date(now),
101
+ };
102
+ }
103
+ /**
104
+ * Get repository by normalized clone URL
105
+ */
106
+ function getRepositoryByNormalizedUrl(db, normalizedCloneUrl) {
107
+ const stmt = db.prepare(`
108
+ SELECT * FROM repositories
109
+ WHERE normalized_clone_url = ?
110
+ `);
111
+ const row = stmt.get(normalizedCloneUrl);
112
+ return row ? mapRepositoryRow(row) : null;
113
+ }
114
+ /**
115
+ * Get repository by ID
116
+ */
117
+ function getRepositoryById(db, id) {
118
+ const stmt = db.prepare(`
119
+ SELECT * FROM repositories
120
+ WHERE id = ?
121
+ `);
122
+ const row = stmt.get(id);
123
+ return row ? mapRepositoryRow(row) : null;
124
+ }
125
+ /**
126
+ * Get repository by path
127
+ */
128
+ function getRepositoryByPath(db, path) {
129
+ const stmt = db.prepare(`
130
+ SELECT * FROM repositories
131
+ WHERE path = ?
132
+ `);
133
+ const row = stmt.get(path);
134
+ return row ? mapRepositoryRow(row) : null;
135
+ }
136
+ /**
137
+ * Update repository
138
+ */
139
+ function updateRepository(db, id, updates) {
140
+ const now = Date.now();
141
+ const assignments = ['updated_at = ?'];
142
+ const params = [now];
143
+ if (updates.name !== undefined) {
144
+ assignments.push('name = ?');
145
+ params.push(updates.name);
146
+ }
147
+ if (updates.enabled !== undefined) {
148
+ assignments.push('enabled = ?');
149
+ params.push(updates.enabled ? 1 : 0);
150
+ }
151
+ if (updates.cloneUrl !== undefined) {
152
+ assignments.push('clone_url = ?');
153
+ params.push(updates.cloneUrl || null);
154
+ }
155
+ if (updates.normalizedCloneUrl !== undefined) {
156
+ assignments.push('normalized_clone_url = ?');
157
+ params.push(updates.normalizedCloneUrl || null);
158
+ }
159
+ params.push(id);
160
+ const stmt = db.prepare(`
161
+ UPDATE repositories
162
+ SET ${assignments.join(', ')}
163
+ WHERE id = ?
164
+ `);
165
+ stmt.run(...params);
166
+ }
167
+ /**
168
+ * Get all repositories
169
+ */
170
+ function getAllRepositories(db) {
171
+ const stmt = db.prepare(`
172
+ SELECT * FROM repositories
173
+ ORDER BY name ASC
174
+ `);
175
+ const rows = stmt.all();
176
+ return rows.map(mapRepositoryRow);
177
+ }
178
+ // ============================================================
179
+ // Repository Exclusion Operations (Issue #190)
180
+ // ============================================================
181
+ /**
182
+ * Maximum number of disabled repositories allowed.
183
+ * Prevents unlimited record accumulation from malicious or buggy DELETE requests.
184
+ * SEC-SF-004
185
+ */
186
+ exports.MAX_DISABLED_REPOSITORIES = 1000;
187
+ /**
188
+ * Resolve and normalize a repository path.
189
+ * All path normalization is centralized here to prevent inconsistencies.
190
+ *
191
+ * NOTE: path.resolve() removes trailing slashes and resolves relative paths
192
+ * but does NOT resolve symlinks. For symlink resolution, use fs.realpathSync().
193
+ * See design policy Section 7 for the symlink handling policy.
194
+ *
195
+ * SF-001: DRY - centralized path normalization
196
+ */
197
+ function resolveRepositoryPath(repoPath) {
198
+ return path_1.default.resolve(repoPath);
199
+ }
200
+ /**
201
+ * Validate and resolve a repository path for API requests.
202
+ * Centralizes all validation checks to avoid duplication across route handlers.
203
+ *
204
+ * Checks performed:
205
+ * 1. Presence and type check (must be non-empty string)
206
+ * 2. Null byte check (path traversal prevention, SEC-MF-001)
207
+ * 3. System directory check (prevents operations on /etc, /usr, etc.)
208
+ *
209
+ * DRY: Used by DELETE /api/repositories and PUT /api/repositories/restore
210
+ */
211
+ function validateRepositoryPath(repositoryPath) {
212
+ if (!repositoryPath || typeof repositoryPath !== 'string') {
213
+ return { valid: false, error: 'repositoryPath is required' };
214
+ }
215
+ if (repositoryPath.includes('\0')) {
216
+ return { valid: false, error: 'Invalid repository path' };
217
+ }
218
+ const resolvedPath = resolveRepositoryPath(repositoryPath);
219
+ if ((0, system_directories_1.isSystemDirectory)(resolvedPath)) {
220
+ return { valid: false, error: 'Invalid repository path' };
221
+ }
222
+ return { valid: true, resolvedPath };
223
+ }
224
+ /**
225
+ * Register environment variable repositories to the repositories table.
226
+ * Idempotent: already registered repositories are skipped (regardless of enabled status).
227
+ *
228
+ * NOTE (MF-C01): createRepository() treats enabled as follows:
229
+ * - true -> SQLite 1 (enabled)
230
+ * - false -> SQLite 0 (disabled)
231
+ * - undefined -> SQLite 1 (enabled, due to `data.enabled !== false ? 1 : 0` logic)
232
+ * We explicitly pass enabled: true to avoid relying on the implicit default.
233
+ *
234
+ * MF-001: SRP - registration logic separated from sync route
235
+ */
236
+ function ensureEnvRepositoriesRegistered(db, repositoryPaths) {
237
+ for (const repoPath of repositoryPaths) {
238
+ const resolvedPath = resolveRepositoryPath(repoPath);
239
+ const existing = getRepositoryByPath(db, resolvedPath);
240
+ if (!existing) {
241
+ createRepository(db, {
242
+ name: path_1.default.basename(resolvedPath),
243
+ path: resolvedPath,
244
+ cloneSource: 'local',
245
+ isEnvManaged: true,
246
+ enabled: true, // Explicit: do not rely on undefined -> 1 default
247
+ });
248
+ }
249
+ }
250
+ }
251
+ /**
252
+ * Filter out excluded repository paths (enabled=0).
253
+ * Exclusion logic is encapsulated here, so changes to exclusion criteria
254
+ * (e.g., pattern-based exclusion, temporary exclusion) only affect this function.
255
+ *
256
+ * @requires ensureEnvRepositoriesRegistered() must be called before this function
257
+ * to ensure all paths exist in the repositories table.
258
+ * Without prior registration, unregistered paths will not be filtered correctly.
259
+ *
260
+ * NOTE (SEC-SF-002): Array.includes() performs case-sensitive string comparison.
261
+ * On macOS (case-insensitive filesystem), paths with different casing would not match.
262
+ * resolveRepositoryPath() normalization on both sides mitigates most cases.
263
+ * On Linux (case-sensitive filesystem), the behavior is consistent.
264
+ *
265
+ * @param db - Database instance
266
+ * @param repositoryPaths - Array of repository paths to filter
267
+ * @returns Filtered array excluding disabled repositories
268
+ *
269
+ * SF-003: OCP - exclusion logic encapsulated
270
+ */
271
+ function filterExcludedPaths(db, repositoryPaths) {
272
+ const excludedPaths = getExcludedRepositoryPaths(db);
273
+ return repositoryPaths.filter(p => !excludedPaths.includes(resolveRepositoryPath(p)));
274
+ }
275
+ /**
276
+ * Register environment variable repositories and filter out excluded ones.
277
+ * Encapsulates the ordering constraint: registration MUST happen before filtering.
278
+ *
279
+ * This function combines ensureEnvRepositoriesRegistered() and filterExcludedPaths()
280
+ * into a single atomic operation to prevent callers from accidentally reversing the order.
281
+ *
282
+ * DRY: Used by server.ts initializeWorktrees() and POST /api/repositories/sync
283
+ *
284
+ * @param db - Database instance
285
+ * @param repositoryPaths - Array of repository paths from environment variables
286
+ * @returns ExclusionSummary with filtered paths, excluded paths, and count
287
+ */
288
+ function registerAndFilterRepositories(db, repositoryPaths) {
289
+ // Step 1: Register (must be before filter - see design policy Section 4)
290
+ ensureEnvRepositoriesRegistered(db, repositoryPaths);
291
+ // Step 2: Filter
292
+ const filteredPaths = filterExcludedPaths(db, repositoryPaths);
293
+ const excludedPaths = repositoryPaths.filter(p => !filteredPaths.includes(p));
294
+ return {
295
+ filteredPaths,
296
+ excludedPaths,
297
+ excludedCount: excludedPaths.length,
298
+ };
299
+ }
300
+ /**
301
+ * Disable a repository by setting enabled=0.
302
+ * If the repository is not registered, create it with enabled=0.
303
+ * All internal logic (lookup + update/create) is encapsulated.
304
+ *
305
+ * NOTE (MF-C01): Explicitly passes enabled: false to createRepository().
306
+ * The internal mapping `data.enabled !== false ? 1 : 0` will correctly
307
+ * store 0 in SQLite. Do NOT pass undefined for enabled.
308
+ *
309
+ * NOTE (SEC-SF-004): When creating a new record, checks the count of
310
+ * disabled repositories against MAX_DISABLED_REPOSITORIES to prevent
311
+ * unlimited record accumulation from malicious or buggy DELETE requests.
312
+ *
313
+ * SF-002: SRP - disable logic encapsulated
314
+ */
315
+ function disableRepository(db, repositoryPath) {
316
+ const resolvedPath = resolveRepositoryPath(repositoryPath);
317
+ const repo = getRepositoryByPath(db, resolvedPath);
318
+ if (repo) {
319
+ updateRepository(db, repo.id, { enabled: false });
320
+ }
321
+ else {
322
+ // SEC-SF-004: Check disabled repository count limit before creating new record
323
+ const disabledCount = db.prepare('SELECT COUNT(*) as count FROM repositories WHERE enabled = 0').get();
324
+ if (disabledCount.count >= exports.MAX_DISABLED_REPOSITORIES) {
325
+ throw new Error('Disabled repository limit exceeded');
326
+ }
327
+ createRepository(db, {
328
+ name: path_1.default.basename(resolvedPath),
329
+ path: resolvedPath,
330
+ cloneSource: 'local',
331
+ isEnvManaged: false,
332
+ enabled: false, // Explicit: do not rely on undefined -> 1 default
333
+ });
334
+ }
335
+ }
336
+ /**
337
+ * Get paths of excluded (enabled=0) repositories
338
+ */
339
+ function getExcludedRepositoryPaths(db) {
340
+ const stmt = db.prepare('SELECT path FROM repositories WHERE enabled = 0');
341
+ const rows = stmt.all();
342
+ return rows.map(r => r.path);
343
+ }
344
+ /**
345
+ * Get excluded repositories with full details
346
+ */
347
+ function getExcludedRepositories(db) {
348
+ const stmt = db.prepare('SELECT * FROM repositories WHERE enabled = 0 ORDER BY name ASC');
349
+ const rows = stmt.all();
350
+ return rows.map(mapRepositoryRow);
351
+ }
352
+ /**
353
+ * Restore an excluded repository by setting enabled=1
354
+ *
355
+ * @returns Restored Repository object, or null if not found
356
+ */
357
+ function restoreRepository(db, repoPath) {
358
+ const resolvedPath = resolveRepositoryPath(repoPath);
359
+ const repo = getRepositoryByPath(db, resolvedPath);
360
+ if (!repo)
361
+ return null;
362
+ updateRepository(db, repo.id, { enabled: true });
363
+ return { ...repo, enabled: true };
364
+ }
365
+ // ============================================================
366
+ // Clone Job Operations
367
+ // ============================================================
368
+ /**
369
+ * Create a new clone job
370
+ */
371
+ function createCloneJob(db, data) {
372
+ const id = (0, crypto_1.randomUUID)();
373
+ const now = Date.now();
374
+ const stmt = db.prepare(`
375
+ INSERT INTO clone_jobs (
376
+ id, clone_url, normalized_clone_url, target_path,
377
+ status, progress, created_at
378
+ )
379
+ VALUES (?, ?, ?, ?, 'pending', 0, ?)
380
+ `);
381
+ stmt.run(id, data.cloneUrl, data.normalizedCloneUrl, data.targetPath, now);
382
+ return {
383
+ id,
384
+ cloneUrl: data.cloneUrl,
385
+ normalizedCloneUrl: data.normalizedCloneUrl,
386
+ targetPath: data.targetPath,
387
+ status: 'pending',
388
+ progress: 0,
389
+ createdAt: new Date(now),
390
+ };
391
+ }
392
+ /**
393
+ * Get clone job by ID
394
+ */
395
+ function getCloneJob(db, id) {
396
+ const stmt = db.prepare(`
397
+ SELECT * FROM clone_jobs
398
+ WHERE id = ?
399
+ `);
400
+ const row = stmt.get(id);
401
+ return row ? mapCloneJobRow(row) : null;
402
+ }
403
+ /**
404
+ * Update clone job
405
+ */
406
+ function updateCloneJob(db, id, updates) {
407
+ const assignments = [];
408
+ const params = [];
409
+ if (updates.status !== undefined) {
410
+ assignments.push('status = ?');
411
+ params.push(updates.status);
412
+ }
413
+ if (updates.pid !== undefined) {
414
+ assignments.push('pid = ?');
415
+ params.push(updates.pid);
416
+ }
417
+ if (updates.progress !== undefined) {
418
+ assignments.push('progress = ?');
419
+ params.push(updates.progress);
420
+ }
421
+ if (updates.repositoryId !== undefined) {
422
+ assignments.push('repository_id = ?');
423
+ params.push(updates.repositoryId);
424
+ }
425
+ if (updates.errorCategory !== undefined) {
426
+ assignments.push('error_category = ?');
427
+ params.push(updates.errorCategory);
428
+ }
429
+ if (updates.errorCode !== undefined) {
430
+ assignments.push('error_code = ?');
431
+ params.push(updates.errorCode);
432
+ }
433
+ if (updates.errorMessage !== undefined) {
434
+ assignments.push('error_message = ?');
435
+ params.push(updates.errorMessage);
436
+ }
437
+ if (updates.startedAt !== undefined) {
438
+ assignments.push('started_at = ?');
439
+ params.push(updates.startedAt.getTime());
440
+ }
441
+ if (updates.completedAt !== undefined) {
442
+ assignments.push('completed_at = ?');
443
+ params.push(updates.completedAt.getTime());
444
+ }
445
+ if (assignments.length === 0) {
446
+ return;
447
+ }
448
+ params.push(id);
449
+ const stmt = db.prepare(`
450
+ UPDATE clone_jobs
451
+ SET ${assignments.join(', ')}
452
+ WHERE id = ?
453
+ `);
454
+ stmt.run(...params);
455
+ }
456
+ /**
457
+ * Get active clone job by normalized URL
458
+ * Active jobs are those with status 'pending' or 'running'
459
+ */
460
+ function getActiveCloneJobByUrl(db, normalizedCloneUrl) {
461
+ const stmt = db.prepare(`
462
+ SELECT * FROM clone_jobs
463
+ WHERE normalized_clone_url = ?
464
+ AND status IN ('pending', 'running')
465
+ ORDER BY created_at DESC
466
+ LIMIT 1
467
+ `);
468
+ const row = stmt.get(normalizedCloneUrl);
469
+ return row ? mapCloneJobRow(row) : null;
470
+ }
471
+ /**
472
+ * Get clone jobs by status
473
+ */
474
+ function getCloneJobsByStatus(db, status) {
475
+ const stmt = db.prepare(`
476
+ SELECT * FROM clone_jobs
477
+ WHERE status = ?
478
+ ORDER BY created_at DESC
479
+ `);
480
+ const rows = stmt.all(status);
481
+ return rows.map(mapCloneJobRow);
482
+ }