claude-yes 1.23.3 → 1.24.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/runningLock.ts ADDED
@@ -0,0 +1,324 @@
1
+ import { execSync } from 'child_process';
2
+ import { existsSync } from 'fs';
3
+ import { mkdir, readFile, rename, unlink, writeFile } from 'fs/promises';
4
+ import { homedir } from 'os';
5
+ import path from 'path';
6
+
7
+ export interface Task {
8
+ cwd: string;
9
+ gitRoot?: string;
10
+ task: string;
11
+ pid: number;
12
+ status: 'running' | 'queued' | 'completed' | 'failed';
13
+ startedAt: number;
14
+ lockedAt: number;
15
+ }
16
+
17
+ export interface LockFile {
18
+ tasks: Task[];
19
+ }
20
+
21
+ interface LockCheckResult {
22
+ isLocked: boolean;
23
+ blockingTasks: Task[];
24
+ lockKey: string;
25
+ }
26
+
27
+ const LOCK_DIR = path.join(homedir(), '.claude-yes');
28
+ const LOCK_FILE = path.join(LOCK_DIR, 'running.lock.json');
29
+ const MAX_RETRIES = 5;
30
+ const RETRY_DELAYS = [50, 100, 200, 400, 800]; // exponential backoff in ms
31
+ const POLL_INTERVAL = 2000; // 2 seconds
32
+
33
+ /**
34
+ * Check if a process is running
35
+ */
36
+ function isProcessRunning(pid: number): boolean {
37
+ try {
38
+ // Sending signal 0 checks if process exists without killing it
39
+ process.kill(pid, 0);
40
+ return true;
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Get git repository root for a directory
48
+ */
49
+ function getGitRoot(cwd: string): string | null {
50
+ try {
51
+ const result = execSync('git rev-parse --show-toplevel', {
52
+ cwd,
53
+ encoding: 'utf8',
54
+ stdio: ['pipe', 'pipe', 'ignore'],
55
+ });
56
+ return result.trim();
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Check if directory is in a git repository
64
+ */
65
+ function isGitRepo(cwd: string): boolean {
66
+ try {
67
+ const gitRoot = getGitRoot(cwd);
68
+ return gitRoot !== null;
69
+ } catch {
70
+ return false;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Resolve path to real path (handling symlinks)
76
+ */
77
+ function resolveRealPath(p: string): string {
78
+ try {
79
+ return path.resolve(p);
80
+ } catch {
81
+ return p;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Sleep for a given number of milliseconds
87
+ */
88
+ function sleep(ms: number): Promise<void> {
89
+ return new Promise((resolve) => setTimeout(resolve, ms));
90
+ }
91
+
92
+ /**
93
+ * Read lock file with retry logic and stale lock cleanup
94
+ */
95
+ async function readLockFile(): Promise<LockFile> {
96
+ try {
97
+ await mkdir(LOCK_DIR, { recursive: true });
98
+
99
+ if (!existsSync(LOCK_FILE)) {
100
+ return { tasks: [] };
101
+ }
102
+
103
+ const content = await readFile(LOCK_FILE, 'utf8');
104
+ const lockFile = JSON.parse(content) as LockFile;
105
+
106
+ // Clean stale locks while reading
107
+ lockFile.tasks = lockFile.tasks.filter((task) => {
108
+ if (isProcessRunning(task.pid)) return true;
109
+ return false;
110
+ });
111
+
112
+ return lockFile;
113
+ } catch (error) {
114
+ // If file is corrupted or doesn't exist, return empty lock file
115
+ return { tasks: [] };
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Write lock file atomically with retry logic
121
+ */
122
+ async function writeLockFile(
123
+ lockFile: LockFile,
124
+ retryCount = 0,
125
+ ): Promise<void> {
126
+ try {
127
+ await mkdir(LOCK_DIR, { recursive: true });
128
+
129
+ const tempFile = `${LOCK_FILE}.tmp.${process.pid}`;
130
+ await writeFile(tempFile, JSON.stringify(lockFile, null, 2), 'utf8');
131
+
132
+ // Atomic rename
133
+ await rename(tempFile, LOCK_FILE);
134
+ } catch (error) {
135
+ if (retryCount < MAX_RETRIES) {
136
+ // Exponential backoff retry
137
+ await sleep(RETRY_DELAYS[retryCount] || 800);
138
+ return writeLockFile(lockFile, retryCount + 1);
139
+ }
140
+ throw error;
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Check if lock exists for the current working directory
146
+ */
147
+ async function checkLock(
148
+ cwd: string,
149
+ prompt: string,
150
+ ): Promise<LockCheckResult> {
151
+ const resolvedCwd = resolveRealPath(cwd);
152
+ const gitRoot = isGitRepo(resolvedCwd) ? getGitRoot(resolvedCwd) : null;
153
+ const lockKey = gitRoot || resolvedCwd;
154
+
155
+ const lockFile = await readLockFile();
156
+
157
+ // Find running tasks for this location
158
+ const blockingTasks = lockFile.tasks.filter((task) => {
159
+ if (!isProcessRunning(task.pid)) return false; // Skip stale locks
160
+ if (task.status !== 'running') return false; // Only check running tasks
161
+
162
+ if (gitRoot && task.gitRoot) {
163
+ // In git repo: check by git root
164
+ return task.gitRoot === gitRoot;
165
+ } else {
166
+ // Not in git repo: exact cwd match
167
+ return task.cwd === lockKey;
168
+ }
169
+ });
170
+
171
+ return {
172
+ isLocked: blockingTasks.length > 0,
173
+ blockingTasks,
174
+ lockKey,
175
+ };
176
+ }
177
+
178
+ /**
179
+ * Add a task to the lock file
180
+ */
181
+ async function addTask(task: Task): Promise<void> {
182
+ const lockFile = await readLockFile();
183
+
184
+ // Remove any existing task with same PID (shouldn't happen, but be safe)
185
+ lockFile.tasks = lockFile.tasks.filter((t) => t.pid !== task.pid);
186
+
187
+ lockFile.tasks.push(task);
188
+ await writeLockFile(lockFile);
189
+ }
190
+
191
+ /**
192
+ * Update task status
193
+ */
194
+ async function updateTaskStatus(
195
+ pid: number,
196
+ status: Task['status'],
197
+ ): Promise<void> {
198
+ const lockFile = await readLockFile();
199
+ const task = lockFile.tasks.find((t) => t.pid === pid);
200
+
201
+ if (task) {
202
+ task.status = status;
203
+ await writeLockFile(lockFile);
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Remove a task from the lock file
209
+ */
210
+ async function removeTask(pid: number): Promise<void> {
211
+ const lockFile = await readLockFile();
212
+ lockFile.tasks = lockFile.tasks.filter((t) => t.pid !== pid);
213
+ await writeLockFile(lockFile);
214
+ }
215
+
216
+ /**
217
+ * Wait for lock to be released
218
+ */
219
+ async function waitForUnlock(
220
+ blockingTasks: Task[],
221
+ currentTask: Task,
222
+ ): Promise<void> {
223
+ const blockingTask = blockingTasks[0];
224
+ console.log(`⏳ Queueing for unlock of: ${blockingTask.task}`);
225
+
226
+ // Add current task as 'queued'
227
+ await addTask({ ...currentTask, status: 'queued' });
228
+
229
+ let dots = 0;
230
+ while (true) {
231
+ await sleep(POLL_INTERVAL);
232
+
233
+ const lockCheck = await checkLock(currentTask.cwd, currentTask.task);
234
+
235
+ if (!lockCheck.isLocked) {
236
+ // Lock released, update status to running
237
+ await updateTaskStatus(currentTask.pid, 'running');
238
+ console.log(`\n✓ Lock released, starting task...`);
239
+ break;
240
+ }
241
+
242
+ // Show progress indicator
243
+ dots = (dots + 1) % 4;
244
+ process.stdout.write(
245
+ `\r⏳ Queueing${'.'.repeat(dots)}${' '.repeat(3 - dots)}`,
246
+ );
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Clean stale locks from the lock file
252
+ */
253
+ export async function cleanStaleLocks(): Promise<void> {
254
+ const lockFile = await readLockFile();
255
+
256
+ const before = lockFile.tasks.length;
257
+ lockFile.tasks = lockFile.tasks.filter((task) => {
258
+ if (isProcessRunning(task.pid)) return true;
259
+
260
+ console.log(`🧹 Cleaned stale lock for PID ${task.pid}`);
261
+ return false;
262
+ });
263
+
264
+ if (lockFile.tasks.length !== before) {
265
+ await writeLockFile(lockFile);
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Acquire lock or wait if locked
271
+ */
272
+ export async function acquireLock(
273
+ cwd: string,
274
+ prompt: string = 'no prompt provided',
275
+ ): Promise<void> {
276
+ const resolvedCwd = resolveRealPath(cwd);
277
+ const gitRoot = isGitRepo(resolvedCwd) ? getGitRoot(resolvedCwd) : null;
278
+
279
+ const task: Task = {
280
+ cwd: resolvedCwd,
281
+ gitRoot: gitRoot || undefined,
282
+ task: prompt.substring(0, 100), // Limit task description length
283
+ pid: process.pid,
284
+ status: 'running',
285
+ startedAt: Date.now(),
286
+ lockedAt: Date.now(),
287
+ };
288
+
289
+ const lockCheck = await checkLock(resolvedCwd, prompt);
290
+
291
+ if (lockCheck.isLocked) {
292
+ await waitForUnlock(lockCheck.blockingTasks, task);
293
+ } else {
294
+ await addTask(task);
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Release lock for current process
300
+ */
301
+ export async function releaseLock(pid: number = process.pid): Promise<void> {
302
+ await removeTask(pid);
303
+ }
304
+
305
+ /**
306
+ * Update status of current task
307
+ */
308
+ export async function updateCurrentTaskStatus(
309
+ status: Task['status'],
310
+ pid: number = process.pid,
311
+ ): Promise<void> {
312
+ await updateTaskStatus(pid, status);
313
+ }
314
+
315
+ /**
316
+ * Check if we should use locking for this directory
317
+ * Only use locking if we're in a git repository
318
+ */
319
+ export function shouldUseLock(cwd: string): boolean {
320
+ const resolvedCwd = resolveRealPath(cwd);
321
+ // Only use lock if in git repo OR if explicitly requested
322
+ // For now, use lock for all cases to handle same-dir conflicts
323
+ return true;
324
+ }