datagrok-tools 6.1.9 → 6.1.11

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.
@@ -13,7 +13,7 @@ Commands:
13
13
  add Add an object template
14
14
  api Create wrapper functions
15
15
  build Build a package or multiple packages
16
- check Check package content (function signatures, etc.)
16
+ check Check package content (function signatures, etgc.)
17
17
  claude Launch Claude Code in a Datagrok dev container
18
18
  config Create and manage config files
19
19
  create Create a package
@@ -26,6 +26,7 @@ Commands:
26
26
  test Run package tests
27
27
  testall Run packages tests
28
28
  migrate Migrate legacy tags to meta.role
29
+ server (s) Manage a Datagrok server (list/get/delete entities, run functions)
29
30
 
30
31
  To get help on a particular command, use:
31
32
  grok <command> --help
@@ -343,22 +344,95 @@ Examples:
343
344
  grok run https://my.datagrok.ai/api --key abc123
344
345
  `;
345
346
  const HELP_REPORT = `
346
- Usage: grok report <subcommand> <instance> <id>
347
+ Usage: grok report <subcommand> [args]
347
348
 
348
349
  Manage Datagrok user error reports
349
350
 
350
351
  Subcommands:
351
352
  fetch Download a report zip from a managed instance
353
+ read Normalize a report (zip or json) into one JSON object on stdout
352
354
  resolve Mark a report as resolved
353
355
  ticket Create a JIRA ticket for a report via the Datlas API
354
356
 
357
+ Read flags:
358
+ --extract-screenshot <path> Write the screenshot binary to <path>
359
+ --extract-d42 <dir> Unpack .d42 sidecar tables into <dir>
360
+ --extract-actions Write a sibling <stem>_actions.json
361
+
355
362
  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
363
+ grok report fetch dev 1528 Download report #1528 from the 'dev' instance
364
+ grok report read /tmp/report.zip Print normalized JSON for a local zip
365
+ grok report read /tmp/report.json Print normalized JSON for a raw report.json
366
+ grok report read dev 1528 Fetch + normalize report #1528 from 'dev'
367
+ grok report read /tmp/report.zip --extract-screenshot ./shot.png
368
+ grok report resolve dev 1528 Resolve report #1528 on the 'dev' instance
358
369
  grok report ticket dev <report-uuid> Create a JIRA ticket for a report
359
370
 
360
371
  The instance name must match a server alias in ~/.grok/config.yaml.
