create-claude-workspace 1.1.52 → 1.1.53
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.
|
@@ -208,18 +208,14 @@ const stoppingRef = { value: false };
|
|
|
208
208
|
function setupSignals(opts, log, checkpoint) {
|
|
209
209
|
const gracefulStop = (label) => {
|
|
210
210
|
if (forceKilling)
|
|
211
|
-
return; // CONC-5: ignore rapid signals
|
|
212
|
-
|
|
213
|
-
forceKilling = true;
|
|
214
|
-
log.info('Force stopping...');
|
|
215
|
-
currentChild?.kill();
|
|
216
|
-
finalCleanup(opts, checkpoint, log);
|
|
217
|
-
process.exit(1);
|
|
218
|
-
}
|
|
219
|
-
log.info(`${label}. Killing child process and stopping (Ctrl+C again to force exit)...`);
|
|
211
|
+
return; // CONC-5: ignore rapid signals during exit
|
|
212
|
+
forceKilling = true;
|
|
220
213
|
stopping = true;
|
|
221
214
|
stoppingRef.value = true;
|
|
215
|
+
log.info(`${label}. Killing child process and exiting...`);
|
|
222
216
|
currentChild?.kill();
|
|
217
|
+
finalCleanup(opts, checkpoint, log);
|
|
218
|
+
process.exit(0);
|
|
223
219
|
};
|
|
224
220
|
process.on('SIGINT', () => gracefulStop('SIGINT'));
|
|
225
221
|
process.on('SIGTERM', () => gracefulStop('SIGTERM'));
|
|
@@ -152,17 +152,17 @@ export function killProcessTree(pid, isWin) {
|
|
|
152
152
|
catch { /* already dead */ }
|
|
153
153
|
return;
|
|
154
154
|
}
|
|
155
|
-
// Unix:
|
|
155
|
+
// Unix: SIGKILL the entire process group immediately (no SIGTERM grace period —
|
|
156
|
+
// when the user presses Ctrl+C they want it dead NOW, not in 5 seconds)
|
|
156
157
|
try {
|
|
157
|
-
process.kill(-pid, '
|
|
158
|
+
process.kill(-pid, 'SIGKILL');
|
|
158
159
|
}
|
|
159
160
|
catch { /* no group or already dead */ }
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}, 5000).unref();
|
|
161
|
+
// Fallback: kill the specific PID if process group kill failed
|
|
162
|
+
try {
|
|
163
|
+
process.kill(pid, 'SIGKILL');
|
|
164
|
+
}
|
|
165
|
+
catch { /* already dead */ }
|
|
166
166
|
}
|
|
167
167
|
// ─── Error signal patterns ───
|
|
168
168
|
const RATE_LIMIT_RE = /rate.?limit|429|overloaded|529|at capacity|capacity reached|503 service unavailable|502 bad gateway/;
|
|
@@ -196,9 +196,8 @@ export function runClaude(opts, log, runOpts = {}) {
|
|
|
196
196
|
flags.push('--dangerously-skip-permissions');
|
|
197
197
|
// Reset stream state for this invocation
|
|
198
198
|
resetStreamState();
|
|
199
|
-
// Spawn — detached on both platforms
|
|
200
|
-
//
|
|
201
|
-
// Second CTRL+C force-kills the child via killProcessTree.
|
|
199
|
+
// Spawn — detached on both platforms so killProcessTree can target the process group.
|
|
200
|
+
// CTRL+C kills the child immediately (SIGKILL/taskkill) and exits the parent via process.exit().
|
|
202
201
|
const child = isWin
|
|
203
202
|
? spawn(process.env.ComSpec ?? 'cmd.exe', ['/c', 'claude', ...flags], {
|
|
204
203
|
cwd: opts.projectDir,
|
|
@@ -238,9 +238,20 @@ describe('resetStreamState', () => {
|
|
|
238
238
|
});
|
|
239
239
|
});
|
|
240
240
|
// ─── killProcessTree integration tests ───
|
|
241
|
-
// Verifies that
|
|
241
|
+
// Verifies that killProcessTree actually kills child process trees immediately
|
|
242
|
+
// (SIGKILL on Unix, taskkill /T /F on Windows — no 5s SIGTERM grace period).
|
|
242
243
|
const isWin = process.platform === 'win32';
|
|
243
244
|
function isProcessAlive(pid) {
|
|
245
|
+
if (isWin) {
|
|
246
|
+
// process.kill(pid, 0) is unreliable on Windows for detached processes — use tasklist
|
|
247
|
+
try {
|
|
248
|
+
const info = execSync(`tasklist /FI "PID eq ${pid}" /NH`, { encoding: 'utf-8', stdio: 'pipe', timeout: 3000 });
|
|
249
|
+
return info.includes(String(pid));
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
244
255
|
try {
|
|
245
256
|
process.kill(pid, 0); // signal 0 = existence check
|
|
246
257
|
return true;
|
|
@@ -265,7 +276,6 @@ function spawnLongRunning() {
|
|
|
265
276
|
}
|
|
266
277
|
/** Spawn a nested process tree: parent node → child node (simulates cmd.exe → claude) */
|
|
267
278
|
function spawnNestedTree() {
|
|
268
|
-
// Parent spawns a child that also sleeps — simulates cmd.exe → claude process tree
|
|
269
279
|
const innerScript = `
|
|
270
280
|
const { spawn } = require('child_process');
|
|
271
281
|
const child = spawn('node', ['-e', 'setTimeout(()=>{},60000)'], { stdio: 'ignore' });
|
|
@@ -285,62 +295,33 @@ function spawnNestedTree() {
|
|
|
285
295
|
});
|
|
286
296
|
}
|
|
287
297
|
describe('killProcessTree', () => {
|
|
288
|
-
it('kills a simple child process', async () => {
|
|
289
|
-
const child = spawnLongRunning();
|
|
290
|
-
const pid = child.pid;
|
|
291
|
-
expect(pid).toBeGreaterThan(0);
|
|
292
|
-
// Give process time to start
|
|
293
|
-
await new Promise(r => setTimeout(r, 500));
|
|
294
|
-
expect(isProcessAlive(pid)).toBe(true);
|
|
295
|
-
// Kill it
|
|
296
|
-
killProcessTree(pid, isWin);
|
|
297
|
-
// Wait for process to die
|
|
298
|
-
await new Promise(r => setTimeout(r, isWin ? 2000 : 1000));
|
|
299
|
-
expect(isProcessAlive(pid)).toBe(false);
|
|
300
|
-
}, 10_000);
|
|
301
298
|
it('kills a nested process tree (simulates cmd.exe → claude)', async () => {
|
|
302
299
|
const parent = spawnNestedTree();
|
|
303
300
|
const parentPid = parent.pid;
|
|
304
301
|
expect(parentPid).toBeGreaterThan(0);
|
|
305
|
-
// Give tree time to start
|
|
306
302
|
await new Promise(r => setTimeout(r, 1500));
|
|
307
303
|
expect(isProcessAlive(parentPid)).toBe(true);
|
|
308
|
-
// Kill the tree
|
|
309
304
|
killProcessTree(parentPid, isWin);
|
|
310
|
-
|
|
311
|
-
await new Promise(r => setTimeout(r, isWin ? 3000 : 2000));
|
|
305
|
+
await new Promise(r => setTimeout(r, isWin ? 3000 : 1000));
|
|
312
306
|
expect(isProcessAlive(parentPid)).toBe(false);
|
|
313
|
-
// On Windows, verify no orphan node.exe children from our tree
|
|
314
|
-
if (isWin) {
|
|
315
|
-
// tasklist can't reliably check by parent PID after parent is dead,
|
|
316
|
-
// but the /T flag in taskkill should have killed all children
|
|
317
|
-
try {
|
|
318
|
-
const taskInfo = execSync(`tasklist /FI "PID eq ${parentPid}" /NH`, {
|
|
319
|
-
encoding: 'utf-8', stdio: 'pipe', timeout: 3000,
|
|
320
|
-
});
|
|
321
|
-
expect(taskInfo).not.toContain(String(parentPid));
|
|
322
|
-
}
|
|
323
|
-
catch { /* process dead, expected */ }
|
|
324
|
-
}
|
|
325
307
|
}, 15_000);
|
|
326
308
|
it('does not throw when killing already-dead process', () => {
|
|
327
|
-
// PID 999999 is almost certainly not a real process
|
|
328
309
|
expect(() => killProcessTree(999999, isWin)).not.toThrow();
|
|
329
310
|
});
|
|
330
|
-
it('kills
|
|
311
|
+
it('kills within 1 second — SIGKILL immediate, no 5s SIGTERM grace', async () => {
|
|
331
312
|
const child = spawnLongRunning();
|
|
332
313
|
const pid = child.pid;
|
|
333
314
|
await new Promise(r => setTimeout(r, 500));
|
|
334
315
|
const start = Date.now();
|
|
335
316
|
killProcessTree(pid, isWin);
|
|
336
|
-
// Poll until dead
|
|
317
|
+
// Poll until dead
|
|
337
318
|
let alive = true;
|
|
338
319
|
while (alive && Date.now() - start < 3000) {
|
|
339
|
-
await new Promise(r => setTimeout(r,
|
|
320
|
+
await new Promise(r => setTimeout(r, 50));
|
|
340
321
|
alive = isProcessAlive(pid);
|
|
341
322
|
}
|
|
342
323
|
const elapsed = Date.now() - start;
|
|
343
324
|
expect(alive).toBe(false);
|
|
344
|
-
expect(elapsed).toBeLessThan(
|
|
325
|
+
expect(elapsed).toBeLessThan(isWin ? 2000 : 1000);
|
|
345
326
|
}, 10_000);
|
|
346
327
|
});
|
|
@@ -45,7 +45,9 @@ First detect environment: `echo $CLAUDE_DOCKER` — if `1`, you are in Docker (D
|
|
|
45
45
|
- macOS: `brew install glab`
|
|
46
46
|
- Linux: `sudo apt install glab` / `sudo dnf install glab`
|
|
47
47
|
|
|
48
|
-
**
|
|
48
|
+
**Loading tokens from `.env`:** Before checking env vars, load tokens from `.env` file in the project root (if it exists). Use: `set -a && [ -f .env ] && . ./.env && set +a`. This allows users to store `GITLAB_TOKEN`, `GH_TOKEN`, `CLICKUP_API_TOKEN` in `.env` instead of exporting them in the shell. The `.env` file MUST be in `.gitignore` (check and add if missing).
|
|
49
|
+
|
|
50
|
+
**Auth in Docker:** If `GH_TOKEN`, `GITLAB_TOKEN`, or `CLICKUP_API_TOKEN` env var is set (directly or via `.env`), it's used automatically (no interactive login needed). Otherwise return `needs_input` with `action: needs_github_token`, `action: needs_gitlab_token`, or `action: needs_clickup_token`.
|
|
49
51
|
|
|
50
52
|
## ClickUp API Helpers
|
|
51
53
|
|
|
@@ -71,6 +71,7 @@ At the beginning of EVERY session (including every Ralph Loop iteration):
|
|
|
71
71
|
|
|
72
72
|
### 1. Read state
|
|
73
73
|
- Read MEMORY.md — find where you left off, what is done, what is next, what phase you're in
|
|
74
|
+
- **Load `.env`**: If `.env` exists in the project root, source it to load tokens (`GITLAB_TOKEN`, `GH_TOKEN`, `CLICKUP_API_TOKEN`, etc.): `set -a && [ -f .env ] && . ./.env && set +a`
|
|
74
75
|
|
|
75
76
|
### 2. Resolve app names and package manager
|
|
76
77
|
- Read CLAUDE.md — find app name(s) and package manager from the Architecture/Tech Stack section
|