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.
- package/.devcontainer/Dockerfile.pkg_dev +36 -0
- package/.devcontainer/PACKAGES_DEV.md +501 -0
- package/.devcontainer/docker-compose.yaml +236 -0
- package/.devcontainer/entrypoint.sh +93 -0
- package/bin/commands/claude.js +613 -0
- package/bin/commands/help.js +40 -0
- package/bin/grok.js +19 -3
- package/bin/utils/func-generation.js +1 -1
- package/package-template/package.json +1 -0
- package/package-template/ts.webpack.config.js +1 -22
- package/package-template/webpack.config.js +1 -22
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/bin/commands/help.js
CHANGED
|
@@ -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,
|