datagrok-tools 5.1.9 → 6.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,613 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.claude = claude;
8
+ var _child_process = require("child_process");
9
+ var _fs = _interopRequireDefault(require("fs"));
10
+ var _http = _interopRequireDefault(require("http"));
11
+ var _net = _interopRequireDefault(require("net"));
12
+ var _os = _interopRequireDefault(require("os"));
13
+ var _path = _interopRequireDefault(require("path"));
14
+ var color = _interopRequireWildcard(require("../utils/color-utils"));
15
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
16
+ /** Convert a Windows path to Docker-compatible forward-slash format. */
17
+ function toDockerPath(p) {
18
+ return p.replace(/\\/g, '/');
19
+ }
20
+
21
+ /** Ensure a directory exists on the host before bind-mounting it. */
22
+ function ensureDir(dir) {
23
+ if (!_fs.default.existsSync(dir)) _fs.default.mkdirSync(dir, {
24
+ recursive: true
25
+ });
26
+ }
27
+
28
+ /**
29
+ * Locate the host Claude config directory by searching multiple candidate paths.
30
+ * Mirrors the search logic from deploy/fat_dev/dg-claude.
31
+ * Returns the path to the .claude directory, or undefined if not found.
32
+ */
33
+ function findClaudeHome() {
34
+ const candidates = [];
35
+
36
+ // 1. Explicit CLAUDE_HOME env var
37
+ if (process.env.CLAUDE_HOME) candidates.push(process.env.CLAUDE_HOME);
38
+
39
+ // 2. OS home directory
40
+ const home = _os.default.homedir();
41
+ candidates.push(_path.default.join(home, '.claude'));
42
+
43
+ // 3. USERPROFILE (Windows — may differ from homedir in some setups)
44
+ if (process.env.USERPROFILE) candidates.push(_path.default.join(process.env.USERPROFILE, '.claude'));
45
+
46
+ // 4. APPDATA fallback (Windows: %APPDATA%/../.claude)
47
+ if (process.env.APPDATA) candidates.push(_path.default.join(process.env.APPDATA, '..', '.claude'));
48
+ for (const candidate of candidates) {
49
+ if (!candidate) continue;
50
+ const resolved = _path.default.resolve(candidate);
51
+ if (_fs.default.existsSync(_path.default.join(resolved, '.credentials.json'))) return resolved;
52
+ }
53
+
54
+ // Fall back to the first existing .claude directory even without .credentials.json
55
+ for (const candidate of candidates) {
56
+ if (!candidate) continue;
57
+ const resolved = _path.default.resolve(candidate);
58
+ if (_fs.default.existsSync(resolved) && _fs.default.statSync(resolved).isDirectory()) return resolved;
59
+ }
60
+ return undefined;
61
+ }
62
+ const COMPOSE_TEMPLATE = `# Generated by grok claude — do not edit
63
+ services:
64
+ postgres:
65
+ image: pgvector/pgvector:pg17
66
+ command: postgres -c 'max_connections=1000'
67
+ environment:
68
+ POSTGRES_USER: postgres
69
+ POSTGRES_PASSWORD: postgres
70
+ volumes:
71
+ - pgdata:/var/lib/postgresql/data
72
+ shm_size: 2gb
73
+ healthcheck:
74
+ test: ["CMD-SHELL", "pg_isready -U postgres"]
75
+ interval: 5s
76
+ retries: 10
77
+ networks:
78
+ dg:
79
+ aliases: [database, postgres]
80
+ restart: unless-stopped
81
+
82
+ rabbitmq:
83
+ image: rabbitmq:4.0.5-management
84
+ platform: linux/amd64
85
+ environment:
86
+ RABBITMQ_DEFAULT_USER: guest
87
+ RABBITMQ_DEFAULT_PASS: guest
88
+ networks:
89
+ dg:
90
+ aliases: [rabbitmq]
91
+ restart: unless-stopped
92
+
93
+ grok_pipe:
94
+ image: datagrok/grok_pipe:latest
95
+ platform: linux/amd64
96
+ environment:
97
+ API_KEY: test-key
98
+ networks:
99
+ dg:
100
+ aliases: [grok_pipe]
101
+ restart: unless-stopped
102
+
103
+ datagrok:
104
+ image: datagrok/datagrok:\${DATAGROK_VERSION:-latest}
105
+ depends_on:
106
+ postgres:
107
+ condition: service_healthy
108
+ rabbitmq:
109
+ condition: service_started
110
+ environment:
111
+ GROK_PARAMETERS: |
112
+ {
113
+ "dbServer": "database",
114
+ "db": "datagrok",
115
+ "dbLogin": "dg",
116
+ "dbPassword": "dg",
117
+ "dbAdminLogin": "postgres",
118
+ "dbAdminPassword": "postgres",
119
+ "adminPassword": "admin",
120
+ "adminDevKey": "admin",
121
+ "webRoot": "http://datagrok:8080",
122
+ "apiRoot": "http://datagrok:8080/api",
123
+ "deployDemo": true,
124
+ "deployTestDemo": false
125
+ }
126
+ ports:
127
+ - "\${DG_PORT:-0}:8080"
128
+ volumes:
129
+ - datagrok_data:/home/grok/data
130
+ - datagrok_cfg:/home/grok/cfg
131
+ networks:
132
+ dg:
133
+ aliases: [datagrok]
134
+ restart: on-failure
135
+
136
+ # ── Optional services (use --profile to enable) ──────────────
137
+
138
+ grok_connect:
139
+ image: datagrok/grok_connect:\${GROK_CONNECT_VERSION:-latest}
140
+ platform: linux/amd64
141
+ environment:
142
+ GROK_CONNECT_PORT: 1234
143
+ networks:
144
+ dg:
145
+ aliases: [grok_connect]
146
+ restart: unless-stopped
147
+
148
+ grok_spawner:
149
+ image: datagrok/grok_spawner:\${GROK_SPAWNER_VERSION:-latest}
150
+ platform: linux/amd64
151
+ user: root
152
+ environment:
153
+ X_API_KEY: test-x-api-key
154
+ GROK_SPAWNER_ENVIRONMENT: \${COMPOSE_PROJECT_NAME:-dg-pkg}
155
+ GROK_SPAWNER_CORE_MODE: "false"
156
+ GROK_SPAWNER_FORCE_DOCKER_TYPE_HOST: "true"
157
+ volumes:
158
+ - /var/run/docker.sock:/var/run/docker.sock
159
+ networks:
160
+ dg:
161
+ aliases: [grok_spawner]
162
+ restart: unless-stopped
163
+ profiles: ["full"]
164
+
165
+ jupyter_kernel_gateway:
166
+ image: datagrok/jupyter_kernel_gateway:\${JKG_VERSION:-latest}
167
+ platform: linux/amd64
168
+ depends_on:
169
+ rabbitmq:
170
+ condition: service_started
171
+ environment:
172
+ GROK_PARAMETERS: |
173
+ {
174
+ "queueSettings": {
175
+ "useQueue": true,
176
+ "amqpHost": "rabbitmq",
177
+ "amqpUser": "guest",
178
+ "amqpPassword": "guest",
179
+ "amqpPort": 5672,
180
+ "pipeHost": "grok_pipe",
181
+ "pipePort": 3000,
182
+ "pipeKey": "test-key",
183
+ "maxConcurrentCalls": 16
184
+ }
185
+ }
186
+ networks:
187
+ dg:
188
+ aliases: [jupyter_kernel_gateway]
189
+ restart: unless-stopped
190
+ profiles: ["scripting", "full"]
191
+
192
+ world:
193
+ image: datagrok/demo_db_postgres:12-1.1.0
194
+ environment:
195
+ POSTGRES_DB: world
196
+ POSTGRES_USER: postgres
197
+ POSTGRES_PASSWORD: postgres
198
+ volumes:
199
+ - demo_world:/var/lib/postgresql/data
200
+ networks:
201
+ dg:
202
+ aliases: [world]
203
+ restart: on-failure
204
+ profiles: ["demo", "full"]
205
+
206
+ test_db:
207
+ image: datagrok/demo_db_postgres:12-1.1.0
208
+ environment:
209
+ POSTGRES_DB: test
210
+ POSTGRES_USER: postgres
211
+ POSTGRES_PASSWORD: postgres
212
+ volumes:
213
+ - demo_test:/var/lib/postgresql/data
214
+ networks:
215
+ dg:
216
+ aliases: [test_db]
217
+ restart: on-failure
218
+ profiles: ["demo", "full"]
219
+
220
+ northwind:
221
+ image: datagrok/demo_db_postgres:12-1.1.0
222
+ environment:
223
+ POSTGRES_DB: northwind
224
+ POSTGRES_USER: postgres
225
+ POSTGRES_PASSWORD: postgres
226
+ volumes:
227
+ - demo_northwind:/var/lib/postgresql/data
228
+ networks:
229
+ dg:
230
+ aliases: [northwind]
231
+ restart: on-failure
232
+ profiles: ["demo", "full"]
233
+
234
+ # ── Tools dev container (pre-built image) ────────────────────
235
+
236
+ tools-dev:
237
+ image: datagrok/tools-dev:\${TOOLS_DEV_VERSION:-latest}
238
+ cap_add:
239
+ - SYS_ADMIN
240
+ security_opt:
241
+ - seccomp=unconfined
242
+ volumes:
243
+ - \${WORKTREE_PATH:-.}:/workspace/repo
244
+ - \${DOCKER_SOCK}:/var/run/docker.sock
245
+ - npm_cache:/home/node/.npm
246
+ environment:
247
+ ANTHROPIC_API_KEY: \${ANTHROPIC_API_KEY:-}
248
+ DG_VERSION: \${DG_VERSION:-latest}
249
+ DG_PUBLIC_REPO: \${DG_PUBLIC_REPO:-https://github.com/datagrok-ai/public.git}
250
+ DG_PUBLIC_BRANCH: \${DG_PUBLIC_BRANCH:-}
251
+ JIRA_URL: \${JIRA_URL:-https://reddata.atlassian.net}
252
+ JIRA_USERNAME: \${JIRA_USERNAME:-}
253
+ JIRA_TOKEN: \${JIRA_TOKEN:-}
254
+ GITHUB_TOKEN: \${GITHUB_TOKEN:-}
255
+ TASK_KEY: \${TASK_KEY:-}
256
+ working_dir: /workspace/repo
257
+ networks:
258
+ dg:
259
+ aliases: [tools-dev]
260
+ stdin_open: true
261
+ tty: true
262
+ restart: unless-stopped
263
+
264
+ volumes:
265
+ pgdata:
266
+ datagrok_data:
267
+ datagrok_cfg:
268
+ demo_world:
269
+ demo_test:
270
+ demo_northwind:
271
+ npm_cache:
272
+
273
+ networks:
274
+ dg:
275
+ name: dg-pkg-\${TASK_KEY:-default}-net
276
+ `;
277
+ function findGitRoot(startDir) {
278
+ let current = startDir;
279
+ while (true) {
280
+ if (_fs.default.existsSync(_path.default.join(current, '.git'))) return current;
281
+ const parent = _path.default.dirname(current);
282
+ if (parent === current) return undefined;
283
+ current = parent;
284
+ }
285
+ }
286
+
287
+ /** Check if the repo root is the public Datagrok repo (has js-api/ at root). */
288
+ function isPublicRepo(repoRoot) {
289
+ return _fs.default.existsSync(_path.default.join(repoRoot, 'js-api')) || _fs.default.existsSync(_path.default.join(repoRoot, 'public', 'js-api'));
290
+ }
291
+
292
+ /**
293
+ * Resolve the Datagrok version to use:
294
+ * - Explicit --version flag takes priority
295
+ * - Public repo: bleeding-edge (tracks development)
296
+ * - Otherwise: latest
297
+ */
298
+ function resolveDatagrokVersion(args, repoRoot) {
299
+ if (args.version) return args.version;
300
+ if (repoRoot && isPublicRepo(repoRoot)) {
301
+ color.info('Public repo detected — using bleeding-edge for datagrok');
302
+ return 'bleeding-edge';
303
+ }
304
+ return 'latest';
305
+ }
306
+ function getProjectDir(taskKey) {
307
+ return _path.default.join(_os.default.tmpdir(), `dg-pkg-${taskKey}`);
308
+ }
309
+
310
+ /** Find a free TCP port by binding to port 0 and reading the assigned port. */
311
+ function findFreePort() {
312
+ return new Promise((resolve, reject) => {
313
+ const srv = _net.default.createServer();
314
+ srv.listen(0, '127.0.0.1', () => {
315
+ const addr = srv.address();
316
+ const port = typeof addr === 'object' && addr ? addr.port : 0;
317
+ srv.close(() => resolve(port));
318
+ });
319
+ srv.on('error', reject);
320
+ });
321
+ }
322
+ function writeProjectFiles(taskKey, args, worktreeRoot, dgPort) {
323
+ const projectDir = getProjectDir(taskKey);
324
+ if (!_fs.default.existsSync(projectDir)) _fs.default.mkdirSync(projectDir, {
325
+ recursive: true
326
+ });
327
+
328
+ // Write the embedded compose template
329
+ _fs.default.writeFileSync(_path.default.join(projectDir, 'docker-compose.yaml'), COMPOSE_TEMPLATE);
330
+
331
+ // Resolve Docker socket path
332
+ const dockerSock = process.platform === 'win32' ? '//var/run/docker.sock' : '/var/run/docker.sock';
333
+
334
+ // Generate .env — all paths use forward slashes for Docker compatibility
335
+ const envLines = [`WORKTREE_PATH=${toDockerPath(worktreeRoot)}`, `DG_PORT=${dgPort}`, `TASK_KEY=${taskKey.toLowerCase()}`, `DOCKER_SOCK=${dockerSock}`, `DATAGROK_VERSION=${args.version || 'latest'}`, `DG_VERSION=${args.version || 'latest'}`, `GROK_CONNECT_VERSION=${args['grok-connect-version'] || 'latest'}`, `GROK_SPAWNER_VERSION=${args['grok-spawner-version'] || 'latest'}`, `JKG_VERSION=${args['jkg-version'] || 'latest'}`, `TOOLS_DEV_VERSION=${args['tools-dev-version'] || 'latest'}`];
336
+ for (const env of ['ANTHROPIC_API_KEY', 'DG_PUBLIC_BRANCH', 'JIRA_URL', 'JIRA_USERNAME', 'JIRA_TOKEN', 'GITHUB_TOKEN']) if (process.env[env]) envLines.push(`${env}=${process.env[env]}`);
337
+ _fs.default.writeFileSync(_path.default.join(projectDir, '.env'), envLines.join('\n') + '\n');
338
+
339
+ // Write host config compose override (mirrors deploy/fat_dev/dg-claude approach)
340
+ const volumes = [];
341
+
342
+ // Claude profile (~/.claude + ~/.claude.json)
343
+ const claudeHome = findClaudeHome();
344
+ if (claudeHome) {
345
+ volumes.push(` - "${toDockerPath(claudeHome)}:/home/node/.claude"`);
346
+ const claudeState = _path.default.join(_path.default.dirname(claudeHome), '.claude.json');
347
+ if (_fs.default.existsSync(claudeState) && _fs.default.statSync(claudeState).isFile()) volumes.push(` - "${toDockerPath(claudeState)}:/home/node/.claude.json"`);
348
+ color.info(`Claude profile: ${claudeHome}`);
349
+ } else color.warn('No Claude profile found. Set CLAUDE_HOME or run "claude" locally to log in.');
350
+ const overridePath = _path.default.join(projectDir, 'docker-compose.override.yaml');
351
+ if (volumes.length > 0) {
352
+ const override = ['services:', ' tools-dev:', ' volumes:', ...volumes].join('\n') + '\n';
353
+ _fs.default.writeFileSync(overridePath, override);
354
+ } else if (_fs.default.existsSync(overridePath)) _fs.default.unlinkSync(overridePath);
355
+ return projectDir;
356
+ }
357
+ function runCompose(projectDir, taskKey, args, ...composeArgs) {
358
+ // Use --project-directory so compose picks up .env automatically
359
+ const cmdArgs = ['compose', '--project-directory', projectDir, '-p', `dg-pkg-${taskKey.toLowerCase()}`];
360
+ // Include host config override if present (Claude profile, grok config)
361
+ const overrideFile = _path.default.join(projectDir, 'docker-compose.override.yaml');
362
+ if (_fs.default.existsSync(overrideFile)) cmdArgs.push('-f', _path.default.join(projectDir, 'docker-compose.yaml'), '-f', overrideFile);
363
+ if (args.profile) cmdArgs.push('--profile', args.profile);
364
+ cmdArgs.push(...composeArgs);
365
+ const result = (0, _child_process.spawnSync)('docker', cmdArgs, {
366
+ stdio: 'inherit'
367
+ });
368
+ return result.status ?? 1;
369
+ }
370
+
371
+ /** Check if Datagrok responds with 200 at /api/info/server. */
372
+ function httpProbe(port) {
373
+ return new Promise(resolve => {
374
+ const req = _http.default.get(`http://localhost:${port}/api/info/server`, {
375
+ timeout: 5000
376
+ }, res => {
377
+ res.resume();
378
+ resolve(res.statusCode === 200);
379
+ });
380
+ req.on('error', () => resolve(false));
381
+ req.on('timeout', () => {
382
+ req.destroy();
383
+ resolve(false);
384
+ });
385
+ });
386
+ }
387
+ async function waitForDatagrok(port, timeoutMs = 180000) {
388
+ const url = `http://localhost:${port}`;
389
+ const start = Date.now();
390
+ color.info(`Waiting for Datagrok at ${url}...`);
391
+ while (Date.now() - start < timeoutMs) {
392
+ if (await httpProbe(port)) {
393
+ color.info('Datagrok is ready.');
394
+ return true;
395
+ }
396
+ await new Promise(resolve => setTimeout(resolve, 3000));
397
+ }
398
+ color.error(`Datagrok did not become healthy within ${timeoutMs / 1000}s`);
399
+ return false;
400
+ }
401
+ function destroyProject(taskKey, args, repoRoot) {
402
+ const projectDir = getProjectDir(taskKey);
403
+ if (!_fs.default.existsSync(_path.default.join(projectDir, 'docker-compose.yaml'))) {
404
+ color.error(`No compose project found for "${taskKey}" in ${projectDir}`);
405
+ return false;
406
+ }
407
+ color.info(`Destroying "${taskKey}"...`);
408
+ runCompose(projectDir, taskKey, args, 'down', '-v');
409
+
410
+ // Remove worktree
411
+ const worktreePath = _path.default.join(_os.default.homedir(), 'pkg-worktrees', taskKey);
412
+ if (_fs.default.existsSync(worktreePath) && repoRoot) {
413
+ (0, _child_process.spawnSync)('git', ['worktree', 'remove', worktreePath, '--force'], {
414
+ cwd: repoRoot,
415
+ stdio: 'inherit'
416
+ });
417
+ (0, _child_process.spawnSync)('git', ['branch', '-D', taskKey], {
418
+ cwd: repoRoot,
419
+ stdio: 'inherit'
420
+ });
421
+ }
422
+
423
+ // Clean up project dir
424
+ _fs.default.rmSync(projectDir, {
425
+ recursive: true,
426
+ force: true
427
+ });
428
+ color.info(`Destroyed "${taskKey}".`);
429
+ return true;
430
+ }
431
+
432
+ /** List all project keys that have a compose file in the temp dir. */
433
+ function listProjects() {
434
+ const prefix = 'dg-pkg-';
435
+ const tmpDir = _os.default.tmpdir();
436
+ const keys = [];
437
+ for (const entry of _fs.default.readdirSync(tmpDir)) {
438
+ if (entry.startsWith(prefix) && _fs.default.existsSync(_path.default.join(tmpDir, entry, 'docker-compose.yaml'))) keys.push(entry.slice(prefix.length));
439
+ }
440
+ return keys;
441
+ }
442
+ async function claude(args) {
443
+ const subcommand = args._[1];
444
+ const taskKey = subcommand === 'destroy' ? args._[2] : subcommand;
445
+
446
+ // Handle destroy: bring down containers, remove worktree, clean up
447
+ if (subcommand === 'destroy') {
448
+ if (!taskKey) {
449
+ color.error('Usage: grok claude destroy <project>');
450
+ return false;
451
+ }
452
+ const repoRoot = findGitRoot(process.cwd());
453
+ return destroyProject(taskKey, args, repoRoot);
454
+ }
455
+
456
+ // Handle destroy-all: destroy all known projects
457
+ if (subcommand === 'destroy-all') {
458
+ const projects = listProjects();
459
+ if (projects.length === 0) {
460
+ color.info('No projects found.');
461
+ return true;
462
+ }
463
+ const repoRoot = findGitRoot(process.cwd());
464
+ for (const key of projects) destroyProject(key, args, repoRoot);
465
+ return true;
466
+ }
467
+
468
+ // Require project name
469
+ if (!taskKey) return false;
470
+
471
+ // Reject master/main as task key
472
+ if (taskKey === 'master' || taskKey === 'main') {
473
+ color.error(`Cannot use '${taskKey}' as project name — always work on a dedicated branch.`);
474
+ return false;
475
+ }
476
+
477
+ // Find repo root (optional — not required for --in-place or non-git directories)
478
+ const repoRoot = findGitRoot(process.cwd());
479
+ const inPlace = args['in-place'] || !repoRoot;
480
+ if (!repoRoot && !args['in-place']) color.warn('Not inside a git repository — running in-place (no worktree).');
481
+
482
+ // Resolve Datagrok version (only datagrok image uses this; others default to latest)
483
+ if (!args.version) {
484
+ args.version = resolveDatagrokVersion(args, repoRoot);
485
+ color.info(`Datagrok version: ${args.version}`);
486
+ }
487
+
488
+ // When inside a public repo, tell the container which branch to clone (avoids slow version resolution)
489
+ if (repoRoot && isPublicRepo(repoRoot) && !process.env.DG_PUBLIC_BRANCH) {
490
+ const branchResult = (0, _child_process.spawnSync)('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
491
+ cwd: repoRoot
492
+ });
493
+ const branch = branchResult.stdout?.toString().trim();
494
+ if (branch) process.env.DG_PUBLIC_BRANCH = branch;
495
+ }
496
+
497
+ // Determine workspace root: worktree or current directory
498
+ let worktreeRoot;
499
+ if (inPlace) {
500
+ worktreeRoot = process.cwd();
501
+ color.info(`Working in-place: ${worktreeRoot}`);
502
+ } else {
503
+ const worktreesDir = _path.default.join(_os.default.homedir(), 'pkg-worktrees');
504
+ const worktreePath = _path.default.join(worktreesDir, taskKey);
505
+ if (!_fs.default.existsSync(worktreePath)) {
506
+ _fs.default.mkdirSync(worktreesDir, {
507
+ recursive: true
508
+ });
509
+ const result = (0, _child_process.spawnSync)('git', ['worktree', 'add', worktreePath, '-b', taskKey], {
510
+ cwd: repoRoot,
511
+ stdio: 'inherit'
512
+ });
513
+ if (result.status !== 0) {
514
+ color.error('Failed to create git worktree.');
515
+ return false;
516
+ }
517
+ color.info(`Created worktree: ${worktreePath}`);
518
+ } else color.info(`Worktree already exists at ${worktreePath}`);
519
+ worktreeRoot = worktreePath;
520
+ }
521
+
522
+ // Warn about missing optional environment variables
523
+ const optionalVars = ['JIRA_URL', 'JIRA_USERNAME', 'JIRA_TOKEN', 'GITHUB_TOKEN'];
524
+ const missing = optionalVars.filter(v => !process.env[v]);
525
+ if (missing.length > 0) color.warn(`Not set: ${missing.join(', ')}`);
526
+
527
+ // Find a free port for Datagrok (stable across container restarts)
528
+ const dgPort = args.port || (await findFreePort());
529
+ color.info(`Datagrok UI will be at: http://localhost:${dgPort}`);
530
+
531
+ // Write compose + .env to temp dir (no external file dependencies)
532
+ const projectDir = writeProjectFiles(taskKey, args, worktreeRoot, dgPort);
533
+ color.info(`Workspace: ${worktreeRoot}`);
534
+
535
+ // Start containers
536
+ color.info('Starting containers...');
537
+ const upStatus = runCompose(projectDir, taskKey, args, 'up', '-d', '--wait');
538
+ if (upStatus !== 0) {
539
+ color.error('Failed to start containers.');
540
+ return false;
541
+ }
542
+
543
+ // Wait for Datagrok to be healthy
544
+ const healthy = await waitForDatagrok(dgPort);
545
+ if (!healthy) color.warn('Datagrok may not be ready. Proceeding anyway...');
546
+
547
+ // Determine whether this is a public repo and compute Claude working directory
548
+ const isPublic = repoRoot ? isPublicRepo(repoRoot) : false;
549
+ const claudeWorkDir = isPublic ? '/workspace/repo/public' : `/workspace/public/packages/${taskKey}`;
550
+
551
+ // Fix ownership on bind-mounted directories (host UID may differ from container node user)
552
+ const containerName = `dg-pkg-${taskKey.toLowerCase()}-tools-dev-1`;
553
+ (0, _child_process.spawnSync)('docker', ['exec', '-u', 'root', containerName, 'bash', '-c', 'chown node:node /workspace 2>/dev/null; ' + 'for d in /home/node/.claude /home/node/.npm; do ' + 'mkdir -p "$d" 2>/dev/null; chown -R node:node "$d" 2>/dev/null; done; ' + '[ -f /home/node/.claude.json ] && chown node:node /home/node/.claude.json 2>/dev/null; true'], {
554
+ stdio: 'inherit'
555
+ });
556
+
557
+ // Copy host grok config into container and ensure 'local' server exists
558
+ (0, _child_process.spawnSync)('docker', ['exec', '-u', 'root', containerName, 'bash', '-c', 'mkdir -p /home/node/.grok && chown node:node /home/node/.grok'], {
559
+ stdio: 'inherit'
560
+ });
561
+ const grokConfigPath = _path.default.join(_os.default.homedir(), '.grok', 'config.yaml');
562
+ if (_fs.default.existsSync(grokConfigPath)) {
563
+ (0, _child_process.spawnSync)('docker', ['cp', grokConfigPath, `${containerName}:/home/node/.grok/config.yaml`], {
564
+ stdio: 'inherit'
565
+ });
566
+ (0, _child_process.spawnSync)('docker', ['exec', '-u', 'root', containerName, 'chown', 'node:node', '/home/node/.grok/config.yaml'], {
567
+ stdio: 'inherit'
568
+ });
569
+ color.info('Copied grok config into container');
570
+ }
571
+
572
+ // Add/ensure 'local' server pointing to compose datagrok (key=admin matches GROK_PARAMETERS)
573
+ (0, _child_process.spawnSync)('docker', ['exec', containerName, 'node', '-e', `
574
+ const fs = require('fs');
575
+ const p = '/home/node/.grok/config.yaml';
576
+ let t = '';
577
+ try { t = fs.readFileSync(p, 'utf8'); } catch {}
578
+ if (!t.trim()) {
579
+ t = 'default: local\\nservers:\\n local:\\n url: http://datagrok:8080/api\\n key: admin\\n';
580
+ } else if (!t.includes('datagrok:8080')) {
581
+ t = t.replace(/^(servers:)/m, '\\$1\\n local:\\n url: http://datagrok:8080/api\\n key: admin');
582
+ t = t.replace(/^default:.*/m, 'default: local');
583
+ } else {
584
+ t = t.replace(/^default:.*/m, 'default: local');
585
+ }
586
+ fs.writeFileSync(p, t);
587
+ `], {
588
+ stdio: 'inherit'
589
+ });
590
+
591
+ // Set up workspace context: for non-public repos, bind-mount workspace into public/packages/
592
+ // so that process.cwd() returns the mount-point path (not the resolved symlink target)
593
+ // and relative paths like ../../js-api resolve correctly inside the public repo tree.
594
+ if (!isPublic) {
595
+ (0, _child_process.spawnSync)('docker', ['exec', '-u', 'root', containerName, 'bash', '-c', 'for i in $(seq 1 60); do [ -d /workspace/public/js-api ] && break; sleep 3; done; ' + 'if [ -d /workspace/public/js-api ]; then ' + ' mkdir -p /workspace/public/packages 2>/dev/null; ' + ` rm -f /workspace/public/packages/${taskKey} 2>/dev/null; ` + ` if ! mountpoint -q /workspace/public/packages/${taskKey} 2>/dev/null; then ` + ` mkdir -p /workspace/public/packages/${taskKey} && ` + ` mount --bind /workspace/repo /workspace/public/packages/${taskKey}; ` + ' fi; ' + ` echo "[grok claude] Workspace at /workspace/public/packages/${taskKey}"; ` + 'else ' + ' echo "[grok claude] Warning: public repo clone not ready"; ' + 'fi'], {
596
+ stdio: 'inherit'
597
+ });
598
+ }
599
+ const claudeArgs = ['--dangerously-skip-permissions'];
600
+ if (args.prompt) claudeArgs.push('-p', args.prompt);
601
+ color.info(`Launching Claude Code in container ${containerName}...`);
602
+ (0, _child_process.spawnSync)('docker', ['exec', '-it', '-w', claudeWorkDir, containerName, 'claude', ...claudeArgs], {
603
+ stdio: 'inherit'
604
+ });
605
+
606
+ // On exit, optionally stop containers
607
+ if (!args.keep) {
608
+ color.info('Stopping containers...');
609
+ runCompose(projectDir, taskKey, args, 'down');
610
+ color.info('Containers stopped.');
611
+ } else color.info(`Containers left running (--keep). Use "grok claude destroy ${taskKey}" to tear down.`);
612
+ return true;
613
+ }
@@ -14,6 +14,7 @@ Commands:
14
14
  api Create wrapper functions
