claude-yes 1.23.3 → 1.24.0
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/cli.ts +7 -0
- package/dist/claude-yes.js +209 -9
- package/dist/cli.js +209 -9
- package/dist/cli.js.map +4 -4
- package/dist/codex-yes.js +209 -9
- package/dist/copilot-yes.js +209 -9
- package/dist/cursor-yes.js +209 -9
- package/dist/gemini-yes.js +209 -9
- package/dist/grok-yes.js +209 -9
- package/dist/index.js +205 -8
- package/dist/index.js.map +6 -5
- package/index.ts +46 -1
- package/package.json +1 -1
- package/runningLock.spec.ts +477 -0
- package/runningLock.ts +324 -0
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
|
+
}
|