361
372
  `;
373
+ const HELP_SERVER = `
374
+ Usage: grok server <entity> <verb> [args] [options]
375
+ grok s <entity> <verb> [args] [options]
376
+
377
+ Manage a Datagrok server from the command line.
378
+
379
+ Entities:
380
+ users, groups, functions, connections, queries, scripts, packages, reports, files, tables
381
+
382
+ Verbs:
383
+ list List entities
384
+ get Get a single entity by ID or name
385
+ delete Delete an entity by ID
386
+
387
+ Special commands:
388
+ grok s functions run <Name:func(args)> Call a function
389
+ grok s functions list [--type <t>] [--language <l>] [--package <p>] [--filter <expr>]
390
+ Type: script|query|function|package; language applies to scripts
391
+ grok s files list [path] [-r] List files (recursive with -r)
392
+ grok s shares add <entity> <group>[,<group>...] [--access View|Edit]
393
+ Share an entity with groups
394
+ grok s shares list <entity-id> List who an entity (UUID) is shared with
395
+ grok s users save --json user.json Create or update a user from JSON
396
+ grok s users block <id-or-login> Block a user from the platform
397
+ grok s users unblock <id-or-login> Unblock a previously blocked user
398
+ grok s groups save --json group.json [--save-relations]
399
+ Create or update a group from JSON
400
+ grok s connections save --json conn.json [--save-credentials]
401
+ Create or update a connection from JSON
402
+ grok s connections test <id-or-name> Test connectivity of an existing connection
403
+ grok s connections test --json conn.json Test connectivity of a connection defined in JSON
404
+ grok s tables upload <name> <file.csv> Upload a CSV as a Datagrok table
405
+ grok s tables download <name-or-id> [-O <file>] Download a table as CSV (stdout by default)
406
+ grok s raw <METHOD> <path> Hit any API endpoint
407
+ grok s describe <entity-type> Show entity JSON schema
408
+
409
+ Options:
410
+ --host <alias|url> Server alias from config or full URL
411
+ --output <format> Output format: table (default), json, csv, quiet
412
+ --filter <text> Smart filter expression
413
+ --limit <n> Page size (default: 50)
414
+ --offset <n> Start offset (default: 0)
415
+ -r, --recursive Recursive (for files list)
416
+ --json <file> Read function parameters from JSON file
417
+
418
+ Examples:
419
+ grok s users list
420
+ grok s connections list --filter "PostgreSQL" --output json
421
+ grok s connections get <id>
422
+ grok s connections delete <id>
423
+ grok s connections save --json conn.json --save-credentials
424
+ grok s connections test "JohnDoe:MyConnection"
425
+ grok s connections test --json conn.json
426
+ grok s users save --json user.json
427
+ grok s groups save --json group.json --save-relations
428
+ grok s shares add "JohnDoe:MyConnection" Chemists,Admins --access Edit
429
+ grok s shares list <entity-uuid>
430
+ grok s functions run 'Chem:smilesToMw("ccc")'
431
+ grok s files list "System:AppData" -r
432
+ grok s raw GET /api/users/current
433
+ grok s describe connections
434
+ grok s users list --host dev
435
+ `;
362
436
  const help = exports.help = {
363
437
  add: HELP_ADD,
364
438
  api: HELP_API,
@@ -376,5 +450,7 @@ const help = exports.help = {
376
450
  test: HELP_TEST,
377
451
  testall: HELP_TESTALL,
378
452
  migrate: HELP_MIGRATE,
453
+ server: HELP_SERVER,
454
+ s: HELP_SERVER,
379
455
  help: HELP
380
456
  };
@@ -12,11 +12,14 @@ var color = _interopRequireWildcard(require("../utils/color-utils"));
12
12
  var _testUtils = require("../utils/test-utils");
13
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
14
  const fetch = require('node-fetch');
15
+ const AdmZip = require('adm-zip');
15
16
  async function report(args) {
16
17
  const subcommand = args._[1];
17
18
  switch (subcommand) {
18
19
  case 'fetch':
19
20
  return await handleFetch(args);
21
+ case 'read':
22
+ return await handleRead(args);
20
23
  case 'resolve':
21
24
  return await handleResolve(args);
22
25
  case 'ticket':
@@ -25,6 +28,53 @@ async function report(args) {
25
28
  return false;
26
29
  }
27
30
  }
31
+ async function fetchReportMeta(url, token, number) {
32
+ const resp = await fetch(`${url}/reports?text=number%3D${encodeURIComponent(number)}`, {
33
+ headers: {
34
+ Authorization: token
35
+ }
36
+ });
37
+ if (!resp.ok) return null;
38
+ const arr = await resp.json();
39
+ if (!Array.isArray(arr) || arr.length === 0) return null;
40
+ return arr[0];
41
+ }
42
+ async function fetchReportBundle(instance, number) {
43
+ const {
44
+ url,
45
+ key
46
+ } = (0, _testUtils.getDevKey)(instance);
47
+ const token = await (0, _testUtils.getToken)(url, key);
48
+ const byNumberResp = await fetch(`${url}/reports/by-number/${encodeURIComponent(number)}/zip`, {
49
+ headers: {
50
+ Authorization: token
51
+ }
52
+ });
53
+ if (byNumberResp.ok) {
54
+ const zipBuffer = await byNumberResp.buffer();
55
+ const metaJson = await fetchReportMeta(url, token, number);
56
+ return {
57
+ zipBuffer,
58
+ metaJson
59
+ };
60
+ }
61
+ if (byNumberResp.status !== 404 && byNumberResp.status !== 405) throw new Error(`HTTP ${byNumberResp.status} from /reports/by-number/${number}/zip`);
62
+ const metaJson = await fetchReportMeta(url, token, number);
63
+ if (metaJson == null) throw new Error(`Report #${number} not found`);
64
+ const reportId = metaJson.id || metaJson.Id;
65
+ if (!reportId) throw new Error('Report found but has no id field');
66
+ const downloadResp = await fetch(`${url}/reports/${reportId}/zip`, {
67
+ headers: {
68
+ Authorization: token
69
+ }
70
+ });
71
+ if (!downloadResp.ok) throw new Error(`Report download failed (HTTP ${downloadResp.status})`);
72
+ const zipBuffer = await downloadResp.buffer();
73
+ return {
74
+ zipBuffer,
75
+ metaJson
76
+ };
77
+ }
28
78
  async function handleFetch(args) {
29
79
  const instance = args._[2];
30
80
  const number = args._[3];
@@ -33,49 +83,194 @@ async function handleFetch(args) {
33
83
  return false;
34
84
  }
35
85
  try {
86
+ console.log(`Fetching report #${number}...`);
36
87
  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;
88
+ zipBuffer,
89
+ metaJson
90
+ } = await fetchReportBundle(instance, number);
91
+ const outputPath = _path.default.join(_os.default.tmpdir(), `report_${instance}_${number}.zip`);
92
+ _fs.default.writeFileSync(outputPath, zipBuffer);
93
+ if (metaJson != null) {
94
+ const metaPath = outputPath.replace('.zip', '_meta.json');
95
+ _fs.default.writeFileSync(metaPath, JSON.stringify(metaJson, null, 2));
50
96
  }
51
- const results = await searchResp.json();
52
- if (!Array.isArray(results) || results.length === 0) {
53
- color.error(`Report #${number} not found`);
54
- return false;
97
+ color.success(`Report saved to: ${outputPath}`);
98
+ console.log(outputPath);
99
+ return true;
100
+ } catch (err) {
101
+ color.error(`Error: ${err.message}`);
102
+ return false;
103
+ }
104
+ }
105
+ function looksLikePath(s) {
106
+ return s.includes('/') || s.includes('\\') || /\.(zip|json)$/i.test(s) || _fs.default.existsSync(s);
107
+ }
108
+ function loadFromZip(z) {
109
+ const entries = z.getEntries();
110
+ const reportEntry = entries.find(e => e.entryName.toLowerCase().endsWith('report.json'));
111
+ if (reportEntry == null) {
112
+ const names = entries.map(e => e.entryName).join(', ');
113
+ throw new Error(`report.json not found in zip. Contents: ${names}`);
114
+ }
115
+ const data = JSON.parse(reportEntry.getData().toString('utf-8'));
116
+ const d42Names = entries.map(e => e.entryName).filter(n => n.toLowerCase().endsWith('.d42'));
117
+ return {
118
+ data,
119
+ zip: z,
120
+ d42Names
121
+ };
122
+ }
123
+ function loadFromBuffer(buf) {
124
+ try {
125
+ const z = new AdmZip(buf);
126
+ return loadFromZip(z);
127
+ } catch {
128
+ const text = buf.toString('utf-8');
129
+ return {
130
+ data: JSON.parse(text),
131
+ zip: null,
132
+ d42Names: []
133
+ };
134
+ }
135
+ }
136
+ function loadFromPath(p) {
137
+ if (p.toLowerCase().endsWith('.json')) {
138
+ const text = _fs.default.readFileSync(p, 'utf-8');
139
+ return {
140
+ data: JSON.parse(text),
141
+ zip: null,
142
+ d42Names: []
143
+ };
144
+ }
145
+ return loadFromBuffer(_fs.default.readFileSync(p));
146
+ }
147
+ function unwrapEnvelope(data) {
148
+ if (data && typeof data === 'object' && '#type' in data && 'data' in data && typeof data.data === 'object' && data.data != null) {
149
+ const meta = {};
150
+ if (data.id != null) meta.id = data.id;
151
+ if (data.createdOn != null) meta.createdOn = data.createdOn;
152
+ if (data['#type'] != null) meta['#type'] = data['#type'];
153
+ return {
154
+ meta,
155
+ body: data.data
156
+ };
157
+ }
158
+ return {
159
+ meta: {},
160
+ body: data
161
+ };
162
+ }
163
+ function loadSidecar(inputPath) {
164
+ const stem = inputPath.replace(/\.[^./\\]+$/, '');
165
+ const sidecar = `${stem}_meta.json`;
166
+ if (sidecar !== inputPath && _fs.default.existsSync(sidecar)) return JSON.parse(_fs.default.readFileSync(sidecar, 'utf-8'));
167
+ return null;
168
+ }
169
+ function ensureParentDir(p) {
170
+ const dir = _path.default.dirname(p);
171
+ if (dir && dir !== '.' && !_fs.default.existsSync(dir)) _fs.default.mkdirSync(dir, {
172
+ recursive: true
173
+ });
174
+ }
175
+ function extractScreenshot(zip, body, outPath) {
176
+ if (zip != null) {
177
+ const entries = zip.getEntries();
178
+ const e = entries.find(x => x.entryName.toLowerCase().endsWith('screenshot.png'));
179
+ if (e != null) {
180
+ ensureParentDir(outPath);
181
+ _fs.default.writeFileSync(outPath, e.getData());
182
+ return true;
55
183
  }
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');
184
+ }
185
+ const b64 = body && body.screenshot;
186
+ if (typeof b64 === 'string' && b64.length > 0) {
187
+ const stripped = b64.includes(',') ? b64.slice(b64.indexOf(',') + 1) : b64;
188
+ try {
189
+ const buf = Buffer.from(stripped, 'base64');
190
+ ensureParentDir(outPath);
191
+ _fs.default.writeFileSync(outPath, buf);
192
+ return true;
193
+ } catch {
60
194
  return false;
61
195
  }
62
- console.log(`Downloading report ${reportId}...`);
63
- const downloadResp = await fetch(`${url}/reports/${reportId}/zip`, {
64
- headers: {
65
- Authorization: token
196
+ }
197
+ return false;
198
+ }
199
+ function extractD42(zip, names, outDir) {
200
+ if (zip == null || names.length === 0) return [];
201
+ _fs.default.mkdirSync(outDir, {
202
+ recursive: true
203
+ });
204
+ const written = [];
205
+ for (var name of names) {
206
+ const entry = zip.getEntry(name);
207
+ if (entry == null) continue;
208
+ const out = _path.default.join(outDir, _path.default.basename(name));
209
+ _fs.default.writeFileSync(out, entry.getData());
210
+ written.push(out);
211
+ }
212
+ return written;
213
+ }
214
+ async function handleRead(args) {
215
+ const positional = args._.slice(2);
216
+ if (positional.length === 0) {
217
+ color.error('Usage: grok report read <path> | <instance> <number>');
218
+ return false;
219
+ }
220
+ const screenshotOut = args['extract-screenshot'];
221
+ const d42Dir = args['extract-d42'];
222
+ const extractActions = args['extract-actions'] === true;
223
+ try {
224
+ let loaded;
225
+ let sidecarMeta = null;
226
+ let inputPath = null;
227
+ let networkBase = null;
228
+ if (positional.length >= 2 && !looksLikePath(positional[0])) {
229
+ const [instance, number] = positional;
230
+ const bundle = await fetchReportBundle(instance, number);
231
+ loaded = loadFromBuffer(bundle.zipBuffer);
232
+ sidecarMeta = bundle.metaJson;
233
+ networkBase = _path.default.join(_os.default.tmpdir(), `report_${instance}_${number}`);
234
+ } else {
235
+ inputPath = _path.default.resolve(positional[0]);
236
+ if (!_fs.default.existsSync(inputPath)) {
237
+ color.error(`File not found: ${inputPath}`);
238
+ return false;
66
239
  }
67
- });
68
- if (!downloadResp.ok) {
69
- color.error(`Report download failed (HTTP ${downloadResp.status})`);
70
- return false;
240
+ loaded = loadFromPath(inputPath);
241
+ sidecarMeta = loadSidecar(inputPath);
71
242
  }
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);
243
+ const {
244
+ meta: envelopeMeta,
245
+ body
246
+ } = unwrapEnvelope(loaded.data);
247
+ const meta = Object.assign({}, envelopeMeta, sidecarMeta || {});
248
+ const output = {
249
+ meta,
250
+ ...body
251
+ };
252
+ const files = {};
253
+ if (screenshotOut) {
254
+ if (extractScreenshot(loaded.zip, body, screenshotOut)) {
255
+ files.screenshot = screenshotOut;
256
+ output.screenshot = screenshotOut;
257
+ }
258
+ }
259
+ if (d42Dir) {
260
+ const written = extractD42(loaded.zip, loaded.d42Names, d42Dir);
261
+ if (written.length > 0) files.d42 = written;
262
+ }
263
+ if (extractActions && Array.isArray(body && body.actions)) {
264
+ const stem = inputPath != null ? inputPath.replace(/\.[^./\\]+$/, '') : networkBase;
265
+ if (stem != null) {
266
+ const actionsPath = `${stem}_actions.json`;
267
+ _fs.default.writeFileSync(actionsPath, JSON.stringify(body.actions, null, 2));
268
+ files.actions = actionsPath;
269
+ }
270
+ }
271
+ if (Object.keys(files).length > 0) output.files = files;
272
+ process.stdout.write(JSON.stringify(output));
273
+ process.stdout.write('\n');
79
274
  return true;
80
275
  } catch (err) {
81
276
  color.error(`Error: ${err.message}`);