create-claude-workspace 1.1.151 → 2.0.0
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.
- package/README.md +33 -1
- package/dist/index.js +29 -56
- package/dist/scheduler/agents/health-checker.mjs +98 -0
- package/dist/scheduler/agents/health-checker.spec.js +143 -0
- package/dist/scheduler/agents/orchestrator.mjs +149 -0
- package/dist/scheduler/agents/orchestrator.spec.js +87 -0
- package/dist/scheduler/agents/prompt-builder.mjs +204 -0
- package/dist/scheduler/agents/prompt-builder.spec.js +240 -0
- package/dist/scheduler/agents/worker-pool.mjs +137 -0
- package/dist/scheduler/agents/worker-pool.spec.js +45 -0
- package/dist/scheduler/git/ci-watcher.mjs +93 -0
- package/dist/scheduler/git/ci-watcher.spec.js +35 -0
- package/dist/scheduler/git/manager.mjs +228 -0
- package/dist/scheduler/git/manager.spec.js +198 -0
- package/dist/scheduler/git/release.mjs +117 -0
- package/dist/scheduler/git/release.spec.js +175 -0
- package/dist/scheduler/index.mjs +309 -0
- package/dist/scheduler/index.spec.js +72 -0
- package/dist/scheduler/integration.spec.js +289 -0
- package/dist/scheduler/loop.mjs +435 -0
- package/dist/scheduler/loop.spec.js +139 -0
- package/dist/scheduler/state/session.mjs +14 -0
- package/dist/scheduler/state/session.spec.js +36 -0
- package/dist/scheduler/state/state.mjs +102 -0
- package/dist/scheduler/state/state.spec.js +175 -0
- package/dist/scheduler/tasks/inbox.mjs +98 -0
- package/dist/scheduler/tasks/inbox.spec.js +168 -0
- package/dist/scheduler/tasks/parser.mjs +228 -0
- package/dist/scheduler/tasks/parser.spec.js +303 -0
- package/dist/scheduler/tasks/queue.mjs +152 -0
- package/dist/scheduler/tasks/queue.spec.js +223 -0
- package/dist/scheduler/types.mjs +20 -0
- package/dist/{scripts/lib → scheduler/ui}/tui.mjs +84 -41
- package/dist/{scripts/lib → scheduler/ui}/tui.spec.js +56 -0
- package/dist/scheduler/util/memory.mjs +126 -0
- package/dist/scheduler/util/memory.spec.js +165 -0
- package/dist/template/.claude/{profiles/angular.md → agents/angular-engineer.md} +9 -4
- package/dist/template/.claude/{profiles/react.md → agents/react-engineer.md} +9 -4
- package/dist/template/.claude/{profiles/svelte.md → agents/svelte-engineer.md} +9 -4
- package/dist/template/.claude/{profiles/vue.md → agents/vue-engineer.md} +9 -4
- package/package.json +3 -4
- package/dist/scripts/autonomous.mjs +0 -492
- package/dist/scripts/autonomous.spec.js +0 -46
- package/dist/scripts/docker-run.mjs +0 -462
- package/dist/scripts/integration.spec.js +0 -108
- package/dist/scripts/lib/formatter.mjs +0 -309
- package/dist/scripts/lib/formatter.spec.js +0 -262
- package/dist/scripts/lib/state.mjs +0 -44
- package/dist/scripts/lib/state.spec.js +0 -59
- package/dist/template/.claude/docker/.dockerignore +0 -8
- package/dist/template/.claude/docker/Dockerfile +0 -54
- package/dist/template/.claude/docker/docker-compose.yml +0 -22
- package/dist/template/.claude/docker/docker-entrypoint.sh +0 -101
- /package/dist/{scripts/lib/types.mjs → scheduler/shared-types.mjs} +0 -0
- /package/dist/{scripts/lib → scheduler/util}/idle-poll.mjs +0 -0
- /package/dist/{scripts/lib → scheduler/util}/idle-poll.spec.js +0 -0
|
@@ -1,462 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// ─── Docker-based isolated runner for Claude Starter Kit ───
|
|
3
|
-
// Single cross-platform script. Builds container, sets up auth, runs autonomous loop.
|
|
4
|
-
import { spawnSync, spawn as spawnAsync } from 'node:child_process';
|
|
5
|
-
import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs';
|
|
6
|
-
import { join } from 'node:path';
|
|
7
|
-
import { homedir, platform as osPlatform, tmpdir, totalmem } from 'node:os';
|
|
8
|
-
// PROJECT_DIR = target project (set by index.ts via cwd), not the npm package
|
|
9
|
-
const PROJECT_DIR = process.cwd();
|
|
10
|
-
const DOCKER_DIR = join(PROJECT_DIR, '.claude', 'docker');
|
|
11
|
-
const AUTH_COMPOSE = join(tmpdir(), 'claude-starter-kit-auth-compose.yml');
|
|
12
|
-
const IS_WIN = osPlatform() === 'win32';
|
|
13
|
-
const IS_MAC = osPlatform() === 'darwin';
|
|
14
|
-
const SHELL = IS_WIN ? 'cmd.exe' : '/bin/sh';
|
|
15
|
-
// ─── Colors ───
|
|
16
|
-
const C = { r: '\x1b[31m', g: '\x1b[32m', y: '\x1b[33m', c: '\x1b[36m', n: '\x1b[0m' };
|
|
17
|
-
const info = (m) => console.log(`${C.g}[INFO]${C.n} ${m}`);
|
|
18
|
-
const warn = (m) => console.log(`${C.y}[WARN]${C.n} ${m}`);
|
|
19
|
-
const error = (m) => console.log(`${C.r}[ERROR]${C.n} ${m}`);
|
|
20
|
-
const step = (m) => console.log(`${C.c}[STEP]${C.n} ${m}`);
|
|
21
|
-
// ─── Helpers ───
|
|
22
|
-
function sleepSync(ms) {
|
|
23
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
24
|
-
}
|
|
25
|
-
function run(cmd, args = [], opts = {}) {
|
|
26
|
-
return spawnSync(cmd, args, {
|
|
27
|
-
shell: SHELL,
|
|
28
|
-
cwd: opts.cwd ?? PROJECT_DIR,
|
|
29
|
-
stdio: opts.capture ? 'pipe' : 'inherit',
|
|
30
|
-
timeout: opts.timeout,
|
|
31
|
-
env: { ...process.env, ...opts.env },
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
function ok(cmd, args = []) {
|
|
35
|
-
return run(cmd, args, { capture: true }).status === 0;
|
|
36
|
-
}
|
|
37
|
-
function hasCmd(cmd) {
|
|
38
|
-
const check = IS_WIN ? 'where' : 'which';
|
|
39
|
-
return spawnSync(check, [cmd], { shell: SHELL, stdio: 'pipe' }).status === 0;
|
|
40
|
-
}
|
|
41
|
-
function toDockerPath(p) {
|
|
42
|
-
return p.replace(/\\/g, '/');
|
|
43
|
-
}
|
|
44
|
-
function compose(args, opts = {}) {
|
|
45
|
-
const files = [];
|
|
46
|
-
if (existsSync(AUTH_COMPOSE))
|
|
47
|
-
files.push('-f', 'docker-compose.yml', '-f', AUTH_COMPOSE);
|
|
48
|
-
const result = spawnSync('docker', ['compose', ...files, ...args], {
|
|
49
|
-
cwd: DOCKER_DIR,
|
|
50
|
-
stdio: opts.capture ? 'pipe' : 'inherit',
|
|
51
|
-
env: { ...process.env },
|
|
52
|
-
});
|
|
53
|
-
if (!opts.capture && result.status !== 0 && result.status !== null) {
|
|
54
|
-
error(`docker compose ${args[0]} failed (exit code ${result.status})`);
|
|
55
|
-
}
|
|
56
|
-
return result;
|
|
57
|
-
}
|
|
58
|
-
function cleanup() {
|
|
59
|
-
try {
|
|
60
|
-
if (existsSync(AUTH_COMPOSE))
|
|
61
|
-
unlinkSync(AUTH_COMPOSE);
|
|
62
|
-
}
|
|
63
|
-
catch { /* ignore */ }
|
|
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 — belt AND suspenders
|
|
78
|
-
try {
|
|
79
|
-
child.kill();
|
|
80
|
-
}
|
|
81
|
-
catch { /* ignore */ }
|
|
82
|
-
try {
|
|
83
|
-
spawnSync('docker', ['compose', ...files, 'kill', 'claude'], {
|
|
84
|
-
cwd: DOCKER_DIR, stdio: 'ignore', timeout: 5_000,
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
catch { /* ignore */ }
|
|
88
|
-
try {
|
|
89
|
-
spawnSync('docker', ['compose', ...files, 'down', '--timeout', '0'], {
|
|
90
|
-
cwd: DOCKER_DIR, stdio: 'ignore', timeout: 5_000,
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
catch { /* ignore */ }
|
|
94
|
-
cleanup();
|
|
95
|
-
process.exit(130);
|
|
96
|
-
};
|
|
97
|
-
process.on('SIGINT', kill);
|
|
98
|
-
process.on('SIGTERM', kill);
|
|
99
|
-
return new Promise((resolve) => {
|
|
100
|
-
child.on('close', (code) => {
|
|
101
|
-
process.removeListener('SIGINT', kill);
|
|
102
|
-
process.removeListener('SIGTERM', kill);
|
|
103
|
-
resolve(code ?? 1);
|
|
104
|
-
});
|
|
105
|
-
child.on('error', () => resolve(1));
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
process.on('exit', cleanup);
|
|
109
|
-
// These handlers are overridden by composeAsync when running — fallback for non-async paths
|
|
110
|
-
process.on('SIGINT', () => { cleanup(); process.exit(130); });
|
|
111
|
-
process.on('SIGTERM', () => { cleanup(); process.exit(143); });
|
|
112
|
-
function parseArgs() {
|
|
113
|
-
const args = process.argv.slice(2);
|
|
114
|
-
const opts = {
|
|
115
|
-
shell: false, rebuild: false, login: false, help: false,
|
|
116
|
-
maxIterations: '', maxTurns: '', delay: '', cooldown: '', resumeSession: '',
|
|
117
|
-
};
|
|
118
|
-
const FLAG_TO_KEY = {
|
|
119
|
-
'--max-iterations': 'maxIterations',
|
|
120
|
-
'--max-turns': 'maxTurns',
|
|
121
|
-
'--delay': 'delay',
|
|
122
|
-
'--cooldown': 'cooldown',
|
|
123
|
-
'--resume-session': 'resumeSession',
|
|
124
|
-
};
|
|
125
|
-
for (let i = 0; i < args.length; i++) {
|
|
126
|
-
const arg = args[i];
|
|
127
|
-
if (arg === '--help' || arg === '-h') {
|
|
128
|
-
opts.help = true;
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
if (arg === '--shell') {
|
|
132
|
-
opts.shell = true;
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
if (arg === '--rebuild') {
|
|
136
|
-
opts.rebuild = true;
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
if (arg === '--login') {
|
|
140
|
-
opts.login = true;
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
const key = FLAG_TO_KEY[arg];
|
|
144
|
-
if (key) {
|
|
145
|
-
if (i + 1 >= args.length || args[i + 1].startsWith('--')) {
|
|
146
|
-
error(`${arg} requires a value`);
|
|
147
|
-
process.exit(1);
|
|
148
|
-
}
|
|
149
|
-
opts[key] = args[++i];
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
error(`Unknown option: ${arg}`);
|
|
153
|
-
process.exit(1);
|
|
154
|
-
}
|
|
155
|
-
return opts;
|
|
156
|
-
}
|
|
157
|
-
function printHelp() {
|
|
158
|
-
console.log(`
|
|
159
|
-
Docker runner for Claude Starter Kit
|
|
160
|
-
|
|
161
|
-
One command to start safe, isolated autonomous development.
|
|
162
|
-
--skip-permissions is added automatically (the container is the sandbox).
|
|
163
|
-
|
|
164
|
-
Usage:
|
|
165
|
-
npx create-claude-workspace docker [options]
|
|
166
|
-
|
|
167
|
-
Options:
|
|
168
|
-
--shell Interactive shell instead of autonomous loop
|
|
169
|
-
--max-iterations <n> Max iterations (default: 50)
|
|
170
|
-
--max-turns <n> Max turns per iteration (default: 50)
|
|
171
|
-
--delay <ms> Pause between tasks (default: 5000)
|
|
172
|
-
--cooldown <ms> Wait after rate limit (default: 60000)
|
|
173
|
-
--rebuild Force rebuild Docker image
|
|
174
|
-
--login Run 'claude auth login' on host before starting container
|
|
175
|
-
--resume-session <id> Resume a previous Claude session by ID
|
|
176
|
-
--help, -h Show this help
|
|
177
|
-
|
|
178
|
-
Authentication:
|
|
179
|
-
The container uses your HOST machine's Claude auth automatically.
|
|
180
|
-
- Max plan: run 'claude auth login' on your host first (or use --login)
|
|
181
|
-
- API key: set ANTHROPIC_API_KEY=sk-... before running
|
|
182
|
-
|
|
183
|
-
npm Publishing (optional):
|
|
184
|
-
Set NPM_TOKEN=npm_XXXXX before running.
|
|
185
|
-
Optionally: NPM_REGISTRY=npm.pkg.github.com (default: registry.npmjs.org)
|
|
186
|
-
|
|
187
|
-
Git Platform (optional — for push, MR/PR, issues):
|
|
188
|
-
GitHub: set GH_TOKEN=ghp_XXXXX before running
|
|
189
|
-
GitLab: set GITLAB_TOKEN=glpat-XXXXX before running
|
|
190
|
-
|
|
191
|
-
Package Manager Configs (automatic):
|
|
192
|
-
~/.npmrc and ~/.bunfig.toml are mounted into the container automatically.
|
|
193
|
-
Scoped registries (e.g., FontAwesome Pro) just work.
|
|
194
|
-
|
|
195
|
-
Examples:
|
|
196
|
-
npx create-claude-workspace docker
|
|
197
|
-
npx create-claude-workspace docker --max-iterations 20
|
|
198
|
-
npx create-claude-workspace docker --shell
|
|
199
|
-
npx create-claude-workspace docker --login
|
|
200
|
-
`);
|
|
201
|
-
}
|
|
202
|
-
// ─── Docker installation ───
|
|
203
|
-
function installDocker() {
|
|
204
|
-
if (IS_WIN) {
|
|
205
|
-
if (hasCmd('winget')) {
|
|
206
|
-
info('Installing Docker Desktop via winget...');
|
|
207
|
-
run('winget', ['install', '-e', '--id', 'Docker.DockerDesktop',
|
|
208
|
-
'--accept-package-agreements', '--accept-source-agreements']);
|
|
209
|
-
warn('Docker Desktop installed. Restart terminal, start Docker Desktop, re-run.');
|
|
210
|
-
process.exit(0);
|
|
211
|
-
}
|
|
212
|
-
if (hasCmd('choco')) {
|
|
213
|
-
info('Installing Docker Desktop via Chocolatey...');
|
|
214
|
-
run('choco', ['install', 'docker-desktop', '-y']);
|
|
215
|
-
warn('Docker Desktop installed. Restart terminal, start Docker Desktop, re-run.');
|
|
216
|
-
process.exit(0);
|
|
217
|
-
}
|
|
218
|
-
error('Docker not found. Install Docker Desktop:');
|
|
219
|
-
error(' https://docs.docker.com/desktop/install/windows-install/');
|
|
220
|
-
process.exit(1);
|
|
221
|
-
}
|
|
222
|
-
else if (IS_MAC) {
|
|
223
|
-
if (hasCmd('brew')) {
|
|
224
|
-
info('Installing Docker Desktop via Homebrew...');
|
|
225
|
-
run('brew', ['install', '--cask', 'docker']);
|
|
226
|
-
info('Start Docker Desktop from Applications, then re-run.');
|
|
227
|
-
process.exit(0);
|
|
228
|
-
}
|
|
229
|
-
error('Install Docker Desktop: https://docs.docker.com/desktop/install/mac-install/');
|
|
230
|
-
process.exit(1);
|
|
231
|
-
}
|
|
232
|
-
else {
|
|
233
|
-
info('Installing Docker via get.docker.com...');
|
|
234
|
-
const result = run('sh', ['-c', 'curl -fsSL https://get.docker.com | sh']);
|
|
235
|
-
if (result.status !== 0) {
|
|
236
|
-
error('Docker installation failed. Install manually: https://docs.docker.com/engine/install/');
|
|
237
|
-
process.exit(1);
|
|
238
|
-
}
|
|
239
|
-
run('sudo', ['usermod', '-aG', 'docker', process.env.USER || ''], { capture: true });
|
|
240
|
-
warn('You may need "newgrp docker" or re-login for group changes.');
|
|
241
|
-
info('Docker installed.');
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
function startDaemon() {
|
|
245
|
-
warn('Docker daemon not running. Starting...');
|
|
246
|
-
if (IS_WIN) {
|
|
247
|
-
const exe = join(process.env.ProgramFiles || 'C:\\Program Files', 'Docker', 'Docker', 'Docker Desktop.exe');
|
|
248
|
-
if (existsSync(exe)) {
|
|
249
|
-
const child = spawnSync(exe, [], { stdio: 'ignore', windowsHide: true });
|
|
250
|
-
if (child.error)
|
|
251
|
-
warn(`Could not start Docker Desktop: ${child.error.message}`);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
else if (IS_MAC) {
|
|
255
|
-
run('open', ['-a', 'Docker'], { capture: true });
|
|
256
|
-
}
|
|
257
|
-
else {
|
|
258
|
-
run('sudo', ['systemctl', 'start', 'docker'], { capture: true });
|
|
259
|
-
}
|
|
260
|
-
info('Waiting for Docker daemon (up to 120s)...');
|
|
261
|
-
for (let i = 0; i < 60; i++) {
|
|
262
|
-
sleepSync(2000);
|
|
263
|
-
if (ok('docker', ['info']))
|
|
264
|
-
return;
|
|
265
|
-
if (i % 5 === 4)
|
|
266
|
-
console.log(` ... waiting (${(i + 1) * 2}s)`);
|
|
267
|
-
}
|
|
268
|
-
error('Docker daemon not running. Start it manually and re-run.');
|
|
269
|
-
process.exit(1);
|
|
270
|
-
}
|
|
271
|
-
// ─── WSL2 memory check (Windows only) ───
|
|
272
|
-
function checkWslMemory() {
|
|
273
|
-
const wslConfig = join(homedir(), '.wslconfig');
|
|
274
|
-
const totalMemGB = Math.round(totalmem() / (1024 ** 3));
|
|
275
|
-
if (!existsSync(wslConfig)) {
|
|
276
|
-
// No .wslconfig = WSL2 defaults (50% of host RAM on newer versions, or 8 GB on older)
|
|
277
|
-
if (totalMemGB > 16) {
|
|
278
|
-
warn(`No ~/.wslconfig found. WSL2 defaults to ~50% of host RAM (~${Math.round(totalMemGB / 2)} GB).`);
|
|
279
|
-
warn(`With ${totalMemGB} GB total RAM, Docker containers may OOM during heavy builds.`);
|
|
280
|
-
warn(`Create %USERPROFILE%\\.wslconfig to allocate more:`);
|
|
281
|
-
console.log('');
|
|
282
|
-
console.log(` [wsl2]`);
|
|
283
|
-
console.log(` memory=${Math.max(totalMemGB - 4, Math.round(totalMemGB * 0.75))}GB`);
|
|
284
|
-
console.log(` swap=8GB`);
|
|
285
|
-
console.log('');
|
|
286
|
-
warn('Then restart WSL: wsl --shutdown');
|
|
287
|
-
console.log('');
|
|
288
|
-
}
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
// Parse .wslconfig for memory= line
|
|
292
|
-
try {
|
|
293
|
-
const content = readFileSync(wslConfig, 'utf8');
|
|
294
|
-
const memMatch = content.match(/^\s*memory\s*=\s*(\d+)\s*(GB|MB|G|M)?/mi);
|
|
295
|
-
if (memMatch) {
|
|
296
|
-
const value = parseInt(memMatch[1], 10);
|
|
297
|
-
const unit = (memMatch[2] || 'GB').toUpperCase();
|
|
298
|
-
const memGB = unit.startsWith('M') ? value / 1024 : value;
|
|
299
|
-
if (memGB < totalMemGB * 0.6) {
|
|
300
|
-
warn(`WSL2 memory limited to ${memGB} GB (host has ${totalMemGB} GB).`);
|
|
301
|
-
warn(`Increase memory in %USERPROFILE%\\.wslconfig for heavy builds:`);
|
|
302
|
-
console.log(` memory=${Math.max(totalMemGB - 4, Math.round(totalMemGB * 0.75))}GB`);
|
|
303
|
-
console.log('');
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
catch { /* ignore read errors */ }
|
|
308
|
-
}
|
|
309
|
-
// ─── Main ───
|
|
310
|
-
async function main() {
|
|
311
|
-
const opts = parseArgs();
|
|
312
|
-
if (opts.help) {
|
|
313
|
-
printHelp();
|
|
314
|
-
process.exit(0);
|
|
315
|
-
}
|
|
316
|
-
console.log(`\n${C.c}╔══════════════════════════════════════════╗${C.n}`);
|
|
317
|
-
console.log(`${C.c}║ Claude Starter Kit — Docker Runner ║${C.n}`);
|
|
318
|
-
console.log(`${C.c}╚══════════════════════════════════════════╝${C.n}\n`);
|
|
319
|
-
// 1. Docker
|
|
320
|
-
step('1/4 Checking Docker...');
|
|
321
|
-
if (!hasCmd('docker'))
|
|
322
|
-
installDocker();
|
|
323
|
-
if (!ok('docker', ['info']))
|
|
324
|
-
startDaemon();
|
|
325
|
-
info('Docker is ready.');
|
|
326
|
-
// WSL2 memory check — warn if default limit may cause OOM crashes
|
|
327
|
-
if (IS_WIN) {
|
|
328
|
-
checkWslMemory();
|
|
329
|
-
}
|
|
330
|
-
// 2. Build
|
|
331
|
-
step('2/4 Building container image...');
|
|
332
|
-
// Always use --pull to pick up base image updates; --no-cache only on explicit --rebuild
|
|
333
|
-
const buildArgs = opts.rebuild ? ['build', '--no-cache', '--pull'] : ['build', '--pull', '-q'];
|
|
334
|
-
const buildResult = compose(buildArgs);
|
|
335
|
-
if (buildResult.status !== 0) {
|
|
336
|
-
error('Docker image build failed. Fix the errors above and re-run.');
|
|
337
|
-
process.exit(1);
|
|
338
|
-
}
|
|
339
|
-
info('Image built.');
|
|
340
|
-
// 3. Auth
|
|
341
|
-
step('3/4 Setting up authentication...');
|
|
342
|
-
if (process.env.ANTHROPIC_API_KEY) {
|
|
343
|
-
info('Using ANTHROPIC_API_KEY from environment.');
|
|
344
|
-
}
|
|
345
|
-
else {
|
|
346
|
-
if (opts.login) {
|
|
347
|
-
if (hasCmd('claude')) {
|
|
348
|
-
info("Running 'claude auth login' on host...");
|
|
349
|
-
run('claude', ['auth', 'login']);
|
|
350
|
-
}
|
|
351
|
-
else {
|
|
352
|
-
error('Claude CLI not found on host. Install it first:');
|
|
353
|
-
console.log(' npm i -g @anthropic-ai/claude-code');
|
|
354
|
-
console.log(' claude auth login');
|
|
355
|
-
console.log('');
|
|
356
|
-
console.log('Or use an API key:');
|
|
357
|
-
console.log(' ANTHROPIC_API_KEY=sk-... npx create-claude-workspace docker');
|
|
358
|
-
process.exit(1);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
const home = homedir();
|
|
362
|
-
const credentialsJson = join(home, '.claude', '.credentials.json');
|
|
363
|
-
if (existsSync(credentialsJson)) {
|
|
364
|
-
info('Mounting host Claude auth credentials into container.');
|
|
365
|
-
// NOT :ro — Claude CLI needs to write refreshed OAuth tokens back to this file
|
|
366
|
-
const vols = [` - ${toDockerPath(credentialsJson)}:/home/claude/.claude/.credentials.json`];
|
|
367
|
-
// Mount package manager configs from host home (scoped registries, auth tokens)
|
|
368
|
-
const hostNpmrc = join(home, '.npmrc');
|
|
369
|
-
const hostBunfig = join(home, '.bunfig.toml');
|
|
370
|
-
if (existsSync(hostNpmrc)) {
|
|
371
|
-
info('Mounting host ~/.npmrc into container.');
|
|
372
|
-
vols.push(` - ${toDockerPath(hostNpmrc)}:/home/claude/.npmrc:ro`);
|
|
373
|
-
}
|
|
374
|
-
if (existsSync(hostBunfig)) {
|
|
375
|
-
info('Mounting host ~/.bunfig.toml into container.');
|
|
376
|
-
vols.push(` - ${toDockerPath(hostBunfig)}:/home/claude/.bunfig.toml:ro`);
|
|
377
|
-
}
|
|
378
|
-
writeFileSync(AUTH_COMPOSE, `services:\n claude:\n volumes:\n${vols.join('\n')}\n`);
|
|
379
|
-
}
|
|
380
|
-
else {
|
|
381
|
-
error('No Claude authentication found. Options:');
|
|
382
|
-
console.log('');
|
|
383
|
-
console.log(' Option 1 — Claude Max plan (OAuth):');
|
|
384
|
-
console.log(' npm i -g @anthropic-ai/claude-code');
|
|
385
|
-
console.log(' claude auth login');
|
|
386
|
-
console.log(' Then re-run this script.');
|
|
387
|
-
console.log('');
|
|
388
|
-
console.log(' Option 2 — API key:');
|
|
389
|
-
console.log(' ANTHROPIC_API_KEY=sk-... npx create-claude-workspace docker');
|
|
390
|
-
console.log('');
|
|
391
|
-
console.log(' Option 3 — Use --login flag:');
|
|
392
|
-
console.log(' npx create-claude-workspace docker --login');
|
|
393
|
-
process.exit(1);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
// 4. Interactive setup if project not initialized
|
|
397
|
-
const needsSetup = !existsSync(join(PROJECT_DIR, 'MEMORY.md'));
|
|
398
|
-
if (needsSetup && !opts.shell) {
|
|
399
|
-
step('4/5 Project not initialized — starting interactive setup...');
|
|
400
|
-
console.log('');
|
|
401
|
-
info('Answer the discovery questions to set up your project.');
|
|
402
|
-
info('This creates CLAUDE.md, PRODUCT.md, TODO.md, and MEMORY.md.');
|
|
403
|
-
info('After setup, the autonomous development loop starts automatically.');
|
|
404
|
-
console.log('');
|
|
405
|
-
const setupResult = compose(['run', '--rm', 'claude', '-c',
|
|
406
|
-
'claude --agent project-initializer --dangerously-skip-permissions "Initialize this project. First check if PLAN.md exists in the project root — if it does, use it as the primary source of information and only ask about details that are missing or unclear. If PLAN.md does not exist, start the full discovery conversation."']);
|
|
407
|
-
if (setupResult.status !== 0) {
|
|
408
|
-
warn('Setup exited with errors. Use --shell for manual setup.');
|
|
409
|
-
process.exit(1);
|
|
410
|
-
}
|
|
411
|
-
if (!existsSync(join(PROJECT_DIR, 'MEMORY.md'))) {
|
|
412
|
-
warn('Setup did not complete (MEMORY.md not found).');
|
|
413
|
-
warn('Run again or use --shell for manual setup.');
|
|
414
|
-
process.exit(1);
|
|
415
|
-
}
|
|
416
|
-
info('Setup complete. Starting autonomous development...');
|
|
417
|
-
console.log('');
|
|
418
|
-
}
|
|
419
|
-
// 5. Run
|
|
420
|
-
step(needsSetup ? '5/5 Starting Claude Code...' : '4/4 Starting Claude Code...');
|
|
421
|
-
if (opts.shell) {
|
|
422
|
-
console.log('');
|
|
423
|
-
info('Interactive shell. Examples:');
|
|
424
|
-
info(' claude # interactive Claude');
|
|
425
|
-
info(' npx create-claude-workspace run # autonomous loop');
|
|
426
|
-
console.log('');
|
|
427
|
-
// Shell mode uses spawnSync (interactive TTY needed)
|
|
428
|
-
compose(['run', '--rm', 'claude']);
|
|
429
|
-
}
|
|
430
|
-
else {
|
|
431
|
-
const autoArgs = ['--skip-permissions'];
|
|
432
|
-
if (opts.maxIterations)
|
|
433
|
-
autoArgs.push('--max-iterations', opts.maxIterations);
|
|
434
|
-
if (opts.maxTurns)
|
|
435
|
-
autoArgs.push('--max-turns', opts.maxTurns);
|
|
436
|
-
if (opts.delay)
|
|
437
|
-
autoArgs.push('--delay', opts.delay);
|
|
438
|
-
if (opts.cooldown)
|
|
439
|
-
autoArgs.push('--cooldown', opts.cooldown);
|
|
440
|
-
if (opts.resumeSession)
|
|
441
|
-
autoArgs.push('--resume-session', opts.resumeSession);
|
|
442
|
-
// Shell-escape each arg to prevent injection from user-provided values
|
|
443
|
-
const escaped = autoArgs.map(a => /^[\w.=/-]+$/.test(a) ? a : `'${a.replace(/'/g, "'\\''")}'`);
|
|
444
|
-
console.log('');
|
|
445
|
-
info('Autonomous mode — isolated in Docker, --skip-permissions is safe.');
|
|
446
|
-
info('Press Ctrl+C to stop.');
|
|
447
|
-
console.log('');
|
|
448
|
-
// No -T flag — Docker allocates a TTY, which natively forwards Ctrl+C to the container.
|
|
449
|
-
// async spawn keeps the event loop alive so our SIGINT handler (docker compose kill) fires as backup.
|
|
450
|
-
const code = await composeAsync(['run', '--rm', 'claude', '-c', `exec npx -y create-claude-workspace run ${escaped.join(' ')}`]);
|
|
451
|
-
process.exit(code);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
export { main as runDockerLoop, parseArgs, printHelp };
|
|
455
|
-
// Run only when executed directly, not when imported
|
|
456
|
-
const isDirectRun = process.argv[1]?.replace(/\\/g, '/').endsWith('docker-run.mjs');
|
|
457
|
-
if (isDirectRun) {
|
|
458
|
-
main().catch(err => {
|
|
459
|
-
console.error('Fatal:', err);
|
|
460
|
-
process.exit(1);
|
|
461
|
-
});
|
|
462
|
-
}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
-
import { mkdirSync, rmSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
|
|
3
|
-
import { resolve, dirname } from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
6
|
-
const TEST_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '__integration_tmp__');
|
|
7
|
-
beforeAll(() => {
|
|
8
|
-
mkdirSync(resolve(TEST_DIR, '.claude'), { recursive: true });
|
|
9
|
-
writeFileSync(resolve(TEST_DIR, 'hello.txt'), 'Hello from integration test!');
|
|
10
|
-
writeFileSync(resolve(TEST_DIR, 'package.json'), JSON.stringify({ name: 'test-project', version: '1.0.0' }));
|
|
11
|
-
});
|
|
12
|
-
afterAll(async () => {
|
|
13
|
-
// Wait briefly for Claude Code child processes to release file handles
|
|
14
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
15
|
-
try {
|
|
16
|
-
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
17
|
-
}
|
|
18
|
-
catch { /* Windows EPERM on locked files */ }
|
|
19
|
-
});
|
|
20
|
-
describe('Agent SDK integration', () => {
|
|
21
|
-
it('can read a file and respond', async () => {
|
|
22
|
-
let gotText = false;
|
|
23
|
-
let responseText = '';
|
|
24
|
-
for await (const message of query({
|
|
25
|
-
prompt: 'Read hello.txt and tell me what it says. Reply with just the file contents, nothing else.',
|
|
26
|
-
options: {
|
|
27
|
-
maxTurns: 5,
|
|
28
|
-
cwd: TEST_DIR,
|
|
29
|
-
allowedTools: ['Read'],
|
|
30
|
-
permissionMode: 'bypassPermissions',
|
|
31
|
-
allowDangerouslySkipPermissions: true,
|
|
32
|
-
},
|
|
33
|
-
})) {
|
|
34
|
-
if (message.type === 'assistant') {
|
|
35
|
-
const content = message.message?.content;
|
|
36
|
-
if (Array.isArray(content)) {
|
|
37
|
-
for (const block of content) {
|
|
38
|
-
if (block.type === 'text') {
|
|
39
|
-
responseText += block.text;
|
|
40
|
-
gotText = true;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
expect(gotText).toBe(true);
|
|
47
|
-
expect(responseText).toContain('Hello from integration test');
|
|
48
|
-
}, 60_000);
|
|
49
|
-
it('can write a file', async () => {
|
|
50
|
-
const targetFile = resolve(TEST_DIR, 'created-by-agent.txt');
|
|
51
|
-
const absPath = targetFile.replace(/\\/g, '/');
|
|
52
|
-
for await (const _message of query({
|
|
53
|
-
prompt: `Write the text "Agent was here" to the file at this exact absolute path: ${absPath}`,
|
|
54
|
-
options: {
|
|
55
|
-
maxTurns: 5,
|
|
56
|
-
cwd: TEST_DIR,
|
|
57
|
-
allowedTools: ['Write'],
|
|
58
|
-
permissionMode: 'bypassPermissions',
|
|
59
|
-
allowDangerouslySkipPermissions: true,
|
|
60
|
-
},
|
|
61
|
-
})) {
|
|
62
|
-
// consume
|
|
63
|
-
}
|
|
64
|
-
expect(existsSync(targetFile)).toBe(true);
|
|
65
|
-
expect(readFileSync(targetFile, 'utf-8')).toContain('Agent was here');
|
|
66
|
-
}, 60_000);
|
|
67
|
-
it('can run a shell command', async () => {
|
|
68
|
-
let responseText = '';
|
|
69
|
-
for await (const message of query({
|
|
70
|
-
prompt: 'Run `cat package.json` and tell me the project name. Reply with just the name.',
|
|
71
|
-
options: {
|
|
72
|
-
maxTurns: 5,
|
|
73
|
-
cwd: TEST_DIR,
|
|
74
|
-
allowedTools: ['Bash'],
|
|
75
|
-
permissionMode: 'bypassPermissions',
|
|
76
|
-
allowDangerouslySkipPermissions: true,
|
|
77
|
-
},
|
|
78
|
-
})) {
|
|
79
|
-
if (message.type === 'assistant') {
|
|
80
|
-
const content = message.message?.content;
|
|
81
|
-
if (Array.isArray(content)) {
|
|
82
|
-
for (const block of content) {
|
|
83
|
-
if (block.type === 'text')
|
|
84
|
-
responseText += block.text;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
expect(responseText).toContain('test-project');
|
|
90
|
-
}, 60_000);
|
|
91
|
-
it('respects maxTurns limit', async () => {
|
|
92
|
-
let messageCount = 0;
|
|
93
|
-
for await (const message of query({
|
|
94
|
-
prompt: 'List all files in the current directory, then read each one.',
|
|
95
|
-
options: {
|
|
96
|
-
maxTurns: 2,
|
|
97
|
-
cwd: TEST_DIR,
|
|
98
|
-
allowedTools: ['Read', 'Bash', 'Glob'],
|
|
99
|
-
permissionMode: 'bypassPermissions',
|
|
100
|
-
allowDangerouslySkipPermissions: true,
|
|
101
|
-
},
|
|
102
|
-
})) {
|
|
103
|
-
messageCount++;
|
|
104
|
-
}
|
|
105
|
-
// Should have completed (with or without finishing all files)
|
|
106
|
-
expect(messageCount).toBeGreaterThan(0);
|
|
107
|
-
}, 60_000);
|
|
108
|
-
});
|