datagrok-tools 6.1.5 → 6.1.7

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/CLAUDE.md CHANGED
@@ -1,31 +1,12 @@
1
1
  # CLAUDE.md
2
2
 
3
- This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
-
5
3
  ## Overview
6
4
 
7
5
  This is **datagrok-tools**, a CLI utility for creating, validating, testing, and publishing packages to Datagrok. The tool is distributed as `grok` command globally via npm.
8
6
 
9
- ## Build & Development Commands
10
-
11
- ### Build the CLI
12
- ```bash
13
- npm run build # Transpile TypeScript to JavaScript using Babel
14
- npm run debug-source-map # Build with source maps for debugging
15
- ```
16
-
17
- The build process uses Babel with `@babel/preset-typescript` to transpile TypeScript files from `bin/` to `bin/` (in-place). **Important:** `.ts` source and `.js` output coexist in the same `bin/` directory — `grok.js` requires the transpiled `.js` files, not the `.ts` sources. After editing any `.ts` file, you must run `npm run build` before testing.
18
-
19
- ### Link for Local Development
20
- ```bash
21
- npm link # Make 'grok' command available globally for testing
22
- ```
7
+ ## Build Notes
23
8
 
24
- ### Testing
25
- While this CLI tool manages testing for Datagrok packages, it doesn't have its own test suite. To test changes:
26
- 1. Build the tool: `npm run build`
27
- 2. Link it: `npm link`
28
- 3. Test commands manually: `grok create test-package`, `grok check`, etc.
9
+ The build process uses Babel with `@babel/preset-typescript` to transpile TypeScript files from `bin/` to `bin/` (in-place). `.ts` source and `.js` output coexist in the same `bin/` directory -- `grok.js` requires the transpiled `.js` files, not the `.ts` sources. After editing any `.ts` file, you must run `npm run build` before testing.
29
10
 
30
11
  ## Code Architecture
31
12
 
@@ -110,14 +91,32 @@ The `publish` command executes these steps:
110
91
  1. Gather files using `ignore-walk` (respects .npmignore/.gitignore/.grokignore)
111
92
  2. Run validation checks (signatures, imports, package.json, changelog)
112
93
  3. Process environment variables in `/connections/*.json` files (replace `${VAR}`)
113
- 4. Create ZIP archive with archiver-promise
114
- 5. Upload to server: `POST ${host}/packages/dev/${devKey}/${packageName}`
94
+ 4. **Process Docker images** (see below)
95
+ 5. Create ZIP archive with archiver-promise (includes `image.json` metadata per container)
96
+ 6. Upload to server: `POST ${host}/packages/dev/${devKey}/${packageName}`
115
97
 
116
98
  **Key flags:**
117
99
  - `--debug` (default) - Package visible only to developer
118
100
  - `--release` - Public package
119
101
  - `--build` / `--rebuild` - Control webpack bundling
120
102
  - `--skip-check` - Skip validation
103
+ - `--rebuild-docker` - Force rebuild Docker images locally before pushing to registry
104
+ - `--skip-docker-rebuild` - Skip auto-rebuild even when dockerfile folder has changed
105
+
106
+ ### Docker Image Handling in `publish`
107
+
108
+ Implemented in `bin/commands/publish.ts` (`processDockerImages()`, `discoverDockerfiles()`).
109
+
110
+ When a package has a `dockerfiles/` directory, `grok publish` automatically manages Docker images:
111
+
112
+ 1. **Discovery** — scans `dockerfiles/` for subdirectories containing a `Dockerfile`. Image name = `<packageName>-<folderName>`.
113
+ 2. **Change detection** — computes SHA256 of the entire dockerfile directory. If the hash differs from what the server has, rebuilds automatically (unless `--skip-docker-rebuild`).
114
+ 3. **Build** — `docker build --platform linux/amd64` (always cross-compiled for linux/amd64).
115
+ 4. **Registry** — tags and pushes to configured Docker registry if one is set.
116
+ 5. **Metadata** — writes `image.json` into the ZIP for each container so the server knows which image to use.
117
+ 6. **Fallback** — if a local build isn't available, queries the server for a compatible pre-built image.
118
+
119
+ **Practical implication:** editing any file inside `dockerfiles/<name>/` will cause `grok publish` to rebuild that container image on the next publish, without needing `--rebuild-docker`.
121
120
 
