@jacebenson/jsn 4.1.0 → 4.2.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.
Files changed (47) hide show
  1. package/README.md +18 -2
  2. package/package.json +4 -1
  3. package/src/app.js +49 -65
  4. package/src/auth.js +46 -11
  5. package/src/capabilities.js +128 -0
  6. package/src/cli.js +129 -79
  7. package/src/commands/_ticket.js +36 -92
  8. package/src/commands/approvals.js +350 -0
  9. package/src/commands/atf.js +635 -0
  10. package/src/commands/auth.js +93 -13
  11. package/src/commands/catalog/enrich.js +139 -0
  12. package/src/commands/catalog.js +15 -133
  13. package/src/commands/cmdb.js +613 -0
  14. package/src/commands/codesearch.js +103 -0
  15. package/src/commands/dev/_generic.js +62 -110
  16. package/src/commands/dev/_simple.js +16 -12
  17. package/src/commands/dev/b4rules.js +1 -1
  18. package/src/commands/dev/domains.js +4 -0
  19. package/src/commands/dev/eval.js +40 -4
  20. package/src/commands/dev/flows.js +3 -0
  21. package/src/commands/dev/logs.js +97 -0
  22. package/src/commands/dev/rest.js +6 -0
  23. package/src/commands/dev/restmessage.js +1 -0
  24. package/src/commands/dev/scopes.js +49 -25
  25. package/src/commands/dev/scrapi.js +5 -0
  26. package/src/commands/dev/soapmessages.js +1 -0
  27. package/src/commands/dev/updatesets.js +145 -15
  28. package/src/commands/dev.js +3 -2
  29. package/src/commands/docs/db.js +14 -0
  30. package/src/commands/docs/docs.js +9 -21
  31. package/src/commands/docs/search.js +33 -10
  32. package/src/commands/groups.js +3 -0
  33. package/src/commands/records.js +254 -9
  34. package/src/commands/setup.js +5 -0
  35. package/src/commands/skill.js +3 -0
  36. package/src/commands/snippets.js +155 -0
  37. package/src/commands/tickets.js +9 -29
  38. package/src/commands/users.js +9 -35
  39. package/src/commands/version.js +3 -0
  40. package/src/errors.js +92 -1
  41. package/src/help.js +1 -0
  42. package/src/helpers.js +22 -11
  43. package/src/mutations.js +36 -91
  44. package/src/output.js +187 -61
  45. package/src/resolve-record.js +71 -0
  46. package/src/sdk.js +108 -8
  47. package/src/session.js +214 -0
