@jacebenson/jsn 0.0.3
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/README.md +398 -0
- package/bin/jsn.js +5 -0
- package/package.json +44 -0
- package/src/app.js +157 -0
- package/src/auth.js +273 -0
- package/src/cli.js +142 -0
- package/src/commands/_ticket.js +256 -0
- package/src/commands/auth.js +62 -0
- package/src/commands/changes.js +7 -0
- package/src/commands/dev/_generic.js +223 -0
- package/src/commands/dev/_simple.js +89 -0
- package/src/commands/dev/eval.js +17 -0
- package/src/commands/dev/flows.js +528 -0
- package/src/commands/dev/forms.js +313 -0
- package/src/commands/dev/lists.js +233 -0
- package/src/commands/dev/logs.js +51 -0
- package/src/commands/dev/rest.js +64 -0
- package/src/commands/dev/scopes.js +96 -0
- package/src/commands/dev/updatesets.js +97 -0
- package/src/commands/dev.js +53 -0
- package/src/commands/groupmembers.js +39 -0
- package/src/commands/grouproles.js +39 -0
- package/src/commands/groups.js +57 -0
- package/src/commands/incidents.js +7 -0
- package/src/commands/profiles.js +79 -0
- package/src/commands/records.js +137 -0
- package/src/commands/requests.js +7 -0
- package/src/commands/setup.js +35 -0
- package/src/commands/tasks.js +7 -0
- package/src/commands/tickets.js +121 -0
- package/src/commands/users.js +57 -0
- package/src/commands/version.js +25 -0
- package/src/config.js +152 -0
- package/src/context.js +62 -0
- package/src/errors.js +101 -0
- package/src/helpers.js +60 -0
- package/src/output.js +410 -0
- package/src/sdk.js +357 -0
package/src/output.js
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
// Output formatting: JSON, Markdown, Styled, Quiet
|
|
2
|
+
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
|
|
6
|
+
export const FormatAuto = 'auto';
|
|
7
|
+
export const FormatJSON = 'json';
|
|
8
|
+
export const FormatMarkdown = 'markdown';
|
|
9
|
+
export const FormatStyled = 'styled';
|
|
10
|
+
export const FormatQuiet = 'quiet';
|
|
11
|
+
|
|
12
|
+
export function hyperlink(text, url) {
|
|
13
|
+
if (!url) return text;
|
|
14
|
+
return `\x1b]8;;${url}\x07${text}\x1b]8;;\x07`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function stripAnsi(str) {
|
|
18
|
+
// eslint-disable-next-line no-control-regex
|
|
19
|
+
return str.replace(/\x1b\[[0-9;]*[a-zA-Z]|\x1b\]8;;[^\x07]*\x07|\x1b\]8;;\x07/g, '');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function _visibleWidth(str) {
|
|
23
|
+
return stripAnsi(str).length;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isTTY(writer) {
|
|
27
|
+
if (!writer) writer = process.stdout;
|
|
28
|
+
return writer.isTTY === true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class OutputWriter {
|
|
32
|
+
constructor(opts = {}) {
|
|
33
|
+
this.format = opts.format || FormatAuto;
|
|
34
|
+
this.writer = opts.writer || process.stdout;
|
|
35
|
+
this.jqFilter = opts.jqFilter || '';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getFormat() {
|
|
39
|
+
return this.format;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
effectiveFormat() {
|
|
43
|
+
if (this.format === FormatAuto) {
|
|
44
|
+
return isTTY(this.writer) ? FormatStyled : FormatJSON;
|
|
45
|
+
}
|
|
46
|
+
return this.format;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
ok(data, opts = {}) {
|
|
50
|
+
const format = this.effectiveFormat();
|
|
51
|
+
switch (format) {
|
|
52
|
+
case FormatJSON:
|
|
53
|
+
return this.writeJSON(data, opts);
|
|
54
|
+
case FormatMarkdown:
|
|
55
|
+
return this.writeMarkdown(data, opts);
|
|
56
|
+
case FormatQuiet:
|
|
57
|
+
return this.writeQuiet(data);
|
|
58
|
+
case FormatStyled:
|
|
59
|
+
return this.writeStyled(data, opts);
|
|
60
|
+
default:
|
|
61
|
+
return this.writeJSON(data, opts);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
err(error) {
|
|
66
|
+
const format = this.effectiveFormat();
|
|
67
|
+
const e = error.code ? error : { code: 'unknown', message: String(error), hint: '' };
|
|
68
|
+
const resp = { ok: false, error: e.message, code: e.code, hint: e.hint || '' };
|
|
69
|
+
switch (format) {
|
|
70
|
+
case FormatJSON:
|
|
71
|
+
case FormatQuiet:
|
|
72
|
+
this.writer.write(JSON.stringify(resp, null, 2) + '\n');
|
|
73
|
+
break;
|
|
74
|
+
case FormatMarkdown:
|
|
75
|
+
this.writer.write(`**Error (${e.code})**: ${e.message}\n`);
|
|
76
|
+
if (e.hint) this.writer.write(`\n*Hint: ${e.hint}*\n`);
|
|
77
|
+
break;
|
|
78
|
+
default:
|
|
79
|
+
if (isTTY(this.writer)) {
|
|
80
|
+
this.writer.write(chalk.red(`Error (${e.code}): ${e.message}\n`));
|
|
81
|
+
if (e.hint) this.writer.write(chalk.yellow(`Hint: ${e.hint}\n`));
|
|
82
|
+
} else {
|
|
83
|
+
this.writer.write(JSON.stringify(resp, null, 2) + '\n');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
writeJSON(data, opts) {
|
|
89
|
+
const resp = { ok: true, data, summary: opts.summary || '', breadcrumbs: opts.breadcrumbs || [] };
|
|
90
|
+
if (opts.notice) resp.notice = opts.notice;
|
|
91
|
+
if (opts.context) resp.context = opts.context;
|
|
92
|
+
if (opts.meta) resp.meta = opts.meta;
|
|
93
|
+
this.writer.write(JSON.stringify(resp, null, 2) + '\n');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
writeQuiet(data) {
|
|
97
|
+
this.writer.write(JSON.stringify(data, null, 2) + '\n');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
writeMarkdown(data, opts) {
|
|
101
|
+
if (opts.summary) {
|
|
102
|
+
this.writer.write(opts.summary + '\n\n');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (Array.isArray(data)) {
|
|
106
|
+
this.writeMarkdownTable(data);
|
|
107
|
+
} else if (data && typeof data === 'object') {
|
|
108
|
+
if (Array.isArray(data.records) && data.records.length > 0) {
|
|
109
|
+
this.writeMarkdownTable(data.records);
|
|
110
|
+
} else {
|
|
111
|
+
this.writer.write('```json\n');
|
|
112
|
+
this.writer.write(JSON.stringify(data, null, 2));
|
|
113
|
+
this.writer.write('\n```\n');
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
this.writer.write(String(data) + '\n');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (opts.breadcrumbs && opts.breadcrumbs.length > 0) {
|
|
120
|
+
this.writer.write('\n### Hints\n');
|
|
121
|
+
for (const bc of opts.breadcrumbs) {
|
|
122
|
+
this.writer.write(`- **${bc.action}**: \`${bc.cmd}\` — ${bc.description}\n`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
writeMarkdownTable(rows) {
|
|
128
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
129
|
+
this.writer.write('(no results)\n');
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const columns = Object.keys(rows[0]);
|
|
133
|
+
// Header
|
|
134
|
+
this.writer.write('| ' + columns.join(' | ') + ' |\n');
|
|
135
|
+
// Separator
|
|
136
|
+
this.writer.write('| ' + columns.map(c => '-'.repeat(c.length)).join(' | ') + ' |\n');
|
|
137
|
+
// Rows
|
|
138
|
+
for (const row of rows) {
|
|
139
|
+
const cells = columns.map(col => {
|
|
140
|
+
const val = row[col];
|
|
141
|
+
if (val == null) return '';
|
|
142
|
+
if (typeof val === 'object') {
|
|
143
|
+
if (val.display_value != null) return String(val.display_value);
|
|
144
|
+
if (val.value != null) return String(val.value);
|
|
145
|
+
return JSON.stringify(val);
|
|
146
|
+
}
|
|
147
|
+
return String(val);
|
|
148
|
+
});
|
|
149
|
+
this.writer.write('| ' + cells.join(' | ') + ' |\n');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
writeStyled(data, opts) {
|
|
154
|
+
const hasFormatted = data && typeof data === 'object' && data._formatted;
|
|
155
|
+
|
|
156
|
+
if (opts.summary && !hasFormatted) {
|
|
157
|
+
this.writer.write(opts.summary + '\n\n');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (Array.isArray(data)) {
|
|
161
|
+
for (const row of data) {
|
|
162
|
+
const firstVal = Object.values(row)[0];
|
|
163
|
+
this.writer.write(String(firstVal) + '\n');
|
|
164
|
+
}
|
|
165
|
+
} else if (data && typeof data === 'object') {
|
|
166
|
+
if (data._formatted) {
|
|
167
|
+
this.writer.write(data._formatted);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const { isRecord, tableName } = detectRecord(data);
|
|
171
|
+
if (isRecord) {
|
|
172
|
+
this.writeFormattedRecord(data, tableName);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (Array.isArray(data.profiles) && data.profiles.length > 0) {
|
|
176
|
+
this.writer.write('\n');
|
|
177
|
+
for (const p of data.profiles) {
|
|
178
|
+
const prefix = p.default ? '* ' : ' ';
|
|
179
|
+
const status = p.authenticated ? '✓' : '✗';
|
|
180
|
+
if (p.username) {
|
|
181
|
+
this.writer.write(`${prefix}${status} ${p.instance} (as ${p.username})\n`);
|
|
182
|
+
} else {
|
|
183
|
+
this.writer.write(`${prefix}${status} ${p.instance}\n`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (Array.isArray(data.records) && data.records.length > 0) {
|
|
189
|
+
this.writeRecordsTable(data);
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
if (!opts.summary) {
|
|
193
|
+
this.writer.write(String(data) + '\n');
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (opts.breadcrumbs && opts.breadcrumbs.length > 0) {
|
|
198
|
+
this.writer.write('\n');
|
|
199
|
+
for (const bc of opts.breadcrumbs) {
|
|
200
|
+
this.writer.write(` → ${bc.action}: ${bc.cmd} — ${bc.description}\n`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
writeRecordsTable(data) {
|
|
206
|
+
const records = data.records;
|
|
207
|
+
const columns = data.columns || (records.length > 0 ? Object.keys(records[0]) : []);
|
|
208
|
+
const table = data.table || '';
|
|
209
|
+
const instanceURL = data.context?.instance_url || '';
|
|
210
|
+
|
|
211
|
+
const colWidths = {
|
|
212
|
+
number: 14,
|
|
213
|
+
short_description: 48,
|
|
214
|
+
priority: 14,
|
|
215
|
+
state: 10,
|
|
216
|
+
assigned_to: 15,
|
|
217
|
+
risk: 10,
|
|
218
|
+
name: 30,
|
|
219
|
+
user_name: 15,
|
|
220
|
+
email: 30,
|
|
221
|
+
sys_id: 32,
|
|
222
|
+
sys_updated_on: 22,
|
|
223
|
+
sys_created_on: 22,
|
|
224
|
+
opened_at: 22,
|
|
225
|
+
closed_at: 22,
|
|
226
|
+
sys_updated_by: 20,
|
|
227
|
+
sys_created_by: 20,
|
|
228
|
+
opened_by: 20,
|
|
229
|
+
u_category: 20,
|
|
230
|
+
u_subcategory: 20,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
this.writer.write('\n');
|
|
234
|
+
|
|
235
|
+
// Header
|
|
236
|
+
for (const col of columns) {
|
|
237
|
+
const width = colWidths[col] || 20;
|
|
238
|
+
this.writer.write(col + ' '.repeat(Math.max(0, width - col.length)) + ' ');
|
|
239
|
+
}
|
|
240
|
+
this.writer.write('\n');
|
|
241
|
+
|
|
242
|
+
// Separator
|
|
243
|
+
for (const col of columns) {
|
|
244
|
+
const width = colWidths[col] || 20;
|
|
245
|
+
this.writer.write('-'.repeat(width) + ' ');
|
|
246
|
+
}
|
|
247
|
+
this.writer.write('\n');
|
|
248
|
+
|
|
249
|
+
// Rows (limit to 20)
|
|
250
|
+
const limit = Math.min(records.length, 20);
|
|
251
|
+
for (let i = 0; i < limit; i++) {
|
|
252
|
+
const row = records[i];
|
|
253
|
+
for (let j = 0; j < columns.length; j++) {
|
|
254
|
+
const col = columns[j];
|
|
255
|
+
let val = row[col] || '';
|
|
256
|
+
const width = colWidths[col] || 20;
|
|
257
|
+
|
|
258
|
+
if (j === 0 && instanceURL && table && row.sys_id) {
|
|
259
|
+
const url = `${instanceURL}/${table}.do?sys_id=${row.sys_id}`;
|
|
260
|
+
val = hyperlink(val, url);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const visible = stripAnsi(val);
|
|
264
|
+
let display = val;
|
|
265
|
+
if (visible.length > width) {
|
|
266
|
+
display = visible.slice(0, width - 3) + '...';
|
|
267
|
+
if (val !== visible) {
|
|
268
|
+
if (j === 0 && instanceURL && table && row.sys_id) {
|
|
269
|
+
const url = `${instanceURL}/${table}.do?sys_id=${row.sys_id}`;
|
|
270
|
+
display = hyperlink(display, url);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
this.writer.write(display);
|
|
276
|
+
this.writer.write(' '.repeat(Math.max(0, width - stripAnsi(display).length)) + ' ');
|
|
277
|
+
}
|
|
278
|
+
this.writer.write('\n');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (records.length > limit) {
|
|
282
|
+
this.writer.write(`\n... and ${records.length - limit} more\n`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
writeFormattedRecord(data, table) {
|
|
287
|
+
// Get instance URL from _context
|
|
288
|
+
let instanceURL = '';
|
|
289
|
+
if (data._context && typeof data._context === 'object') {
|
|
290
|
+
instanceURL = data._context.instance_url || '';
|
|
291
|
+
delete data._context;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
let title = '';
|
|
295
|
+
if (data.number) {
|
|
296
|
+
title = getDisplayValue(data.number);
|
|
297
|
+
}
|
|
298
|
+
if (!title && data.name) {
|
|
299
|
+
title = getDisplayValue(data.name);
|
|
300
|
+
}
|
|
301
|
+
if (!title && table) title = table;
|
|
302
|
+
|
|
303
|
+
this.writer.write(`\n${title} (${table})\n\n`);
|
|
304
|
+
|
|
305
|
+
const groups = {
|
|
306
|
+
Core: ['number', 'sys_id', 'sys_class_name', 'state', 'active', 'short_description', 'description', 'priority', 'urgency', 'impact', 'risk', 'type'],
|
|
307
|
+
People: ['opened_by', 'assigned_to', 'assignment_group', 'closed_by', 'requested_by', 'additional_assignee_list', 'watch_list'],
|
|
308
|
+
'Status': ['approval', 'approval_set', 'approval_history', 'escalation', 'made_sla', 'on_hold', 'on_hold_reason'],
|
|
309
|
+
'Dates & Times': ['opened_at', 'sys_created_on', 'sys_updated_on', 'closed_at', 'work_start', 'work_end', 'due_date', 'expected_start', 'sla_due', 'activity_due'],
|
|
310
|
+
System: ['sys_domain', 'sys_domain_path', 'sys_created_by', 'sys_updated_by', 'sys_mod_count', 'sys_tags'],
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const displayed = new Set();
|
|
314
|
+
|
|
315
|
+
for (const [groupName, fields] of Object.entries(groups)) {
|
|
316
|
+
let hasFields = false;
|
|
317
|
+
let groupContent = '';
|
|
318
|
+
for (const field of fields) {
|
|
319
|
+
if (field in data) {
|
|
320
|
+
displayed.add(field);
|
|
321
|
+
const displayVal = getDisplayValue(data[field]);
|
|
322
|
+
if (displayVal !== '' || field === 'description' || field === 'short_description') {
|
|
323
|
+
if (!hasFields) {
|
|
324
|
+
hasFields = true;
|
|
325
|
+
groupContent = `─ ${groupName} ─\n`;
|
|
326
|
+
}
|
|
327
|
+
if (displayVal.includes('\n')) {
|
|
328
|
+
groupContent += ` ${field}:\n`;
|
|
329
|
+
for (const line of displayVal.split('\n')) {
|
|
330
|
+
groupContent += ` ${line}\n`;
|
|
331
|
+
}
|
|
332
|
+
} else {
|
|
333
|
+
groupContent += ` ${field}: ${displayVal}\n`;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (hasFields) {
|
|
339
|
+
this.writer.write(groupContent);
|
|
340
|
+
this.writer.write('\n');
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const otherFields = Object.keys(data).filter(f => !displayed.has(f) && !f.startsWith('_')).sort();
|
|
345
|
+
if (otherFields.length > 0) {
|
|
346
|
+
this.writer.write('─ Other ─\n');
|
|
347
|
+
for (const field of otherFields) {
|
|
348
|
+
const displayVal = getDisplayValue(data[field]);
|
|
349
|
+
if (displayVal.includes('\n')) {
|
|
350
|
+
this.writer.write(` ${field}:\n`);
|
|
351
|
+
for (const line of displayVal.split('\n')) {
|
|
352
|
+
this.writer.write(` ${line}\n`);
|
|
353
|
+
}
|
|
354
|
+
} else {
|
|
355
|
+
this.writer.write(` ${field}: ${displayVal}\n`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
this.writer.write('\n');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (instanceURL && table) {
|
|
362
|
+
const sysID = getDisplayValue(data.sys_id);
|
|
363
|
+
if (sysID) {
|
|
364
|
+
const urlTable = table.toLowerCase().replace(/\s+/g, '_');
|
|
365
|
+
const recordURL = `${instanceURL}/${urlTable}.do?sys_id=${sysID}`;
|
|
366
|
+
this.writer.write(`Link: ${recordURL}\n\n`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function detectRecord(data) {
|
|
373
|
+
let hasSysID = false;
|
|
374
|
+
let hasSysClass = false;
|
|
375
|
+
let tableName = '';
|
|
376
|
+
|
|
377
|
+
if (data.sys_id) hasSysID = true;
|
|
378
|
+
if (data.sys_class_name) {
|
|
379
|
+
hasSysClass = true;
|
|
380
|
+
const sc = data.sys_class_name;
|
|
381
|
+
if (typeof sc === 'object') {
|
|
382
|
+
tableName = sc.display_value || sc.value || '';
|
|
383
|
+
} else {
|
|
384
|
+
tableName = sc;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (!tableName && data.number) {
|
|
389
|
+
const num = data.number;
|
|
390
|
+
const numStr = typeof num === 'object' ? (num.display_value || num.value) : num;
|
|
391
|
+
if (numStr && numStr.length > 3) {
|
|
392
|
+
const prefix = numStr.slice(0, 3);
|
|
393
|
+
const map = { INC: 'incident', CHG: 'change_request', RIT: 'sc_req_item', SCT: 'sc_task', PRB: 'problem' };
|
|
394
|
+
if (map[prefix]) tableName = map[prefix];
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return { isRecord: hasSysID && (hasSysClass || tableName), tableName };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function getDisplayValue(val) {
|
|
402
|
+
if (val == null) return '';
|
|
403
|
+
if (typeof val === 'string') return val;
|
|
404
|
+
if (typeof val === 'object') {
|
|
405
|
+
if (val.display_value != null) return String(val.display_value);
|
|
406
|
+
if (val.value != null) return String(val.value);
|
|
407
|
+
return JSON.stringify(val);
|
|
408
|
+
}
|
|
409
|
+
return String(val);
|
|
410
|
+
}
|