@jacebenson/jsn 3.0.6 → 3.1.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 +60 -285
- package/package.json +5 -2
- package/scripts/postinstall.mjs +80 -0
- package/scripts/pre-commit-check.sh +61 -0
- package/scripts/release.js +62 -0
- package/scripts/tag-reminder.js +30 -0
- package/skills/servicenow/SKILL.md +47 -92
- package/src/app.js +5 -5
- package/src/cli.js +268 -161
- package/src/commands/_ticket.js +15 -2
- package/src/commands/auth.js +10 -1
- package/src/commands/catalog.js +179 -292
- package/src/commands/dev/_generic.js +50 -62
- package/src/commands/dev/_simple.js +27 -0
- package/src/commands/dev/b4rules.js +5 -0
- package/src/commands/dev/eval.js +1 -1
- package/src/commands/dev/flows.js +31 -8
- package/src/commands/dev/forms.js +131 -253
- package/src/commands/dev/lists.js +16 -6
- package/src/commands/dev/logs.js +52 -9
- package/src/commands/dev/scopes.js +4 -3
- package/src/commands/dev/scrapi.js +13 -13
- package/src/commands/dev/updatesets.js +6 -5
- package/src/commands/groupmembers.js +10 -1
- package/src/commands/grouproles.js +10 -1
- package/src/commands/groups.js +12 -1
- package/src/commands/records.js +16 -4
- package/src/commands/skill.js +11 -1
- package/src/commands/tickets.js +12 -1
- package/src/commands/users.js +12 -1
- package/src/help.js +125 -35
- package/src/helpers.js +50 -34
- package/src/paginated-search.js +210 -0
- package/src/sdk.js +4 -0
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { formatRecordForDisplay, getStringField, isHexString, parseDataArg } from '../../helpers.js';
|
|
4
4
|
import { getCurrentUser, getCurrentApplication } from '../../context.js';
|
|
5
|
-
import
|
|
6
|
-
import search from '@inquirer/search';
|
|
5
|
+
import { paginatedSearch } from '../../paginated-search.js';
|
|
7
6
|
import { isTTY, FormatAuto } from '../../output.js';
|
|
8
7
|
|
|
9
8
|
function vowelArticle(word) {
|
|
@@ -16,13 +15,13 @@ function buildHints(name, singular, readOnly) {
|
|
|
16
15
|
if (!readOnly) {
|
|
17
16
|
crumbs.push({
|
|
18
17
|
action: 'create',
|
|
19
|
-
cmd: `jsn
|
|
18
|
+
cmd: `jsn ${name} create --name ... --label "..."`,
|
|
20
19
|
description: `Create ${vowelArticle(singular)} ${singular}`,
|
|
21
20
|
});
|
|
22
21
|
}
|
|
23
22
|
crumbs.push({
|
|
24
23
|
action: 'show',
|
|
25
|
-
cmd: `jsn
|
|
24
|
+
cmd: `jsn ${name} show <name_or_sys_id>`,
|
|
26
25
|
description: `Show ${singular} details`,
|
|
27
26
|
});
|
|
28
27
|
return crumbs;
|
|
@@ -81,15 +80,16 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
|
|
|
81
80
|
const singular = toSingular(name, opts.singular);
|
|
82
81
|
const readOnly = opts.readOnly || false;
|
|
83
82
|
const scopeValidation = opts.scopeValidation || false;
|
|
83
|
+
const extraQuery = opts.extraQuery || '';
|
|
84
84
|
const showSummary = opts.showSummary || ((record, id) => `${singular.charAt(0).toUpperCase() + singular.slice(1)}: ${getStringField(record, 'name') || id}`);
|
|
85
85
|
const showBreadcrumbs = opts.showBreadcrumbs || ((record, id) => {
|
|
86
86
|
const crumbs = [
|
|
87
|
-
{ action: 'list', cmd: `jsn
|
|
87
|
+
{ action: 'list', cmd: `jsn ${name} list`, description: `Back to all ${name}` },
|
|
88
88
|
];
|
|
89
89
|
if (!readOnly) {
|
|
90
90
|
crumbs.unshift(
|
|
91
|
-
{ action: 'delete', cmd: `jsn
|
|
92
|
-
{ action: 'update', cmd: `jsn
|
|
91
|
+
{ action: 'delete', cmd: `jsn ${name} delete ${id}`, description: `Delete this ${singular}` },
|
|
92
|
+
{ action: 'update', cmd: `jsn ${name} update ${id} --data '{...}'`, description: `Update this ${singular}` }
|
|
93
93
|
);
|
|
94
94
|
}
|
|
95
95
|
return crumbs;
|
|
@@ -104,71 +104,58 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
|
|
|
104
104
|
builder: (y) => y
|
|
105
105
|
.option('query', { type: 'string', describe: 'Encoded query (e.g. "nameLIKEincident" or "active=true^priority=1")' })
|
|
106
106
|
.option('columns', { alias: ['c', 'fields'], type: 'string', describe: 'Comma-separated columns (e.g. "name,label,super_class")' })
|
|
107
|
-
.option('limit', { alias: 'l', type: 'number', default:
|
|
107
|
+
.option('limit', { alias: 'l', type: 'number', default: 50, describe: 'Max records' }),
|
|
108
108
|
handler: wrap(async (argv, app) => {
|
|
109
109
|
const query = argv.query || '';
|
|
110
110
|
const columns = argv.columns ? argv.columns.split(',') : defaultColumns;
|
|
111
111
|
const limit = argv.limit;
|
|
112
112
|
|
|
113
|
-
// Interactive picker
|
|
113
|
+
// Interactive picker with pagination
|
|
114
114
|
const interactive = isTTY(process.stdout) && isTTY(process.stdin);
|
|
115
115
|
if (interactive && app.output.getFormat() === FormatAuto && !query) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
pickerParams.set('sysparm_query', 'ORDERBYDESCsys_updated_on');
|
|
116
|
+
// Get total count
|
|
117
|
+
let totalCount = 0;
|
|
118
|
+
try {
|
|
119
|
+
totalCount = await app.sdk.aggregateCount(table, extraQuery || '');
|
|
120
|
+
} catch { /* non-critical */ }
|
|
122
121
|
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
122
|
+
const source = async (term, offset, { signal } = {}) => {
|
|
123
|
+
const params = new URLSearchParams();
|
|
124
|
+
params.set('sysparm_limit', String(limit));
|
|
125
|
+
params.set('sysparm_display_value', 'all');
|
|
126
|
+
params.set('sysparm_fields', ['sys_id', 'name', 'sys_scope'].join(','));
|
|
127
|
+
if (term) {
|
|
128
|
+
params.set('sysparm_query', `nameLIKE${term}^ORDERBYDESCsys_updated_on`);
|
|
129
|
+
} else {
|
|
130
|
+
params.set('sysparm_query', (extraQuery ? extraQuery + '^' : '') + 'ORDERBYDESCsys_updated_on');
|
|
131
|
+
params.set('sysparm_offset', String(offset));
|
|
132
|
+
}
|
|
133
|
+
const records = await app.sdk.list(table, params);
|
|
134
|
+
return records.map(r => {
|
|
135
|
+
const recordName = getStringField(r, 'name') || getStringField(r, 'sys_id');
|
|
136
|
+
const scope = getStringField(r, 'sys_scope') || '';
|
|
137
|
+
let label = recordName;
|
|
138
|
+
if (scope && scope !== 'global') label += ` [${scope}]`;
|
|
139
|
+
return { name: label, value: recordName };
|
|
135
140
|
});
|
|
136
|
-
|
|
137
|
-
}
|
|
141
|
+
};
|
|
138
142
|
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
return { name: label, value: recordName };
|
|
143
|
+
const selectedName = await paginatedSearch({
|
|
144
|
+
message: `Select ${vowelArticle(singular)} ${singular}`,
|
|
145
|
+
pageSize: 10,
|
|
146
|
+
totalCount,
|
|
147
|
+
source,
|
|
145
148
|
});
|
|
146
149
|
|
|
147
|
-
let selectedName;
|
|
148
|
-
try {
|
|
149
|
-
selectedName = await search({
|
|
150
|
-
message: `Select ${vowelArticle(singular)} ${singular}:`,
|
|
151
|
-
source: async (input) => {
|
|
152
|
-
if (!input) return choices;
|
|
153
|
-
const term = input.toLowerCase();
|
|
154
|
-
return choices.filter(c => c.name.toLowerCase().includes(term));
|
|
155
|
-
},
|
|
156
|
-
});
|
|
157
|
-
} catch (err) {
|
|
158
|
-
if (err.name === 'ExitPromptError' || (err.message && err.message.includes('force closed'))) {
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
throw err;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
150
|
if (selectedName) {
|
|
151
|
+
const recordName = typeof selectedName === 'object' ? selectedName.value : selectedName;
|
|
165
152
|
const showParams = new URLSearchParams();
|
|
166
|
-
showParams.set('sysparm_query', `name=${
|
|
153
|
+
showParams.set('sysparm_query', `name=${recordName}`);
|
|
167
154
|
showParams.set('sysparm_display_value', 'all');
|
|
168
155
|
showParams.set('sysparm_limit', '1');
|
|
169
156
|
const showRecords = await app.sdk.list(table, showParams);
|
|
170
157
|
if (showRecords.length === 0) {
|
|
171
|
-
throw new Error(`${singular} not found: ${
|
|
158
|
+
throw new Error(`${singular} not found: ${recordName}`);
|
|
172
159
|
}
|
|
173
160
|
const record = showRecords[0];
|
|
174
161
|
record._context = { instance_url: app.getEffectiveInstance(), table };
|
|
@@ -176,10 +163,10 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
|
|
|
176
163
|
await opts.onShow(record, app);
|
|
177
164
|
}
|
|
178
165
|
app.ok(record, {
|
|
179
|
-
summary: `${singular.charAt(0).toUpperCase() + singular.slice(1)}: ${
|
|
166
|
+
summary: `${singular.charAt(0).toUpperCase() + singular.slice(1)}: ${recordName}`,
|
|
180
167
|
breadcrumbs: [
|
|
181
|
-
...(readOnly ? [] : [{ action: 'update', cmd: `jsn
|
|
182
|
-
{ action: 'list', cmd: `jsn
|
|
168
|
+
...(readOnly ? [] : [{ action: 'update', cmd: `jsn ${name} update ${recordName} --data '{...}'`, description: `Update this ${singular}` }]),
|
|
169
|
+
{ action: 'list', cmd: `jsn ${name} list`, description: `Back to all ${name}` },
|
|
183
170
|
],
|
|
184
171
|
});
|
|
185
172
|
}
|
|
@@ -191,7 +178,8 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
|
|
|
191
178
|
params.set('sysparm_limit', String(limit));
|
|
192
179
|
params.set('sysparm_display_value', 'all');
|
|
193
180
|
params.set('sysparm_fields', ['sys_id', ...columns].join(','));
|
|
194
|
-
const
|
|
181
|
+
const baseQ = extraQuery ? extraQuery + '^' : '';
|
|
182
|
+
const q = query ? query + '^ORDERBYDESCsys_updated_on' : (baseQ + 'ORDERBYDESCsys_updated_on');
|
|
195
183
|
params.set('sysparm_query', q);
|
|
196
184
|
const records = await app.sdk.list(table, params);
|
|
197
185
|
app.ok({
|
|
@@ -256,7 +244,7 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
|
|
|
256
244
|
app.ok(record, {
|
|
257
245
|
summary: `Created ${singular}`,
|
|
258
246
|
breadcrumbs: [
|
|
259
|
-
{ action: 'show', cmd: `jsn
|
|
247
|
+
{ action: 'show', cmd: `jsn ${name} show ${getStringField(record, 'name') || getStringField(record, 'sys_id')}`, description: `View the new ${singular}` },
|
|
260
248
|
],
|
|
261
249
|
});
|
|
262
250
|
}),
|
|
@@ -284,7 +272,7 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
|
|
|
284
272
|
if (scopeValidation) {
|
|
285
273
|
const scopeErr = await checkScope(app.sdk, recordScope);
|
|
286
274
|
if (scopeErr) {
|
|
287
|
-
throw new Error(`record is in scope '${scopeErr.recordScope}', but your current scope is '${scopeErr.currentScope}'. Switch scope first: jsn
|
|
275
|
+
throw new Error(`record is in scope '${scopeErr.recordScope}', but your current scope is '${scopeErr.currentScope}'. Switch scope first: jsn scopes set ${scopeErr.recordScope}`);
|
|
288
276
|
}
|
|
289
277
|
}
|
|
290
278
|
|
|
@@ -315,7 +303,7 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
|
|
|
315
303
|
if (scopeValidation) {
|
|
316
304
|
const scopeErr = await checkScope(app.sdk, recordScope);
|
|
317
305
|
if (scopeErr) {
|
|
318
|
-
throw new Error(`record is in scope '${scopeErr.recordScope}', but your current scope is '${scopeErr.currentScope}'. Switch scope first: jsn
|
|
306
|
+
throw new Error(`record is in scope '${scopeErr.recordScope}', but your current scope is '${scopeErr.currentScope}'. Switch scope first: jsn scopes set ${scopeErr.recordScope}`);
|
|
319
307
|
}
|
|
320
308
|
}
|
|
321
309
|
|
|
@@ -358,7 +346,7 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
|
|
|
358
346
|
console.log(' update Update a record');
|
|
359
347
|
console.log(' delete Delete a record');
|
|
360
348
|
}
|
|
361
|
-
console.log(`\nRun "jsn
|
|
349
|
+
console.log(`\nRun "jsn ${name} <command> --help" for details.`);
|
|
362
350
|
},
|
|
363
351
|
};
|
|
364
352
|
}
|
|
@@ -97,4 +97,31 @@ export const securitytypesCmd = (wrap) => buildDevCmd('securitytypes', 'sys_secu
|
|
|
97
97
|
export const uxscriptsCmd = (wrap) => buildDevCmd('uxscripts', 'sys_ux_lib_source_script', ['uxscript', 'ux'], ['name', 'active', 'sys_scope'], wrap, { singular: 'UX script', scopeValidation: true });
|
|
98
98
|
export const aliasesCmd = (wrap) => buildDevCmd('aliases', 'sys_alias', ['alias', 'als'], ['name', 'table', 'sys_scope'], wrap, { singular: 'alias', scopeValidation: true });
|
|
99
99
|
export const catalogscriptsCmd = (wrap) => buildDevCmd('catalogscripts', 'catalog_script_client', ['catalogscript', 'cs'], ['name', 'table', 'active', 'sys_scope'], wrap, { singular: 'catalog script', scopeValidation: true });
|
|
100
|
+
|
|
101
|
+
// ── Automation: Async ──
|
|
102
|
+
export const scriptactionsCmd = (wrap) => buildDevCmd('scriptactions', 'sysevent_script_action', ['scriptaction'], ['name', 'active', 'sys_scope'], wrap, { singular: 'script action', scopeValidation: true });
|
|
103
|
+
export const scheduledjobsCmd = (wrap) => buildDevCmd('scheduledjobs', 'sysauto_script', ['scheduledjob'], ['name', 'active', 'run_type', 'sys_scope'], wrap, { singular: 'scheduled job', scopeValidation: true });
|
|
104
|
+
export const asyncrulesCmd = (wrap) => buildDevCmd('asyncrules', 'sys_script', ['asyncrule'], ['name', 'collection', 'active', 'order', 'sys_scope'], wrap, { singular: 'async business rule', scopeValidation: true, extraQuery: 'order>=1000' });
|
|
105
|
+
export const triggersCmd = (wrap) => buildDevCmd('triggers', 'sysevent_register', ['trigger'], ['name', 'event_name', 'active', 'sys_scope'], wrap, { singular: 'event trigger', scopeValidation: true });
|
|
106
|
+
|
|
107
|
+
// ── Automation: In Memory ──
|
|
108
|
+
export const workflowsCmd = (wrap) => buildDevCmd('workflows', 'wf_workflow', ['workflow'], ['name', 'table', 'active', 'sys_scope'], wrap, { singular: 'workflow', scopeValidation: true, readOnly: true });
|
|
109
|
+
export const decisiontablesCmd = (wrap) => buildDevCmd('decisiontables', 'sys_decision', ['decisiontable'], ['name', 'active', 'sys_scope'], wrap, { singular: 'decision table', scopeValidation: true, readOnly: true });
|
|
110
|
+
export const assignmentsCmd = (wrap) => buildDevCmd('assignments', 'sysrule_assignment', ['assignment'], ['name', 'active', 'sys_scope'], wrap, { singular: 'assignment rule', scopeValidation: true, readOnly: true });
|
|
111
|
+
|
|
112
|
+
// ── Automation: Inbound ──
|
|
113
|
+
export const emailCmd = (wrap) => buildDevCmd('email', 'sysevent_email_action', ['emails'], ['name', 'active', 'event_name', 'sys_scope'], wrap, { singular: 'inbound email action', scopeValidation: true });
|
|
114
|
+
|
|
115
|
+
// ── Automation: Outbound ──
|
|
116
|
+
export const restmessageCmd = (wrap) => buildDevCmd('restmessage', 'sys_rest_message', ['rm', 'restmsg'], ['name', 'endpoint', 'active', 'sys_scope'], wrap, { singular: 'REST message', scopeValidation: true });
|
|
117
|
+
export const restmethodsCmd = (wrap) => buildDevCmd('restmethods', 'sys_rest_message_fn', ['restmethod'], ['name', 'http_method', 'active', 'sys_scope'], wrap, { singular: 'REST method', scopeValidation: true });
|
|
118
|
+
export const soapmessagesCmd = (wrap) => buildDevCmd('soapmessages', 'sys_soap_message', ['soapmsg'], ['name', 'active', 'sys_scope'], wrap, { singular: 'SOAP message', scopeValidation: true });
|
|
119
|
+
export const soapfunctionsCmd = (wrap) => buildDevCmd('soapfunctions', 'sys_soap_message_function', ['soapfunc'], ['name', 'active', 'sys_scope'], wrap, { singular: 'SOAP function', scopeValidation: true });
|
|
120
|
+
|
|
121
|
+
// ── UX: Core UI ──
|
|
122
|
+
export const uimacrosCmd = (wrap) => buildDevCmd('uimacros', 'sys_ui_macro', ['uimacro'], ['name', 'active', 'sys_scope'], wrap, { singular: 'UI macro', scopeValidation: true });
|
|
123
|
+
|
|
124
|
+
// ── UX: Workspaces ──
|
|
125
|
+
export const uxlistsCmd = (wrap) => buildDevCmd('uxlists', 'sys_ux_list', ['uxlist'], ['name', 'active', 'sys_scope'], wrap, { singular: 'workspace list', scopeValidation: true, readOnly: true });
|
|
126
|
+
export const uxapplicabilityCmd = (wrap) => buildDevCmd('uxapplicability', 'sys_ux_applicability_m2m', ['uxapp'], ['name', 'active', 'sys_scope'], wrap, { singular: 'workspace applicability', scopeValidation: true, readOnly: true });
|
|
100
127
|
export const cataloguipoliciesCmd = (wrap) => buildDevCmd('cataloguipolicies', 'catalog_ui_policy', ['cataloguipolicy', 'cup'], ['short_description', 'table', 'active', 'sys_scope'], wrap, { singular: 'catalog UI policy', scopeValidation: true });
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { buildDevCmd } from './_generic.js';
|
|
2
|
+
|
|
3
|
+
// Before-query business rules — applies filters to GlideRecord queries before they execute.
|
|
4
|
+
// These are sys_script records with order < 100.
|
|
5
|
+
export const b4rulesCmd = (wrap) => buildDevCmd('b4rules', 'sys_script', ['b4rule'], ['name', 'collection', 'active', 'order', 'sys_scope'], wrap, { singular: 'before-query business rule', scopeValidation: true, extraQuery: 'order<100' });
|
package/src/commands/dev/eval.js
CHANGED
|
@@ -45,7 +45,7 @@ export function evalCmd(wrap) {
|
|
|
45
45
|
summary: 'Script executed' + warning,
|
|
46
46
|
breadcrumbs: [{
|
|
47
47
|
action: 'eval',
|
|
48
|
-
cmd: 'jsn
|
|
48
|
+
cmd: 'jsn eval --script \'...\'',
|
|
49
49
|
description: 'Execute a background script on the ServiceNow instance',
|
|
50
50
|
}],
|
|
51
51
|
});
|
|
@@ -14,22 +14,24 @@ export function flowsCmd(wrap) {
|
|
|
14
14
|
builder: (y) => y
|
|
15
15
|
.option('query', { type: 'string', describe: 'Encoded query (e.g. "nameLIKEincident" or "active=true")' })
|
|
16
16
|
.option('columns', { alias: ['c', 'fields'], type: 'string', describe: 'Comma-separated columns (e.g. "number,short_description")' })
|
|
17
|
-
.option('limit', { alias: 'l', type: 'number', default:
|
|
17
|
+
.option('limit', { alias: 'l', type: 'number', default: 50, describe: 'Max records' }),
|
|
18
18
|
handler: wrap(async (argv, app) => {
|
|
19
|
+
app.requireInstance();
|
|
19
20
|
const columns = argv.columns ? argv.columns.split(',') : ['name', 'active', 'description', 'sys_created_by', 'sys_updated_on'];
|
|
20
21
|
const query = argv.query || '';
|
|
21
22
|
|
|
22
23
|
// Interactive picker
|
|
23
24
|
const picked = await interactiveList({
|
|
24
25
|
app, table: 'sys_hub_flow', singular: 'flow', columns, limit: argv.limit, query, labelField: 'name',
|
|
25
|
-
formatLabel: r => `${getStringField(r, '
|
|
26
|
+
formatLabel: r => `${getStringField(r, 'active') === 'true' ? '🟢' : '🔴'} ${getStringField(r, 'name')}`,
|
|
26
27
|
});
|
|
28
|
+
if (picked === undefined) return; // user cancelled
|
|
27
29
|
if (picked) {
|
|
28
30
|
const inspection = await app.sdk.inspectFlow(getStringField(picked, 'sys_id'));
|
|
29
31
|
const formatted = formatFlowInspection(inspection, app.getEffectiveInstance());
|
|
30
32
|
return app.ok({ ...inspection, _formatted: formatted }, {
|
|
31
33
|
summary: `Flow: ${inspection.flow.name}`,
|
|
32
|
-
breadcrumbs: [{ action: 'list', cmd: 'jsn
|
|
34
|
+
breadcrumbs: [{ action: 'list', cmd: 'jsn flows list', description: 'Back to all flows' }],
|
|
33
35
|
});
|
|
34
36
|
}
|
|
35
37
|
|
|
@@ -61,7 +63,7 @@ export function flowsCmd(wrap) {
|
|
|
61
63
|
app.ok(data, {
|
|
62
64
|
summary: `Flow: ${inspection.flow.name}`,
|
|
63
65
|
breadcrumbs: [
|
|
64
|
-
{ action: 'list', cmd: 'jsn
|
|
66
|
+
{ action: 'list', cmd: 'jsn flows list', description: 'Back to all flows' },
|
|
65
67
|
],
|
|
66
68
|
});
|
|
67
69
|
}),
|
|
@@ -73,7 +75,7 @@ export function flowsCmd(wrap) {
|
|
|
73
75
|
.option('data', { type: 'string', describe: 'JSON data for the flow' }),
|
|
74
76
|
handler: wrap(async (_argv, _app) => {
|
|
75
77
|
throw new Error('Flow creation requires the Flow Designer GraphQL API - not yet implemented.\n'
|
|
76
|
-
+ 'Use the ServiceNow web UI to create flows, then use "jsn
|
|
78
|
+
+ 'Use the ServiceNow web UI to create flows, then use "jsn flows list" to view them.');
|
|
77
79
|
}),
|
|
78
80
|
})
|
|
79
81
|
.command({
|
|
@@ -83,7 +85,7 @@ export function flowsCmd(wrap) {
|
|
|
83
85
|
.option('data', { type: 'string', describe: 'JSON data to update' }),
|
|
84
86
|
handler: wrap(async (_argv, _app) => {
|
|
85
87
|
throw new Error('Flow updates require the Flow Designer GraphQL API - not yet implemented.\n'
|
|
86
|
-
+ 'Use the ServiceNow web UI to update flows, then use "jsn
|
|
88
|
+
+ 'Use the ServiceNow web UI to update flows, then use "jsn flows list" to view them.');
|
|
87
89
|
}),
|
|
88
90
|
})
|
|
89
91
|
.command({
|
|
@@ -95,7 +97,22 @@ export function flowsCmd(wrap) {
|
|
|
95
97
|
}),
|
|
96
98
|
});
|
|
97
99
|
},
|
|
98
|
-
handler: () => {
|
|
100
|
+
handler: () => {
|
|
101
|
+
console.log('Manage Flow Designer flows from the sys_hub_flow table.');
|
|
102
|
+
console.log('');
|
|
103
|
+
console.log('Available subcommands:');
|
|
104
|
+
console.log(' list List flows');
|
|
105
|
+
console.log(' show <identifier> Show flow details by name or sys_id');
|
|
106
|
+
console.log(' create Create a new flow (not yet implemented)');
|
|
107
|
+
console.log(' update <identifier> Update a flow (not yet implemented)');
|
|
108
|
+
console.log(' delete <identifier> Delete a flow (not yet implemented)');
|
|
109
|
+
console.log('');
|
|
110
|
+
console.log('Run "jsn flows <command> --help" for details.');
|
|
111
|
+
console.log('');
|
|
112
|
+
console.log('Note: Flow inspection detail varies. V2 flows (with payload)');
|
|
113
|
+
console.log('show full action inputs and conditions. Subflows and V1 flows');
|
|
114
|
+
console.log('show step names only.');
|
|
115
|
+
},
|
|
99
116
|
};
|
|
100
117
|
}
|
|
101
118
|
|
|
@@ -168,7 +185,13 @@ function formatFlowInspection(inspection, instanceURL) {
|
|
|
168
185
|
lines.push('');
|
|
169
186
|
lines.push('⚡ FLOW STRUCTURE');
|
|
170
187
|
lines.push('─'.repeat(50));
|
|
171
|
-
|
|
188
|
+
const structureLines = formatFlowStructure(inspection);
|
|
189
|
+
lines.push(...structureLines);
|
|
190
|
+
// Note limitation for V1/subflows without payload detail
|
|
191
|
+
if (!inspection.payload || Object.keys(inspection.payload).length === 0) {
|
|
192
|
+
lines.push('');
|
|
193
|
+
lines.push(' (step detail limited — this flow lacks a V2 payload)');
|
|
194
|
+
}
|
|
172
195
|
|
|
173
196
|
lines.push('');
|
|
174
197
|
return lines.join('\n') + '\n';
|