@jacebenson/jsn 4.3.7-dev.31 → 4.3.7-dev.33

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacebenson/jsn",
3
- "version": "4.3.7-dev.31",
3
+ "version": "4.3.7-dev.33",
4
4
  "description": "A command-line interface for ServiceNow that follows the Unix philosophy: simple, composable, and scriptable.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { input, password as passwordPrompt, select } from '@inquirer/prompts';
4
- import { confirmDelete, formatRecordForDisplay, getStringField, interactiveList, canPrompt } from '../helpers.js';
4
+ import { confirmDelete, formatRecordForDisplay, getStringField, interactiveList, canPrompt, assertSafeExactMatch } from '../helpers.js';
5
5
  import { FormatAuto } from '../output.js';
6
6
  import { declareCapabilities } from '../capabilities.js';
7
7
  import { resolveRecord, unwrapSysId } from '../resolve-record.js';
@@ -91,6 +91,85 @@ async function resolveMidServer(sdk, identifier) {
91
91
  }
92
92
  }
93
93
 
94
+ async function showMidDiagnostics(app, argv, kind) {
95
+ app.requireInstance();
96
+ let mid;
97
+ if (argv.identifier) {
98
+ mid = await resolveMidServer(app.sdk, argv.identifier);
99
+ } else {
100
+ if (!canPrompt() || app.output.getFormat() !== FormatAuto) {
101
+ throw new Error('An identifier is required outside an interactive terminal');
102
+ }
103
+ mid = await interactiveList({
104
+ app, table: AGENT_TABLE, singular: 'MID Server', columns: LIST_FIELDS,
105
+ limit: 50, labelField: 'name', message: `Select a MID Server for ${kind}`,
106
+ formatLabel: record => `${getStringField(record, 'name')} [${getStringField(record, 'status')}]`,
107
+ });
108
+ if (!mid) return;
109
+ }
110
+ const name = getStringField(mid, 'name');
111
+ const sysId = unwrapSysId(mid);
112
+ assertSafeExactMatch(name);
113
+ assertSafeExactMatch(sysId);
114
+ const issues = kind === 'issues';
115
+ const table = issues ? 'ecc_agent_issue' : 'mid_server_event';
116
+ const columns = issues
117
+ ? ['sys_id', 'state', 'source', 'message', 'last_detected']
118
+ : ['sys_id', 'event', 'source', 'message', 'sys_created_on'];
119
+ const query = issues
120
+ ? `mid_server=${sysId}${argv.all ? '' : '^state!=resolved'}^ORDERBYDESClast_detected`
121
+ : `agent=${name}^ORDERBYDESCsys_created_on`;
122
+ const limit = argv.limit ?? 50;
123
+ const offset = argv.offset ?? 0;
124
+ if (!Number.isInteger(limit) || limit < 1 || limit > 1000 || !Number.isInteger(offset) || offset < 0) {
125
+ throw new Error('Use --limit between 1 and 1000 and a nonnegative --offset');
126
+ }
127
+ const records = await app.sdk.list(table, new URLSearchParams({
128
+ sysparm_query: query, sysparm_fields: columns.join(','), sysparm_display_value: 'all',
129
+ sysparm_limit: String(limit), sysparm_offset: String(offset),
130
+ }));
131
+ const instanceURL = app.getEffectiveInstance();
132
+ const url = new URL(`/${table}_list.do`, instanceURL);
133
+ url.searchParams.set('sysparm_query', query);
134
+ const rows = records.map(record => ({
135
+ ...record,
136
+ url: new URL(`/${table}.do?sys_id=${encodeURIComponent(unwrapSysId(record))}`, instanceURL).href,
137
+ }));
138
+ const label = issues ? (argv.all ? 'issues' : 'unresolved issues') : 'lifecycle events';
139
+ const summary = rows.length
140
+ ? `Showing ${rows.length} ${label} for MID Server '${name}' (offset ${offset})`
141
+ : `No ${label} returned for MID Server '${name}' (offset ${offset})`;
142
+ const lines = [summary, ''];
143
+ for (const record of rows) {
144
+ lines.push(`${getStringField(record, issues ? 'last_detected' : 'sys_created_on')} ${getStringField(record, issues ? 'state' : 'event')}`);
145
+ const source = getStringField(record, 'source');
146
+ const message = getStringField(record, 'message');
147
+ if (source) lines.push(` ${source}`);
148
+ if (message) lines.push(` ${message}`);
149
+ lines.push(` ${record.url}`, '');
150
+ }
151
+ lines.push(`View in ServiceNow: ${url.href}`);
152
+ app.ok({
153
+ mid_server: name, sys_id: sysId, table, count: rows.length, records: rows,
154
+ pagination: { limit, offset }, url: url.href, context: { instance_url: instanceURL },
155
+ _formatted: `${lines.join('\n')}\n`,
156
+ }, { summary });
157
+ }
158
+
159
+ function midDiagnosticsCommand(kind, wrap) {
160
+ return {
161
+ command: `${kind} [identifier]`,
162
+ describe: kind === 'issues' ? 'Show MID Server issues (unresolved by default)' : 'Show MID Server lifecycle history',
163
+ builder: b => {
164
+ b.option('limit', { alias: 'l', type: 'number', default: 50, describe: 'Max records (1-1000)' })
165
+ .option('offset', { type: 'number', default: 0, describe: 'Result offset' });
166
+ if (kind === 'issues') b.option('all', { type: 'boolean', default: false, describe: 'Include resolved issues' });
167
+ return b;
168
+ },
169
+ handler: wrap(async (argv, app) => showMidDiagnostics(app, argv, kind)),
170
+ };
171
+ }
172
+
94
173
  async function listAgents(app, argv) {
95
174
  const columns = argv.columns
96
175
  ? [...new Set(argv.columns.split(',').map(column => column.trim()).filter(Boolean))]
@@ -244,6 +323,8 @@ export function midserversCmd(wrap) {
244
323
  aliases: ['midserver', 'mid'],
245
324
  describe: 'Manage ServiceNow MID Servers and run commands through them',
246
325
  builder: (y) => y
326
+ .command(midDiagnosticsCommand('issues', wrap))
327
+ .command(midDiagnosticsCommand('events', wrap))
247
328
  .command({
248
329
  command: 'list',
249
330
  aliases: ['ls'],
@@ -457,12 +538,21 @@ export function midserversCmd(wrap) {
457
538
  const responseError = getStringField(response, 'error');
458
539
  if (responseError) throw new Error(`MID Server command failed: ${responseError}`);
459
540
  const result = parseCommandResult(queueValue(response.payload));
541
+ const responseSysId = unwrapSysId(response);
542
+ const responseURL = responseSysId
543
+ ? new URL(`/ecc_queue.do?sys_id=${encodeURIComponent(responseSysId)}`, app.getEffectiveInstance()).href
544
+ : '';
545
+ const summary = `Executed command on MID Server '${getStringField(record, 'name')}'`;
546
+ const lines = [summary, '', result.stdout || '(no stdout)'];
547
+ if (result.stderr) lines.push('', 'stderr:', result.stderr);
548
+ if (responseURL) lines.push('', `ECC input: ${responseURL}`);
460
549
  app.ok({
461
550
  mid_server: getStringField(record, 'name'),
462
551
  command,
463
552
  output: { sys_id: outputSysId, ...output },
464
- response: { ...response, ...result },
465
- }, { summary: `Executed command on MID Server '${getStringField(record, 'name')}'` });
553
+ response: { ...response, ...result, url: responseURL },
554
+ _formatted: `${lines.join('\n')}\n`,
555
+ }, { summary });
466
556
  }),
467
557
  }),
468
558
  handler: () => {
@@ -470,6 +560,8 @@ export function midserversCmd(wrap) {
470
560
  console.log('');
471
561
  console.log('Available subcommands:');
472
562
  console.log(' list List MID Servers');
563
+ console.log(' issues [name_or_sys_id] Show unresolved issues (--all includes resolved)');
564
+ console.log(' events [name_or_sys_id] Show lifecycle history');
473
565
  console.log(' show <name_or_sys_id> Show a MID Server');
474
566
  console.log(' add --name <name> Create a MID Server record');
475
567
  console.log(' remove <name_or_sys_id> Remove a MID Server record');