@magentrix-corp/magentrix-cli 1.3.10 → 1.3.11
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/actions/iris/buildStage.js +93 -5
- package/actions/iris/delete.js +46 -1
- package/actions/iris/dev.js +129 -13
- package/actions/iris/recover.js +47 -7
- package/package.json +1 -1
- package/utils/iris/backup.js +262 -52
- package/utils/iris/builder.js +334 -112
- package/utils/iris/config-reader.js +206 -31
- package/utils/iris/deleteHelper.js +55 -7
- package/utils/iris/errors.js +537 -0
- package/utils/iris/linker.js +118 -13
- package/utils/iris/lock.js +360 -0
- package/utils/iris/validation.js +360 -0
package/utils/iris/linker.js
CHANGED
|
@@ -2,9 +2,68 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import { resolve, basename } from 'node:path';
|
|
3
3
|
import Config from '../config.js';
|
|
4
4
|
import { readVueConfig } from './config-reader.js';
|
|
5
|
+
import { validateSlug } from './validation.js';
|
|
6
|
+
import { formatError, formatCorruptedConfigError, detectErrorType, ErrorTypes } from './errors.js';
|
|
5
7
|
|
|
6
8
|
const config = new Config();
|
|
7
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Safe wrapper for reading config that handles corruption.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} key - Config key to read
|
|
14
|
+
* @param {Object} options - Config options
|
|
15
|
+
* @returns {{success: boolean, data: any, error: string | null}}
|
|
16
|
+
*/
|
|
17
|
+
function safeConfigRead(key, options) {
|
|
18
|
+
try {
|
|
19
|
+
const data = config.read(key, options);
|
|
20
|
+
return { success: true, data, error: null };
|
|
21
|
+
} catch (err) {
|
|
22
|
+
const errorType = detectErrorType(err);
|
|
23
|
+
if (errorType === ErrorTypes.CORRUPTED) {
|
|
24
|
+
return {
|
|
25
|
+
success: false,
|
|
26
|
+
data: null,
|
|
27
|
+
error: formatCorruptedConfigError({
|
|
28
|
+
configPath: config.getGlobalConfigPath?.() || '~/.config/magentrix/config.json',
|
|
29
|
+
error: err
|
|
30
|
+
})
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return { success: false, data: null, error: err.message };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Safe wrapper for saving config that handles errors.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} key - Config key to save
|
|
41
|
+
* @param {any} value - Value to save
|
|
42
|
+
* @param {Object} options - Config options
|
|
43
|
+
* @returns {{success: boolean, error: string | null}}
|
|
44
|
+
*/
|
|
45
|
+
function safeConfigSave(key, value, options) {
|
|
46
|
+
try {
|
|
47
|
+
config.save(key, value, options);
|
|
48
|
+
return { success: true, error: null };
|
|
49
|
+
} catch (err) {
|
|
50
|
+
const errorType = detectErrorType(err);
|
|
51
|
+
if (errorType === ErrorTypes.PERMISSION) {
|
|
52
|
+
return {
|
|
53
|
+
success: false,
|
|
54
|
+
error: `Permission denied saving config. Check file permissions for the global config directory.`
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (errorType === ErrorTypes.DISK_FULL) {
|
|
58
|
+
return {
|
|
59
|
+
success: false,
|
|
60
|
+
error: `Disk full - cannot save config. Free up disk space and try again.`
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return { success: false, error: `Failed to save config: ${err.message}` };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
8
67
|
/**
|
|
9
68
|
* Get all linked Vue projects from global config.
|
|
10
69
|
*
|
|
@@ -17,19 +76,27 @@ const config = new Config();
|
|
|
17
76
|
* }>}
|
|
18
77
|
*/
|
|
19
78
|
export function getLinkedProjects() {
|
|
20
|
-
const
|
|
79
|
+
const result = safeConfigRead('linkedVueProjects', {
|
|
21
80
|
global: true // Store globally so projects persist across Magentrix workspaces
|
|
22
81
|
});
|
|
23
|
-
|
|
82
|
+
|
|
83
|
+
if (!result.success) {
|
|
84
|
+
// Log error but return empty array to allow graceful degradation
|
|
85
|
+
console.error(result.error);
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return result.data || [];
|
|
24
90
|
}
|
|
25
91
|
|
|
26
92
|
/**
|
|
27
93
|
* Save linked projects to global config.
|
|
28
94
|
*
|
|
29
95
|
* @param {Array} projects - Array of linked projects
|
|
96
|
+
* @returns {{success: boolean, error: string | null}}
|
|
30
97
|
*/
|
|
31
98
|
function saveLinkedProjects(projects) {
|
|
32
|
-
|
|
99
|
+
return safeConfigSave('linkedVueProjects', projects, {
|
|
33
100
|
global: true
|
|
34
101
|
});
|
|
35
102
|
}
|
|
@@ -230,7 +297,16 @@ export function linkVueProject(projectPath) {
|
|
|
230
297
|
appIconId: vueConfig.appIconId,
|
|
231
298
|
siteUrl: vueConfig.siteUrl
|
|
232
299
|
};
|
|
233
|
-
|
|
300
|
+
|
|
301
|
+
const saveResult = saveLinkedProjects(projects);
|
|
302
|
+
if (!saveResult.success) {
|
|
303
|
+
return {
|
|
304
|
+
success: false,
|
|
305
|
+
project: null,
|
|
306
|
+
error: saveResult.error,
|
|
307
|
+
updated: false
|
|
308
|
+
};
|
|
309
|
+
}
|
|
234
310
|
|
|
235
311
|
return {
|
|
236
312
|
success: true,
|
|
@@ -265,7 +341,16 @@ export function linkVueProject(projectPath) {
|
|
|
265
341
|
// Add to linked projects
|
|
266
342
|
const projects = getLinkedProjects();
|
|
267
343
|
projects.push(newProject);
|
|
268
|
-
|
|
344
|
+
|
|
345
|
+
const saveResult = saveLinkedProjects(projects);
|
|
346
|
+
if (!saveResult.success) {
|
|
347
|
+
return {
|
|
348
|
+
success: false,
|
|
349
|
+
project: null,
|
|
350
|
+
error: saveResult.error,
|
|
351
|
+
updated: false
|
|
352
|
+
};
|
|
353
|
+
}
|
|
269
354
|
|
|
270
355
|
return {
|
|
271
356
|
success: true,
|
|
@@ -310,7 +395,15 @@ export function unlinkVueProject(projectPathOrSlug) {
|
|
|
310
395
|
|
|
311
396
|
// Remove from list
|
|
312
397
|
const [removed] = projects.splice(index, 1);
|
|
313
|
-
saveLinkedProjects(projects);
|
|
398
|
+
const saveResult = saveLinkedProjects(projects);
|
|
399
|
+
|
|
400
|
+
if (!saveResult.success) {
|
|
401
|
+
return {
|
|
402
|
+
success: false,
|
|
403
|
+
project: null,
|
|
404
|
+
error: saveResult.error
|
|
405
|
+
};
|
|
406
|
+
}
|
|
314
407
|
|
|
315
408
|
return {
|
|
316
409
|
success: true,
|
|
@@ -323,8 +416,10 @@ export function unlinkVueProject(projectPathOrSlug) {
|
|
|
323
416
|
* Remove all invalid (non-existent paths) linked projects.
|
|
324
417
|
*
|
|
325
418
|
* @returns {{
|
|
419
|
+
* success: boolean,
|
|
326
420
|
* removed: number,
|
|
327
|
-
* projects: Array<{path: string, slug: string, appName: string}
|
|
421
|
+
* projects: Array<{path: string, slug: string, appName: string}>,
|
|
422
|
+
* error: string | null
|
|
328
423
|
* }}
|
|
329
424
|
*/
|
|
330
425
|
export function cleanupInvalidProjects() {
|
|
@@ -341,12 +436,22 @@ export function cleanupInvalidProjects() {
|
|
|
341
436
|
}
|
|
342
437
|
|
|
343
438
|
if (removedProjects.length > 0) {
|
|
344
|
-
saveLinkedProjects(validProjects);
|
|
439
|
+
const saveResult = saveLinkedProjects(validProjects);
|
|
440
|
+
if (!saveResult.success) {
|
|
441
|
+
return {
|
|
442
|
+
success: false,
|
|
443
|
+
removed: 0,
|
|
444
|
+
projects: [],
|
|
445
|
+
error: saveResult.error
|
|
446
|
+
};
|
|
447
|
+
}
|
|
345
448
|
}
|
|
346
449
|
|
|
347
450
|
return {
|
|
451
|
+
success: true,
|
|
348
452
|
removed: removedProjects.length,
|
|
349
|
-
projects: removedProjects
|
|
453
|
+
projects: removedProjects,
|
|
454
|
+
error: null
|
|
350
455
|
};
|
|
351
456
|
}
|
|
352
457
|
|
|
@@ -354,20 +459,20 @@ export function cleanupInvalidProjects() {
|
|
|
354
459
|
* Update the last build timestamp for a linked project.
|
|
355
460
|
*
|
|
356
461
|
* @param {string} slug - The app slug
|
|
357
|
-
* @returns {boolean
|
|
462
|
+
* @returns {{success: boolean, error: string | null}}
|
|
358
463
|
*/
|
|
359
464
|
export function updateLastBuild(slug) {
|
|
360
465
|
const projects = getLinkedProjects();
|
|
361
466
|
const index = projects.findIndex(p => p.slug === slug);
|
|
362
467
|
|
|
363
468
|
if (index === -1) {
|
|
364
|
-
return false;
|
|
469
|
+
return { success: false, error: 'Project not found' };
|
|
365
470
|
}
|
|
366
471
|
|
|
367
472
|
projects[index].lastBuild = new Date().toISOString();
|
|
368
|
-
saveLinkedProjects(projects);
|
|
473
|
+
const saveResult = saveLinkedProjects(projects);
|
|
369
474
|
|
|
370
|
-
return
|
|
475
|
+
return saveResult;
|
|
371
476
|
}
|
|
372
477
|
|
|
373
478
|
/**
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operation locking system for Iris Vue integration.
|
|
3
|
+
* Prevents concurrent operations that could cause data corruption.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, writeFileSync, readFileSync, unlinkSync, mkdirSync } from 'node:fs';
|
|
7
|
+
import { join, dirname } from 'node:path';
|
|
8
|
+
import { formatConcurrentOperationError } from './errors.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Lock directory relative to workspace.
|
|
12
|
+
*/
|
|
13
|
+
const LOCK_DIR = '.magentrix/locks';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Lock types for different operations.
|
|
17
|
+
*/
|
|
18
|
+
export const LockTypes = {
|
|
19
|
+
BUILD: 'build',
|
|
20
|
+
STAGE: 'stage',
|
|
21
|
+
PUBLISH: 'publish',
|
|
22
|
+
DELETE: 'delete',
|
|
23
|
+
RECOVER: 'recover',
|
|
24
|
+
DEV_SERVER: 'dev-server'
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Default lock timeout in milliseconds (5 minutes).
|
|
29
|
+
* Locks older than this are considered stale and can be overwritten.
|
|
30
|
+
*/
|
|
31
|
+
const DEFAULT_LOCK_TIMEOUT = 5 * 60 * 1000;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get the lock file path for a given lock type and context.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} lockType - One of LockTypes
|
|
37
|
+
* @param {string} context - Context identifier (e.g., workspace path, project slug)
|
|
38
|
+
* @param {string} basePath - Base path for lock files (defaults to cwd)
|
|
39
|
+
* @returns {string} - Path to the lock file
|
|
40
|
+
*/
|
|
41
|
+
function getLockPath(lockType, context = 'global', basePath = process.cwd()) {
|
|
42
|
+
const lockDir = join(basePath, LOCK_DIR);
|
|
43
|
+
const safeContext = context.replace(/[^a-zA-Z0-9-_]/g, '_');
|
|
44
|
+
return join(lockDir, `${lockType}-${safeContext}.lock`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Create lock file data.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} operation - Description of the operation
|
|
51
|
+
* @returns {Object} - Lock data
|
|
52
|
+
*/
|
|
53
|
+
function createLockData(operation) {
|
|
54
|
+
return {
|
|
55
|
+
pid: process.pid,
|
|
56
|
+
operation,
|
|
57
|
+
startedAt: new Date().toISOString(),
|
|
58
|
+
hostname: process.env.HOSTNAME || process.env.COMPUTERNAME || 'unknown'
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check if a lock is stale (older than timeout).
|
|
64
|
+
*
|
|
65
|
+
* @param {Object} lockData - The lock file data
|
|
66
|
+
* @param {number} timeout - Timeout in milliseconds
|
|
67
|
+
* @returns {boolean} - True if lock is stale
|
|
68
|
+
*/
|
|
69
|
+
function isLockStale(lockData, timeout = DEFAULT_LOCK_TIMEOUT) {
|
|
70
|
+
if (!lockData.startedAt) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const startTime = new Date(lockData.startedAt).getTime();
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
|
|
77
|
+
return (now - startTime) > timeout;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if the process that created the lock is still running.
|
|
82
|
+
*
|
|
83
|
+
* @param {number} pid - Process ID to check
|
|
84
|
+
* @returns {boolean} - True if process is running
|
|
85
|
+
*/
|
|
86
|
+
function isProcessRunning(pid) {
|
|
87
|
+
try {
|
|
88
|
+
// Sending signal 0 checks if process exists without killing it
|
|
89
|
+
process.kill(pid, 0);
|
|
90
|
+
return true;
|
|
91
|
+
} catch (err) {
|
|
92
|
+
// ESRCH means process doesn't exist
|
|
93
|
+
// EPERM means process exists but we don't have permission
|
|
94
|
+
return err.code === 'EPERM';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Acquire a lock for an operation.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} lockType - One of LockTypes
|
|
102
|
+
* @param {Object} options - Lock options
|
|
103
|
+
* @param {string} options.context - Context identifier
|
|
104
|
+
* @param {string} options.operation - Description of the operation
|
|
105
|
+
* @param {string} options.basePath - Base path for lock files
|
|
106
|
+
* @param {number} options.timeout - Lock timeout in milliseconds
|
|
107
|
+
* @param {boolean} options.force - Force acquire even if locked
|
|
108
|
+
* @returns {{
|
|
109
|
+
* acquired: boolean,
|
|
110
|
+
* lockPath: string,
|
|
111
|
+
* error: string | null,
|
|
112
|
+
* existingLock: Object | null
|
|
113
|
+
* }}
|
|
114
|
+
*/
|
|
115
|
+
export function acquireLock(lockType, options = {}) {
|
|
116
|
+
const {
|
|
117
|
+
context = 'global',
|
|
118
|
+
operation = lockType,
|
|
119
|
+
basePath = process.cwd(),
|
|
120
|
+
timeout = DEFAULT_LOCK_TIMEOUT,
|
|
121
|
+
force = false
|
|
122
|
+
} = options;
|
|
123
|
+
|
|
124
|
+
const lockPath = getLockPath(lockType, context, basePath);
|
|
125
|
+
const lockDir = dirname(lockPath);
|
|
126
|
+
|
|
127
|
+
const result = {
|
|
128
|
+
acquired: false,
|
|
129
|
+
lockPath,
|
|
130
|
+
error: null,
|
|
131
|
+
existingLock: null
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Ensure lock directory exists
|
|
135
|
+
try {
|
|
136
|
+
if (!existsSync(lockDir)) {
|
|
137
|
+
mkdirSync(lockDir, { recursive: true });
|
|
138
|
+
}
|
|
139
|
+
} catch (err) {
|
|
140
|
+
result.error = `Failed to create lock directory: ${err.message}`;
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Check for existing lock
|
|
145
|
+
if (existsSync(lockPath)) {
|
|
146
|
+
try {
|
|
147
|
+
const existingData = JSON.parse(readFileSync(lockPath, 'utf-8'));
|
|
148
|
+
result.existingLock = existingData;
|
|
149
|
+
|
|
150
|
+
// Check if lock is stale or process is dead
|
|
151
|
+
const stale = isLockStale(existingData, timeout);
|
|
152
|
+
const processGone = existingData.pid && !isProcessRunning(existingData.pid);
|
|
153
|
+
|
|
154
|
+
if (!force && !stale && !processGone) {
|
|
155
|
+
// Lock is valid and held by another process
|
|
156
|
+
result.error = formatConcurrentOperationError({
|
|
157
|
+
operation,
|
|
158
|
+
lockHolder: `${existingData.operation} (PID: ${existingData.pid})`,
|
|
159
|
+
lockFile: lockPath
|
|
160
|
+
});
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Lock is stale or orphaned, we can take over
|
|
165
|
+
console.log(`Note: Removed stale lock from ${existingData.operation}`);
|
|
166
|
+
} catch (err) {
|
|
167
|
+
// Invalid lock file, we can overwrite it
|
|
168
|
+
console.log(`Note: Removed invalid lock file`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Create new lock
|
|
173
|
+
try {
|
|
174
|
+
const lockData = createLockData(operation);
|
|
175
|
+
writeFileSync(lockPath, JSON.stringify(lockData, null, 2), 'utf-8');
|
|
176
|
+
result.acquired = true;
|
|
177
|
+
} catch (err) {
|
|
178
|
+
result.error = `Failed to create lock: ${err.message}`;
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Release a lock.
|
|
187
|
+
*
|
|
188
|
+
* @param {string} lockType - One of LockTypes
|
|
189
|
+
* @param {Object} options - Options
|
|
190
|
+
* @param {string} options.context - Context identifier
|
|
191
|
+
* @param {string} options.basePath - Base path for lock files
|
|
192
|
+
* @returns {boolean} - True if lock was released
|
|
193
|
+
*/
|
|
194
|
+
export function releaseLock(lockType, options = {}) {
|
|
195
|
+
const {
|
|
196
|
+
context = 'global',
|
|
197
|
+
basePath = process.cwd()
|
|
198
|
+
} = options;
|
|
199
|
+
|
|
200
|
+
const lockPath = getLockPath(lockType, context, basePath);
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
if (existsSync(lockPath)) {
|
|
204
|
+
// Verify this is our lock before deleting
|
|
205
|
+
try {
|
|
206
|
+
const lockData = JSON.parse(readFileSync(lockPath, 'utf-8'));
|
|
207
|
+
if (lockData.pid !== process.pid) {
|
|
208
|
+
// Not our lock, don't delete
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
} catch {
|
|
212
|
+
// Can't read lock, assume it's okay to delete
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
unlinkSync(lockPath);
|
|
216
|
+
}
|
|
217
|
+
return true;
|
|
218
|
+
} catch (err) {
|
|
219
|
+
// Failed to release, but don't crash
|
|
220
|
+
console.log(`Warning: Could not release lock: ${err.message}`);
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Check if an operation is locked.
|
|
227
|
+
*
|
|
228
|
+
* @param {string} lockType - One of LockTypes
|
|
229
|
+
* @param {Object} options - Options
|
|
230
|
+
* @param {string} options.context - Context identifier
|
|
231
|
+
* @param {string} options.basePath - Base path for lock files
|
|
232
|
+
* @param {number} options.timeout - Lock timeout in milliseconds
|
|
233
|
+
* @returns {{
|
|
234
|
+
* locked: boolean,
|
|
235
|
+
* lockData: Object | null,
|
|
236
|
+
* stale: boolean
|
|
237
|
+
* }}
|
|
238
|
+
*/
|
|
239
|
+
export function checkLock(lockType, options = {}) {
|
|
240
|
+
const {
|
|
241
|
+
context = 'global',
|
|
242
|
+
basePath = process.cwd(),
|
|
243
|
+
timeout = DEFAULT_LOCK_TIMEOUT
|
|
244
|
+
} = options;
|
|
245
|
+
|
|
246
|
+
const lockPath = getLockPath(lockType, context, basePath);
|
|
247
|
+
|
|
248
|
+
const result = {
|
|
249
|
+
locked: false,
|
|
250
|
+
lockData: null,
|
|
251
|
+
stale: false
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
if (!existsSync(lockPath)) {
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
const lockData = JSON.parse(readFileSync(lockPath, 'utf-8'));
|
|
260
|
+
result.lockData = lockData;
|
|
261
|
+
|
|
262
|
+
const stale = isLockStale(lockData, timeout);
|
|
263
|
+
const processGone = lockData.pid && !isProcessRunning(lockData.pid);
|
|
264
|
+
|
|
265
|
+
result.stale = stale || processGone;
|
|
266
|
+
result.locked = !result.stale;
|
|
267
|
+
} catch {
|
|
268
|
+
// Invalid lock file
|
|
269
|
+
result.stale = true;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return result;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Execute a function with a lock.
|
|
277
|
+
* Automatically acquires lock before execution and releases after.
|
|
278
|
+
*
|
|
279
|
+
* @param {string} lockType - One of LockTypes
|
|
280
|
+
* @param {Function} fn - Async function to execute
|
|
281
|
+
* @param {Object} options - Lock options
|
|
282
|
+
* @returns {Promise<*>} - Result of the function
|
|
283
|
+
*/
|
|
284
|
+
export async function withLock(lockType, fn, options = {}) {
|
|
285
|
+
const lockResult = acquireLock(lockType, options);
|
|
286
|
+
|
|
287
|
+
if (!lockResult.acquired) {
|
|
288
|
+
throw new Error(lockResult.error || 'Failed to acquire lock');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
return await fn();
|
|
293
|
+
} finally {
|
|
294
|
+
releaseLock(lockType, options);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Clean up all stale locks in a directory.
|
|
300
|
+
*
|
|
301
|
+
* @param {string} basePath - Base path for lock files
|
|
302
|
+
* @param {number} timeout - Lock timeout in milliseconds
|
|
303
|
+
* @returns {number} - Number of locks cleaned up
|
|
304
|
+
*/
|
|
305
|
+
export function cleanupStaleLocks(basePath = process.cwd(), timeout = DEFAULT_LOCK_TIMEOUT) {
|
|
306
|
+
const lockDir = join(basePath, LOCK_DIR);
|
|
307
|
+
let cleaned = 0;
|
|
308
|
+
|
|
309
|
+
if (!existsSync(lockDir)) {
|
|
310
|
+
return 0;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
const { readdirSync } = require('node:fs');
|
|
315
|
+
const files = readdirSync(lockDir);
|
|
316
|
+
|
|
317
|
+
for (const file of files) {
|
|
318
|
+
if (!file.endsWith('.lock')) continue;
|
|
319
|
+
|
|
320
|
+
const lockPath = join(lockDir, file);
|
|
321
|
+
try {
|
|
322
|
+
const lockData = JSON.parse(readFileSync(lockPath, 'utf-8'));
|
|
323
|
+
|
|
324
|
+
if (isLockStale(lockData, timeout) || !isProcessRunning(lockData.pid)) {
|
|
325
|
+
unlinkSync(lockPath);
|
|
326
|
+
cleaned++;
|
|
327
|
+
}
|
|
328
|
+
} catch {
|
|
329
|
+
// Invalid lock file, remove it
|
|
330
|
+
try {
|
|
331
|
+
unlinkSync(lockPath);
|
|
332
|
+
cleaned++;
|
|
333
|
+
} catch {
|
|
334
|
+
// Couldn't delete, skip
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
} catch {
|
|
339
|
+
// Can't read directory, skip cleanup
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return cleaned;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Create a project-specific lock context from a path.
|
|
347
|
+
*
|
|
348
|
+
* @param {string} projectPath - Path to the project
|
|
349
|
+
* @returns {string} - Lock context identifier
|
|
350
|
+
*/
|
|
351
|
+
export function createProjectContext(projectPath) {
|
|
352
|
+
// Create a short hash-like identifier from the path
|
|
353
|
+
let hash = 0;
|
|
354
|
+
for (let i = 0; i < projectPath.length; i++) {
|
|
355
|
+
const char = projectPath.charCodeAt(i);
|
|
356
|
+
hash = ((hash << 5) - hash) + char;
|
|
357
|
+
hash = hash & hash; // Convert to 32bit integer
|
|
358
|
+
}
|
|
359
|
+
return `project-${Math.abs(hash).toString(36)}`;
|
|
360
|
+
}
|