@jacebenson/jsn 4.2.1 → 4.2.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 +20 -0
- package/package.json +1 -1
- package/src/app.js +2 -2
- package/src/auth.js +12 -7
- package/src/cli.js +6 -0
- package/src/commands/auth.js +36 -14
- package/src/commands/dev/flows.js +120 -2
- package/src/commands/dev/rest.js +8 -1
- package/src/commands/instance.js +362 -0
- package/src/commands/platform.js +127 -0
- package/src/commands/records.js +46 -1
- package/src/commands/transactions.js +100 -0
- package/src/flow-context.js +184 -0
- package/src/records/inspect.js +14 -13
- package/src/sdk.js +68 -6
package/README.md
CHANGED
|
@@ -53,6 +53,12 @@ jsn incidents create --description "Server down" --priority 1
|
|
|
53
53
|
# Admin tasks
|
|
54
54
|
jsn flows list # Interactive picker with pagination
|
|
55
55
|
jsn flows show "Assign Task" # Full flow detail
|
|
56
|
+
jsn flows executions # All states, newest first
|
|
57
|
+
jsn flows executions --active # Only waiting/running/queued
|
|
58
|
+
jsn flows executions --since "2026-08-25 00:00:00" --until "2026-08-26 00:00:00"
|
|
59
|
+
jsn flows executions --summary # Server totals + sampled duration metrics by flow
|
|
60
|
+
jsn flows executions --limit 100 # Inspect more rows from the matching population
|
|
61
|
+
jsn flows executions --record <sys_id> # Executions for one source record
|
|
56
62
|
jsn rules list --query "collection=incident"
|
|
57
63
|
jsn updatesets set "My Feature"
|
|
58
64
|
|
|
@@ -78,6 +84,20 @@ jsn snippets run open-inc
|
|
|
78
84
|
jsn logs follow --level error --tail 10
|
|
79
85
|
```
|
|
80
86
|
|
|
87
|
+
### Flow execution fields
|
|
88
|
+
|
|
89
|
+
`jsn flows executions` reads `sys_flow_context` and returns both the raw row and a normalized `execution` object. JSN discovers the runtime columns from `sys_dictionary` first, then uses these mappings:
|
|
90
|
+
|
|
91
|
+
- `started`: `started`, `start_time`, `started_at`, then `sys_created_on`
|
|
92
|
+
- `ended`: `ended`, `end_time`, `ended_at`, then `completed_on`
|
|
93
|
+
- `duration_seconds`: stored `duration`, `run_time`, or `execution_duration`, otherwise `ended - started`
|
|
94
|
+
- `waiting_age_seconds`: current time minus `started` while the status is waiting, queued, paused, or pending
|
|
95
|
+
- `execution_age_seconds`: current time minus `started` while the status is running, executing, or processing
|
|
96
|
+
- `status`: `state`, then `status`, then `execution_state`
|
|
97
|
+
- `error`: `error`, `error_message`, `exception`, then `message`
|
|
98
|
+
|
|
99
|
+
The timestamp fields are instance-dependent. `sys_created_on` is a fallback for when the context row was created, not a claim that it is the true runtime start time.
|
|
100
|
+
|
|
81
101
|
## Commands
|
|
82
102
|
|
|
83
103
|
Run `jsn` for the full grouped list. Commands are organized by ServiceNow domain:
|
package/package.json
CHANGED
package/src/app.js
CHANGED
|
@@ -131,10 +131,10 @@ export class App {
|
|
|
131
131
|
process.stderr.write('# Use `jsn scopes` to change scope, `jsn updatesets set` to change updateset\n');
|
|
132
132
|
process.stderr.write('PROFILE USER [SCOPE] UPDATE SET\n');
|
|
133
133
|
|
|
134
|
-
const profileStr = hyperlink(
|
|
134
|
+
const profileStr = `${hyperlink(String(this.context.profileName).padEnd(9), instanceLink)}${roBadge}`;
|
|
135
135
|
const userStr = hyperlink(String(displayUserName).padEnd(9), userLink);
|
|
136
136
|
const scopeStr = hyperlink(String(scopeFormatted).padEnd(17), scopeLink);
|
|
137
|
-
const updateSetStr = hyperlink(updateSet, updateSetLink);
|
|
137
|
+
const updateSetStr = hyperlink(String(updateSet), updateSetLink);
|
|
138
138
|
|
|
139
139
|
process.stderr.write(`${profileStr} ${userStr} ${scopeStr} ${updateSetStr}\n\n`);
|
|
140
140
|
|
package/src/auth.js
CHANGED
|
@@ -366,6 +366,9 @@ export class AuthManager {
|
|
|
366
366
|
if (getBasicAuthFromEnv(instance)) return true;
|
|
367
367
|
const creds = loadCredentials(instance, this._activeUsername());
|
|
368
368
|
if (!creds) return false;
|
|
369
|
+
if (creds.auth_method === 'basic') {
|
|
370
|
+
return Boolean(creds.username && creds.password);
|
|
371
|
+
}
|
|
369
372
|
if (creds.expires_at && Date.now() >= creds.expires_at * 1000) return false;
|
|
370
373
|
return !!creds.access_token;
|
|
371
374
|
}
|
|
@@ -384,11 +387,11 @@ export class AuthManager {
|
|
|
384
387
|
return this.getCredentialsFor(instance);
|
|
385
388
|
}
|
|
386
389
|
|
|
387
|
-
getCredentialsFor(instance) {
|
|
390
|
+
getCredentialsFor(instance, username = this._activeUsername()) {
|
|
388
391
|
// Check basic auth from env vars first
|
|
389
392
|
const basicCreds = getBasicAuthFromEnv(instance);
|
|
390
393
|
if (basicCreds) return basicCreds;
|
|
391
|
-
const creds = loadCredentials(instance,
|
|
394
|
+
const creds = loadCredentials(instance, username);
|
|
392
395
|
if (!creds) {
|
|
393
396
|
throw errAuth(`Not authenticated for ${instance}`);
|
|
394
397
|
}
|
|
@@ -483,17 +486,19 @@ export class AuthManager {
|
|
|
483
486
|
* Reads SN_USERNAME/SN_PASSWORD (or SN_<INSTANCE>_USERNAME/SN_<INSTANCE>_PASSWORD).
|
|
484
487
|
* Stores the basic auth credentials so they persist across sessions.
|
|
485
488
|
*/
|
|
486
|
-
async loginWithPassword(instanceURL) {
|
|
489
|
+
async loginWithPassword(instanceURL, username = this._activeUsername()) {
|
|
487
490
|
instanceURL = normalizeInstanceURL(instanceURL);
|
|
488
|
-
const
|
|
489
|
-
|
|
491
|
+
const envCreds = getBasicAuthFromEnv(instanceURL);
|
|
492
|
+
const storedCreds = loadCredentials(instanceURL, username);
|
|
493
|
+
const creds = envCreds || storedCreds;
|
|
494
|
+
if (!creds || creds.auth_method !== 'basic' || !creds.username || !creds.password) {
|
|
490
495
|
throw errAuth(
|
|
491
|
-
`No basic auth credentials found in environment.\n\n` +
|
|
496
|
+
`No basic auth credentials found in environment or stored profile.\n\n` +
|
|
492
497
|
`Set the following environment variables:\n` +
|
|
493
498
|
` SN_USERNAME=admin (or SN_${envVarName(instanceURL)}_USERNAME)\n` +
|
|
494
499
|
` SN_PASSWORD=<password> (or SN_${envVarName(instanceURL)}_PASSWORD)\n\n` +
|
|
495
500
|
`Then run:\n` +
|
|
496
|
-
` jsn auth login --
|
|
501
|
+
` jsn auth login --basic ${instanceURL}`
|
|
497
502
|
);
|
|
498
503
|
}
|
|
499
504
|
saveCredentials(instanceURL, creds, creds.username);
|
package/src/cli.js
CHANGED
|
@@ -29,6 +29,9 @@ import { groupsCmd } from './commands/groups.js';
|
|
|
29
29
|
import { groupMembersCmd } from './commands/groupmembers.js';
|
|
30
30
|
import { groupRolesCmd } from './commands/grouproles.js';
|
|
31
31
|
import { ticketsCmd } from './commands/tickets.js';
|
|
32
|
+
import { transactionsCmd } from './commands/transactions.js';
|
|
33
|
+
import { instanceCmd } from './commands/instance.js';
|
|
34
|
+
import { platformCmd } from './commands/platform.js';
|
|
32
35
|
import { codesearchCmd } from './commands/codesearch.js';
|
|
33
36
|
import { versionCmd } from './commands/version.js';
|
|
34
37
|
import { devCmd } from './commands/dev.js';
|
|
@@ -354,6 +357,9 @@ export function buildCLI() {
|
|
|
354
357
|
// DATA
|
|
355
358
|
.command(cmdbCmd(wrap))
|
|
356
359
|
.command(recordsCmd(wrap))
|
|
360
|
+
.command(transactionsCmd(wrap))
|
|
361
|
+
.command(instanceCmd(wrap))
|
|
362
|
+
.command(platformCmd(wrap))
|
|
357
363
|
.command(snippetsCmd(wrap))
|
|
358
364
|
.command(ticketsCmd(wrap))
|
|
359
365
|
.command(codesearchCmd(wrap))
|
package/src/commands/auth.js
CHANGED
|
@@ -82,6 +82,18 @@ export function resolveInstanceArg(arg, cfg) {
|
|
|
82
82
|
return normalizeInstanceURL(`${trimmed}.service-now.com`);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Select the login method. Explicit Basic Auth wins; otherwise reuse the
|
|
87
|
+
* method recorded on an existing profile. New instances remain OAuth by
|
|
88
|
+
* default.
|
|
89
|
+
*/
|
|
90
|
+
export function shouldUseBasicAuth(argv = {}, cfg = {}, instanceURL = '') {
|
|
91
|
+
if (argv.basic || argv.password) return true;
|
|
92
|
+
return Object.values(cfg.profiles || {}).some(profile =>
|
|
93
|
+
profile.instance_url === instanceURL && profile.auth_method === 'basic'
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
85
97
|
/**
|
|
86
98
|
* Pick a profile by name — interactive search picker when ambiguous,
|
|
87
99
|
* auto-selects when there's exactly one, throws when none configured.
|
|
@@ -111,9 +123,10 @@ export async function pickProfile(app, message) {
|
|
|
111
123
|
* always asks otherwise.
|
|
112
124
|
*/
|
|
113
125
|
export function resolveWizardInstance(app, argv = {}) {
|
|
114
|
-
//
|
|
115
|
-
// the
|
|
116
|
-
|
|
126
|
+
// Only explicit inputs may pre-fill the wizard. The active/default profile
|
|
127
|
+
// belongs to the existing connection and must not silently become the new
|
|
128
|
+
// instance when setup is adding a profile.
|
|
129
|
+
return process.env.SERVICENOW_INSTANCE_URL || app._overrideInstance || argv.instance || '';
|
|
117
130
|
}
|
|
118
131
|
|
|
119
132
|
/**
|
|
@@ -426,7 +439,7 @@ export async function removeProfile(app, name) {
|
|
|
426
439
|
export function authCmd(wrap) {
|
|
427
440
|
return {
|
|
428
441
|
command: 'auth [subcommand]',
|
|
429
|
-
describe: 'Manage authentication (OAuth or
|
|
442
|
+
describe: 'Manage authentication (OAuth or Basic Auth)',
|
|
430
443
|
builder: (yargs) => {
|
|
431
444
|
return yargs
|
|
432
445
|
.command({
|
|
@@ -437,8 +450,9 @@ export function authCmd(wrap) {
|
|
|
437
450
|
describe: 'Authorization code from browser (bypasses interactive prompt)',
|
|
438
451
|
type: 'string',
|
|
439
452
|
})
|
|
440
|
-
.option('
|
|
441
|
-
|
|
453
|
+
.option('basic', {
|
|
454
|
+
alias: 'password',
|
|
455
|
+
describe: 'Authenticate with Basic Auth via env vars (SN_USERNAME/SN_PASSWORD)',
|
|
442
456
|
type: 'boolean',
|
|
443
457
|
})
|
|
444
458
|
.option('print-url', {
|
|
@@ -497,15 +511,20 @@ export function authCmd(wrap) {
|
|
|
497
511
|
Examples:
|
|
498
512
|
jsn auth login https://dev12345.service-now.com
|
|
499
513
|
jsn auth login dev373698
|
|
500
|
-
jsn auth login --
|
|
514
|
+
jsn auth login --basic https://dev328604.service-now.com
|
|
501
515
|
|
|
502
516
|
Find your instance URL in your browser's address bar when logged into ServiceNow.`);
|
|
503
517
|
}
|
|
504
518
|
}
|
|
505
519
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
520
|
+
const targetProfile = Object.values(app.config.profiles || {})
|
|
521
|
+
.find(profile => profile.instance_url === instanceURL);
|
|
522
|
+
const targetUsername = targetProfile?.username || '';
|
|
523
|
+
const useBasic = shouldUseBasicAuth(argv, app.config, instanceURL);
|
|
524
|
+
// --basic (with --password as a compatibility alias): authenticate
|
|
525
|
+
// with basic auth from env vars and persist it for this profile.
|
|
526
|
+
if (useBasic) {
|
|
527
|
+
await app.auth.loginWithPassword(instanceURL, targetUsername || undefined);
|
|
509
528
|
}
|
|
510
529
|
// --print-url with --wait-file: print URL and wait for code file
|
|
511
530
|
else if (argv.printUrl && argv.waitFile) {
|
|
@@ -528,9 +547,12 @@ Find your instance URL in your browser's address bar when logged into ServiceNow
|
|
|
528
547
|
// Verify auth by fetching current user
|
|
529
548
|
let username = '';
|
|
530
549
|
try {
|
|
531
|
-
if (!app.sdk) {
|
|
550
|
+
if (!app.sdk || targetUsername) {
|
|
532
551
|
const { SDKClient } = await import('../sdk.js');
|
|
533
|
-
|
|
552
|
+
const authProvider = targetUsername
|
|
553
|
+
? { getCredentials: () => app.auth.getCredentialsFor(instanceURL, targetUsername) }
|
|
554
|
+
: app.auth;
|
|
555
|
+
app.sdk = new SDKClient(instanceURL, authProvider);
|
|
534
556
|
}
|
|
535
557
|
const user = await app.sdk.getCurrentUser();
|
|
536
558
|
username = user?.user_name || user?.name || '';
|
|
@@ -560,7 +582,7 @@ Find your instance URL in your browser's address bar when logged into ServiceNow
|
|
|
560
582
|
app.config.profiles[profileName] = {
|
|
561
583
|
...(app.config.profiles[profileName] || {}),
|
|
562
584
|
instance_url: instanceURL,
|
|
563
|
-
auth_method:
|
|
585
|
+
auth_method: useBasic ? 'basic' : 'oauth',
|
|
564
586
|
username: username || undefined,
|
|
565
587
|
read_only: argv['read-only'] || undefined,
|
|
566
588
|
skip_confirmations: argv['skip-confirmations'] || undefined,
|
|
@@ -571,7 +593,7 @@ Find your instance URL in your browser's address bar when logged into ServiceNow
|
|
|
571
593
|
// so they're keyed by <user>@<instance> for per-user isolation.
|
|
572
594
|
// The legacy bare-instance key is cleaned up behind the
|
|
573
595
|
// AuthManager seam (credential-store internals don't leak here).
|
|
574
|
-
if (!
|
|
596
|
+
if (!useBasic && username) {
|
|
575
597
|
app.auth.migrateLegacyCredential(instanceURL, username);
|
|
576
598
|
}
|
|
577
599
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { formatRecordForDisplay, getStringField, interactiveList } from '../../helpers.js';
|
|
2
|
+
import { discoverFlowContextFields, normalizeFlowContext, summarizeFlowContexts, formatFlowContextSummary, aggregateFlowContextMappings, mergeFlowContextStats, buildFlowContextQuery } from '../../flow-context.js';
|
|
2
3
|
import { declareCapabilities } from '../../capabilities.js';
|
|
3
4
|
|
|
4
5
|
declareCapabilities('flows', { mutationSubcommands: ['create', 'update', 'delete'], devAlias: true });
|
|
@@ -66,6 +67,62 @@ export function flowsCmd(wrap) {
|
|
|
66
67
|
}, { summary: `${records.length} flow(s)` });
|
|
67
68
|
}),
|
|
68
69
|
})
|
|
70
|
+
.command({
|
|
71
|
+
command: 'executions',
|
|
72
|
+
aliases: ['runs', 'contexts'],
|
|
73
|
+
describe: 'Show Flow Designer executions from sys_flow_context',
|
|
74
|
+
builder: (y) => y
|
|
75
|
+
.option('query', { type: 'string', describe: 'Encoded query (e.g. "state=WAITING" or "source_record=<sys_id>")' })
|
|
76
|
+
.option('record', { type: 'string', describe: 'Only executions for this source record sys_id' })
|
|
77
|
+
.option('since', { type: 'string', describe: 'Only contexts created on or after this timestamp' })
|
|
78
|
+
.option('until', { type: 'string', describe: 'Only contexts created on or before this timestamp' })
|
|
79
|
+
.option('limit', { alias: 'l', type: 'number', default: 50, describe: 'Max executions' })
|
|
80
|
+
.option('summary', { type: 'boolean', default: false, describe: 'Return local counts and duration metrics by flow' })
|
|
81
|
+
.option('active', { type: 'boolean', default: false, describe: 'Only waiting, running, and queued executions' })
|
|
82
|
+
.option('all', { type: 'boolean', default: false, describe: 'Compatibility flag; all states are now shown by default' }),
|
|
83
|
+
handler: wrap(async (argv, app) => {
|
|
84
|
+
app.requireInstance();
|
|
85
|
+
const fields = await discoverFlowContextFields(app.sdk);
|
|
86
|
+
const query = buildFlowContextQuery({
|
|
87
|
+
record: argv.record,
|
|
88
|
+
since: argv.since,
|
|
89
|
+
until: argv.until,
|
|
90
|
+
query: argv.query || (argv.active ? 'stateINWAITING,RUNNING,QUEUED' : ''),
|
|
91
|
+
});
|
|
92
|
+
const params = new URLSearchParams();
|
|
93
|
+
params.set('sysparm_query', query);
|
|
94
|
+
params.set('sysparm_limit', String(argv.limit));
|
|
95
|
+
params.set('sysparm_fields', [...fields].join(','));
|
|
96
|
+
params.set('sysparm_display_value', 'all');
|
|
97
|
+
const records = await app.sdk.list('sys_flow_context', params);
|
|
98
|
+
const executions = records.map(record => normalizeFlowContext(record));
|
|
99
|
+
const sampledSummary = summarizeFlowContexts(executions);
|
|
100
|
+
let executionSummary = sampledSummary;
|
|
101
|
+
if (argv.summary) {
|
|
102
|
+
const [totalCount, stats] = await Promise.all([
|
|
103
|
+
app.sdk.aggregateCount('sys_flow_context', query).catch(() => null),
|
|
104
|
+
app.sdk.aggregate('sys_flow_context', { query, groupBy: ['name', 'state'], count: true }).catch(() => null),
|
|
105
|
+
]);
|
|
106
|
+
executionSummary = mergeFlowContextStats(stats?.groups || stats?.stats || [], sampledSummary, totalCount);
|
|
107
|
+
}
|
|
108
|
+
const data = {
|
|
109
|
+
table: 'sys_flow_context',
|
|
110
|
+
count: argv.summary ? executionSummary.total_count ?? executions.length : executions.length,
|
|
111
|
+
sample_count: executions.length,
|
|
112
|
+
query,
|
|
113
|
+
fields: [...fields],
|
|
114
|
+
field_mapping: aggregateFlowContextMappings(executions),
|
|
115
|
+
missing_fields: [...new Set(executions.flatMap(execution => execution.missing_fields))],
|
|
116
|
+
summary: executionSummary,
|
|
117
|
+
executions: argv.summary ? undefined : executions,
|
|
118
|
+
_formatted: argv.summary ? formatFlowContextSummary(executionSummary) : undefined,
|
|
119
|
+
context: { instance_url: app.getEffectiveInstance(), timezone: 'UTC' },
|
|
120
|
+
};
|
|
121
|
+
app.ok(data, { summary: argv.summary
|
|
122
|
+
? `${executionSummary.total_count ?? executions.length} matching flow execution(s), ${executions.length} sampled`
|
|
123
|
+
: `${executions.length} flow execution(s)` });
|
|
124
|
+
}),
|
|
125
|
+
})
|
|
69
126
|
.command({
|
|
70
127
|
command: 'show <identifier>',
|
|
71
128
|
aliases: ['get'],
|
|
@@ -128,6 +185,7 @@ export function flowsCmd(wrap) {
|
|
|
128
185
|
console.log('');
|
|
129
186
|
console.log('Available subcommands:');
|
|
130
187
|
console.log(' list List flows');
|
|
188
|
+
console.log(' executions Show flow executions from sys_flow_context');
|
|
131
189
|
console.log(' show <identifier> Show flow details by name or sys_id');
|
|
132
190
|
console.log(' create Create a new flow (not yet implemented)');
|
|
133
191
|
console.log(' update <identifier> Update a flow (not yet implemented)');
|
|
@@ -650,10 +708,66 @@ async function formatStepLine(stepNum, pad, step, ctx) {
|
|
|
650
708
|
case 'subflow':
|
|
651
709
|
return formatSubFlowStep(stepNum, pad, step.data, ctx);
|
|
652
710
|
default:
|
|
653
|
-
return
|
|
711
|
+
return formatActionStepWithChildren(stepNum, pad, step.data, ctx);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
async function formatActionStepWithChildren(stepNum, pad, action, ctx) {
|
|
716
|
+
const lines = formatActionStep(stepNum, pad, action, ctx);
|
|
717
|
+
const depth = ctx?.depth ?? 1;
|
|
718
|
+
if (!ctx?.sdk?.inspectCustomAction || depth <= 1) return lines;
|
|
719
|
+
|
|
720
|
+
const actionName = getActionName(action);
|
|
721
|
+
if (!actionName) return lines;
|
|
722
|
+
|
|
723
|
+
if (!ctx.customActionCache) ctx.customActionCache = new Map();
|
|
724
|
+
if (!ctx.customActionCache.has(actionName)) {
|
|
725
|
+
ctx.customActionCache.set(actionName, ctx.sdk.inspectCustomAction(actionName).catch(() => null));
|
|
726
|
+
}
|
|
727
|
+
const details = await ctx.customActionCache.get(actionName);
|
|
728
|
+
const steps = details?.steps;
|
|
729
|
+
if (!Array.isArray(steps) || steps.length === 0 || !hasUsefulActionSteps(actionName, steps)) return lines;
|
|
730
|
+
|
|
731
|
+
lines.push(`${pad} ↳ Internal action steps`);
|
|
732
|
+
const ordered = [...steps].sort((a, b) => parseOrderField(a) - parseOrderField(b));
|
|
733
|
+
ordered.forEach((step, index) => {
|
|
734
|
+
const label = firstNonEmpty(getStringField(step, 'label'), getStringField(step, 'name'), 'Action step');
|
|
735
|
+
const type = getNestedString(step, 'step_type', 'display_value') || getStringField(step, 'step_type');
|
|
736
|
+
const suffix = type ? ` - ${type}` : '';
|
|
737
|
+
lines.push(`${pad} Step ${index + 1}: ${label}${suffix}`);
|
|
738
|
+
appendStepValues(lines, pad + ' ', step, ctx);
|
|
739
|
+
});
|
|
740
|
+
return lines;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function hasUsefulActionSteps(actionName, steps) {
|
|
744
|
+
if (steps.length !== 1) return true;
|
|
745
|
+
const step = steps[0];
|
|
746
|
+
const label = firstNonEmpty(getStringField(step, 'label'), getStringField(step, 'name'))
|
|
747
|
+
.replace(/\s+step$/i, '').trim().toLowerCase();
|
|
748
|
+
return label !== actionName.trim().toLowerCase();
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function appendStepValues(lines, pad, step, ctx) {
|
|
752
|
+
for (const field of ['inputs', 'outputs', 'extended_inputs', 'extended_outputs']) {
|
|
753
|
+
const value = getStringField(step, field);
|
|
754
|
+
if (!value) continue;
|
|
755
|
+
const resolved = resolveGuidPills(value, ctx?.labelCache);
|
|
756
|
+
for (const part of resolved.split('^')) {
|
|
757
|
+
if (part) lines.push(`${pad}${field}: ${part}`);
|
|
758
|
+
}
|
|
654
759
|
}
|
|
655
760
|
}
|
|
656
761
|
|
|
762
|
+
function getActionName(action) {
|
|
763
|
+
return firstNonEmpty(
|
|
764
|
+
getNestedString(action, 'actionType', 'fName'),
|
|
765
|
+
getStringField(action, 'actionName'),
|
|
766
|
+
getStringField(action, 'actionInternalName'),
|
|
767
|
+
getStringField(action, 'name'),
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
|
|
657
771
|
export function formatActionStep(stepNum, pad, action, ctx) {
|
|
658
772
|
const lines = [];
|
|
659
773
|
let actionName = firstNonEmpty(
|
|
@@ -700,7 +814,6 @@ export function formatActionStep(stepNum, pad, action, ctx) {
|
|
|
700
814
|
if (inputName === 'table_name' && showsTableSuffix) continue;
|
|
701
815
|
|
|
702
816
|
let inputValue = firstNonEmpty(getStringField(raw, 'displayValue'), getStringField(raw, 'value'));
|
|
703
|
-
if (!inputValue) continue;
|
|
704
817
|
|
|
705
818
|
// Catalog variable selections arrive as "sys_id:table" pairs; resolve
|
|
706
819
|
// them to readable names when we have a resolver map (built per flow).
|
|
@@ -716,6 +829,11 @@ export function formatActionStep(stepNum, pad, action, ctx) {
|
|
|
716
829
|
label = firstNonEmpty(getStringField(raw.parameter, 'label'), label);
|
|
717
830
|
}
|
|
718
831
|
|
|
832
|
+
if (!inputValue) {
|
|
833
|
+
lines.push(`${pad} ${label}: (not set)`);
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
|
|
719
837
|
// ServiceNow writes multi-field payloads (encoded-query style) as
|
|
720
838
|
// "field=value^field2=value2". Split those onto one line per field;
|
|
721
839
|
// plain long strings stay on a single line, untruncated.
|
package/src/commands/dev/rest.js
CHANGED
|
@@ -26,6 +26,11 @@ export function restCmd(wrap) {
|
|
|
26
26
|
type: 'string',
|
|
27
27
|
describe: 'Table name shorthand for api/now/table/{table}',
|
|
28
28
|
})
|
|
29
|
+
.option('raw', {
|
|
30
|
+
type: 'boolean',
|
|
31
|
+
default: false,
|
|
32
|
+
describe: 'Return non-JSON response bodies as text (for XML/HTML diagnostic endpoints)',
|
|
33
|
+
})
|
|
29
34
|
.option('query', {
|
|
30
35
|
type: 'string',
|
|
31
36
|
describe: 'Query parameters string',
|
|
@@ -63,7 +68,9 @@ export function restCmd(wrap) {
|
|
|
63
68
|
requestOptions.body = argv.data;
|
|
64
69
|
}
|
|
65
70
|
|
|
66
|
-
const result =
|
|
71
|
+
const result = argv.raw
|
|
72
|
+
? await app.sdk.rawRequest(url, requestOptions)
|
|
73
|
+
: await app.sdk.request(url, requestOptions);
|
|
67
74
|
app.ok(result, { summary: `${method} ${endpoint}` });
|
|
68
75
|
}),
|
|
69
76
|
};
|