122
121
  ### Testing Framework
123
122
 
@@ -273,16 +272,6 @@ servers:
273
272
  key: ''
274
273
  ```
275
274
 
276
- ### Webpack Externals
277
-
278
- The CLI validates that imports match webpack externals to prevent bundling datagrok-api and other provided libraries. Common externals:
279
- - `datagrok-api`
280
- - `rxjs`
281
- - `cash-dom`
282
- - `dayjs`
283
- - `openchemlib/full`
284
- - `wu`
285
-
286
275
  ## File References
287
276
 
288
277
  When working with commands, key entry points are:
@@ -21,6 +21,8 @@ Commands:
21
21
  init Modify a package template
22
22
  link Link \`datagrok-api\` and libraries for local development
23
23
  publish Upload a package
24
+ report Manage user error reports (fetch, resolve, create ticket)
25
+ run Build, publish, and open in browser
24
26
  test Run package tests
25
27
  testall Run packages tests
26
28
  migrate Migrate legacy tags to meta.role
@@ -321,6 +323,42 @@ Examples:
321
323
  // file and converting your scripts in the \`package.json\` file
322
324
  // `;
323
325
 
326
+ const HELP_RUN = `
327
+ Usage: grok run [host]
328
+
329
+ Build, publish, and open the package in the browser.
330
+
331
+ Runs \`grok build\`, publishes the package to the server, then opens the server in the default browser.
332
+
333
+ Options:
334
+ [-k | --key] [--release] [-v | --verbose]
335
+
336
+ --key Developer key (overrides config)
337
+ --release Publish as a release version (default: debug)
338
+ --verbose Print detailed output
339
+
340
+ Examples:
341
+ grok run Build, publish to default server, and open browser
342
+ grok run dev Build, publish to 'dev' server alias, and open browser
343
+ grok run https://my.datagrok.ai/api --key abc123
344
+ `;
345
+ const HELP_REPORT = `
346
+ Usage: grok report <subcommand> <instance> <id>
347
+
348
+ Manage Datagrok user error reports
349
+
350
+ Subcommands:
351
+ fetch Download a report zip from a managed instance
352
+ resolve Mark a report as resolved
353
+ ticket Create a JIRA ticket for a report via the Datlas API
354
+
355
+ Examples:
356
+ grok report fetch dev 1528 Download report #1528 from the 'dev' instance
357
+ grok report resolve dev 1528 Resolve report #1528 on the 'dev' instance
358
+ grok report ticket dev <report-uuid> Create a JIRA ticket for a report
359
+
360
+ The instance name must match a server alias in ~/.grok/config.yaml.
361
+ `;
324
362
  const help = exports.help = {
325
363
  add: HELP_ADD,
326
364
  api: HELP_API,
@@ -333,6 +371,8 @@ const help = exports.help = {
333
371
  init: HELP_INIT,
334
372
  link: HELP_LINK,
335
373
  publish: HELP_PUBLISH,
374
+ report: HELP_REPORT,
375
+ run: HELP_RUN,
336
376
  test: HELP_TEST,
337
377
  testall: HELP_TESTALL,
338
378
  migrate: HELP_MIGRATE,
@@ -60,6 +60,18 @@ function discoverDockerfiles(packageName, version, debug) {
60
60
  fullLocalName: `${imageName}:${imageTag}`
61
61
  });
62
62
  }
63
+
64
+ // Handle Dockerfile directly in dockerfiles/ (single-container layout)
65
+ const rootDockerfile = _path.default.join(dockerfilesDir, 'Dockerfile');
66
+ if (_fs.default.existsSync(rootDockerfile)) {
67
+ const cleanName = utils.removeScope(packageName).toLowerCase();
68
+ results.push({
69
+ imageName: cleanName,
70
+ imageTag: version,
71
+ dirName: '.',
72
+ fullLocalName: `${cleanName}:${version}`
73
+ });
74
+ }
63
75
  return results;
64
76
  }
