@jacebenson/jsn 4.3.12-dev.70 → 4.3.13-dev.100.1
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 +55 -0
- package/package.json +2 -1
- package/skills/servicenow/SKILL.md +20 -172
- package/src/app.js +29 -40
- package/src/auth-recovery.js +83 -0
- package/src/auth.js +2 -0
- package/src/capabilities.js +9 -0
- package/src/cli-adapter.js +12 -0
- package/src/cli.js +39 -9
- package/src/command-audience.js +35 -0
- package/src/commands/_generic.js +45 -28
- package/src/commands/_simple.js +30 -33
- package/src/commands/_ticket.js +24 -20
- package/src/commands/attachments.js +88 -0
- package/src/commands/auth.js +94 -19
- package/src/commands/b4rules.js +8 -3
- package/src/commands/changes.js +2 -4
- package/src/commands/columns.js +2 -1
- package/src/commands/customizations.js +13 -7
- package/src/commands/discover.js +54 -0
- package/src/commands/docs/db.js +9 -1
- package/src/commands/docs/docs.js +25 -2
- package/src/commands/docs/ingest.js +20 -3
- package/src/commands/docs/install.js +86 -0
- package/src/commands/docs/locks.js +34 -0
- package/src/commands/docs/refresh.js +22 -2
- package/src/commands/docs/replace.js +30 -0
- package/src/commands/docs/snapshot-check.js +11 -0
- package/src/commands/docs/snapshot.js +130 -0
- package/src/commands/docs/sync.js +16 -6
- package/src/commands/forms.js +22 -97
- package/src/commands/history.js +57 -0
- package/src/commands/incidents.js +2 -4
- package/src/commands/midservers.js +117 -10
- package/src/commands/records.js +54 -104
- package/src/commands/requests.js +9 -15
- package/src/commands/rest.js +15 -2
- package/src/commands/scopes.js +1 -1
- package/src/commands/setup.js +46 -9
- package/src/commands/skill.js +72 -76
- package/src/commands/tasks.js +2 -4
- package/src/commands/tickets.js +20 -19
- package/src/commands/uipolicies.js +2 -27
- package/src/commands/updatesets.js +65 -13
- package/src/commands/version.js +14 -8
- package/src/constrained-query.js +20 -0
- package/src/context.js +6 -6
- package/src/customization-guidance.js +31 -0
- package/src/customizations-report.js +6 -0
- package/src/customizations.js +28 -13
- package/src/errors.js +23 -1
- package/src/essential-records.js +40 -0
- package/src/field-behavior.js +15 -0
- package/src/help.js +10 -2
- package/src/metadata/records.js +249 -0
- package/src/midserver-linux-remote.js +210 -0
- package/src/midserver-remote-recipe.js +49 -0
- package/src/midserver-remote.js +221 -0
- package/src/midserver-windows-remote.js +318 -0
- package/src/midserver-zip.js +2 -2
- package/src/mutation-context.js +74 -0
- package/src/mutation-history.js +75 -0
- package/src/mutations.js +3 -2
- package/src/output.js +34 -2
- package/src/record-command-profiles.js +70 -0
- package/src/record-profiles.js +93 -0
- package/src/records-engine.js +57 -0
- package/src/related-lists.js +153 -0
- package/src/resolve-record.js +50 -28
- package/src/sdk-batch.js +130 -0
- package/src/sdk.js +157 -18
- package/src/update-set-xml.js +89 -0
package/src/commands/records.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
1
|
+
import { formatCommandList, formatRecordForDisplay, buildQuerySuffix, buildRecordPayload, getStringField, interactiveList, resolveFieldsParam, checkDerivedFields, confirmDelete, verifyWriteBack } from '../helpers.js';
|
|
2
|
+
import { listRecords, recordListParams, getRecord, createRecord, updateRecord, deleteRecord } from '../records-engine.js';
|
|
3
|
+
import { unwrapSysId } from '../resolve-record.js';
|
|
4
|
+
import { errUsage } from '../errors.js';
|
|
4
5
|
import { declareCapabilities } from '../capabilities.js';
|
|
5
6
|
|
|
6
7
|
declareCapabilities('records', {
|
|
7
|
-
mutationSubcommands: ['create', 'update', 'delete', 'bulk'
|
|
8
|
+
mutationSubcommands: ['create', 'update', 'delete', 'bulk'],
|
|
8
9
|
});
|
|
9
10
|
|
|
10
11
|
const tableDefaultColumns = {
|
|
@@ -67,6 +68,15 @@ function getDefaultColumns(table) {
|
|
|
67
68
|
return tableDefaultColumns[table] || ['sys_id'];
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
function recordLookup(argv) {
|
|
72
|
+
const sysId = argv['sys-id'];
|
|
73
|
+
if ((sysId === undefined) === (argv.identifier === undefined)) throw errUsage('Supply exactly one of --sys-id or --identifier.');
|
|
74
|
+
if (sysId !== undefined && argv.identifierField && argv.identifierField !== 'sys_id') throw errUsage('--sys-id cannot be combined with a different --identifier-field.');
|
|
75
|
+
return { table: argv.table, identifier: sysId ?? argv.identifier,
|
|
76
|
+
identifierField: sysId !== undefined ? 'sys_id' : (argv.identifierField || 'sys_id'),
|
|
77
|
+
displayValues: argv.displayValues, resource: 'Record' };
|
|
78
|
+
}
|
|
79
|
+
|
|
70
80
|
export function recordsCmd(wrap) {
|
|
71
81
|
return {
|
|
72
82
|
command: 'records [subcommand]',
|
|
@@ -83,7 +93,16 @@ export function recordsCmd(wrap) {
|
|
|
83
93
|
.option('columns', { alias: ['c', 'fields'], type: 'string', describe: 'Comma-separated columns (e.g. "number,short_description")' })
|
|
84
94
|
.option('limit', { type: 'number', default: 20, describe: 'Max records' })
|
|
85
95
|
.option('offset', { type: 'number', default: 0, describe: 'Offset' })
|
|
86
|
-
.option('
|
|
96
|
+
.option('display-values', { type: 'string', choices: ['raw', 'display', 'both'], describe: 'Reference values: raw, display or both' })
|
|
97
|
+
.option('count', { type: 'boolean', default: true, describe: 'Include the total-matching count (use --no-count to opt out)' })
|
|
98
|
+
.epilogue([
|
|
99
|
+
'In an interactive terminal with automatic output, browse a picker (10 visible items).',
|
|
100
|
+
'Use arrow keys to browse, type to search sys_id on the server, scroll to load more,',
|
|
101
|
+
'and press Enter to select or Esc to cancel. --sys-id skips the picker.',
|
|
102
|
+
'Use --json for noninteractive structured output; pipes and explicit formats also skip the picker.',
|
|
103
|
+
'Example: jsn records list --table incident --query "active=true" --limit 5 --json',
|
|
104
|
+
'Discover fields with: jsn columns --table incident',
|
|
105
|
+
].join('\n')),
|
|
87
106
|
handler: wrap(async (argv, app) => {
|
|
88
107
|
const table = argv.table;
|
|
89
108
|
const columns = argv.columns ? argv.columns.split(',') : getDefaultColumns(table);
|
|
@@ -122,15 +141,10 @@ export function recordsCmd(wrap) {
|
|
|
122
141
|
}
|
|
123
142
|
|
|
124
143
|
// Text/table fallback (or sys-id direct lookup)
|
|
125
|
-
const params = new URLSearchParams();
|
|
126
|
-
params.set('sysparm_limit', argv['sys-id'] ? '1' : String(argv.limit));
|
|
127
|
-
params.set('sysparm_offset', String(argv.offset));
|
|
128
|
-
params.set('sysparm_display_value', 'all');
|
|
129
144
|
const fields = resolveFieldsParam(columns);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
const displayRecords = fields ? records.map(r => formatRecordForDisplay(r, columns)) : records;
|
|
145
|
+
const params = recordListParams({ limit: argv['sys-id'] ? 1 : argv.limit, offset: argv.offset, fields, query });
|
|
146
|
+
const records = await listRecords(app.sdk, { table, params, displayValues: argv.displayValues });
|
|
147
|
+
const displayRecords = fields && !argv.displayValues ? records.map(r => formatRecordForDisplay(r, columns)) : records;
|
|
134
148
|
|
|
135
149
|
// include_counts: default opt-in on every list. Opt out per-profile
|
|
136
150
|
// (config include_counts:false) or per-invocation (--no-count).
|
|
@@ -182,27 +196,23 @@ export function recordsCmd(wrap) {
|
|
|
182
196
|
})
|
|
183
197
|
.command({
|
|
184
198
|
command: 'get',
|
|
185
|
-
describe: 'Get
|
|
199
|
+
describe: 'Get one record by sys_id or an explicit identifier field',
|
|
186
200
|
builder: (y) => y
|
|
187
201
|
.option('table', { type: 'string', demandOption: true, describe: 'Table name' })
|
|
188
|
-
.option('sys-id', { type: 'string',
|
|
202
|
+
.option('sys-id', { type: 'string', describe: 'Full or unambiguous partial record sys_id (including legacy IDs)' })
|
|
203
|
+
.option('identifier', { type: 'string', describe: 'Identifier value; use --identifier-field number/name/etc for human identifiers' })
|
|
204
|
+
.option('identifier-field', { type: 'string', describe: 'Exact identifier field (default sys_id); disambiguates names and legacy sys_ids' })
|
|
205
|
+
.option('display-values', { type: 'string', choices: ['raw', 'display', 'both'], describe: 'Reference values: raw, display or both' })
|
|
189
206
|
.option('columns', { alias: ['c', 'fields'], type: 'string', describe: 'Comma-separated columns (e.g. "number,short_description")' })
|
|
190
|
-
.option('attachments', { type: 'boolean', default: false, describe: 'Also list the record\'s attachments' })
|
|
207
|
+
.option('attachments', { type: 'boolean', default: false, describe: 'Also list the record\'s attachments' })
|
|
208
|
+
.epilogue('Exact lookup precedes sys_id prefix, then suffix. Ambiguous matches fail.\nExample: jsn records get --table incident --sys-id <sys_id> --json\nExample: jsn records get --table incident --identifier INC0010001 --identifier-field number --json'),
|
|
191
209
|
handler: wrap(async (argv, app) => {
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
params.set('sysparm_limit', '1');
|
|
196
|
-
params.set('sysparm_display_value', 'true');
|
|
197
|
-
if (argv.columns && argv.columns !== '*') params.set('sysparm_fields', `sys_id,${argv.columns}`);
|
|
198
|
-
const records = await app.sdk.list(argv.table, params);
|
|
199
|
-
if (records.length === 0) {
|
|
200
|
-
throw new Error(`Record not found: ${argv['sys-id']}`);
|
|
201
|
-
}
|
|
202
|
-
const record = records[0];
|
|
210
|
+
const lookup = recordLookup(argv);
|
|
211
|
+
const fields = argv.columns && argv.columns !== '*' ? argv.columns.split(',') : undefined;
|
|
212
|
+
const record = await getRecord(app.sdk, { ...lookup, fields, displayValue: 'true' });
|
|
203
213
|
if (argv.attachments) {
|
|
204
214
|
try {
|
|
205
|
-
const atts = await app.sdk.listAttachments(
|
|
215
|
+
const atts = await app.sdk.listAttachments(unwrapSysId(record));
|
|
206
216
|
record._attachments = atts;
|
|
207
217
|
} catch {
|
|
208
218
|
record._attachments = [];
|
|
@@ -232,7 +242,7 @@ export function recordsCmd(wrap) {
|
|
|
232
242
|
for (const w of warnings) {
|
|
233
243
|
process.stderr.write(`⚠ ${w.hint}\n`);
|
|
234
244
|
}
|
|
235
|
-
const record = await app.sdk
|
|
245
|
+
const record = await createRecord(app.sdk, { table: argv.table, data: recordData });
|
|
236
246
|
const sysID = getStringField(record, 'sys_id');
|
|
237
247
|
const mismatches = await verifyWriteBack(app, argv.table, sysID, recordData);
|
|
238
248
|
for (const m of mismatches) {
|
|
@@ -249,7 +259,9 @@ export function recordsCmd(wrap) {
|
|
|
249
259
|
describe: 'Update an existing record',
|
|
250
260
|
builder: (y) => y
|
|
251
261
|
.option('table', { type: 'string', demandOption: true, describe: 'Table name' })
|
|
252
|
-
.option('sys-id', { type: 'string',
|
|
262
|
+
.option('sys-id', { type: 'string', describe: 'Full or unambiguous partial record sys_id (including legacy IDs)' })
|
|
263
|
+
.option('identifier', { type: 'string', describe: 'Identifier value; use --identifier-field number/name/etc for human identifiers' })
|
|
264
|
+
.option('identifier-field', { type: 'string', describe: 'Exact identifier field (default sys_id); disambiguates names and legacy sys_ids' })
|
|
253
265
|
.option('data', { type: 'string', describe: 'JSON fields' })
|
|
254
266
|
.option('data-file', { type: 'string', describe: 'Read JSON payload from file' })
|
|
255
267
|
.option('data-stdin', { type: 'boolean', describe: 'Read JSON payload from stdin (pipe-friendly)' })
|
|
@@ -261,8 +273,10 @@ export function recordsCmd(wrap) {
|
|
|
261
273
|
for (const w of warnings) {
|
|
262
274
|
process.stderr.write(`⚠ ${w.hint}\n`);
|
|
263
275
|
}
|
|
264
|
-
const
|
|
265
|
-
const
|
|
276
|
+
const lookup = recordLookup(argv);
|
|
277
|
+
const resolved = await getRecord(app.sdk, lookup);
|
|
278
|
+
const record = await updateRecord(app.sdk, { table: argv.table, record: resolved, data: recordData });
|
|
279
|
+
const mismatches = await verifyWriteBack(app, argv.table, unwrapSysId(resolved), recordData);
|
|
266
280
|
for (const m of mismatches) {
|
|
267
281
|
process.stderr.write(`⚠ field "${m.field}" was not persisted (sent: "${m.sent}", got: "${m.got}")\n`);
|
|
268
282
|
}
|
|
@@ -277,82 +291,17 @@ export function recordsCmd(wrap) {
|
|
|
277
291
|
describe: 'Delete a record',
|
|
278
292
|
builder: (y) => y
|
|
279
293
|
.option('table', { type: 'string', demandOption: true, describe: 'Table name' })
|
|
280
|
-
.option('sys-id', { type: 'string',
|
|
294
|
+
.option('sys-id', { type: 'string', describe: 'Full or unambiguous partial record sys_id (including legacy IDs)' })
|
|
295
|
+
.option('identifier', { type: 'string', describe: 'Identifier value; use --identifier-field number/name/etc for human identifiers' })
|
|
296
|
+
.option('identifier-field', { type: 'string', describe: 'Exact identifier field (default sys_id); disambiguates names and legacy sys_ids' })
|
|
281
297
|
.option('force', { type: 'boolean', default: false, describe: 'Skip confirmation' }),
|
|
282
298
|
handler: wrap(async (argv, app) => {
|
|
283
|
-
|
|
284
|
-
await app
|
|
285
|
-
|
|
299
|
+
const lookup = recordLookup(argv);
|
|
300
|
+
await confirmDelete(app, argv, `Delete record from ${argv.table} (${lookup.identifier})`);
|
|
301
|
+
const sysID = await deleteRecord(app.sdk, lookup);
|
|
302
|
+
app.ok({ message: 'Record deleted', table: argv.table, sys_id: sysID }, { summary: `Deleted record from ${argv.table}` });
|
|
286
303
|
}),
|
|
287
304
|
})
|
|
288
|
-
.command({
|
|
289
|
-
command: 'attachments',
|
|
290
|
-
describe: 'Work with attachments on a record',
|
|
291
|
-
builder: (y) => y
|
|
292
|
-
.command({
|
|
293
|
-
command: 'list',
|
|
294
|
-
describe: 'List attachments on a record',
|
|
295
|
-
builder: (yy) => yy
|
|
296
|
-
.option('sys-id', { alias: 's', type: 'string', demandOption: true, describe: 'Parent record sys_id' })
|
|
297
|
-
.option('table', { type: 'string', describe: 'Parent table name (context only)' }),
|
|
298
|
-
handler: wrap(async (argv, app) => {
|
|
299
|
-
app.requireInstance();
|
|
300
|
-
const attachments = await app.sdk.listAttachments(argv['sys-id']);
|
|
301
|
-
app.ok({
|
|
302
|
-
sys_id: argv['sys-id'],
|
|
303
|
-
table: argv.table || '',
|
|
304
|
-
count: attachments.length,
|
|
305
|
-
attachments,
|
|
306
|
-
context: { instance_url: app.getEffectiveInstance() },
|
|
307
|
-
}, { summary: `${attachments.length} attachment(s) on record ${argv['sys-id']}` });
|
|
308
|
-
}),
|
|
309
|
-
})
|
|
310
|
-
.command({
|
|
311
|
-
command: 'get <attachment-id>',
|
|
312
|
-
describe: 'Download an attachment to a local file',
|
|
313
|
-
builder: (yy) => yy
|
|
314
|
-
.positional('attachment-id', { describe: 'Attachment sys_id', type: 'string' })
|
|
315
|
-
.option('sys-id', { alias: 's', type: 'string', demandOption: true, describe: 'Parent record sys_id (used to resolve the file name)' })
|
|
316
|
-
.option('out', { alias: 'o', type: 'string', describe: 'Output file path (default: attachment file name in cwd)' }),
|
|
317
|
-
handler: wrap(async (argv, app) => {
|
|
318
|
-
app.requireInstance();
|
|
319
|
-
const buf = await app.sdk.getAttachment(argv['attachment-id']);
|
|
320
|
-
let name = argv.out;
|
|
321
|
-
if (!name) {
|
|
322
|
-
// Best-effort: derive the file name from the attachment row.
|
|
323
|
-
let fileName = `${argv['attachment-id']}.bin`;
|
|
324
|
-
try {
|
|
325
|
-
const rows = await app.sdk.listAttachments(argv['sys-id']);
|
|
326
|
-
const hit = (rows || []).find((a) => getStringField(a, 'sys_id') === argv['attachment-id']);
|
|
327
|
-
const fn = getStringField(hit, 'file_name');
|
|
328
|
-
if (fn) fileName = fn;
|
|
329
|
-
} catch { /* keep default */ }
|
|
330
|
-
name = fileName;
|
|
331
|
-
}
|
|
332
|
-
await fs.promises.writeFile(name, buf);
|
|
333
|
-
app.ok({ sys_id: argv['attachment-id'], out: name, bytes: buf.length }, { summary: `Wrote ${buf.length} bytes to ${name}` });
|
|
334
|
-
}),
|
|
335
|
-
})
|
|
336
|
-
.command({
|
|
337
|
-
command: 'add <file>',
|
|
338
|
-
describe: 'Upload a file as a new attachment on a record (mutation)',
|
|
339
|
-
builder: (yy) => yy
|
|
340
|
-
.positional('file', { describe: 'Local file path to upload', type: 'string' })
|
|
341
|
-
.option('sys-id', { alias: 's', type: 'string', demandOption: true, describe: 'Parent record sys_id' })
|
|
342
|
-
.option('table', { type: 'string', demandOption: true, describe: 'Parent table for the record (e.g. incident)' })
|
|
343
|
-
.option('name', { type: 'string', describe: 'Display file name (default: basename of --file)' })
|
|
344
|
-
.option('force', { type: 'boolean', default: false, describe: 'Skip confirmation' }),
|
|
345
|
-
handler: wrap(async (argv, app) => {
|
|
346
|
-
app.requireInstance();
|
|
347
|
-
await confirmDelete(app, argv, `Upload "${argv.file}" as an attachment on ${argv.table} ${argv['sys-id']}?`);
|
|
348
|
-
const content = await fs.promises.readFile(argv.file);
|
|
349
|
-
const fileName = argv.name || path.basename(argv.file);
|
|
350
|
-
const created = await app.sdk.addAttachment(argv.table, argv['sys-id'], content, fileName);
|
|
351
|
-
app.ok({ sys_id: created?.sys_id, file_name: fileName, table: argv.table, recordsys_id: argv['sys-id'] }, { summary: `Attached ${fileName}` });
|
|
352
|
-
}),
|
|
353
|
-
})
|
|
354
|
-
.demandCommand(1, 'Specify an attachment action: list, get, or add'),
|
|
355
|
-
})
|
|
356
305
|
.command({
|
|
357
306
|
command: 'aggregate',
|
|
358
307
|
describe: 'Run ServiceNow Stats API aggregation with optional grouping',
|
|
@@ -360,6 +309,7 @@ export function recordsCmd(wrap) {
|
|
|
360
309
|
.option('table', { type: 'string', demandOption: true, describe: 'Table name' })
|
|
361
310
|
.option('query', { type: 'string', describe: 'Encoded query to constrain the aggregation' })
|
|
362
311
|
.option('group-by', { type: 'string', describe: 'Comma-separated fields to group by' })
|
|
312
|
+
.option('display-values', { type: 'string', choices: ['raw', 'display', 'both'], describe: 'Reference values: raw, display or both' })
|
|
363
313
|
.option('count', { type: 'boolean', default: true, describe: 'Include record counts' })
|
|
364
314
|
.option('avg-fields', { type: 'string', describe: 'Comma-separated fields to average' })
|
|
365
315
|
.option('sum-fields', { type: 'string', describe: 'Comma-separated fields to sum' })
|
package/src/commands/requests.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { enrichRelatedLists, relatedListOptions } from '../related-lists.js';
|
|
2
|
+
import { getRecord, recordOptions } from '../records-engine.js';
|
|
3
|
+
import { unwrapSysId } from '../resolve-record.js';
|
|
4
|
+
import { buildProfileCommand } from '../record-profiles.js';
|
|
3
5
|
|
|
4
|
-
const requestDefaultColumns = ['number', 'short_description', 'stage', 'assigned_to'];
|
|
5
6
|
|
|
6
7
|
export function requestsCmd(wrap) {
|
|
7
8
|
// Get the base command definition from the shared ticket builder
|
|
8
|
-
const baseCmd =
|
|
9
|
+
const baseCmd = buildProfileCommand('requests', wrap);
|
|
9
10
|
|
|
10
11
|
// Override the builder to inject our enriched show handler.
|
|
11
12
|
// yargs allows re-registration — the last .command() call wins the slot.
|
|
@@ -16,6 +17,7 @@ export function requestsCmd(wrap) {
|
|
|
16
17
|
command: 'show <number>',
|
|
17
18
|
aliases: ['get'],
|
|
18
19
|
describe: 'Show a specific request with attachments and catalog variables',
|
|
20
|
+
builder: y => relatedListOptions(recordOptions(y)),
|
|
19
21
|
handler: wrap(enrichedShowHandler),
|
|
20
22
|
});
|
|
21
23
|
};
|
|
@@ -25,17 +27,8 @@ export function requestsCmd(wrap) {
|
|
|
25
27
|
|
|
26
28
|
async function enrichedShowHandler(argv, app) {
|
|
27
29
|
const number = argv.number;
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
params.set('sysparm_query', `number=${number}`);
|
|
31
|
-
params.set('sysparm_display_value', 'all');
|
|
32
|
-
params.set('sysparm_limit', '1');
|
|
33
|
-
const records = await app.sdk.list('sc_req_item', params);
|
|
34
|
-
if (records.length === 0) {
|
|
35
|
-
throw new Error(`Request not found: ${number}`);
|
|
36
|
-
}
|
|
37
|
-
const record = records[0];
|
|
38
|
-
const sysID = getStringField(record, 'sys_id');
|
|
30
|
+
const record = await getRecord(app.sdk, { table: 'sc_req_item', identifier: number, matchField: 'number', identifierField: argv.identifierField, displayValues: argv.displayValues, resource: 'Request' });
|
|
31
|
+
const sysID = unwrapSysId(record);
|
|
39
32
|
|
|
40
33
|
// Fetch attachments and catalog variables in parallel
|
|
41
34
|
const [attachments, variables] = await Promise.all([
|
|
@@ -50,6 +43,7 @@ async function enrichedShowHandler(argv, app) {
|
|
|
50
43
|
table: 'sc_req_item',
|
|
51
44
|
};
|
|
52
45
|
|
|
46
|
+
await enrichRelatedLists(record, { table: 'sc_req_item', argv, app });
|
|
53
47
|
app.ok(record, {
|
|
54
48
|
summary: `Request ${number}`,
|
|
55
49
|
breadcrumbs: [
|
package/src/commands/rest.js
CHANGED
|
@@ -2,7 +2,7 @@ import { declareCapabilities } from '../capabilities.js';
|
|
|
2
2
|
|
|
3
3
|
// Raw REST passthrough — can hit any endpoint with any method, so the whole
|
|
4
4
|
// command is a mutation surface on read-only profiles.
|
|
5
|
-
declareCapabilities('rest', { mutationSubcommands: [''] });
|
|
5
|
+
declareCapabilities('rest', { mutationSubcommands: [''], mutationContextReadMethods: ['GET', 'HEAD', 'OPTIONS'] });
|
|
6
6
|
|
|
7
7
|
export function restCmd(wrap) {
|
|
8
8
|
return {
|
|
@@ -10,6 +10,10 @@ export function restCmd(wrap) {
|
|
|
10
10
|
describe: 'Make raw REST API calls',
|
|
11
11
|
builder: (yargs) => {
|
|
12
12
|
return yargs
|
|
13
|
+
.positional('endpoint', {
|
|
14
|
+
type: 'string',
|
|
15
|
+
describe: 'API path, optionally including query parameters; with --table, a record sys_id',
|
|
16
|
+
})
|
|
13
17
|
.option('method', {
|
|
14
18
|
alias: 'X',
|
|
15
19
|
type: 'string',
|
|
@@ -34,7 +38,16 @@ export function restCmd(wrap) {
|
|
|
34
38
|
.option('query', {
|
|
35
39
|
type: 'string',
|
|
36
40
|
describe: 'Query parameters string',
|
|
37
|
-
})
|
|
41
|
+
})
|
|
42
|
+
.epilogue([
|
|
43
|
+
'Prefer a specific jsn command or records for normal table operations.',
|
|
44
|
+
'Raw REST is blocked on read-only profiles for every method, including GET.',
|
|
45
|
+
'Examples:',
|
|
46
|
+
' jsn rest "/api/now/table/incident?sysparm_limit=3" --json',
|
|
47
|
+
' jsn rest --table incident --query "sysparm_limit=3" --json',
|
|
48
|
+
' jsn rest /stats.do --raw',
|
|
49
|
+
'Use either query parameters in the endpoint or --query, rather than both.',
|
|
50
|
+
].join('\n'));
|
|
38
51
|
},
|
|
39
52
|
handler: wrap(async (argv, app) => {
|
|
40
53
|
const validMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
|
package/src/commands/scopes.js
CHANGED
|
@@ -3,7 +3,7 @@ import { requireCurrentUserSysId, setCurrentApplication } from '../context.js';
|
|
|
3
3
|
import { declareCapabilities } from '../capabilities.js';
|
|
4
4
|
import { unwrapSysId } from '../resolve-record.js';
|
|
5
5
|
|
|
6
|
-
declareCapabilities('scopes', { mutationSubcommands: ['create', 'set'] });
|
|
6
|
+
declareCapabilities('scopes', { mutationSubcommands: ['create', 'set'], mutationContextExemptSubcommands: ['create', 'set'] });
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Resolve a scope identifier to a single sys_scope record.
|
package/src/commands/setup.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { loginWizard, loginProfile, showAuthStatus, switchProfile, modifyProfile, logoutProfile, removeConnection } from './auth.js';
|
|
2
|
-
import {
|
|
2
|
+
import { errUsageHint } from '../errors.js';
|
|
3
3
|
import { declareCapabilities } from '../capabilities.js';
|
|
4
4
|
|
|
5
5
|
// Setup manages local profile config — runs without a configured instance.
|
|
@@ -7,15 +7,36 @@ import { declareCapabilities } from '../capabilities.js';
|
|
|
7
7
|
declareCapabilities('setup', { noInstance: true, skipContextHeader: true });
|
|
8
8
|
|
|
9
9
|
const HUB_CHOICES = [
|
|
10
|
-
{ name: 'Add a connection', value: 'add' },
|
|
11
|
-
{ name: 'Sign in again', value: 'login' },
|
|
12
|
-
{ name: 'Check connections', value: 'status' },
|
|
13
|
-
{ name: 'Choose the default profile', value: 'switch' },
|
|
14
|
-
{ name: 'Change settings', value: 'modify' },
|
|
15
|
-
{ name: 'Sign out (keep the profile)', value: 'logout' },
|
|
16
|
-
{ name: 'Remove a connection', value: 'remove' },
|
|
10
|
+
{ name: 'Add a connection', value: 'add', command: 'jsn auth login <instance> --name <name> --basic' },
|
|
11
|
+
{ name: 'Sign in again', value: 'login', command: 'jsn auth login --profile <name> (with the authentication options below)' },
|
|
12
|
+
{ name: 'Check connections', value: 'status', command: 'jsn auth status --details --json (use --local to skip instance probes)' },
|
|
13
|
+
{ name: 'Choose the default profile', value: 'switch', command: 'jsn auth switch <name>' },
|
|
14
|
+
{ name: 'Change settings', value: 'modify', command: 'jsn auth modify --profile <name> --read-only --confirm-deletes --no-include-counts' },
|
|
15
|
+
{ name: 'Sign out (keep the profile)', value: 'logout', command: 'jsn auth logout --profile <name> --force (only after explicit approval)' },
|
|
16
|
+
{ name: 'Remove a connection', value: 'remove', command: 'jsn auth remove <name>' },
|
|
17
|
+
{ name: 'Install optional offline documentation', value: 'docs', command: 'jsn docs install --source snapshot|github|skip' },
|
|
17
18
|
];
|
|
18
19
|
|
|
20
|
+
const setupHelp = () => [
|
|
21
|
+
'With no profiles, setup opens the add-connection wizard; otherwise it opens the management menu.',
|
|
22
|
+
'The wizard and these scriptable commands share the same authentication and configuration operations:',
|
|
23
|
+
...HUB_CHOICES.map(choice => ` ${choice.name}: ${choice.command}`),
|
|
24
|
+
'',
|
|
25
|
+
'Authentication input without secrets in command arguments:',
|
|
26
|
+
' Basic: set SN_USERNAME and SN_PASSWORD through your environment, then use --basic.',
|
|
27
|
+
' OAuth: jsn auth login <instance> --name <name> --oauth --print-url --wait-file <code-file> --json',
|
|
28
|
+
' Browser session: jsn auth login <instance> --name <name> --headers-file <session-file> --json',
|
|
29
|
+
' Piped browser session: jsn auth login <instance> --name <name> --headers-file - --json',
|
|
30
|
+
' A browser window can also be requested explicitly with --browser; it requires a human to sign in.',
|
|
31
|
+
' Refresh OAuth: jsn auth refresh --profile <name>',
|
|
32
|
+
' Renew captured browser headers: jsn auth refresh --profile <name> --headers-file <session-file>',
|
|
33
|
+
'',
|
|
34
|
+
'New-profile preferences: --read-only, --confirm-deletes, --include-counts and --make-default.',
|
|
35
|
+
'Use --no-read-only, --no-confirm-deletes, --no-include-counts or --no-make-default for the opposite value.',
|
|
36
|
+
'Use --json for structured output. Explicit flags avoid implicit pickers; ambiguous targets return an error.',
|
|
37
|
+
'Discover all options with jsn auth <command> --help.',
|
|
38
|
+
].join('\n');
|
|
39
|
+
|
|
19
40
|
/** The setup hub prompt; q and Escape resolve with no selected action. */
|
|
20
41
|
export const authHubPrompt = async (...args) => {
|
|
21
42
|
const { createPrompt, useKeypress, usePagination, useState, isDownKey, isEnterKey, isUpKey } = await import('@inquirer/core');
|
|
@@ -81,19 +102,35 @@ export function setupCmd(wrap) {
|
|
|
81
102
|
return {
|
|
82
103
|
command: 'setup',
|
|
83
104
|
describe: 'Set up and manage your ServiceNow instances (interactive)',
|
|
105
|
+
builder: y => y.option('docs-source', { type: 'string', choices: ['snapshot', 'github', 'skip'], describe: 'Install optional documentation directly without the authentication wizard' }).epilogue(setupHelp()),
|
|
84
106
|
handler: wrap(async (argv, app) => {
|
|
107
|
+
if (argv['docs-source']) {
|
|
108
|
+
const { installDocumentation } = await import('./docs/install.js');
|
|
109
|
+
app.ok(await installDocumentation(argv['docs-source']));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
85
112
|
const { canUsePicker } = await import('../helpers.js');
|
|
86
113
|
if (!app.isInteractive() || !canUsePicker(app, argv)) {
|
|
87
|
-
throw
|
|
114
|
+
throw errUsageHint('"jsn setup" is interactive. Use explicit auth commands instead.', setupHelp());
|
|
88
115
|
}
|
|
89
116
|
const profileNames = Object.keys(app.config.profiles || {});
|
|
90
117
|
if (profileNames.length === 0) {
|
|
91
118
|
const result = await loginWizard(app, argv);
|
|
92
119
|
await showAuthStatus({ ...argv, profile: result.profileName }, app);
|
|
120
|
+
const { docsDbExists } = await import('./docs/db.js');
|
|
121
|
+
if (!docsDbExists()) {
|
|
122
|
+
const { offerDocumentation } = await import('./docs/install.js');
|
|
123
|
+
app.ok(await offerDocumentation(app, argv));
|
|
124
|
+
}
|
|
93
125
|
return;
|
|
94
126
|
}
|
|
95
127
|
const action = await authHubMenu();
|
|
96
128
|
if (action === undefined) return;
|
|
129
|
+
if (action === 'docs') {
|
|
130
|
+
const { offerDocumentation } = await import('./docs/install.js');
|
|
131
|
+
app.ok(await offerDocumentation(app, argv));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
97
134
|
const result = await dispatchSetupAction(app, argv, action);
|
|
98
135
|
if (action !== 'status') await showAuthStatus({ ...argv, profile: result?.profile || result?.profileName }, app);
|
|
99
136
|
}),
|