speccrew 0.7.38 → 0.7.40
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/package.json
CHANGED
|
@@ -152,7 +152,7 @@ function acquireLock(filePath) {
|
|
|
152
152
|
try {
|
|
153
153
|
const lockStat = fs.statSync(lockPath);
|
|
154
154
|
const ageSeconds = (Date.now() - lockStat.mtimeMs) / 1000;
|
|
155
|
-
if (ageSeconds >
|
|
155
|
+
if (ageSeconds > 30) {
|
|
156
156
|
console.error(`Warning: Stale lock file detected (age: ${Math.round(ageSeconds)}s), removing: ${lockPath}`);
|
|
157
157
|
fs.unlinkSync(lockPath);
|
|
158
158
|
// Do not consume retry count, continue to next loop attempt to acquire lock
|
|
@@ -161,10 +161,32 @@ function acquireLock(filePath) {
|
|
|
161
161
|
} catch (statErr) {
|
|
162
162
|
// Lock file was deleted during stat, continue retrying
|
|
163
163
|
}
|
|
164
|
+
} else {
|
|
165
|
+
// Non-EEXIST error (EACCES, EPERM, etc.) - log for debugging
|
|
166
|
+
if (retryCount === 0) {
|
|
167
|
+
console.error(`Warning: Lock creation failed with ${error.code} for: ${lockPath}`);
|
|
168
|
+
}
|
|
164
169
|
}
|
|
165
170
|
retryCount++;
|
|
166
171
|
if (retryCount >= maxRetries) {
|
|
167
|
-
|
|
172
|
+
// Force acquire as last resort
|
|
173
|
+
try {
|
|
174
|
+
// Remove any existing lock file regardless of age
|
|
175
|
+
if (fs.existsSync(lockPath)) {
|
|
176
|
+
fs.unlinkSync(lockPath);
|
|
177
|
+
}
|
|
178
|
+
// Brief random delay to avoid thundering herd on force acquire
|
|
179
|
+
const forceDelay = Math.floor(Math.random() * 500);
|
|
180
|
+
const forceStart = Date.now();
|
|
181
|
+
while (Date.now() - forceStart < forceDelay) {}
|
|
182
|
+
|
|
183
|
+
const fd = fs.openSync(lockPath, 'wx');
|
|
184
|
+
fs.closeSync(fd);
|
|
185
|
+
console.error(`Warning: Lock acquired by force after ${maxRetries} retries for: ${filePath}`);
|
|
186
|
+
return lockPath;
|
|
187
|
+
} catch (forceErr) {
|
|
188
|
+
throw new Error(`Failed to acquire file lock for '${filePath}' after ${maxRetries} attempts (force acquire also failed: ${forceErr.code})`);
|
|
189
|
+
}
|
|
168
190
|
}
|
|
169
191
|
// Retry with jitter to avoid thundering herd
|
|
170
192
|
const delay = 200 + Math.floor(Math.random() * 300);
|
|
@@ -616,13 +638,38 @@ function cmdUpdateTask(args) {
|
|
|
616
638
|
lockPath = acquireLock(filePath);
|
|
617
639
|
const data = readJsonFile(filePath);
|
|
618
640
|
|
|
619
|
-
// Find task
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
641
|
+
// Find task (support both flat structure and nested stages structure)
|
|
642
|
+
let task = null;
|
|
643
|
+
let taskIndex = -1;
|
|
644
|
+
let taskArray = null;
|
|
645
|
+
let isStageMode = false;
|
|
646
|
+
let targetStage = null;
|
|
647
|
+
|
|
648
|
+
if (args.stage) {
|
|
649
|
+
// Nested structure: stages.{stage}.features
|
|
650
|
+
isStageMode = true;
|
|
651
|
+
if (!data.stages || !data.stages[args.stage]) {
|
|
652
|
+
outputError(`Stage not found: ${args.stage}`);
|
|
653
|
+
}
|
|
654
|
+
targetStage = data.stages[args.stage];
|
|
655
|
+
if (!targetStage.features || !Array.isArray(targetStage.features)) {
|
|
656
|
+
outputError(`Stage has no features array: ${args.stage}`);
|
|
657
|
+
}
|
|
658
|
+
taskArray = targetStage.features;
|
|
659
|
+
taskIndex = taskArray.findIndex(t => t.id === args.taskId);
|
|
660
|
+
if (taskIndex === -1) {
|
|
661
|
+
outputError(`Task not found in stage ${args.stage}: ${args.taskId}`);
|
|
662
|
+
}
|
|
663
|
+
} else {
|
|
664
|
+
// Flat structure: data.tasks
|
|
665
|
+
taskArray = data.tasks;
|
|
666
|
+
taskIndex = taskArray?.findIndex(t => t.id === args.taskId);
|
|
667
|
+
if (taskIndex === -1 || taskIndex === undefined) {
|
|
668
|
+
outputError(`Task not found: ${args.taskId}`);
|
|
669
|
+
}
|
|
623
670
|
}
|
|
624
671
|
|
|
625
|
-
|
|
672
|
+
task = taskArray[taskIndex];
|
|
626
673
|
const now = getTimestamp();
|
|
627
674
|
|
|
628
675
|
// Update status
|
|
@@ -668,11 +715,17 @@ function cmdUpdateTask(args) {
|
|
|
668
715
|
}
|
|
669
716
|
|
|
670
717
|
// Update task
|
|
671
|
-
|
|
718
|
+
taskArray[taskIndex] = task;
|
|
672
719
|
data.updated_at = now;
|
|
673
720
|
|
|
674
721
|
// Recalculate counts
|
|
675
|
-
|
|
722
|
+
if (isStageMode && targetStage.counts) {
|
|
723
|
+
// Update stage-level counts
|
|
724
|
+
targetStage.counts = calculateCounts(taskArray);
|
|
725
|
+
} else {
|
|
726
|
+
// Update global counts
|
|
727
|
+
data.counts = calculateCounts(data.tasks);
|
|
728
|
+
}
|
|
676
729
|
|
|
677
730
|
// Atomic write
|
|
678
731
|
atomicWriteJson(filePath, data);
|