create-claude-workspace 1.1.49 → 1.1.51

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.
@@ -216,9 +216,10 @@ function setupSignals(opts, log, checkpoint) {
216
216
  finalCleanup(opts, checkpoint, log);
217
217
  process.exit(1);
218
218
  }
219
- log.info(`${label}. Stopping after current iteration (Ctrl+C again to force)...`);
219
+ log.info(`${label}. Killing child process and stopping (Ctrl+C again to force exit)...`);
220
220
  stopping = true;
221
221
  stoppingRef.value = true;
222
+ currentChild?.kill();
222
223
  };
223
224
  process.on('SIGINT', () => gracefulStop('SIGINT'));
224
225
  process.on('SIGTERM', () => gracefulStop('SIGTERM'));
@@ -340,7 +340,7 @@ function main() {
340
340
  info('Autonomous mode — isolated in Docker, --skip-permissions is safe.');
341
341
  info('Press Ctrl+C to stop after current iteration.');
342
342
  console.log('');
343
- compose(['run', '--rm', '-T', 'claude', '-c', `npx create-claude-workspace run ${escaped.join(' ')}`]);
343
+ compose(['run', '--rm', '-T', 'claude', '-c', `exec npx create-claude-workspace run ${escaped.join(' ')}`]);
344
344
  }
345
345
  }
346
346
  export { main as runDockerLoop, parseArgs, printHelp };
@@ -144,7 +144,7 @@ function parseStreamEvent(json) {
144
144
  }
145
145
  }
146
146
  // ─── Process tree kill (cross-platform) ───
