@magentrix-corp/magentrix-cli 1.3.16 → 1.3.17
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/LICENSE +25 -25
- package/README.md +1166 -1166
- package/actions/autopublish.old.js +293 -293
- package/actions/config.js +182 -182
- package/actions/create.js +466 -466
- package/actions/help.js +164 -164
- package/actions/iris/buildStage.js +874 -874
- package/actions/iris/delete.js +256 -256
- package/actions/iris/dev.js +391 -391
- package/actions/iris/index.js +6 -6
- package/actions/iris/link.js +375 -375
- package/actions/iris/recover.js +268 -268
- package/actions/main.js +80 -80
- package/actions/publish.js +1420 -1420
- package/actions/pull.js +684 -684
- package/actions/setup.js +148 -148
- package/actions/status.js +17 -17
- package/actions/update.js +248 -248
- package/bin/magentrix.js +393 -393
- package/package.json +55 -55
- package/utils/assetPaths.js +158 -158
- package/utils/autopublishLock.js +77 -77
- package/utils/cacher.js +206 -206
- package/utils/cli/checkInstanceUrl.js +76 -74
- package/utils/cli/helpers/compare.js +282 -282
- package/utils/cli/helpers/ensureApiKey.js +63 -63
- package/utils/cli/helpers/ensureCredentials.js +68 -68
- package/utils/cli/helpers/ensureInstanceUrl.js +75 -75
- package/utils/cli/writeRecords.js +262 -262
- package/utils/compare.js +135 -135
- package/utils/compress.js +17 -17
- package/utils/config.js +527 -527
- package/utils/debug.js +144 -144
- package/utils/diagnostics/testPublishLogic.js +96 -96
- package/utils/diff.js +49 -49
- package/utils/downloadAssets.js +291 -291
- package/utils/filetag.js +115 -115
- package/utils/hash.js +14 -14
- package/utils/iris/backup.js +411 -411
- package/utils/iris/builder.js +541 -541
- package/utils/iris/config-reader.js +664 -664
- package/utils/iris/deleteHelper.js +150 -150
- package/utils/iris/errors.js +537 -537
- package/utils/iris/linker.js +601 -601
- package/utils/iris/lock.js +360 -360
- package/utils/iris/validation.js +360 -360
- package/utils/iris/validator.js +281 -281
- package/utils/iris/zipper.js +248 -248
- package/utils/logger.js +291 -291
- package/utils/magentrix/api/assets.js +220 -220
- package/utils/magentrix/api/auth.js +107 -107
- package/utils/magentrix/api/createEntity.js +61 -61
- package/utils/magentrix/api/deleteEntity.js +55 -55
- package/utils/magentrix/api/iris.js +251 -251
- package/utils/magentrix/api/meqlQuery.js +36 -36
- package/utils/magentrix/api/retrieveEntity.js +86 -86
- package/utils/magentrix/api/updateEntity.js +66 -66
- package/utils/magentrix/fetch.js +168 -168
- package/utils/merge.js +22 -22
- package/utils/permissionError.js +70 -70
- package/utils/preferences.js +40 -40
- package/utils/progress.js +469 -469
- package/utils/spinner.js +43 -43
- package/utils/template.js +52 -52
- package/utils/updateFileBase.js +121 -121
- package/utils/workspaces.js +108 -108
- package/vars/config.js +11 -11
- package/vars/global.js +50 -50
package/utils/iris/lock.js
CHANGED
|
@@ -1,360 +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
|
-
}
|
|
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
|
+
}
|