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.
- package/.next/BUILD_ID +1 -1
- package/.next/app-build-manifest.json +9 -9
- package/.next/app-path-routes-manifest.json +1 -1
- package/.next/build-manifest.json +2 -2
- package/.next/cache/.tsbuildinfo +1 -1
- package/.next/cache/config.json +3 -3
- package/.next/cache/webpack/client-production/0.pack +0 -0
- package/.next/cache/webpack/client-production/1.pack +0 -0
- package/.next/cache/webpack/client-production/2.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack.old +0 -0
- package/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/.next/cache/webpack/server-production/0.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack +0 -0
- package/.next/next-server.js.nft.json +1 -1
- package/.next/prerender-manifest.json +1 -1
- package/.next/required-server-files.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +1 -1
- package/.next/server/app/api/repositories/excluded/route.js +8 -8
- package/.next/server/app/api/repositories/restore/route.js +7 -7
- package/.next/server/app/api/repositories/route.js +5 -5
- package/.next/server/app/api/repositories/sync/route.js +5 -5
- package/.next/server/app/api/slash-commands.body +1 -1
- package/.next/server/app/api/worktrees/[id]/auto-yes/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/capture/route.js +2 -2
- package/.next/server/app/api/worktrees/[id]/current-output/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/prompt-response/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/terminal/route.js +1 -1
- package/.next/server/app/api/worktrees/route.js +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/files/[...path]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/page.js +1 -1
- package/.next/server/app/worktrees/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/simple-terminal/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/terminal/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +7 -7
- package/.next/server/chunks/5488.js +5 -5
- package/.next/server/chunks/7536.js +1 -1
- package/.next/server/chunks/9367.js +2 -2
- package/.next/server/functions-config-manifest.json +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/app/worktrees/[id]/{page-d64624eb67af57c0.js → page-8bd88bdc29607413.js} +1 -1
- package/.next/trace +5 -5
- package/dist/server/server.js +25 -2
- package/dist/server/src/lib/auto-yes-manager.js +88 -7
- package/dist/server/src/lib/claude-poller.js +4 -0
- package/dist/server/src/lib/claude-session.js +48 -19
- package/dist/server/src/lib/cli-patterns.js +60 -4
- package/dist/server/src/lib/db-repository.js +482 -0
- package/dist/server/src/lib/prompt-detector.js +199 -109
- package/dist/server/src/lib/response-poller.js +73 -27
- package/dist/server/src/lib/tmux.js +48 -0
- package/package.json +1 -1
- /package/.next/static/{bdUePCj-b9Gv5okYGp49O → oUD-A998xeBoez6zsrTH3}/_buildManifest.js +0 -0
- /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
|
+
}
|