147
- function killProcessTree(pid, isWin) {
147
+ export function killProcessTree(pid, isWin) {
148
148
  if (isWin) {
149
149
  try {
150
150
  execSync(`taskkill /PID ${pid} /T /F`, { stdio: 'ignore' });
@@ -1,5 +1,6 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
- import { resetStreamState, validateResult, formatStreamEvent } from './claude-runner.mjs';
2
+ import { resetStreamState, validateResult, formatStreamEvent, killProcessTree } from './claude-runner.mjs';
3
+ import { spawn, execSync } from 'node:child_process';
3
4
  describe('validateResult', () => {
4
5
  it('returns valid result for complete object', () => {
5
6
  const input = { status: 'completed', action: 'commit', message: 'Done task #1' };
@@ -236,3 +237,110 @@ describe('resetStreamState', () => {
236
237
  resetStreamState(); // should not throw
237
238
  });
238
239
  });
240
+ // ─── killProcessTree integration tests ───
241
+ // Verifies that Ctrl+C (killProcessTree) actually kills child processes.
242
+ const isWin = process.platform === 'win32';
243
+ function isProcessAlive(pid) {
244
+ try {
245
+ process.kill(pid, 0); // signal 0 = existence check
246
+ return true;
247
+ }
248
+ catch {
249
+ return false;
250
+ }
251
+ }
252
+ function spawnLongRunning() {
253
+ const script = 'setTimeout(()=>{},60000)';
254
+ if (isWin) {
255
+ return spawn(process.env.ComSpec ?? 'cmd.exe', ['/c', 'node', '-e', script], {
256
+ stdio: 'ignore',
257
+ detached: true,
258
+ windowsHide: true,
259
+ });
260
+ }
261
+ return spawn('node', ['-e', script], {
262
+ stdio: 'ignore',
263
+ detached: true,
264
+ });
265
+ }
266
+ /** Spawn a nested process tree: parent node → child node (simulates cmd.exe → claude) */
267
+ function spawnNestedTree() {
268
+ // Parent spawns a child that also sleeps — simulates cmd.exe → claude process tree
269
+ const innerScript = `
270
+ const { spawn } = require('child_process');
271
+ const child = spawn('node', ['-e', 'setTimeout(()=>{},60000)'], { stdio: 'ignore' });
272
+ child.unref();
273
+ setTimeout(()=>{},60000);
274
+ `.replace(/\n/g, ' ');
275
+ if (isWin) {
276
+ return spawn(process.env.ComSpec ?? 'cmd.exe', ['/c', 'node', '-e', innerScript], {
277
+ stdio: 'ignore',
278
+ detached: true,
279
+ windowsHide: true,
280
+ });
281
+ }
282
+ return spawn('node', ['-e', innerScript], {
283
+ stdio: 'ignore',
284
+ detached: true,
285
+ });
286
+ }
287
+ 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
+ it('kills a nested process tree (simulates cmd.exe → claude)', async () => {
302
+ const parent = spawnNestedTree();
303
+ const parentPid = parent.pid;
304
+ expect(parentPid).toBeGreaterThan(0);
305
+ // Give tree time to start
306
+ await new Promise(r => setTimeout(r, 1500));
307
+ expect(isProcessAlive(parentPid)).toBe(true);
308
+ // Kill the tree
309
+ killProcessTree(parentPid, isWin);
310
+ // Wait for tree to die
311
+ await new Promise(r => setTimeout(r, isWin ? 3000 : 2000));
312
+ 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
+ }, 15_000);
326
+ it('does not throw when killing already-dead process', () => {
327
+ // PID 999999 is almost certainly not a real process
328
+ expect(() => killProcessTree(999999, isWin)).not.toThrow();
329
+ });
330
+ it('kills process fast enough for interactive use (< 3s)', async () => {
331
+ const child = spawnLongRunning();
332
+ const pid = child.pid;
333
+ await new Promise(r => setTimeout(r, 500));
334
+ const start = Date.now();
335
+ killProcessTree(pid, isWin);
336
+ // Poll until dead or timeout
337
+ let alive = true;
338
+ while (alive && Date.now() - start < 3000) {
339
+ await new Promise(r => setTimeout(r, 100));
340
+ alive = isProcessAlive(pid);
341
+ }
342
+ const elapsed = Date.now() - start;
343
+ expect(alive).toBe(false);
344
+ expect(elapsed).toBeLessThan(3000);
345
+ }, 10_000);
346
+ });
@@ -6,14 +6,29 @@ RUN apt-get update && \
6
6
 
7
7
 
8
8
  # Non-root user — Claude Code refuses --dangerously-skip-permissions as root
9
- # Passwordless sudo for on-demand CLI installs (gh, glab) by agents
10
- RUN apt-get update && apt-get install -y --no-install-recommends sudo && \
9
+ RUN apt-get update && apt-get install -y --no-install-recommends sudo gpg && \
11
10
  apt-get clean && rm -rf /var/lib/apt/lists/* && \
12
11
  useradd -m -s /bin/bash claude && \
13
12
  echo 'claude ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/claude && \
14
13
  mkdir -p /home/claude/.claude && \
15
14
  chown -R claude:claude /home/claude
16
15
 
16
+ # Install gh (GitHub CLI) from official apt repo
17
+ RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
18
+ | gpg --dearmor -o /usr/share/keyrings/githubcli-archive-keyring.gpg && \
19
+ echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
20
+ > /etc/apt/sources.list.d/github-cli.list && \
21
+ apt-get update && apt-get install -y --no-install-recommends gh && \
22
+ apt-get clean && rm -rf /var/lib/apt/lists/*
23
+
24
+ # Install glab (GitLab CLI) — latest .deb from GitLab releases
25
+ RUN GLAB_VER=$(curl -fsSL "https://gitlab.com/api/v4/projects/34675721/releases/permalink/latest" \
26
+ | node -pe 'JSON.parse(require("fs").readFileSync("/dev/stdin","utf8")).tag_name.slice(1)') && \
27
+ DEB_ARCH=$(dpkg --print-architecture) && \
28
+ curl -fsSL "https://gitlab.com/gitlab-org/cli/-/releases/v${GLAB_VER}/downloads/glab_${GLAB_VER}_linux_${DEB_ARCH}.deb" \
29
+ -o /tmp/glab.deb && \
30
+ dpkg -i /tmp/glab.deb && rm /tmp/glab.deb
31
+
17
32
  # Install Claude Code natively as the claude user
18
33
  RUN gosu claude bash -c 'curl -fsSL https://claude.ai/install.sh | bash'
19
34
  ENV PATH="/home/claude/.local/bin:$PATH"
@@ -1,6 +1,8 @@
1
1
  services:
2
2
  claude:
3
3
  build: .
4
+ init: true
5
+ stop_grace_period: 30s
4
6
  stdin_open: true
5
7
  tty: true
6
8
  volumes:
@@ -36,6 +36,9 @@ if [[ -n "${GITLAB_TOKEN:-}" ]]; then
36
36
  git config --global credential.helper store
37
37
  echo "https://oauth2:${GITLAB_TOKEN}@gitlab.com" >> /home/claude/.git-credentials
38
38
  chown claude:claude /home/claude/.git-credentials
39
+ # Configure glab auth (glab reads GITLAB_TOKEN env var automatically,
40
+ # but explicit login ensures 'glab auth status' reports correctly)
41
+ gosu claude glab auth login --hostname gitlab.com --token "$GITLAB_TOKEN" 2>/dev/null || true
39
42
  fi
40
43
 
41
44
  # Auth pre-check — fail fast if no credentials available
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-workspace",
3
- "version": "1.1.49",
3
+ "version": "1.1.51",
4
4
  "description": "Scaffold a project with Claude Code agents for autonomous AI-driven development",
5
5
  "type": "module",
6
6
  "bin": {