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
- if (stopping) {
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: kill process group
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, 'SIGTERM');
158
+ process.kill(-pid, 'SIGKILL');
158
159
  }
159
160
  catch { /* no group or already dead */ }
160
- setTimeout(() => {
161
- try {
162
- process.kill(-pid, 'SIGKILL');
163
- }
164
- catch { /* already dead */ }
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 to isolate child from CTRL+C.
200
- // First CTRL+C sets stopping flag (parent only); child finishes current step.
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 Ctrl+C (killProcessTree) actually kills child processes.
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
- // Wait for tree to die
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 process fast enough for interactive use (< 3s)', async () => {
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 or timeout
317
+ // Poll until dead
337
318
  let alive = true;
338
319
  while (alive && Date.now() - start < 3000) {
339
- await new Promise(r => setTimeout(r, 100));
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(3000);
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
- **Auth in Docker:** If `GH_TOKEN`, `GITLAB_TOKEN`, or `CLICKUP_API_TOKEN` env var is set, 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`.
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-workspace",
3
- "version": "1.1.52",
3
+ "version": "1.1.53",
4
4
  "description": "Scaffold a project with Claude Code agents for autonomous AI-driven development",
5
5
  "type": "module",
6
6
  "bin": {