65
77
  function dockerCommand(args) {
@@ -0,0 +1,184 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.report = report;
8
+ var _fs = _interopRequireDefault(require("fs"));
9
+ var _os = _interopRequireDefault(require("os"));
10
+ var _path = _interopRequireDefault(require("path"));
11
+ var color = _interopRequireWildcard(require("../utils/color-utils"));
12
+ var _testUtils = require("../utils/test-utils");
13
+ 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); }
14
+ const fetch = require('node-fetch');
15
+ async function report(args) {
16
+ const subcommand = args._[1];
17
+ switch (subcommand) {
18
+ case 'fetch':
19
+ return await handleFetch(args);
20
+ case 'resolve':
21
+ return await handleResolve(args);
22
+ case 'ticket':
23
+ return await handleTicket(args);
24
+ default:
25
+ return false;
26
+ }
27
+ }
28
+ async function handleFetch(args) {
29
+ const instance = args._[2];
30
+ const number = args._[3];
31
+ if (!instance || !number) {
32
+ color.error('Usage: grok report fetch <instance> <number>');
33
+ return false;
34
+ }
35
+ try {
36
+ const {
37
+ url,
38
+ key
39
+ } = (0, _testUtils.getDevKey)(instance);
40
+ const token = await (0, _testUtils.getToken)(url, key);
41
+ console.log(`Searching for report #${number}...`);
42
+ const searchResp = await fetch(`${url}/reports?text=number%3D${encodeURIComponent(number)}`, {
43
+ headers: {
44
+ Authorization: token
45
+ }
46
+ });
47
+ if (!searchResp.ok) {
48
+ color.error(`Report search failed (HTTP ${searchResp.status})`);
49
+ return false;
50
+ }
51
+ const results = await searchResp.json();
52
+ if (!Array.isArray(results) || results.length === 0) {
53
+ color.error(`Report #${number} not found`);
54
+ return false;
55
+ }
56
+ const reportData = results[0];
57
+ const reportId = reportData.id || reportData.Id;
58
+ if (!reportId) {
59
+ color.error('Report found but has no id field');
60
+ return false;
61
+ }
62
+ console.log(`Downloading report ${reportId}...`);
63
+ const downloadResp = await fetch(`${url}/reports/${reportId}/zip`, {
64
+ headers: {
65
+ Authorization: token
66
+ }
67
+ });
68
+ if (!downloadResp.ok) {
69
+ color.error(`Report download failed (HTTP ${downloadResp.status})`);
70
+ return false;
71
+ }
72
+ const buffer = await downloadResp.buffer();
73
+ const outputPath = _path.default.join(_os.default.tmpdir(), `report_${instance}_${number}.zip`);
74
+ _fs.default.writeFileSync(outputPath, buffer);
75
+ const metaPath = outputPath.replace('.zip', '_meta.json');
76
+ _fs.default.writeFileSync(metaPath, JSON.stringify(reportData, null, 2));
77
+ color.success(`Report saved to: ${outputPath}`);
78
+ console.log(outputPath);
79
+ return true;
80
+ } catch (err) {
81
+ color.error(`Error: ${err.message}`);
82
+ return false;
83
+ }
84
+ }
85
+ async function handleResolve(args) {
86
+ const instance = args._[2];
87
+ const number = args._[3];
88
+ if (!instance || !number) {
89
+ color.error('Usage: grok report resolve <instance> <number>');
90
+ return false;
91
+ }
92
+ try {
93
+ const metaPath = _path.default.join(_os.default.tmpdir(), `report_${instance}_${number}_meta.json`);
94
+ if (!_fs.default.existsSync(metaPath)) {
95
+ color.error(`Meta file not found: ${metaPath}`);
96
+ color.warn('Hint: was the report fetched via `grok report fetch` first?');
97
+ return false;
98
+ }
99
+ const meta = JSON.parse(_fs.default.readFileSync(metaPath, 'utf-8'));
100
+ const reportId = meta.id || meta.Id;
101
+ if (!reportId) {
102
+ color.error(`No report id in meta file: ${metaPath}`);
103
+ return false;
104
+ }
105
+ const {
106
+ url,
107
+ key
108
+ } = (0, _testUtils.getDevKey)(instance);
109
+ const token = await (0, _testUtils.getToken)(url, key);
110
+ console.log(`Resolving report #${number} (id: ${reportId})...`);
111
+ const resp = await fetch(`${url}/reports/${reportId}/resolve`, {
112
+ method: 'POST',
113
+ headers: {
114
+ Authorization: token
115
+ }
116
+ });
117
+ if (resp.ok) {
118
+ color.success(`Report #${number} resolved on ${instance}`);
119
+ return true;
120
+ }
121
+ const body = await resp.text();
122
+ color.error(`Resolve failed (HTTP ${resp.status}): ${body}`);
123
+ return false;
124
+ } catch (err) {
125
+ color.error(`Error: ${err.message}`);
126
+ return false;
127
+ }
128
+ }
129
+ async function handleTicket(args) {
130
+ const instance = args._[2];
131
+ const reportId = args._[3];
132
+ if (!instance || !reportId) {
133
+ color.error('Usage: grok report ticket <instance> <report-id>');
134
+ return false;
135
+ }
136
+ try {
137
+ const {
138
+ url,
139
+ key
140
+ } = (0, _testUtils.getDevKey)(instance);
141
+ const token = await (0, _testUtils.getToken)(url, key);
142
+ console.log('Getting current user...');
143
+ const userResp = await fetch(`${url}/users/current`, {
144
+ headers: {
145
+ Authorization: token
146
+ }
147
+ });
148
+ if (!userResp.ok) {
149
+ color.error(`Failed to get current user (HTTP ${userResp.status})`);
150
+ return false;
151
+ }
152
+ const user = await userResp.json();
153
+ const userId = user.id || user.Id;
154
+ if (!userId) {
155
+ color.error('No user id in response');
156
+ return false;
157
+ }
158
+ console.log(`Creating JIRA ticket for report ${reportId}...`);
159
+ const ticketResp = await fetch(`${url}/reports/${reportId}/jira?assigneeId=${userId}`, {
160
+ method: 'POST',
161
+ headers: {
162
+ Authorization: token,
163
+ 'Content-Type': 'application/json'
164
+ }
165
+ });
166
+ if (ticketResp.status !== 200 && ticketResp.status !== 201) {
167
+ const body = await ticketResp.text();
168
+ color.error(`JIRA ticket creation failed (HTTP ${ticketResp.status}): ${body}`);
169
+ return false;
170
+ }
171
+ const result = await ticketResp.json();
172
+ const ticketKey = result.key;
173
+ if (!ticketKey) {
174
+ color.error(`No ticket key in response: ${JSON.stringify(result).slice(0, 200)}`);
175
+ return false;
176
+ }
177
+ color.success(`Created ticket: ${ticketKey}`);
178
+ console.log(ticketKey);
179
+ return true;
180
+ } catch (err) {
181
+ color.error(`Error: ${err.message}`);
182
+ return false;
183
+ }
184
+ }
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.run = run;
8
+ var _fs = _interopRequireDefault(require("fs"));
9
+ var _os = _interopRequireDefault(require("os"));
10
+ var _path = _interopRequireDefault(require("path"));
11
+ var _child_process = require("child_process");
12
+ var _jsYaml = _interopRequireDefault(require("js-yaml"));
13
+ var utils = _interopRequireWildcard(require("../utils/utils"));
14
+ var color = _interopRequireWildcard(require("../utils/color-utils"));
15
+ var _publish = require("./publish");
16
+ 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); }
17
+ const grokDir = _path.default.join(_os.default.homedir(), '.grok');
18
+ const confPath = _path.default.join(grokDir, 'config.yaml');
19
+ function getWebUrl(apiUrl) {
20
+ const u = new URL(apiUrl);
21
+ u.pathname = u.pathname.replace(/\/api\/?$/, '') || '/';
22
+ return u.toString().replace(/\/$/, '');
23
+ }
24
+ function openBrowser(url) {
25
+ let command;
26
+ switch (process.platform) {
27
+ case 'darwin':
28
+ command = `open "${url}"`;
29
+ break;
30
+ case 'win32':
31
+ command = `start "" "${url}"`;
32
+ break;
33
+ default:
34
+ command = `xdg-open "${url}"`;
35
+ }
36
+ (0, _child_process.exec)(command, err => {
37
+ if (err) color.warn(`Could not open browser: ${err.message}`);
38
+ });
39
+ }
40
+ const MISSING_MODULE_PATTERNS = ['cannot find module', 'module not found', 'can\'t resolve'];
41
+ async function buildPackage(dir) {
42
+ const buildCmd = 'npm run build -- --env incremental';
43
+ const packageJson = JSON.parse(_fs.default.readFileSync(_path.default.join(dir, 'package.json'), 'utf-8'));
44
+ const name = packageJson.friendlyName || packageJson.name;
45
+ console.log(`Building ${name}...`);
46
+ try {
47
+ await utils.runScript(buildCmd, dir, color.isVerbose());
48
+ color.success(`Successfully built ${name}`);
49
+ return true;
50
+ } catch (error) {
51
+ const msg = (error?.message ?? '').toLowerCase();
52
+ if (MISSING_MODULE_PATTERNS.some(p => msg.includes(p))) {
53
+ color.warn('Missing modules detected, running npm install...');
54
+ try {
55
+ await utils.runScript('npm install', dir, color.isVerbose());
56
+ await utils.runScript(buildCmd, dir, color.isVerbose());
57
+ color.success(`Successfully built ${name}`);
58
+ return true;
59
+ } catch (retryError) {
60
+ color.error(`Failed to build ${name}`);
61
+ if (retryError.message) color.error(retryError.message);
62
+ return false;
63
+ }
64
+ }
65
+ color.error(`Failed to build ${name}`);
66
+ if (error.message) color.error(error.message);
67
+ return false;
68
+ }
69
+ }
70
+ async function run(args) {
71
+ color.setVerbose(args.verbose || false);
72
+
73
+ // Step 1: Build (skip npm install to preserve npm link; retry with npm install only if needed)
74
+ const built = await buildPackage(process.cwd());
75
+ if (!built) return false;
76
+
77
+ // Step 2: Resolve server URL and key
78
+ if (!_fs.default.existsSync(confPath)) {
79
+ color.error(`Config not found at ${confPath}. Run \`grok config\` first.`);
80
+ return false;
81
+ }
82
+ const config = _jsYaml.default.load(_fs.default.readFileSync(confPath, {
83
+ encoding: 'utf-8'
84
+ }));
85
+ const urls = utils.mapURL(config);
86
+ let host = config.default;
87
+ if (args['_'].length === 2) host = args['_'][1];
88
+ let key = '';
89
+ let url = '';
90
+ let registry;
91
+ try {
92
+ url = new URL(host).href;
93
+ if (url.endsWith('/')) url = url.slice(0, -1);
94
+ if (url in urls) {
95
+ const alias = urls[url];
96
+ key = config['servers'][alias]['key'];
97
+ registry = config['servers'][alias]['registry'];
98
+ }
99
+ } catch (error) {
100
+ if (!(host in config.servers)) {
101
+ color.error(`Unknown server alias. Please add it to ${confPath}`);
102
+ return false;
103
+ }
104
+ url = config['servers'][host]['url'];
105
+ key = config['servers'][host]['key'];
106
+ registry = config['servers'][host]['registry'];
107
+ }
108
+ if (args.key) key = args.key;
109
+ if (key === '') {
110
+ color.warn('Please provide the key with `--key` option or add it by running `grok config`');
111
+ return false;
112
+ }
113
+ const packDir = _path.default.join(process.cwd(), 'package.json');
114
+ if (!_fs.default.existsSync(packDir)) {
115
+ color.error('`package.json` doesn\'t exist');
116
+ return false;
117
+ }
118
+ const packageName = JSON.parse(_fs.default.readFileSync(packDir, {
119
+ encoding: 'utf-8'
120
+ })).name;
121
+
122
+ // Step 3: Publish
123
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
124
+ const code = await (0, _publish.processPackage)(!args.release, false, url, key, packageName, false, undefined, host, registry);
125
+ if (code !== 0) return false;
126
+
127
+ // Step 4: Open browser
128
+ const webUrl = getWebUrl(url);
129
+ color.success(`Opening ${webUrl}`);
130
+ openBrowser(webUrl);
131
+ return true;
132
+ }
package/bin/grok.js CHANGED
@@ -18,6 +18,8 @@ const commands = {
18
18
  init: require('./commands/init').init,
19
19
  link: require('./commands/link').link,
20
20
  publish: require('./commands/publish').publish,
21
+ report: require('./commands/report').report,
22
+ run: require('./commands/run').run,
21
23
  test: require('./commands/test').test,
22
24
  testall: require('./commands/test-all').testAll,
23
25
  stresstest: require('./commands/stress-tests').stressTests,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datagrok-tools",
3
- "version": "6.1.5",
3
+ "version": "6.1.7",
4
4
  "description": "Utility to upload and publish packages to Datagrok",
5
5
  "homepage": "https://github.com/datagrok-ai/public/tree/master/tools#readme",
6
6
  "dependencies": {