datagrok-tools 6.1.10 → 6.1.12

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.
@@ -12,19 +12,73 @@ 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':
23
26
  return await handleTicket(args);
27
+ case 'comment':
28
+ return await handleComment(args);
29
+ case 'label':
30
+ return await handleLabel(args);
24
31
  default:
25
32
  return false;
26
33
  }
27
34
  }
35
+ async function fetchReportMeta(url, token, number) {
36
+ const resp = await fetch(`${url}/reports?text=number%3D${encodeURIComponent(number)}`, {
37
+ headers: {
38
+ Authorization: token
39
+ }
40
+ });
41
+ if (!resp.ok) return null;
42
+ const arr = await resp.json();
43
+ if (!Array.isArray(arr) || arr.length === 0) return null;
44
+ return arr[0];
45
+ }
46
+ async function fetchReportBundle(instance, number) {
47
+ const {
48
+ url,
49
+ key
50
+ } = (0, _testUtils.getDevKey)(instance);
51
+ const token = await (0, _testUtils.getToken)(url, key);
52
+ const byNumberResp = await fetch(`${url}/reports/by-number/${encodeURIComponent(number)}/zip`, {
53
+ headers: {
54
+ Authorization: token
55
+ }
56
+ });
57
+ if (byNumberResp.ok) {
58
+ const zipBuffer = await byNumberResp.buffer();
59
+ const metaJson = await fetchReportMeta(url, token, number);
60
+ return {
61
+ zipBuffer,
62
+ metaJson
63
+ };
64
+ }
65
+ if (byNumberResp.status !== 404 && byNumberResp.status !== 405) throw new Error(`HTTP ${byNumberResp.status} from /reports/by-number/${number}/zip`);
66
+ const metaJson = await fetchReportMeta(url, token, number);
67
+ if (metaJson == null) throw new Error(`Report #${number} not found`);
68
+ const reportId = metaJson.id || metaJson.Id;
69
+ if (!reportId) throw new Error('Report found but has no id field');
70
+ const downloadResp = await fetch(`${url}/reports/${reportId}/zip`, {
71
+ headers: {
72
+ Authorization: token
73
+ }
74
+ });
75
+ if (!downloadResp.ok) throw new Error(`Report download failed (HTTP ${downloadResp.status})`);
76
+ const zipBuffer = await downloadResp.buffer();
77
+ return {
78
+ zipBuffer,
79
+ metaJson
80
+ };
81
+ }
28
82
  async function handleFetch(args) {
29
83
  const instance = args._[2];
30
84
  const number = args._[3];
@@ -33,49 +87,207 @@ async function handleFetch(args) {
33
87
  return false;
34
88
  }
35
89
  try {
90
+ console.log(`Fetching report #${number}...`);
36
91
  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;
92
+ zipBuffer,
93
+ metaJson
94
+ } = await fetchReportBundle(instance, number);
95
+ const outputPath = _path.default.join(_os.default.tmpdir(), `report_${instance}_${number}.zip`);
96
+ _fs.default.writeFileSync(outputPath, zipBuffer);
97
+ if (metaJson != null) {
98
+ const metaPath = outputPath.replace('.zip', '_meta.json');
99
+ _fs.default.writeFileSync(metaPath, JSON.stringify(metaJson, null, 2));
50
100
  }
51
- const results = await searchResp.json();
52
- if (!Array.isArray(results) || results.length === 0) {
53
- color.error(`Report #${number} not found`);
101
+ color.success(`Report saved to: ${outputPath}`);
102
+ console.log(outputPath);
103
+ return true;
104
+ } catch (err) {
105
+ color.error(`Error: ${err.message}`);
106
+ return false;
107
+ }
108
+ }
109
+ function looksLikePath(s) {
110
+ if (s.includes('/') || s.includes('\\')) return true;
111
+ if (/\.(zip|json)$/i.test(s)) return true;
112
+ // Only treat a bare name as a path if it's an actual file. A *directory*
113
+ // collision (e.g. running `grok report read public 2147` from a checkout
114
+ // that has a `public/` submodule next to it) must not flip into the
115
+ // single-arg path branch — that would EISDIR on `readFileSync(p)`.
116
+ if (_fs.default.existsSync(s)) {
117
+ try {
118
+ return _fs.default.statSync(s).isFile();
119
+ } catch {
54
120
  return false;
55
121
  }
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');
122
+ }
123
+ return false;
124
+ }
125
+ function loadFromZip(z) {
126
+ const entries = z.getEntries();
127
+ const reportEntry = entries.find(e => e.entryName.toLowerCase().endsWith('report.json'));
128
+ if (reportEntry == null) {
129
+ const names = entries.map(e => e.entryName).join(', ');
130
+ throw new Error(`report.json not found in zip. Contents: ${names}`);
131
+ }
132
+ const data = JSON.parse(reportEntry.getData().toString('utf-8'));
133
+ const d42Names = entries.map(e => e.entryName).filter(n => n.toLowerCase().endsWith('.d42'));
134
+ return {
135
+ data,
136
+ zip: z,
137
+ d42Names
138
+ };
139
+ }
140
+ function loadFromBuffer(buf) {
141
+ try {
142
+ const z = new AdmZip(buf);
143
+ return loadFromZip(z);
144
+ } catch {
145
+ const text = buf.toString('utf-8');
146
+ return {
147
+ data: JSON.parse(text),
148
+ zip: null,
149
+ d42Names: []
150
+ };
151
+ }
152
+ }
153
+ function loadFromPath(p) {
154
+ if (p.toLowerCase().endsWith('.json')) {
155
+ const text = _fs.default.readFileSync(p, 'utf-8');
156
+ return {
157
+ data: JSON.parse(text),
158
+ zip: null,
159
+ d42Names: []
160
+ };
161
+ }
162
+ return loadFromBuffer(_fs.default.readFileSync(p));
163
+ }
164
+ function unwrapEnvelope(data) {
165
+ if (data && typeof data === 'object' && '#type' in data && 'data' in data && typeof data.data === 'object' && data.data != null) {
166
+ const meta = {};
167
+ if (data.id != null) meta.id = data.id;
168
+ if (data.createdOn != null) meta.createdOn = data.createdOn;
169
+ if (data['#type'] != null) meta['#type'] = data['#type'];
170
+ return {
171
+ meta,
172
+ body: data.data
173
+ };
174
+ }
175
+ return {
176
+ meta: {},
177
+ body: data
178
+ };
179
+ }
180
+ function loadSidecar(inputPath) {
181
+ const stem = inputPath.replace(/\.[^./\\]+$/, '');
182
+ const sidecar = `${stem}_meta.json`;
183
+ if (sidecar !== inputPath && _fs.default.existsSync(sidecar)) return JSON.parse(_fs.default.readFileSync(sidecar, 'utf-8'));
184
+ return null;
185
+ }
186
+ function ensureParentDir(p) {
187
+ const dir = _path.default.dirname(p);
188
+ if (dir && dir !== '.' && !_fs.default.existsSync(dir)) _fs.default.mkdirSync(dir, {
189
+ recursive: true
190
+ });
191
+ }
192
+ function extractScreenshot(zip, body, outPath) {
193
+ if (zip != null) {
194
+ const entries = zip.getEntries();
195
+ const e = entries.find(x => x.entryName.toLowerCase().endsWith('screenshot.png'));
196
+ if (e != null) {
197
+ ensureParentDir(outPath);
198
+ _fs.default.writeFileSync(outPath, e.getData());
199
+ return true;
200
+ }
201
+ }
202
+ const b64 = body && body.screenshot;
203
+ if (typeof b64 === 'string' && b64.length > 0) {
204
+ const stripped = b64.includes(',') ? b64.slice(b64.indexOf(',') + 1) : b64;
205
+ try {
206
+ const buf = Buffer.from(stripped, 'base64');
207
+ ensureParentDir(outPath);
208
+ _fs.default.writeFileSync(outPath, buf);
209
+ return true;
210
+ } catch {
60
211
  return false;
61
212
  }
62
- console.log(`Downloading report ${reportId}...`);
63
- const downloadResp = await fetch(`${url}/reports/${reportId}/zip`, {
64
- headers: {
65
- Authorization: token
213
+ }
214
+ return false;
215
+ }
216
+ function extractD42(zip, names, outDir) {
217
+ if (zip == null || names.length === 0) return [];
218
+ _fs.default.mkdirSync(outDir, {
219
+ recursive: true
220
+ });
221
+ const written = [];
222
+ for (const name of names) {
223
+ const entry = zip.getEntry(name);
224
+ if (entry == null) continue;
225
+ const out = _path.default.join(outDir, _path.default.basename(name));
226
+ _fs.default.writeFileSync(out, entry.getData());
227
+ written.push(out);
228
+ }
229
+ return written;
230
+ }
231
+ async function handleRead(args) {
232
+ const positional = args._.slice(2);
233
+ if (positional.length === 0) {
234
+ color.error('Usage: grok report read <path> | <instance> <number>');
235
+ return false;
236
+ }
237
+ const screenshotOut = args['extract-screenshot'];
238
+ const d42Dir = args['extract-d42'];
239
+ const extractActions = args['extract-actions'] === true;
240
+ try {
241
+ let loaded;
242
+ let sidecarMeta = null;
243
+ let inputPath = null;
244
+ let networkBase = null;
245
+ if (positional.length >= 2 && !looksLikePath(positional[0])) {
246
+ const [instance, number] = positional;
247
+ const bundle = await fetchReportBundle(instance, number);
248
+ loaded = loadFromBuffer(bundle.zipBuffer);
249
+ sidecarMeta = bundle.metaJson;
250
+ networkBase = _path.default.join(_os.default.tmpdir(), `report_${instance}_${number}`);
251
+ } else {
252
+ inputPath = _path.default.resolve(positional[0]);
253
+ if (!_fs.default.existsSync(inputPath)) {
254
+ color.error(`File not found: ${inputPath}`);
255
+ return false;
66
256
  }
67
- });
68
- if (!downloadResp.ok) {
69
- color.error(`Report download failed (HTTP ${downloadResp.status})`);
70
- return false;
257
+ loaded = loadFromPath(inputPath);
258
+ sidecarMeta = loadSidecar(inputPath);
71
259
  }
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);
260
+ const {
261
+ meta: envelopeMeta,
262
+ body
263
+ } = unwrapEnvelope(loaded.data);
264
+ const meta = Object.assign({}, envelopeMeta, sidecarMeta || {});
265
+ const output = {
266
+ meta,
267
+ ...body
268
+ };
269
+ const files = {};
270
+ if (screenshotOut) {
271
+ if (extractScreenshot(loaded.zip, body, screenshotOut)) {
272
+ files.screenshot = screenshotOut;
273
+ output.screenshot = screenshotOut;
274
+ }
275
+ }
276
+ if (d42Dir) {
277
+ const written = extractD42(loaded.zip, loaded.d42Names, d42Dir);
278
+ if (written.length > 0) files.d42 = written;
279
+ }
280
+ if (extractActions && Array.isArray(body && body.actions)) {
281
+ const stem = inputPath != null ? inputPath.replace(/\.[^./\\]+$/, '') : networkBase;
282
+ if (stem != null) {
283
+ const actionsPath = `${stem}_actions.json`;
284
+ _fs.default.writeFileSync(actionsPath, JSON.stringify(body.actions, null, 2));
285
+ files.actions = actionsPath;
286
+ }
287
+ }
288
+ if (Object.keys(files).length > 0) output.files = files;
289
+ process.stdout.write(JSON.stringify(output));
290
+ process.stdout.write('\n');
79
291
  return true;
80
292
  } catch (err) {
81
293
  color.error(`Error: ${err.message}`);
@@ -181,4 +393,132 @@ async function handleTicket(args) {
181
393
  color.error(`Error: ${err.message}`);
182
394
  return false;
183
395
  }
396
+ }
397
+
398
+ // ─── JIRA REST helpers (used by `grok report comment` / `grok report label`) ─
399
+ //
400
+ // These talk DIRECTLY to Atlassian Cloud REST v2 (not Datagrok). Auth is HTTP
401
+ // Basic with `JIRA_USER` (Atlassian email) + `JIRA_TOKEN` (API token from
402
+ // id.atlassian.com/manage-profile/security/api-tokens). Base URL defaults to
403
+ // the Datagrok org instance; override via --jira-url or $JIRA_URL.
404
+ //
405
+ // Why v2 and not v3: v3 requires comment bodies in ADF (Atlassian Document
406
+ // Format) JSON; v2 accepts plain text / wiki-markup. The handoff prompt emits
407
+ // markdown, which JIRA's plain-text path renders acceptably.
408
+
409
+ function resolveJiraBase(args) {
410
+ const cli = args['jira-url'] || '';
411
+ const env = process.env.JIRA_URL || '';
412
+ return (cli || env || 'https://reddata.atlassian.net').replace(/\/+$/, '');
413
+ }
414
+ function jiraAuthHeader() {
415
+ const user = process.env.JIRA_USER;
416
+ const token = process.env.JIRA_TOKEN;
417
+ if (!user || !token) return null;
418
+ return 'Basic ' + Buffer.from(`${user}:${token}`).toString('base64');
419
+ }
420
+ async function handleComment(args) {
421
+ const ticket = args._[2];
422
+ if (!ticket) {
423
+ color.error('Usage: grok report comment <ticket-key> [--body <text> | --body-file <path>] [--jira-url <url>]');
424
+ return false;
425
+ }
426
+ const auth = jiraAuthHeader();
427
+ if (auth == null) {
428
+ color.error('JIRA_USER and JIRA_TOKEN env vars are required for `grok report comment`.');
429
+ return false;
430
+ }
431
+ let body;
432
+ if (typeof args['body-file'] === 'string') {
433
+ const p = args['body-file'];
434
+ try {
435
+ body = _fs.default.readFileSync(p, 'utf-8');
436
+ } catch (e) {
437
+ color.error(`Failed to read --body-file ${p}: ${e.message}`);
438
+ return false;
439
+ }
440
+ } else if (typeof args['body'] === 'string') {
441
+ body = args['body'];
442
+ } else {
443
+ // Read from stdin if neither --body nor --body-file is provided.
444
+ body = _fs.default.readFileSync(0, 'utf-8');
445
+ }
446
+ if (!body || body.trim().length === 0) {
447
+ color.error('Comment body is empty (use --body, --body-file, or pipe to stdin).');
448
+ return false;
449
+ }
450
+ const base = resolveJiraBase(args);
451
+ const url = `${base}/rest/api/2/issue/${encodeURIComponent(ticket)}/comment`;
452
+ try {
453
+ const resp = await fetch(url, {
454
+ method: 'POST',
455
+ headers: {
456
+ Authorization: auth,
457
+ 'Content-Type': 'application/json',
458
+ Accept: 'application/json'
459
+ },
460
+ body: JSON.stringify({
461
+ body
462
+ })
463
+ });
464
+ if (resp.status !== 200 && resp.status !== 201) {
465
+ const text = await resp.text();
466
+ color.error(`JIRA comment POST failed (HTTP ${resp.status}): ${text.slice(0, 400)}`);
467
+ return false;
468
+ }
469
+ const result = await resp.json();
470
+ const id = result && (result.id || result.Id);
471
+ if (!id) {
472
+ color.error(`JIRA returned no comment id: ${JSON.stringify(result).slice(0, 200)}`);
473
+ return false;
474
+ }
475
+ color.success(`Posted comment ${id} on ${ticket}`);
476
+ console.log(id);
477
+ return true;
478
+ } catch (err) {
479
+ color.error(`Error: ${err.message}`);
480
+ return false;
481
+ }
482
+ }
483
+ async function handleLabel(args) {
484
+ const ticket = args._[2];
485
+ const labels = args._.slice(3).filter(s => s.length > 0);
486
+ if (!ticket || labels.length === 0) {
487
+ color.error('Usage: grok report label <ticket-key> <label> [<label2> ...] [--jira-url <url>]');
488
+ return false;
489
+ }
490
+ const auth = jiraAuthHeader();
491
+ if (auth == null) {
492
+ color.error('JIRA_USER and JIRA_TOKEN env vars are required for `grok report label`.');
493
+ return false;
494
+ }
495
+ const base = resolveJiraBase(args);
496
+ const url = `${base}/rest/api/2/issue/${encodeURIComponent(ticket)}`;
497
+ const update = {
498
+ labels: labels.map(l => ({
499
+ add: l
500
+ }))
501
+ };
502
+ try {
503
+ const resp = await fetch(url, {
504
+ method: 'PUT',
505
+ headers: {
506
+ Authorization: auth,
507
+ 'Content-Type': 'application/json'
508
+ },
509
+ body: JSON.stringify({
510
+ update
511
+ })
512
+ });
513
+ if (resp.status !== 204 && resp.status !== 200) {
514
+ const text = await resp.text();
515
+ color.error(`JIRA label PUT failed (HTTP ${resp.status}): ${text.slice(0, 400)}`);
516
+ return false;
517
+ }
518
+ color.success(`Applied labels to ${ticket}: ${labels.join(', ')}`);
519
+ return true;
520
+ } catch (err) {
521
+ color.error(`Error: ${err.message}`);
522
+ return false;
523
+ }
184
524
  }