datagrok-tools 6.1.4 → 6.1.6
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/bin/commands/help.js +19 -0
- package/bin/commands/publish.js +93 -51
- package/bin/commands/report.js +184 -0
- package/bin/grok.js +1 -0
- package/package.json +1 -1
package/bin/commands/help.js
CHANGED
|
@@ -21,6 +21,7 @@ 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)
|
|
24
25
|
test Run package tests
|
|
25
26
|
testall Run packages tests
|
|
26
27
|
migrate Migrate legacy tags to meta.role
|
|
@@ -321,6 +322,23 @@ Examples:
|
|
|
321
322
|
// file and converting your scripts in the \`package.json\` file
|
|
322
323
|
// `;
|
|
323
324
|
|
|
325
|
+
const HELP_REPORT = `
|
|
326
|
+
Usage: grok report <subcommand> <instance> <id>
|
|
327
|
+
|
|
328
|
+
Manage Datagrok user error reports
|
|
329
|
+
|
|
330
|
+
Subcommands:
|
|
331
|
+
fetch Download a report zip from a managed instance
|
|
332
|
+
resolve Mark a report as resolved
|
|
333
|
+
ticket Create a JIRA ticket for a report via the Datlas API
|
|
334
|
+
|
|
335
|
+
Examples:
|
|
336
|
+
grok report fetch dev 1528 Download report #1528 from the 'dev' instance
|
|
337
|
+
grok report resolve dev 1528 Resolve report #1528 on the 'dev' instance
|
|
338
|
+
grok report ticket dev <report-uuid> Create a JIRA ticket for a report
|
|
339
|
+
|
|
340
|
+
The instance name must match a server alias in ~/.grok/config.yaml.
|
|
341
|
+
`;
|
|
324
342
|
const help = exports.help = {
|
|
325
343
|
add: HELP_ADD,
|
|
326
344
|
api: HELP_API,
|
|
@@ -333,6 +351,7 @@ const help = exports.help = {
|
|
|
333
351
|
init: HELP_INIT,
|
|
334
352
|
link: HELP_LINK,
|
|
335
353
|
publish: HELP_PUBLISH,
|
|
354
|
+
report: HELP_REPORT,
|
|
336
355
|
test: HELP_TEST,
|
|
337
356
|
testall: HELP_TESTALL,
|
|
338
357
|
migrate: HELP_MIGRATE,
|
package/bin/commands/publish.js
CHANGED
|
@@ -52,7 +52,7 @@ function discoverDockerfiles(packageName, version, debug) {
|
|
|
52
52
|
if (!_fs.default.existsSync(dockerfilePath)) continue;
|
|
53
53
|
const cleanName = utils.removeScope(packageName).toLowerCase();
|
|
54
54
|
const imageName = `${cleanName}-${entry.name.toLowerCase()}`;
|
|
55
|
-
const imageTag =
|
|
55
|
+
const imageTag = version;
|
|
56
56
|
results.push({
|
|
57
57
|
imageName,
|
|
58
58
|
imageTag,
|
|
@@ -68,17 +68,26 @@ function dockerCommand(args) {
|
|
|
68
68
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
69
69
|
}).trim();
|
|
70
70
|
}
|
|
71
|
-
function localImageExists(fullName) {
|
|
71
|
+
function localImageExists(fullName, checkPlatform = true) {
|
|
72
72
|
try {
|
|
73
73
|
const output = dockerCommand(`images --format "{{.Repository}}:{{.Tag}}"`);
|
|
74
|
-
|
|
74
|
+
if (!output.split('\n').some(line => line.trim() === fullName)) return false;
|
|
75
|
+
if (checkPlatform) {
|
|
76
|
+
// Verify the image is linux/amd64 — ARM images won't run on Datagrok servers
|
|
77
|
+
const inspect = dockerCommand(`inspect --format "{{.Os}}/{{.Architecture}}" ${fullName}`);
|
|
78
|
+
if (inspect.trim() !== 'linux/amd64') {
|
|
79
|
+
color.warn(`Local image ${fullName} is ${inspect.trim()}, not linux/amd64 — treating as not found`);
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return true;
|
|
75
84
|
} catch {
|
|
76
85
|
return false;
|
|
77
86
|
}
|
|
78
87
|
}
|
|
79
88
|
function dockerLogin(registry, devKey) {
|
|
80
89
|
try {
|
|
81
|
-
dockerCommand(`login ${registry} -u
|
|
90
|
+
dockerCommand(`login ${registry} -u any -p ${devKey}`);
|
|
82
91
|
return true;
|
|
83
92
|
} catch (e) {
|
|
84
93
|
color.warn(`Docker login to ${registry} failed: ${e.message || e}`);
|
|
@@ -106,10 +115,22 @@ function dockerPush(image) {
|
|
|
106
115
|
return false;
|
|
107
116
|
}
|
|
108
117
|
}
|
|
109
|
-
function
|
|
118
|
+
function isLocalhostRegistry(registry) {
|
|
119
|
+
return registry.startsWith('localhost') || registry.startsWith('127.0.0.1');
|
|
120
|
+
}
|
|
121
|
+
function dockerRemove(imageName) {
|
|
122
|
+
try {
|
|
123
|
+
dockerCommand(`rmi ${imageName}`);
|
|
124
|
+
return true;
|
|
125
|
+
} catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function dockerBuild(imageName, dockerfilePath, context, usePlatform = true) {
|
|
110
130
|
try {
|
|
111
|
-
|
|
112
|
-
|
|
131
|
+
const platformFlag = usePlatform ? '--platform linux/amd64 ' : '';
|
|
132
|
+
color.log(`Building Docker image ${imageName}${usePlatform ? ' (platform: linux/amd64)' : ''}...`);
|
|
133
|
+
execSync(`docker build ${platformFlag}-t ${imageName} -f ${dockerfilePath} ${context}`, {
|
|
113
134
|
encoding: 'utf-8',
|
|
114
135
|
stdio: 'inherit'
|
|
115
136
|
});
|
|
@@ -233,62 +254,83 @@ async function processDockerImages(packageName, version, registry, devKey, host,
|
|
|
233
254
|
if (dockerImages.length === 0) return;
|
|
234
255
|
color.log(`Found ${dockerImages.length} Dockerfile(s)`);
|
|
235
256
|
if (registry) dockerLogin(registry, devKey);
|
|
257
|
+
const needsPlatform = !!registry && !isLocalhostRegistry(registry);
|
|
236
258
|
for (const img of dockerImages) {
|
|
237
259
|
color.info(`Processing docker image ${img.fullLocalName}...`);
|
|
238
260
|
let result;
|
|
239
261
|
const dockerfileDir = _path.default.join('dockerfiles', img.dirName);
|
|
240
262
|
const dockerfilePath = _path.default.join(dockerfileDir, 'Dockerfile');
|
|
241
263
|
const contentHash = calculateFolderHash(_path.default.join(curDir, dockerfileDir));
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
264
|
+
|
|
265
|
+
// In release mode, append short content hash to tag for cache-busting
|
|
266
|
+
const shortHash = contentHash.substring(0, 8);
|
|
267
|
+
const registryTag = debug ? img.imageTag : `${img.imageTag}.${shortHash}`;
|
|
268
|
+
const remoteFullName = `${img.imageName}:${registryTag}`;
|
|
269
|
+
const buildAndPush = () => {
|
|
270
|
+
if (dockerBuild(img.fullLocalName, dockerfilePath, dockerfileDir, needsPlatform)) {
|
|
271
|
+
if (registry) {
|
|
272
|
+
const remoteTag = `${registry}/datagrok/${remoteFullName}`;
|
|
273
|
+
dockerTag(img.fullLocalName, remoteTag);
|
|
274
|
+
}
|
|
245
275
|
color.success(`Built and tagged ${img.fullLocalName}`);
|
|
246
|
-
|
|
247
|
-
result = await fallbackImage(img, host, devKey, registry, version, contentHash);
|
|
276
|
+
return pushImage(img.imageName, registryTag, registry);
|
|
248
277
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
278
|
+
return null;
|
|
279
|
+
};
|
|
280
|
+
if (rebuildDocker) {
|
|
281
|
+
// Delete old images before rebuilding
|
|
282
|
+
dockerRemove(img.fullLocalName);
|
|
283
|
+
if (registry) dockerRemove(`${registry}/datagrok/${remoteFullName}`);
|
|
284
|
+
result = buildAndPush() ?? (await fallbackImage(img, host, devKey, registry, version, contentHash));
|
|
252
285
|
} else {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
color.success(`Falling back to ${fallback.image} (dockerfile unchanged)`);
|
|
266
|
-
} else if (fallback.image && fallback.hashMatch === false && !skipDockerRebuild) {
|
|
267
|
-
color.warn(`Dockerfile folder has changed. Rebuilding image...`);
|
|
268
|
-
if (dockerBuild(img.fullLocalName, dockerfilePath, dockerfileDir)) {
|
|
269
|
-
result = pushImage(img, registry);
|
|
270
|
-
color.success(`Built and tagged ${img.fullLocalName}`);
|
|
271
|
-
} else {
|
|
272
|
-
result = {
|
|
273
|
-
image: fallback.image,
|
|
274
|
-
fallback: true,
|
|
275
|
-
requestedVersion: img.imageTag
|
|
276
|
-
};
|
|
277
|
-
color.warn(`Build failed. Falling back to ${fallback.image} (hash mismatch)`);
|
|
286
|
+
// Look for registry-qualified image first, then unqualified
|
|
287
|
+
let foundLocalName = null;
|
|
288
|
+
if (registry) {
|
|
289
|
+
const registryQualified = `${registry}/datagrok/${remoteFullName}`;
|
|
290
|
+
if (localImageExists(registryQualified, needsPlatform)) foundLocalName = registryQualified;
|
|
291
|
+
}
|
|
292
|
+
if (!foundLocalName && localImageExists(img.fullLocalName, needsPlatform)) foundLocalName = img.fullLocalName;
|
|
293
|
+
if (foundLocalName) {
|
|
294
|
+
color.success(`Found local image ${foundLocalName}`);
|
|
295
|
+
if (registry) {
|
|
296
|
+
const remoteTag = `${registry}/datagrok/${remoteFullName}`;
|
|
297
|
+
if (foundLocalName !== remoteTag) dockerTag(foundLocalName, remoteTag);
|
|
278
298
|
}
|
|
299
|
+
result = pushImage(img.imageName, registryTag, registry);
|
|
279
300
|
} else {
|
|
280
|
-
|
|
281
|
-
color.
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
color.
|
|
285
|
-
} else {
|
|
301
|
+
color.warn(`Local image not found. Expected: ${img.fullLocalName}`);
|
|
302
|
+
color.log(` Build it with: docker build -t ${img.fullLocalName} -f ${dockerfilePath} ${dockerfileDir}`);
|
|
303
|
+
const fallback = await fallbackImage(img, host, devKey, registry, version, contentHash);
|
|
304
|
+
if (fallback.serverError) {
|
|
305
|
+
color.error(`Cannot resolve fallback: ${fallback.serverError}`);
|
|
286
306
|
result = {
|
|
287
307
|
image: null,
|
|
288
308
|
fallback: true,
|
|
289
|
-
requestedVersion:
|
|
309
|
+
requestedVersion: registryTag
|
|
310
|
+
};
|
|
311
|
+
} else if (fallback.image && fallback.hashMatch === true) {
|
|
312
|
+
result = fallback;
|
|
313
|
+
color.success(`Falling back to ${fallback.image} (dockerfile unchanged)`);
|
|
314
|
+
} else if (fallback.image && fallback.hashMatch === false && !skipDockerRebuild) {
|
|
315
|
+
color.warn(`Dockerfile folder has changed. Rebuilding image...`);
|
|
316
|
+
result = buildAndPush() ?? {
|
|
317
|
+
image: fallback.image,
|
|
318
|
+
fallback: true,
|
|
319
|
+
requestedVersion: registryTag
|
|
290
320
|
};
|
|
291
|
-
color.
|
|
321
|
+
if (!result || result.fallback) color.warn(`Build failed. Falling back to ${fallback.image} (hash mismatch)`);
|
|
322
|
+
} else {
|
|
323
|
+
// No fallback and no local image — must build
|
|
324
|
+
color.warn(`No fallback available. Building ${img.fullLocalName}...`);
|
|
325
|
+
const built = buildAndPush();
|
|
326
|
+
if (built) result = built;else {
|
|
327
|
+
result = {
|
|
328
|
+
image: null,
|
|
329
|
+
fallback: true,
|
|
330
|
+
requestedVersion: registryTag
|
|
331
|
+
};
|
|
332
|
+
color.error(`Failed to build ${img.fullLocalName}. No container will be available.`);
|
|
333
|
+
}
|
|
292
334
|
}
|
|
293
335
|
}
|
|
294
336
|
}
|
|
@@ -300,11 +342,11 @@ async function processDockerImages(packageName, version, registry, devKey, host,
|
|
|
300
342
|
color.log(`Added ${imageJsonPath}`);
|
|
301
343
|
}
|
|
302
344
|
}
|
|
303
|
-
function pushImage(
|
|
304
|
-
const canonicalImage = `datagrok/${
|
|
345
|
+
function pushImage(imageName, tag, registry) {
|
|
346
|
+
const canonicalImage = `datagrok/${imageName}:${tag}`;
|
|
305
347
|
if (registry) {
|
|
306
348
|
const remoteTag = `${registry}/${canonicalImage}`;
|
|
307
|
-
|
|
349
|
+
// Image should already be tagged from build or retag step
|
|
308
350
|
if (dockerPush(remoteTag)) return {
|
|
309
351
|
image: canonicalImage,
|
|
310
352
|
fallback: false
|
|
@@ -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
|
+
}
|
package/bin/grok.js
CHANGED
|
@@ -18,6 +18,7 @@ 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,
|
|
21
22
|
test: require('./commands/test').test,
|
|
22
23
|
testall: require('./commands/test-all').testAll,
|
|
23
24
|
stresstest: require('./commands/stress-tests').stressTests,
|
package/package.json
CHANGED