create-claude-workspace 1.1.53 → 1.1.55

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.
@@ -203,32 +203,41 @@ function preflight(opts, log) {
203
203
  }
204
204
  // ─── Signal handling ───
205
205
  let stopping = false;
206
- let forceKilling = false;
207
206
  const stoppingRef = { value: false };
208
207
  function setupSignals(opts, log, checkpoint) {
209
- const gracefulStop = (label) => {
210
- if (forceKilling)
211
- return; // CONC-5: ignore rapid signals during exit
212
- forceKilling = true;
208
+ const die = (label, code) => {
213
209
  stopping = true;
214
210
  stoppingRef.value = true;
215
- log.info(`${label}. Killing child process and exiting...`);
216
- currentChild?.kill();
217
- finalCleanup(opts, checkpoint, log);
218
- process.exit(0);
211
+ try {
212
+ log.info(`${label}. Killing child process...`);
213
+ }
214
+ catch { /* ignore */ }
215
+ try {
216
+ currentChild?.kill();
217
+ }
218
+ catch { /* ignore */ }
219
+ try {
220
+ finalCleanup(opts, checkpoint, log);
221
+ }
222
+ catch { /* ignore */ }
223
+ process.exit(code);
219
224
  };
220
- process.on('SIGINT', () => gracefulStop('SIGINT'));
221
- process.on('SIGTERM', () => gracefulStop('SIGTERM'));
222
- process.on('SIGPIPE', () => { }); // CONC-6: ignore
225
+ process.on('SIGINT', () => die('SIGINT', 0));
226
+ process.on('SIGTERM', () => die('SIGTERM', 0));
227
+ process.on('SIGPIPE', () => { }); // ignore
223
228
  process.on('uncaughtException', (err) => {
224
- log.error(`Uncaught exception: ${err.message}`);
225
- finalCleanup(opts, checkpoint, log);
226
- process.exit(1);
229
+ try {
230
+ log.error(`Uncaught exception: ${err.message}`);
231
+ }
232
+ catch { /* ignore */ }
233
+ die('uncaughtException', 1);
227
234
  });
228
235
  process.on('unhandledRejection', (reason) => {
229
- log.error(`Unhandled rejection: ${reason}`);
230
- finalCleanup(opts, checkpoint, log);
231
- process.exit(1);
236
+ try {
237
+ log.error(`Unhandled rejection: ${reason}`);
238
+ }
239
+ catch { /* ignore */ }
240
+ die('unhandledRejection', 1);
232
241
  });
233
242
  }
234
243
  function finalCleanup(opts, checkpoint, log) {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // ─── Docker-based isolated runner for Claude Starter Kit ───
3
3
  // Single cross-platform script. Builds container, sets up auth, runs autonomous loop.
4
- import { spawnSync } from 'node:child_process';
4
+ import { spawnSync, spawn as spawnAsync } from 'node:child_process';
5
5
  import { existsSync, writeFileSync, unlinkSync } from 'node:fs';
6
6
  import { join } from 'node:path';
7
7
  import { homedir, platform as osPlatform, tmpdir } from 'node:os';
@@ -62,7 +62,41 @@ function cleanup() {
62
62
  }
63
63
  catch { /* ignore */ }
64
64
  }
65
+ /** Run docker compose asynchronously — returns a promise that resolves on exit.
66
+ * Ctrl+C triggers `docker compose kill` to forcefully stop the container. */
67
+ function composeAsync(args) {
68
+ const files = [];
69
+ if (existsSync(AUTH_COMPOSE))
70
+ files.push('-f', 'docker-compose.yml', '-f', AUTH_COMPOSE);
71
+ const child = spawnAsync('docker', ['compose', ...files, ...args], {
72
+ cwd: DOCKER_DIR,
73
+ stdio: 'inherit',
74
+ env: { ...process.env },
75
+ });
76
+ const kill = () => {
77
+ // Forcefully kill the container — don't rely on Docker's signal forwarding
78
+ try {
79
+ spawnSync('docker', ['compose', ...files, 'kill'], {
80
+ cwd: DOCKER_DIR, stdio: 'ignore', timeout: 10_000,
81
+ });
82
+ }
83
+ catch { /* ignore */ }
84
+ cleanup();
85
+ process.exit(130);
86
+ };
87
+ process.on('SIGINT', kill);
88
+ process.on('SIGTERM', kill);
89
+ return new Promise((resolve) => {
90
+ child.on('close', (code) => {
91
+ process.removeListener('SIGINT', kill);
92
+ process.removeListener('SIGTERM', kill);
93
+ resolve(code ?? 1);
94
+ });
95
+ child.on('error', () => resolve(1));
96
+ });
97
+ }
65
98
  process.on('exit', cleanup);
99
+ // These handlers are overridden by composeAsync when running — fallback for non-async paths
66
100
  process.on('SIGINT', () => { cleanup(); process.exit(130); });
67
101
  process.on('SIGTERM', () => { cleanup(); process.exit(143); });
68
102
  function parseArgs() {
@@ -221,7 +255,7 @@ function startDaemon() {
221
255
  process.exit(1);
222
256
  }
223
257
  // ─── Main ───
224
- function main() {
258
+ async function main() {
225
259
  const opts = parseArgs();
226
260
  if (opts.help) {
227
261
  printHelp();
@@ -320,6 +354,7 @@ function main() {
320
354
  info(' claude # interactive Claude');
321
355
  info(' npx create-claude-workspace run # autonomous loop');
322
356
  console.log('');
357
+ // Shell mode uses spawnSync (interactive TTY needed)
323
358
  compose(['run', '--rm', 'claude']);
324
359
  }
325
360
  else {
@@ -338,14 +373,19 @@ function main() {
338
373
  const escaped = autoArgs.map(a => /^[\w.=/-]+$/.test(a) ? a : `'${a.replace(/'/g, "'\\''")}'`);
339
374
  console.log('');
340
375
  info('Autonomous mode — isolated in Docker, --skip-permissions is safe.');
341
- info('Press Ctrl+C to stop after current iteration.');
376
+ info('Press Ctrl+C to stop.');
342
377
  console.log('');
343
- compose(['run', '--rm', '-T', 'claude', '-c', `exec npx create-claude-workspace run ${escaped.join(' ')}`]);
378
+ // Use async spawn so SIGINT handler can fire and kill the container
379
+ const code = await composeAsync(['run', '--rm', '-T', 'claude', '-c', `exec npx create-claude-workspace run ${escaped.join(' ')}`]);
380
+ process.exit(code);
344
381
  }
345
382
  }
346
383
  export { main as runDockerLoop, parseArgs, printHelp };
347
384
  // Run only when executed directly, not when imported
348
385
  const isDirectRun = process.argv[1]?.replace(/\\/g, '/').endsWith('docker-run.mjs');
349
386
  if (isDirectRun) {
350
- main();
387
+ main().catch(err => {
388
+ console.error('Fatal:', err);
389
+ process.exit(1);
390
+ });
351
391
  }
@@ -152,13 +152,12 @@ export function killProcessTree(pid, isWin) {
152
152
  catch { /* already dead */ }
153
153
  return;
154
154
  }
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)
155
+ // Unix: SIGKILL immediately no SIGTERM grace period.
156
+ // Try process group first (negative PID), fall back to specific PID.
157
157
  try {
158
158
  process.kill(-pid, 'SIGKILL');
159
159
  }
160
160
  catch { /* no group or already dead */ }
161
- // Fallback: kill the specific PID if process group kill failed
162
161
  try {
163
162
  process.kill(pid, 'SIGKILL');
164
163
  }
@@ -196,19 +195,19 @@ export function runClaude(opts, log, runOpts = {}) {
196
195
  flags.push('--dangerously-skip-permissions');
197
196
  // Reset stream state for this invocation
198
197
  resetStreamState();
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().
198
+ // Spawn WITHOUT detached child stays in the same process group as parent.
199
+ // When user presses Ctrl+C, BOTH parent and child receive the signal from the OS.
200
+ // Parent's SIGINT handler kills child (backup) and calls process.exit().
201
+ // Child's own SIGINT handler also fires, causing it to exit.
201
202
  const child = isWin
202
203
  ? spawn(process.env.ComSpec ?? 'cmd.exe', ['/c', 'claude', ...flags], {
203
204
  cwd: opts.projectDir,
204
205
  stdio: ['pipe', 'pipe', 'pipe'],
205
- detached: true,
206
206
  windowsHide: true,
207
207
  })
208
208
  : spawn('claude', [...flags, prompt], {
209
209
  cwd: opts.projectDir,
210
210
  stdio: ['ignore', 'pipe', 'pipe'],
211
- detached: true,
212
211
  });
213
212
  if (isWin && child.stdin) {
214
213
  child.stdin.write(prompt);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-workspace",
3
- "version": "1.1.53",
3
+ "version": "1.1.55",
4
4
  "description": "Scaffold a project with Claude Code agents for autonomous AI-driven development",
5
5
  "type": "module",
6
6
  "bin": {