15
15
  build Build a package or multiple packages
16
16
  check Check package content (function signatures, etc.)
17
+ claude Launch Claude Code in a Datagrok dev container
17
18
  config Create and manage config files
18
19
  create Create a package
19
20
  init Modify a package template
@@ -29,6 +30,44 @@ To get help on a particular command, use:
29
30
  Read more about the package development workflow:
30
31
  https://datagrok.ai/help/develop/develop
31
32
  `;
33
+ const HELP_CLAUDE = `
34
+ Usage: grok claude <project> Start or reattach to a project
35
+ grok claude destroy <project> Stop containers + remove worktree
36
+ grok claude destroy-all Destroy all projects
37
+
38
+ Launch Claude Code inside a Datagrok dev container. Creates a git worktree
39
+ for the project, starts a full Datagrok stack (postgres, rabbitmq, grok_pipe,
40
+ datagrok) and opens Claude Code in a tools-dev container.
41
+
42
+ Version is auto-detected: bleeding-edge for the public repo,
43
+ latest stable release (from Docker Hub) for other repos.
44
+
45
+ Options:
46
+ [--version <tag>] [--profile <name>] [--keep]
47
+ [--port <N>] [--prompt <text>] [--in-place]
48
+ [--grok-connect-version <tag>] [--grok-spawner-version <tag>]
49
+ [--jkg-version <tag>] [--tools-dev-version <tag>]
50
+
51
+ --version Datagrok image version (default: bleeding-edge for public repo, latest otherwise)
52
+ --profile Compose profile: demo, scripting, full (default: none)
53
+ --keep Don't stop containers on exit
54
+ --port Datagrok host port (default: random available)
55
+ --prompt Pass initial prompt to Claude Code (non-interactive)
56
+ --in-place Use current directory instead of creating a git worktree
57
+ --grok-connect-version grok_connect image version (default: latest)
58
+ --grok-spawner-version grok_spawner image version (default: latest)
59
+ --jkg-version jupyter_kernel_gateway image version (default: latest)
60
+ --tools-dev-version tools-dev image version (default: latest)
61
+
62
+ Examples:
63
+ grok claude GROK-12345 Start working on a task
64
+ grok claude GROK-12345 --version 1.22.0 Use specific Datagrok version
65
+ grok claude GROK-12345 --profile full --keep Start all services, keep running
66
+ grok claude GROK-12345 --prompt "fix the bug" One-shot command
67
+ grok claude GROK-12345 --in-place Work in current directory
68
+ grok claude destroy GROK-12345 Tear down a task
69
+ grok claude destroy-all Tear down everything
70
+ `;
32
71
  const HELP_ADD = `
33
72
  Usage: grok add <entity> <name>
34
73
 
@@ -272,6 +311,7 @@ const help = exports.help = {
272
311
  api: HELP_API,
273
312
  build: HELP_BUILD,
274
313
  check: HELP_CHECK,
314
+ claude: HELP_CLAUDE,
275
315
  config: HELP_CONFIG,
276
316
  create: HELP_CREATE,
277
317
  init: HELP_INIT,