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
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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', () =>
|
|
221
|
-
process.on('SIGTERM', () =>
|
|
222
|
-
process.on('SIGPIPE', () => { }); //
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
376
|
+
info('Press Ctrl+C to stop.');
|
|
342
377
|
console.log('');
|
|
343
|
-
|
|
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
|
|
156
|
-
//
|
|
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
|
|
200
|
-
//
|
|
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);
|