@@ -0,0 +1,350 @@
1
+ // Approvals commands — top-level `jsn approvals`
2
+ //
3
+ // Surface (mirrors the atf command set):
4
+ // list List approval requests (default: pending, state=requested)
5
+ // approve <sys_id> Approve a pending request (mutation, confirm required)
6
+ // reject <sys_id> Reject a pending request (mutation, confirm required)
7
+ // submit <table> <sys-id> Request approval on a record (sets approval=requested)
8
+ // history <record> Show approval progression for a record (number or sys_id)
9
+ //
10
+ // Data model: sysapproval_approver rows. Each row is one approver's decision on
11
+ // one record. The record side lives on the task tables (approval field holds the
12
+ // rollup state: not requested/requested/approved/rejected/cancelled).
13
+ //
14
+ // state raw values (sys_choice, verified live on dev227772):
15
+ // not requested / requested / approved / rejected / cancelled / not_required / not_entitled
16
+ // Encoded queries must use the RAW value (display shows "Requested").
17
+ //
18
+ // approve/reject are mutations → confirmDelete + registered in MUTATION_COMMANDS.
19
+
20
+ import {
21
+ getStringField, interactiveList,
22
+ resolveFieldsParam, assertSafeExactMatch, confirmDelete,
23
+ } from '../helpers.js';
24
+ import { errUsage, errNotFound } from '../errors.js';
25
+ import { declareCapabilities } from '../capabilities.js';
26
+
27
+ // approve/reject/submit change approval state on records.
28
+ declareCapabilities('approvals', { mutationSubcommands: ['approve', 'reject', 'submit'] });
29
+
30
+ // Raw sysapproval_approver.state values (verified live).
31
+ const APPROVAL_STATES = [
32
+ 'not requested', 'requested', 'approved', 'rejected',
33
+ 'cancelled', 'not_required', 'not_entitled',
34
+ ];
35
+
36
+ const DEFAULT_COLUMNS = ['state', 'approver', 'document_id', 'comments', 'sys_updated_on'];
37
+
38
+ /**
39
+ * Map a user-supplied state label to the raw stored value. Accepts display
40
+ * labels ("Requested"), raw values ("requested"), or abbreviated forms
41
+ * ("approved", "rej"). Returns '' for "any", throws for unknown.
42
+ */
43
+ export function normalizeApprovalState(input) {
44
+ if (!input) return '';
45
+ const s = String(input).trim().toLowerCase();
46
+ if (!s || s === 'any' || s === 'all') return '';
47
+ if (s === 'pending') return 'requested';
48
+ if (s === 'no longer required') return 'not_required';
49
+ if (s === 'not requested' || s === 'not-entitled' || s === 'not_entitled') {
50
+ return s.replace(/-/g, '_');
51
+ }
52
+ const exact = APPROVAL_STATES.find((v) => v === s);
53
+ if (exact) return exact;
54
+ // prefix match (e.g. "rej" → rejected, "not req" → not requested)
55
+ const match = APPROVAL_STATES.find((v) => v.startsWith(s));
56
+ if (match) return match;
57
+ throw errUsage(`Unknown approval state "${input}". Valid: ${APPROVAL_STATES.join(', ')}, pending, any`);
58
+ }
59
+
60
+ /**
61
+ * Build the encoded query for the approvals list.
62
+ * @param {object} opts
63
+ * @param {string} opts.state — raw state value ('' = any)
64
+ * @param {string} opts.mineSysID — current user's sys_id for approver filter ('' = skip)
65
+ * @param {string} opts.recordSysID — sys_id of the record being approved ('' = skip)
66
+ * @param {string} opts.userQuery — additional encoded query ANDed on top
67
+ */
68
+ export function buildApproverQuery({ state = '', mineSysID = '', recordSysID = '', userQuery = '' }) {
69
+ const parts = [];
70
+ if (state) parts.push(`state=${state}`);
71
+ if (mineSysID) parts.push(`approver=${mineSysID}`);
72
+ if (recordSysID) parts.push(`document_id=${recordSysID}`);
73
+ if (userQuery) parts.push(userQuery);
74
+ return parts.join('^');
75
+ }
76
+
77
+ /**
78
+ * Resolve a record (number like CHG0000082, or 32-hex sys_id) via the task
79
+ * superclass. Works for any task-derived table (change, incident, sc_req_item…).
80
+ * @returns {Promise<object>} raw task record
81
+ */
82
+ export async function resolveTaskRecord(app, identifier, label = 'record') {
83
+ assertSafeExactMatch(identifier);
84
+ const isSysID = /^[0-9a-fA-F]{32}$/.test(identifier);
85
+ const params = new URLSearchParams();
86
+ params.set('sysparm_query', isSysID ? `sys_id=${identifier}` : `number=${identifier}`);
87
+ params.set('sysparm_limit', '1');
88
+ params.set('sysparm_display_value', 'all');
89
+ const records = await app.sdk.list('task', params);
90
+ if (!records || records.length === 0) throw errNotFound(label, identifier);
91
+ return records[0];
92
+ }
93
+
94
+ /**
95
+ * Fetch the approval rows for a record (the "approval progression").
96
+ */
97
+ export async function approvalRowsForRecord(app, recordSysID) {
98
+ const params = new URLSearchParams();
99
+ params.set('sysparm_query', `document_id=${recordSysID}`);
100
+ params.set('sysparm_limit', '100');
101
+ params.set('sysparm_display_value', 'all');
102
+ params.set('sysparm_fields', 'state,approver,comments,order,sys_updated_on,reassignment_count');
103
+ return app.sdk.list('sysapproval_approver', params);
104
+ }
105
+
106
+ /**
107
+ * Approve/reject an approver row.
108
+ * @returns {Promise<object>} updated row
109
+ */
110
+ export async function setApprovalState(app, approverSysID, state, comments = '') {
111
+ const data = { state };
112
+ if (comments) data.comments = comments;
113
+ const updated = await app.sdk.update('sysapproval_approver', approverSysID, data);
114
+ if (!updated) throw errUsage('Approval update returned no record');
115
+ return updated;
116
+ }
117
+
118
+ /**
119
+ * Request approval on a record (sets its approval field to "requested").
120
+ * @returns {Promise<object>} updated record
121
+ */
122
+ export async function submitForApproval(app, table, sysID) {
123
+ const updated = await app.sdk.update(table, sysID, { approval: 'requested' });
124
+ if (!updated) throw errUsage('Submit-for-approval returned no record');
125
+ return updated;
126
+ }
127
+
128
+ /** Current profile username ('' when no profile configured). */
129
+ export function currentUsername(app) {
130
+ return app?.context?.username || '';
131
+ }
132
+
133
+ /**
134
+ * Resolve the current session user's sys_id for `--mine` filtering.
135
+ * Ladder: /api/now/ui/me (modern instances) → profile username → env username.
136
+ * Throws with a clear hint when the user cannot be resolved — never silently
137
+ * returns '' (which would drop the filter and list every approver).
138
+ *
139
+ * Reads the username fresh from the auth manager (config live-read) rather
140
+ * than app.context, which is snapshotted at App construction and goes stale
141
+ * when --profile switches the active profile mid-command.
142
+ */
143
+ export async function resolveCurrentUserSysID(app) {
144
+ // 1) ui/me — the platform "who am I" endpoint (missing on older instances)
145
+ try {
146
+ const resp = await app.sdk.request(`${app.sdk.baseURL}/api/now/ui/me`, { method: 'GET' });
147
+ const me = resp?.result;
148
+ const sysID = me?.userID || me?.sys_id || me?.value || '';
149
+ if (sysID) return String(sysID);
150
+ } catch { /* fall through */ }
151
+
152
+ // 2) profile username (fresh read) → sys_user lookup
153
+ const username = app.auth?._activeUsername?.() || currentUsername(app) || process.env.SN_USERNAME;
154
+ if (username) {
155
+ const params = new URLSearchParams();
156
+ params.set('sysparm_query', `user_name=${username}`);
157
+ params.set('sysparm_limit', '1');
158
+ params.set('sysparm_fields', 'sys_id');
159
+ const users = await app.sdk.list('sys_user', params);
160
+ if (users && users.length > 0) {
161
+ const id = getStringField(users[0], 'sys_id');
162
+ if (id) return id;
163
+ }
164
+ }
165
+
166
+ throw errUsage(
167
+ 'Cannot resolve the current user for --mine. ' +
168
+ 'Set a username on the profile (jsn auth login) or pass --query "approver=<sys_id>" explicitly.'
169
+ );
170
+ }
171
+
172
+ export function approvalsCmd(wrap) {
173
+ return {
174
+ command: 'approvals [subcommand]',
175
+ describe: 'Manage approval requests (list, approve, reject, submit, history)',
176
+ builder: (yargs) => {
177
+ return yargs
178
+ .command({
179
+ command: 'list',
180
+ aliases: ['ls'],
181
+ describe: 'List approval requests (default: pending, state=requested)',
182
+ builder: (y) => y
183
+ .option('state', { type: 'string', describe: `State filter (default pending). One of: ${APPROVAL_STATES.join(', ')}, pending, any` })
184
+ .option('mine', { type: 'boolean', default: false, describe: 'Only approvals assigned to the current user' })
185
+ .option('for', { type: 'string', describe: 'Only approvals for this record (number or sys_id)' })
186
+ .option('query', { type: 'string', describe: 'Additional encoded query (ANDed)' })
187
+ .option('columns', { alias: ['c', 'fields'], type: 'string', describe: 'Comma-separated columns' })
188
+ .option('limit', { alias: 'l', type: 'number', default: 50, describe: 'Max records' }),
189
+ handler: wrap(async (argv, app) => {
190
+ app.requireInstance();
191
+ const state = normalizeApprovalState(argv.state);
192
+ let recordSysID = '';
193
+ if (argv.for) {
194
+ const rec = await resolveTaskRecord(app, argv.for, 'record');
195
+ recordSysID = getStringField(rec, 'sys_id');
196
+ }
197
+ let mineSysID = '';
198
+ if (argv.mine) mineSysID = await resolveCurrentUserSysID(app);
199
+ const query = buildApproverQuery({
200
+ state: state || (argv.state === undefined ? 'requested' : ''),
201
+ mineSysID,
202
+ recordSysID,
203
+ userQuery: argv.query,
204
+ });
205
+ const columns = argv.columns ? argv.columns.split(',') : DEFAULT_COLUMNS;
206
+
207
+ const picked = await interactiveList({
208
+ app, table: 'sysapproval_approver', singular: 'approval', columns, limit: argv.limit,
209
+ query, labelField: 'sys_id',
210
+ formatLabel: (r) => {
211
+ const doc = getStringField(r, 'document_id') || getStringField(r, 'sysapproval') || '?';
212
+ const approver = getStringField(r, 'approver');
213
+ return `${doc} — ${getStringField(r, 'state')}${approver ? ` (${approver})` : ''}`;
214
+ },
215
+ });
216
+ if (picked === undefined) return; // user cancelled
217
+ if (picked) {
218
+ const sysID = getStringField(picked, 'sys_id');
219
+ const pending = getStringField(picked, 'state') === 'Requested';
220
+ return app.ok(picked, {
221
+ summary: `Approval: ${getStringField(picked, 'document_id') || sysID}`,
222
+ breadcrumbs: [
223
+ ...(pending ? [
224
+ { action: 'approve', cmd: `jsn approvals approve ${sysID}`, description: 'Approve this request' },
225
+ { action: 'reject', cmd: `jsn approvals reject ${sysID}`, description: 'Reject this request' },
226
+ ] : []),
227
+ { action: 'history', cmd: `jsn approvals history ${getStringField(picked, 'document_id') || sysID}`, description: 'Show approval progression' },
228
+ ].filter(Boolean),
229
+ });
230
+ }
231
+
232
+ const params = new URLSearchParams();
233
+ params.set('sysparm_limit', String(argv.limit));
234
+ params.set('sysparm_display_value', 'all');
235
+ const fields = resolveFieldsParam(columns);
236
+ if (fields) params.set('sysparm_fields', fields);
237
+ if (query) params.set('sysparm_query', query);
238
+ const records = await app.sdk.list('sysapproval_approver', params);
239
+ app.ok({
240
+ table: 'sysapproval_approver',
241
+ count: records.length,
242
+ columns,
243
+ records,
244
+ context: { instance_url: app.getEffectiveInstance() },
245
+ }, { summary: `${records.length} approval(s)` });
246
+ }),
247
+ })
248
+ .command({
249
+ command: 'approve <sys-id>',
250
+ describe: 'Approve a pending approval request. Mutation — requires confirmation',
251
+ builder: (y) => y
252
+ .positional('sys-id', { describe: 'Approval request sys_id (from approvals list)', type: 'string' })
253
+ .option('comments', { type: 'string', describe: 'Approval comments' })
254
+ .option('force', { type: 'boolean', default: false, describe: 'Skip confirmation' }),
255
+ handler: wrap(async (argv, app) => {
256
+ app.requireInstance();
257
+ await confirmDelete(app, argv, `Approve approval request ${argv['sys-id']}?`);
258
+ assertSafeExactMatch(argv['sys-id']);
259
+ const updated = await setApprovalState(app, argv['sys-id'], 'approved', argv.comments || '');
260
+ app.ok(updated, {
261
+ summary: `Approved ${getStringField(updated, 'document_id') || argv['sys-id']}`,
262
+ breadcrumbs: [
263
+ { action: 'history', cmd: `jsn approvals history ${getStringField(updated, 'document_id') || argv['sys-id']}`, description: 'Show approval progression' },
264
+ ],
265
+ });
266
+ }),
267
+ })
268
+ .command({
269
+ command: 'reject <sys-id>',
270
+ describe: 'Reject a pending approval request. Mutation — requires confirmation',
271
+ builder: (y) => y
272
+ .positional('sys-id', { describe: 'Approval request sys_id (from approvals list)', type: 'string' })
273
+ .option('comments', { type: 'string', describe: 'Rejection comments' })
274
+ .option('force', { type: 'boolean', default: false, describe: 'Skip confirmation' }),
275
+ handler: wrap(async (argv, app) => {
276
+ app.requireInstance();
277
+ await confirmDelete(app, argv, `Reject approval request ${argv['sys-id']}?`);
278
+ assertSafeExactMatch(argv['sys-id']);
279
+ const updated = await setApprovalState(app, argv['sys-id'], 'rejected', argv.comments || '');
280
+ app.ok(updated, {
281
+ summary: `Rejected ${getStringField(updated, 'document_id') || argv['sys-id']}`,
282
+ breadcrumbs: [
283
+ { action: 'history', cmd: `jsn approvals history ${getStringField(updated, 'document_id') || argv['sys-id']}`, description: 'Show approval progression' },
284
+ ],
285
+ });
286
+ }),
287
+ })
288
+ .command({
289
+ command: 'submit <table> <sys-id>',
290
+ describe: 'Request approval on a record (sets approval=requested). Mutation — requires confirmation',
291
+ builder: (y) => y
292
+ .positional('table', { describe: 'Record table (e.g. change_request, incident)', type: 'string' })
293
+ .positional('sys-id', { describe: 'Record sys_id', type: 'string' })
294
+ .option('force', { type: 'boolean', default: false, describe: 'Skip confirmation' }),
295
+ handler: wrap(async (argv, app) => {
296
+ app.requireInstance();
297
+ await confirmDelete(app, argv, `Submit ${argv.table} ${argv['sys-id']} for approval?`);
298
+ assertSafeExactMatch(argv['sys-id']);
299
+ const updated = await submitForApproval(app, argv.table, argv['sys-id']);
300
+ app.ok(updated, {
301
+ summary: `Submitted ${argv.table} ${argv['sys-id']} for approval`,
302
+ breadcrumbs: [
303
+ { action: 'list', cmd: 'jsn approvals list', description: 'See the generated approval requests' },
304
+ ],
305
+ });
306
+ }),
307
+ })
308
+ .command({
309
+ command: 'history <record>',
310
+ describe: 'Show approval progression for a record (number or sys_id)',
311
+ builder: (y) => y
312
+ .positional('record', { describe: 'Record number (CHG0000082) or sys_id', type: 'string' }),
313
+ handler: wrap(async (argv, app) => {
314
+ app.requireInstance();
315
+ const rec = await resolveTaskRecord(app, argv.record, 'record');
316
+ const sysID = getStringField(rec, 'sys_id');
317
+ const rows = await approvalRowsForRecord(app, sysID);
318
+ app.ok({
319
+ record: {
320
+ number: getStringField(rec, 'number'),
321
+ table: getStringField(rec, 'sys_class_name'),
322
+ approval: getStringField(rec, 'approval'),
323
+ state: getStringField(rec, 'state'),
324
+ },
325
+ approvals: rows,
326
+ count: rows.length,
327
+ context: { instance_url: app.getEffectiveInstance() },
328
+ }, {
329
+ summary: `${getStringField(rec, 'number')}: approval=${getStringField(rec, 'approval') || '-'}, ${rows.length} approver row(s)`,
330
+ breadcrumbs: rows.length ? [
331
+ { action: 'list', cmd: `jsn approvals list --for ${getStringField(rec, 'number')}`, description: 'List approval requests for this record' },
332
+ ] : [],
333
+ });
334
+ }),
335
+ });
336
+ },
337
+ handler: (argv) => {
338
+ if (argv._[1]) return; // a subcommand ran — its own handler handled it
339
+ console.log('Manage ServiceNow approval requests.\n');
340
+ console.log('Commands:');
341
+ console.log(' list List approval requests (--mine, --for <record>, --state)');
342
+ console.log(' approve <sys-id> Approve a request (mutation)');
343
+ console.log(' reject <sys-id> Reject a request (mutation)');
344
+ console.log(' submit <table> <id> Request approval on a record (mutation)');
345
+ console.log(' history <record> Show approval progression (number or sys_id)');
346
+ console.log('\nRun "jsn approvals <command> --help" for details.');
347
+ console.log('\napprove/reject/submit are mutations: confirm when prompted, or pass --force / profile skip_confirmations.');
348
+ },
349
+ };
350
+ }