create-claude-workspace 1.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.
@@ -0,0 +1,363 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Docker-based isolated runner for Claude Starter Kit.
5
+ * Single cross-platform script replacing docker-run.sh + docker-run.ps1.
6
+ *
7
+ * Usage:
8
+ * node .claude/scripts/docker-run.mjs # autonomous (default)
9
+ * node .claude/scripts/docker-run.mjs --shell # interactive shell
10
+ * node .claude/scripts/docker-run.mjs --help # all options
11
+ */
12
+
13
+ import { spawnSync } from 'node:child_process';
14
+ import { existsSync, writeFileSync, unlinkSync } from 'node:fs';
15
+ import { join, resolve, dirname } from 'node:path';
16
+ import { homedir, platform as osPlatform, tmpdir } from 'node:os';
17
+ import { fileURLToPath } from 'node:url';
18
+
19
+ const __dirname = dirname(fileURLToPath(import.meta.url));
20
+ const PROJECT_DIR = resolve(__dirname, '../..');
21
+ const AUTH_COMPOSE = join(tmpdir(), 'claude-starter-kit-auth-compose.yml');
22
+ const IS_WIN = osPlatform() === 'win32';
23
+ const IS_MAC = osPlatform() === 'darwin';
24
+
25
+ // ─── Colors ───
26
+
27
+ const C = { r: '\x1b[31m', g: '\x1b[32m', y: '\x1b[33m', c: '\x1b[36m', n: '\x1b[0m' };
28
+ const info = (m) => console.log(`${C.g}[INFO]${C.n} ${m}`);
29
+ const warn = (m) => console.log(`${C.y}[WARN]${C.n} ${m}`);
30
+ const error = (m) => console.log(`${C.r}[ERROR]${C.n} ${m}`);
31
+ const step = (m) => console.log(`${C.c}[STEP]${C.n} ${m}`);
32
+
33
+ // ─── Helpers ───
34
+
35
+ function sleepSync(ms) {
36
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
37
+ }
38
+
39
+ function run(cmd, args = [], opts = {}) {
40
+ return spawnSync(cmd, args, {
41
+ shell: true,
42
+ cwd: opts.cwd ?? PROJECT_DIR,
43
+ stdio: opts.capture ? 'pipe' : 'inherit',
44
+ timeout: opts.timeout,
45
+ env: { ...process.env, ...opts.env },
46
+ });
47
+ }
48
+
49
+ function ok(cmd, args = []) {
50
+ return run(cmd, args, { capture: true }).status === 0;
51
+ }
52
+
53
+ function hasCmd(cmd) {
54
+ const check = IS_WIN ? 'where' : 'which';
55
+ return spawnSync(check, [cmd], { shell: true, stdio: 'pipe' }).status === 0;
56
+ }
57
+
58
+ /** Convert Windows backslash path to forward slashes for Docker */
59
+ function toDockerPath(p) {
60
+ return p.replace(/\\/g, '/');
61
+ }
62
+
63
+ /**
64
+ * Run docker compose with the auth override file if it exists.
65
+ * Does NOT use shell: true — on Windows, cmd.exe splits arguments after
66
+ * '-c' into separate words, breaking 'bash -c "node ..."' commands.
67
+ * Without shell, Node.js passes each array element as a distinct argv entry.
68
+ */
69
+ function compose(args, opts = {}) {
70
+ const files = ['-f', 'docker-compose.yml'];
71
+ if (existsSync(AUTH_COMPOSE)) files.push('-f', AUTH_COMPOSE);
72
+ return spawnSync('docker', ['compose', ...files, ...args], {
73
+ cwd: PROJECT_DIR,
74
+ stdio: opts.capture ? 'pipe' : 'inherit',
75
+ env: { ...process.env },
76
+ });
77
+ }
78
+
79
+ function cleanup() {
80
+ try { if (existsSync(AUTH_COMPOSE)) unlinkSync(AUTH_COMPOSE); } catch {}
81
+ }
82
+
83
+ process.on('exit', cleanup);
84
+ process.on('SIGINT', () => { cleanup(); process.exit(130); });
85
+ process.on('SIGTERM', () => { cleanup(); process.exit(143); });
86
+
87
+ // ─── Parse args ───
88
+
89
+ function parseArgs() {
90
+ const args = process.argv.slice(2);
91
+ const opts = {
92
+ shell: false, rebuild: false, login: false, help: false, verbose: false,
93
+ maxIterations: '', maxTurns: '', delay: '', cooldown: '',
94
+ };
95
+
96
+ for (let i = 0; i < args.length; i++) {
97
+ switch (args[i]) {
98
+ case '--help': case '-h': opts.help = true; break;
99
+ case '--shell': opts.shell = true; break;
100
+ case '--rebuild': opts.rebuild = true; break;
101
+ case '--login': opts.login = true; break;
102
+ case '--verbose': opts.verbose = true; break;
103
+ case '--max-iterations': case '--max-turns': case '--delay': case '--cooldown': {
104
+ const flag = args[i];
105
+ if (i + 1 >= args.length || args[i + 1].startsWith('--')) {
106
+ error(`${flag} requires a value`); process.exit(1);
107
+ }
108
+ const key = flag.slice(2).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
109
+ opts[key] = args[++i];
110
+ break;
111
+ }
112
+ default:
113
+ error(`Unknown option: ${args[i]}`); process.exit(1);
114
+ }
115
+ }
116
+ return opts;
117
+ }
118
+
119
+ function printHelp() {
120
+ console.log(`
121
+ Docker runner for Claude Starter Kit
122
+
123
+ One command to start safe, isolated autonomous development.
124
+ --skip-permissions is added automatically (the container is the sandbox).
125
+
126
+ Usage:
127
+ node .claude/scripts/docker-run.mjs [options]
128
+
129
+ Options:
130
+ --shell Interactive shell instead of autonomous loop
131
+ --max-iterations <n> Max iterations (default: 50)
132
+ --max-turns <n> Max turns per iteration (default: 50)
133
+ --delay <ms> Pause between tasks (default: 5000)
134
+ --cooldown <ms> Wait after rate limit (default: 60000)
135
+ --rebuild Force rebuild Docker image
136
+ --login Run 'claude auth login' on host before starting container
137
+ --verbose Show live Claude activity (tool calls, file edits, text)
138
+ --help, -h Show this help
139
+
140
+ Authentication:
141
+ The container uses your HOST machine's Claude auth automatically.
142
+ - Max plan: run 'claude auth login' on your host first (or use --login)
143
+ - API key: set ANTHROPIC_API_KEY=sk-... before running
144
+
145
+ npm Publishing (optional):
146
+ Set NPM_TOKEN=npm_XXXXX before running.
147
+ Optionally: NPM_REGISTRY=npm.pkg.github.com (default: registry.npmjs.org)
148
+
149
+ Examples:
150
+ node .claude/scripts/docker-run.mjs
151
+ node .claude/scripts/docker-run.mjs --max-iterations 20
152
+ node .claude/scripts/docker-run.mjs --shell
153
+ node .claude/scripts/docker-run.mjs --login
154
+ `);
155
+ }
156
+
157
+ // ─── Docker installation ───
158
+
159
+ function installDocker() {
160
+ if (IS_WIN) {
161
+ if (hasCmd('winget')) {
162
+ info('Installing Docker Desktop via winget...');
163
+ run('winget', ['install', '-e', '--id', 'Docker.DockerDesktop',
164
+ '--accept-package-agreements', '--accept-source-agreements']);
165
+ warn('Docker Desktop installed. Restart terminal, start Docker Desktop, re-run.');
166
+ process.exit(0);
167
+ }
168
+ if (hasCmd('choco')) {
169
+ info('Installing Docker Desktop via Chocolatey...');
170
+ run('choco', ['install', 'docker-desktop', '-y']);
171
+ warn('Docker Desktop installed. Restart terminal, start Docker Desktop, re-run.');
172
+ process.exit(0);
173
+ }
174
+ error('Docker not found. Install Docker Desktop:');
175
+ error(' https://docs.docker.com/desktop/install/windows-install/');
176
+ process.exit(1);
177
+
178
+ } else if (IS_MAC) {
179
+ if (hasCmd('brew')) {
180
+ info('Installing Docker Desktop via Homebrew...');
181
+ run('brew', ['install', '--cask', 'docker']);
182
+ info('Start Docker Desktop from Applications, then re-run.');
183
+ process.exit(0);
184
+ }
185
+ error('Install Docker Desktop: https://docs.docker.com/desktop/install/mac-install/');
186
+ process.exit(1);
187
+
188
+ } else {
189
+ // Linux — official convenience script
190
+ info('Installing Docker via get.docker.com...');
191
+ const result = run('sh', ['-c', 'curl -fsSL https://get.docker.com | sh']);
192
+ if (result.status !== 0) {
193
+ error('Docker installation failed. Install manually: https://docs.docker.com/engine/install/');
194
+ process.exit(1);
195
+ }
196
+ run('sudo', ['usermod', '-aG', 'docker', process.env.USER || ''], { capture: true });
197
+ warn('You may need "newgrp docker" or re-login for group changes.');
198
+ info('Docker installed.');
199
+ }
200
+ }
201
+
202
+ function startDaemon() {
203
+ warn('Docker daemon not running. Starting...');
204
+
205
+ if (IS_WIN) {
206
+ const exe = join(process.env.ProgramFiles || 'C:\\Program Files',
207
+ 'Docker', 'Docker', 'Docker Desktop.exe');
208
+ if (existsSync(exe)) {
209
+ spawnSync('cmd', ['/c', 'start', '', `"${exe}"`], { stdio: 'ignore' });
210
+ }
211
+ } else if (IS_MAC) {
212
+ run('open', ['-a', 'Docker'], { capture: true });
213
+ } else {
214
+ run('sudo', ['systemctl', 'start', 'docker'], { capture: true });
215
+ }
216
+
217
+ info('Waiting for Docker daemon (up to 120s)...');
218
+ for (let i = 0; i < 60; i++) {
219
+ sleepSync(2000);
220
+ if (ok('docker', ['info'])) return;
221
+ if (i % 5 === 4) console.log(` ... waiting (${(i + 1) * 2}s)`);
222
+ }
223
+ error('Docker daemon not running. Start it manually and re-run.');
224
+ process.exit(1);
225
+ }
226
+
227
+ // ─── Main ───
228
+
229
+ const opts = parseArgs();
230
+ if (opts.help) { printHelp(); process.exit(0); }
231
+
232
+ console.log(`\n${C.c}╔══════════════════════════════════════════╗${C.n}`);
233
+ console.log(`${C.c}║ Claude Starter Kit — Docker Runner ║${C.n}`);
234
+ console.log(`${C.c}╚══════════════════════════════════════════╝${C.n}\n`);
235
+
236
+ // 1. Docker
237
+ step('1/4 Checking Docker...');
238
+ if (!hasCmd('docker')) installDocker();
239
+ if (!ok('docker', ['info'])) startDaemon();
240
+ info('Docker is ready.');
241
+
242
+ // 2. Build
243
+ step('2/4 Building container image...');
244
+ compose(opts.rebuild ? ['build', '--no-cache'] : ['build', '-q']);
245
+ info('Image built.');
246
+
247
+ // 3. Auth
248
+ step('3/4 Setting up authentication...');
249
+
250
+ if (process.env.ANTHROPIC_API_KEY) {
251
+ info('Using ANTHROPIC_API_KEY from environment.');
252
+ } else {
253
+ // --login: authenticate on the HOST where browser + TTY work
254
+ if (opts.login) {
255
+ if (hasCmd('claude')) {
256
+ info("Running 'claude auth login' on host...");
257
+ run('claude', ['auth', 'login']);
258
+ } else {
259
+ error('Claude CLI not found on host. Install it first:');
260
+ console.log(' npm i -g @anthropic-ai/claude-code');
261
+ console.log(' claude auth login');
262
+ console.log('');
263
+ console.log('Or use an API key:');
264
+ console.log(' ANTHROPIC_API_KEY=sk-... node .claude/scripts/docker-run.mjs');
265
+ process.exit(1);
266
+ }
267
+ }
268
+
269
+ // Mount ONLY auth credentials from the host, NOT the entire ~/.claude/.
270
+ // The ~/.claude/ directory contains plugins, settings, and marketplace
271
+ // configs with Windows paths (C:\Users\...). Mounting it into a Linux
272
+ // container causes Claude Code to misinterpret those paths and create
273
+ // junk directories in the project root. The named volume 'claude-home'
274
+ // from docker-compose.yml handles ~/.claude/ inside the container.
275
+ //
276
+ // Auth is stored in ~/.claude/.credentials.json (OAuth tokens).
277
+ // Do NOT mount ~/.claude.json — it contains host-specific settings
278
+ // (installMethod, tips, stats) that cause warnings in the container.
279
+ // Do NOT mount ~/.claude/ — it has plugin configs with Windows paths
280
+ // that create junk directories in the project root.
281
+ //
282
+ // We use a compose override YAML file instead of -v flags because
283
+ // 'docker compose run -v' on Windows misparses drive-letter paths.
284
+ const home = homedir();
285
+ const credentialsJson = join(home, '.claude', '.credentials.json');
286
+
287
+ if (existsSync(credentialsJson)) {
288
+ info('Mounting host Claude auth credentials into container.');
289
+ const vol = ` - ${toDockerPath(credentialsJson)}:/home/claude/.claude/.credentials.json:ro`;
290
+ writeFileSync(AUTH_COMPOSE, `services:\n claude:\n volumes:\n${vol}\n`);
291
+ } else {
292
+ error('No Claude authentication found. Options:');
293
+ console.log('');
294
+ console.log(' Option 1 — Claude Max plan (OAuth):');
295
+ console.log(' npm i -g @anthropic-ai/claude-code');
296
+ console.log(' claude auth login');
297
+ console.log(' Then re-run this script.');
298
+ console.log('');
299
+ console.log(' Option 2 — API key:');
300
+ console.log(' ANTHROPIC_API_KEY=sk-... node .claude/scripts/docker-run.mjs');
301
+ console.log('');
302
+ console.log(' Option 3 — Use --login flag:');
303
+ console.log(' node .claude/scripts/docker-run.mjs --login');
304
+ process.exit(1);
305
+ }
306
+ }
307
+
308
+ // 4. Interactive setup if project not initialized
309
+ const needsSetup = !existsSync(join(PROJECT_DIR, 'MEMORY.md'));
310
+
311
+ if (needsSetup && !opts.shell) {
312
+ step('4/5 Project not initialized — starting interactive setup...');
313
+ console.log('');
314
+ info('Answer the discovery questions to set up your project.');
315
+ info('This creates CLAUDE.md, PRODUCT.md, TODO.md, and MEMORY.md.');
316
+ info('After setup, the autonomous development loop starts automatically.');
317
+ console.log('');
318
+
319
+ // Interactive mode — no -T flag, TTY allocated for user input.
320
+ // The prompt argument starts the discovery conversation immediately
321
+ // while keeping the session interactive for follow-up questions.
322
+ const setupResult = compose(['run', '--rm', 'claude', '-c',
323
+ '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."']);
324
+
325
+ if (setupResult.status !== 0) {
326
+ warn('Setup exited with errors. Use --shell for manual setup.');
327
+ process.exit(1);
328
+ }
329
+
330
+ if (!existsSync(join(PROJECT_DIR, 'MEMORY.md'))) {
331
+ warn('Setup did not complete (MEMORY.md not found).');
332
+ warn('Run again or use --shell for manual setup.');
333
+ process.exit(1);
334
+ }
335
+
336
+ info('Setup complete. Starting autonomous development...');
337
+ console.log('');
338
+ }
339
+
340
+ // 5. Run
341
+ step(needsSetup ? '5/5 Starting Claude Code...' : '4/4 Starting Claude Code...');
342
+
343
+ if (opts.shell) {
344
+ console.log('');
345
+ info('Interactive shell. Examples:');
346
+ info(' claude # interactive Claude');
347
+ info(' node .claude/scripts/autonomous.mjs --skip-permissions # autonomous loop');
348
+ console.log('');
349
+ compose(['run', '--rm', 'claude']);
350
+ } else {
351
+ let autoArgs = '--skip-permissions';
352
+ if (opts.maxIterations) autoArgs += ` --max-iterations ${opts.maxIterations}`;
353
+ if (opts.maxTurns) autoArgs += ` --max-turns ${opts.maxTurns}`;
354
+ if (opts.delay) autoArgs += ` --delay ${opts.delay}`;
355
+ if (opts.cooldown) autoArgs += ` --cooldown ${opts.cooldown}`;
356
+ if (opts.verbose) autoArgs += ' --verbose';
357
+
358
+ console.log('');
359
+ info('Autonomous mode — isolated in Docker, --skip-permissions is safe.');
360
+ info('Press Ctrl+C to stop after current iteration.');
361
+ console.log('');
362
+ compose(['run', '--rm', '-T', 'claude', '-c', `node .claude/scripts/autonomous.mjs ${autoArgs}`]);
363
+ }