@jacebenson/jsn 4.3.7-dev.39 → 4.3.7-dev.41
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 +1 -1
- package/src/cli-adapter.js +96 -4
- package/src/cli.js +101 -5
- package/src/commands/cmdb.js +18 -0
- package/src/commands/docs/docs.js +18 -16
- package/src/commands/flows.js +18 -4
- package/src/commands/platform.js +17 -0
- package/src/commands/records.js +19 -15
- package/src/errors.js +52 -5
- package/src/flow-create.js +166 -85
- package/src/flow-designer-interactive.js +98 -8
- package/src/flow-triggers.js +395 -0
- package/src/help.js +93 -28
- package/src/output.js +7 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
// Trigger contract for Flow Designer creation.
|
|
2
|
+
//
|
|
3
|
+
// Flow Designer does NOT give a newly POSTed ProcessFlow a default trigger, so
|
|
4
|
+
// the trigger instance must be INSERTED through the same snFlowDesigner
|
|
5
|
+
// GraphQL `flowPatch` that inserts actions. Everything the insert needs is read
|
|
6
|
+
// from the instance's own schema tables — no trigger input is hardcoded:
|
|
7
|
+
//
|
|
8
|
+
// sys_hub_trigger_definition one row per trigger ("Created", "Daily", …)
|
|
9
|
+
// sys_hub_trigger_category the picker's first level
|
|
10
|
+
// sys_hub_trigger_input the inputs a definition declares. The owning
|
|
11
|
+
// definition is carried by `name`, which is
|
|
12
|
+
// `var__m_sys_hub_trigger_input_<definitionId>`;
|
|
13
|
+
// the `model` reference is EMPTY on this
|
|
14
|
+
// instance for most definitions, and a
|
|
15
|
+
// `trigger_definition=` query is silently
|
|
16
|
+
// ignored, so `name` is the only reliable
|
|
17
|
+
// relation.
|
|
18
|
+
// sys_db_object / sys_dictionary the record trigger's Table label and display
|
|
19
|
+
// column (the ProcessFlow
|
|
20
|
+
// /trigger/record/table descriptor endpoint
|
|
21
|
+
// does not exist: HTTP 400 "Requested URI does
|
|
22
|
+
// not represent any resource").
|
|
23
|
+
//
|
|
24
|
+
// `internal_type` on sys_hub_trigger_input holds the parameter's real type
|
|
25
|
+
// ("glide_time", "table_name", "conditions", …) as its raw value; its display
|
|
26
|
+
// value is the human label ("Time", "Table Name", …).
|
|
27
|
+
|
|
28
|
+
export const TRIGGER_DEFINITION_TABLE = 'sys_hub_trigger_definition';
|
|
29
|
+
export const TRIGGER_CATEGORY_TABLE = 'sys_hub_trigger_category';
|
|
30
|
+
export const TRIGGER_INPUT_TABLE = 'sys_hub_trigger_input';
|
|
31
|
+
|
|
32
|
+
export const TRIGGER_INPUT_NAME_PREFIX = 'var__m_sys_hub_trigger_input_';
|
|
33
|
+
|
|
34
|
+
// Families this CLI can actually create, each verified against a live instance
|
|
35
|
+
// with a disposable draft flow (see the PR body for the probe sys_ids). The
|
|
36
|
+
// picker intersects this set with the instance's own active definitions, so an
|
|
37
|
+
// unimplemented family can never be offered.
|
|
38
|
+
export const SUPPORTED_TRIGGER_TYPES = new Set([
|
|
39
|
+
'record_create',
|
|
40
|
+
'record_update',
|
|
41
|
+
'record_create_or_update',
|
|
42
|
+
'daily',
|
|
43
|
+
'weekly',
|
|
44
|
+
'monthly',
|
|
45
|
+
'repeat',
|
|
46
|
+
'run_once',
|
|
47
|
+
'email',
|
|
48
|
+
'knowledge_management',
|
|
49
|
+
'service_catalog',
|
|
50
|
+
'sla_task',
|
|
51
|
+
'remote_table_query',
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
// Definitions that exist on the instance but are deliberately not offered, with
|
|
55
|
+
// the reason. Kept explicit so the picker can explain the omission instead of
|
|
56
|
+
// silently hiding a family.
|
|
57
|
+
export const EXCLUDED_TRIGGER_TYPES = new Map([
|
|
58
|
+
['analytics', 'the Proactive Analytics trigger definition is inactive on this instance'],
|
|
59
|
+
['rest', 'the Trigger Rest definition has no resolvable trigger category'],
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
// Families whose declared inputs name a table. The manifest's `table` shortcut
|
|
63
|
+
// maps onto whichever of these the chosen definition declares.
|
|
64
|
+
const TABLE_INPUT_NAMES = ['table', 'u_table', 'target_table'];
|
|
65
|
+
// Same idea for the encoded-query condition.
|
|
66
|
+
const CONDITION_INPUT_NAMES = ['condition', 'email_conditions', 'business_calendar_condition'];
|
|
67
|
+
|
|
68
|
+
// Trigger input types this CLI can prompt for. Anything else must be supplied
|
|
69
|
+
// through --data-file so its complete schema and value are preserved.
|
|
70
|
+
export const PROMPTABLE_TRIGGER_TYPES = new Set([
|
|
71
|
+
'string', 'table_name', 'field_name', 'conditions', 'choice', 'reference',
|
|
72
|
+
'glide_time', 'glide_date_time', 'glide_duration', 'glide_list',
|
|
73
|
+
'integer', 'decimal', 'float', 'boolean', 'day_of_week', 'month_of_year',
|
|
74
|
+
'script', 'html', 'url', 'email', 'document_id', 'date', 'datetime',
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
export const NUMERIC_TRIGGER_TYPES = new Set(['integer', 'decimal', 'float']);
|
|
78
|
+
|
|
79
|
+
function rawValue(value) {
|
|
80
|
+
if (value && typeof value === 'object') return value.value ?? value.sys_id ?? value.name ?? '';
|
|
81
|
+
return value == null ? '' : value;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function displayValue(value) {
|
|
85
|
+
if (value && typeof value === 'object') return value.display_value ?? value.value ?? '';
|
|
86
|
+
return value == null ? '' : value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function text(value) {
|
|
90
|
+
const result = rawValue(value);
|
|
91
|
+
return result == null ? '' : String(result);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function isSysId(value) {
|
|
95
|
+
return typeof value === 'string' && /^[0-9a-f]{32}$/i.test(value);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function boolValue(value) {
|
|
99
|
+
return String(rawValue(value)).toLowerCase() === 'true';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function numeric(value, fallback = 0) {
|
|
103
|
+
const parsed = Number(rawValue(value));
|
|
104
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The category tree the picker shows, built from the instance's own
|
|
109
|
+
* sys_hub_trigger_category rows and the category reference each active
|
|
110
|
+
* definition carries.
|
|
111
|
+
*
|
|
112
|
+
* Definitions whose category does not resolve to a readable category record
|
|
113
|
+
* are returned separately in `ungrouped` — they are never selectable, but the
|
|
114
|
+
* caller can report them.
|
|
115
|
+
*/
|
|
116
|
+
export async function readTriggerCatalog(sdk) {
|
|
117
|
+
const [definitions, categories] = await Promise.all([
|
|
118
|
+
sdk.list(TRIGGER_DEFINITION_TABLE, new URLSearchParams({
|
|
119
|
+
sysparm_query: 'active=true^ORDERBYname',
|
|
120
|
+
sysparm_limit: '200',
|
|
121
|
+
sysparm_display_value: 'all',
|
|
122
|
+
sysparm_fields: 'sys_id,name,type,active,category,description',
|
|
123
|
+
})),
|
|
124
|
+
sdk.list(TRIGGER_CATEGORY_TABLE, new URLSearchParams({
|
|
125
|
+
sysparm_limit: '200',
|
|
126
|
+
sysparm_display_value: 'all',
|
|
127
|
+
sysparm_fields: 'sys_id,name',
|
|
128
|
+
})),
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
const categoryNames = new Map(categories.map(row => [text(row.sys_id), displayValue(row.name) || text(row.name)]));
|
|
132
|
+
const grouped = new Map();
|
|
133
|
+
const ungrouped = [];
|
|
134
|
+
|
|
135
|
+
for (const row of definitions) {
|
|
136
|
+
const categoryId = text(row.category);
|
|
137
|
+
const categoryName = categoryNames.get(categoryId) || '';
|
|
138
|
+
const definition = {
|
|
139
|
+
sysId: text(row.sys_id),
|
|
140
|
+
type: text(row.type),
|
|
141
|
+
name: displayValue(row.name) || text(row.name),
|
|
142
|
+
description: displayValue(row.description) || '',
|
|
143
|
+
supported: SUPPORTED_TRIGGER_TYPES.has(text(row.type)),
|
|
144
|
+
excludedReason: EXCLUDED_TRIGGER_TYPES.get(text(row.type)) || (SUPPORTED_TRIGGER_TYPES.has(text(row.type)) ? '' : 'this CLI has no verified creation contract for this trigger'),
|
|
145
|
+
};
|
|
146
|
+
if (!categoryId || !categoryName) {
|
|
147
|
+
ungrouped.push(definition);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (!grouped.has(categoryId)) grouped.set(categoryId, { id: categoryId, name: categoryName, triggers: [] });
|
|
151
|
+
grouped.get(categoryId).triggers.push(definition);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const categoriesOut = [...grouped.values()]
|
|
155
|
+
.map(category => ({ ...category, triggers: category.triggers.sort((a, b) => a.name.localeCompare(b.name)) }))
|
|
156
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
157
|
+
|
|
158
|
+
return { categories: categoriesOut, ungrouped };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The pickable definitions for one category, in the shape the picker needs. */
|
|
162
|
+
export function selectableTriggers(category) {
|
|
163
|
+
return (category?.triggers || []).filter(trigger => trigger.supported);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Definition descriptions are authored HTML; strip it and keep the first
|
|
167
|
+
// sentence so a picker row stays readable.
|
|
168
|
+
export function triggerChoiceLabel(trigger) {
|
|
169
|
+
const text = String(trigger?.description || '')
|
|
170
|
+
.replace(/<[^>]*>/g, ' ')
|
|
171
|
+
.replace(/&[a-z]+;/gi, ' ')
|
|
172
|
+
.replace(/\s+/g, ' ')
|
|
173
|
+
.trim();
|
|
174
|
+
if (!text) return trigger?.name || '';
|
|
175
|
+
const sentence = text.split(/\.\s/)[0];
|
|
176
|
+
return `${trigger.name} — ${sentence.length > 90 ? `${sentence.slice(0, 87)}…` : sentence}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The inputs a trigger definition declares, straight from
|
|
181
|
+
* sys_hub_trigger_input. Answers "what must I supply for this family?" from the
|
|
182
|
+
* instance rather than from a hand-written table.
|
|
183
|
+
*/
|
|
184
|
+
export async function readTriggerInputDefinitions(sdk, definitionId) {
|
|
185
|
+
if (!isSysId(definitionId)) throw new Error(`Trigger definition id must be an exact sys_id, got "${definitionId}"`);
|
|
186
|
+
const rows = await sdk.list(TRIGGER_INPUT_TABLE, new URLSearchParams({
|
|
187
|
+
sysparm_query: `name=${TRIGGER_INPUT_NAME_PREFIX}${definitionId}^ORDERBYorder`,
|
|
188
|
+
sysparm_limit: '200',
|
|
189
|
+
sysparm_display_value: 'all',
|
|
190
|
+
sysparm_fields: 'sys_id,element,label,column_label,internal_type,mandatory,order,max_length,default_value,attributes,choice,reference,use_dependent_field,dependent_on_field,read_only,hint',
|
|
191
|
+
}));
|
|
192
|
+
return rows
|
|
193
|
+
.map(normalizeTriggerInput)
|
|
194
|
+
.filter(input => input.name);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function normalizeTriggerInput(row) {
|
|
198
|
+
const parameter = {
|
|
199
|
+
id: text(row.sys_id),
|
|
200
|
+
label: displayValue(row.label) || displayValue(row.column_label) || text(row.element),
|
|
201
|
+
name: text(row.element),
|
|
202
|
+
type: text(row.internal_type),
|
|
203
|
+
hint: displayValue(row.hint) || '',
|
|
204
|
+
order: numeric(row.order),
|
|
205
|
+
extended: false,
|
|
206
|
+
mandatory: boolValue(row.mandatory),
|
|
207
|
+
readonly: boolValue(row.read_only),
|
|
208
|
+
maxsize: numeric(row.max_length),
|
|
209
|
+
children: [],
|
|
210
|
+
reference: text(row.reference) ? displayValue(row.reference) : '',
|
|
211
|
+
reference_display: '',
|
|
212
|
+
ref_qual: '',
|
|
213
|
+
choiceOption: text(row.choice),
|
|
214
|
+
table: '',
|
|
215
|
+
columnName: '',
|
|
216
|
+
defaultValue: rawValue(row.default_value) ?? '',
|
|
217
|
+
use_dependent: boolValue(row.use_dependent_field),
|
|
218
|
+
dependent_on: text(row.dependent_on_field),
|
|
219
|
+
show_ref_finder: false,
|
|
220
|
+
local: false,
|
|
221
|
+
// On INPUT the GraphQL type is String; the read path renders it as an object.
|
|
222
|
+
attributes: text(row.attributes),
|
|
223
|
+
};
|
|
224
|
+
return {
|
|
225
|
+
id: parameter.id,
|
|
226
|
+
name: parameter.name,
|
|
227
|
+
label: parameter.label,
|
|
228
|
+
type: parameter.type,
|
|
229
|
+
mandatory: parameter.mandatory,
|
|
230
|
+
order: parameter.order,
|
|
231
|
+
defaultValue: parameter.defaultValue === '' ? '' : String(parameter.defaultValue),
|
|
232
|
+
reference: parameter.reference,
|
|
233
|
+
parameter,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The record trigger's table metadata. The documented ProcessFlow descriptor
|
|
239
|
+
* route (/api/now/processflow/trigger/record/table) does not exist on this
|
|
240
|
+
* instance, so the label comes from sys_db_object and the display column from
|
|
241
|
+
* the nearest sys_dictionary `display=true` element walking up the
|
|
242
|
+
* super_class chain (a child table such as `ticket` inherits `number` from
|
|
243
|
+
* `task`).
|
|
244
|
+
*/
|
|
245
|
+
export async function readTableDescriptor(sdk, table) {
|
|
246
|
+
const name = String(table || '').trim();
|
|
247
|
+
if (!name) throw new Error('Record trigger requires a table name');
|
|
248
|
+
const rows = await sdk.list('sys_db_object', new URLSearchParams({
|
|
249
|
+
sysparm_query: `name=${name}`,
|
|
250
|
+
sysparm_limit: '2',
|
|
251
|
+
sysparm_display_value: 'all',
|
|
252
|
+
sysparm_fields: 'name,label,super_class',
|
|
253
|
+
}));
|
|
254
|
+
if (rows.length !== 1) throw new Error(rows.length === 0 ? `Table not found: ${name}` : `Table name is ambiguous: ${name}`);
|
|
255
|
+
const label = displayValue(rows[0].label) || text(rows[0].label);
|
|
256
|
+
if (!label) throw new Error(`Table ${name} has no label`);
|
|
257
|
+
return { table: name, label, displayField: await readDisplayField(sdk, name, text(rows[0].super_class)) };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function readDisplayField(sdk, table, superId) {
|
|
261
|
+
let current = table;
|
|
262
|
+
let parentId = superId;
|
|
263
|
+
for (let depth = 0; depth < 16 && current; depth += 1) {
|
|
264
|
+
const field = await sdk.list('sys_dictionary', new URLSearchParams({
|
|
265
|
+
sysparm_query: `name=${current}^display=true`,
|
|
266
|
+
sysparm_limit: '5',
|
|
267
|
+
sysparm_display_value: 'all',
|
|
268
|
+
sysparm_fields: 'element',
|
|
269
|
+
}));
|
|
270
|
+
const element = field.map(row => text(row.element)).find(Boolean);
|
|
271
|
+
if (element) return element;
|
|
272
|
+
if (!parentId) return '';
|
|
273
|
+
const parent = await sdk.list('sys_db_object', new URLSearchParams({
|
|
274
|
+
sysparm_query: `sys_id=${parentId}`,
|
|
275
|
+
sysparm_limit: '2',
|
|
276
|
+
sysparm_display_value: 'all',
|
|
277
|
+
sysparm_fields: 'name,super_class',
|
|
278
|
+
}));
|
|
279
|
+
if (parent.length !== 1) return '';
|
|
280
|
+
current = text(parent[0].name);
|
|
281
|
+
parentId = text(parent[0].super_class);
|
|
282
|
+
}
|
|
283
|
+
return '';
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** The declared input a manifest shortcut (`table`, `condition`) should drive. */
|
|
287
|
+
export function tableInputFor(declared) {
|
|
288
|
+
return declared.find(input => TABLE_INPUT_NAMES.includes(input.name)) || null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function conditionInputFor(declared) {
|
|
292
|
+
return declared.find(input => CONDITION_INPUT_NAMES.includes(input.name)) || null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function wrapped(value) {
|
|
296
|
+
const scalar = value === '' || value === null || value === undefined ? '' : String(value);
|
|
297
|
+
return { schemaless: false, schemalessValue: '', value: scalar };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function declaredDefault(input) {
|
|
301
|
+
return input.defaultValue === '' || input.defaultValue === undefined ? '' : input.defaultValue;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Turn a definition's declared inputs plus the caller's values into the exact
|
|
306
|
+
* `inputs` array the snFlowDesigner insert expects.
|
|
307
|
+
*
|
|
308
|
+
* Every declared input is written, using the instance's own default when the
|
|
309
|
+
* caller supplies nothing, because Flow Designer stores the full input set
|
|
310
|
+
* (a real record trigger carries run_on_extended / run_flow_in /
|
|
311
|
+
* run_when_setting / … alongside table and condition).
|
|
312
|
+
*/
|
|
313
|
+
export function buildTriggerInputs(declared, supplied = {}, tableDescriptor = null) {
|
|
314
|
+
const byName = new Map(declared.map(input => [input.name, input]));
|
|
315
|
+
for (const name of Object.keys(supplied)) {
|
|
316
|
+
if (!byName.has(name)) throw new Error(`Trigger input "${name}" is not declared by this trigger definition`);
|
|
317
|
+
}
|
|
318
|
+
for (const input of declared) {
|
|
319
|
+
const value = Object.prototype.hasOwnProperty.call(supplied, input.name) ? supplied[input.name] : declaredDefault(input);
|
|
320
|
+
const resolved = value === '' || value === null || value === undefined ? '' : String(value);
|
|
321
|
+
if (resolved === '' && input.mandatory) throw new Error(`Trigger input "${input.name}" (${input.label || input.type}) is required`);
|
|
322
|
+
const tableValue = tableDescriptor && TABLE_INPUT_NAMES.includes(input.name);
|
|
323
|
+
const renderedValue = tableValue ? tableDescriptor.table : resolved;
|
|
324
|
+
const renderedDisplay = tableValue ? tableDescriptor.label : resolved;
|
|
325
|
+
if (!input.parameter?.id) throw new Error(`Trigger input "${input.name}" is missing its provider identity; refusing to invent one`);
|
|
326
|
+
byName.get(input.name).value = renderedValue;
|
|
327
|
+
byName.get(input.name).displayValue = renderedDisplay;
|
|
328
|
+
byName.get(input.name).displayField = tableValue ? tableDescriptor.displayField || '' : '';
|
|
329
|
+
}
|
|
330
|
+
return declared.map(input => ({
|
|
331
|
+
name: input.name,
|
|
332
|
+
value: wrapped(input.value),
|
|
333
|
+
displayValue: wrapped(input.displayValue),
|
|
334
|
+
displayField: input.displayField || '',
|
|
335
|
+
scriptActive: false,
|
|
336
|
+
label: input.parameter.label,
|
|
337
|
+
internalType: input.parameter.type,
|
|
338
|
+
fromTemplate: false,
|
|
339
|
+
order: input.parameter.order,
|
|
340
|
+
valueSysId: '',
|
|
341
|
+
mandatory: input.parameter.mandatory === true,
|
|
342
|
+
parameter: input.parameter,
|
|
343
|
+
}));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** The triggerInstances.insert payload for the snFlowDesigner flow patch. */
|
|
347
|
+
export function buildTriggerInstance({ flowId, definition, triggerType = '', inputs, metadata = '{}' }) {
|
|
348
|
+
if (!flowId) throw new Error('Trigger insert requires the created flow id');
|
|
349
|
+
if (!definition?.sysId || !definition?.type) throw new Error('Trigger insert requires a resolved trigger definition');
|
|
350
|
+
return {
|
|
351
|
+
flowSysId: flowId,
|
|
352
|
+
name: definition.name,
|
|
353
|
+
type: definition.type,
|
|
354
|
+
triggerType: triggerType || '',
|
|
355
|
+
triggerDefinitionId: definition.sysId,
|
|
356
|
+
metadata,
|
|
357
|
+
inputs,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// The snFlowDesigner GraphQL input types take `attributes` as a String
|
|
362
|
+
// ("key=value,key=value") even though the ProcessFlow read path renders it as an
|
|
363
|
+
// object. Sending the object fails validation: "not a valid 'String'".
|
|
364
|
+
export function attributesToString(value) {
|
|
365
|
+
if (value === null || value === undefined) return '';
|
|
366
|
+
if (typeof value === 'string') return value;
|
|
367
|
+
if (typeof value !== 'object' || Array.isArray(value)) throw new Error('Provider attributes must be a string or a flat object');
|
|
368
|
+
return Object.entries(value).map(([key, item]) => `${key}=${item}`).join(',');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Accepted `parameter` keys on global_snFlowDesigner_FlowDesignParameter.
|
|
372
|
+
// GraphQL introspection is disabled on this instance, so this list was
|
|
373
|
+
// established empirically by probing each candidate field against the live
|
|
374
|
+
// schema (unknown keys answer "contains a field not in
|
|
375
|
+
// 'global_snFlowDesigner_FlowDesignParameter'"). Anything the ProcessFlow
|
|
376
|
+
// action definition returns that is NOT in this list — notably the synthetic
|
|
377
|
+
// `default` this CLI adds, plus `scriptActive`/`value`/`displayValue` — must be
|
|
378
|
+
// carried elsewhere or dropped, or the mutation fails validation.
|
|
379
|
+
const FLOW_DESIGN_PARAMETER_FIELDS = [
|
|
380
|
+
'id', 'label', 'name', 'type', 'type_label', 'hint', 'order', 'extended',
|
|
381
|
+
'mandatory', 'readonly', 'maxsize', 'children', 'data_structure', 'reference',
|
|
382
|
+
'reference_display', 'ref_qual', 'choiceOption', 'table', 'columnName',
|
|
383
|
+
'defaultValue', 'defaultDisplayValue', 'use_dependent', 'dependent_on',
|
|
384
|
+
'show_ref_finder', 'local', 'attributes', 'sys_class_name', 'choices',
|
|
385
|
+
'defaultChoices', 'uiDisplayType', 'uiDisplayTypeLabel', 'placeholder',
|
|
386
|
+
'comments', 'internal_link', 'dynamic', 'searchField',
|
|
387
|
+
];
|
|
388
|
+
|
|
389
|
+
/** A provider schema entry narrowed to the GraphQL parameter input type. */
|
|
390
|
+
export function flowDesignParameter(entry) {
|
|
391
|
+
const out = {};
|
|
392
|
+
for (const key of FLOW_DESIGN_PARAMETER_FIELDS) if (entry?.[key] !== undefined) out[key] = entry[key];
|
|
393
|
+
out.attributes = attributesToString(entry?.attributes);
|
|
394
|
+
return out;
|
|
395
|
+
}
|
package/src/help.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
// Custom grouped help renderer
|
|
2
2
|
// Organized by ServiceNow domain model with subgroup dividers
|
|
3
|
+
//
|
|
4
|
+
// The command rows are generated from the LIVE command registry
|
|
5
|
+
// (`cli.commandRegistry()`): every registered root command appears, with the
|
|
6
|
+
// description its own registration declares. The tables below are placement
|
|
7
|
+
// hints only — group order, dividers, and a fallback description for a command
|
|
8
|
+
// that declares none. A command that is registered but not hinted lands in the
|
|
9
|
+
// trailing "MORE" group, so help can never silently drop a command again
|
|
10
|
+
// (`approvals`, `cmdb`, `docs`, `perf`, `platform`, … were missing from the
|
|
11
|
+
// hand-written list while being fully registered).
|
|
3
12
|
|
|
4
13
|
function renderGroup(name, commands) {
|
|
5
14
|
const lines = [`\n${name}`];
|
|
@@ -23,6 +32,8 @@ const CORE_COMMANDS = [
|
|
|
23
32
|
{ name: 'changes', alias: 'chg', desc: 'Manage change requests' },
|
|
24
33
|
{ name: 'requests', alias: 'req', desc: 'Manage service catalog requests' },
|
|
25
34
|
{ name: 'tasks', alias: 'task', desc: 'Manage catalog tasks' },
|
|
35
|
+
{ name: 'tickets', desc: 'Query generic tickets' },
|
|
36
|
+
{ name: 'approvals', desc: 'Approval requests (list, approve, reject, submit, history)' },
|
|
26
37
|
];
|
|
27
38
|
|
|
28
39
|
const AUTOMATION_COMMANDS = [
|
|
@@ -96,6 +107,7 @@ const DATA_COMMANDS = [
|
|
|
96
107
|
{ name: 'tables', alias: 't', desc: 'Table definitions' },
|
|
97
108
|
{ name: 'columns', alias: 'col', desc: 'Column definitions' },
|
|
98
109
|
{ name: 'relationships', alias: 'rel', desc: 'Table relationships' },
|
|
110
|
+
{ name: 'cmdb', desc: 'CMDB: CI lookup and relationship traversal (read-only)' },
|
|
99
111
|
|
|
100
112
|
{ divider: '── Shared Code, Transforms, Logs ──' },
|
|
101
113
|
{ name: 'includes', alias: 'si', desc: 'Script includes' },
|
|
@@ -104,6 +116,8 @@ const DATA_COMMANDS = [
|
|
|
104
116
|
{ name: 'properties', alias: 'prop', desc: 'System properties' },
|
|
105
117
|
|
|
106
118
|
{ name: 'records', desc: 'Generic Table API (any table)' },
|
|
119
|
+
{ name: 'transactions', desc: 'Client transaction timings' },
|
|
120
|
+
{ name: 'snippets', desc: 'Saved query snippets' },
|
|
107
121
|
];
|
|
108
122
|
|
|
109
123
|
const CONFIG_COMMANDS = [
|
|
@@ -112,12 +126,20 @@ const CONFIG_COMMANDS = [
|
|
|
112
126
|
{ name: 'updatesets', desc: 'Update sets (set, create, export)' },
|
|
113
127
|
{ name: 'scopes', desc: 'Application scopes' },
|
|
114
128
|
{ name: 'domains', desc: 'Domains (domain separation)' },
|
|
129
|
+
{ name: 'midservers', desc: 'MID Servers and MID-run commands' },
|
|
130
|
+
{ name: 'platform', desc: 'Platform health and node statistics (read-only)' },
|
|
131
|
+
{ name: 'completion', desc: 'Shell completion script' },
|
|
115
132
|
];
|
|
116
133
|
|
|
117
134
|
const DEVELOPER_COMMANDS = [
|
|
118
135
|
{ name: 'atf', desc: 'Automated Test Framework (list, suites, run, results)' },
|
|
119
136
|
{ name: 'eval', desc: 'Run server-side JavaScript' },
|
|
120
137
|
{ name: 'rest', desc: 'Raw REST API calls' },
|
|
138
|
+
{ name: 'graphql', desc: 'GraphQL queries' },
|
|
139
|
+
{ name: 'codesearch', desc: 'Search code across ServiceNow artifacts' },
|
|
140
|
+
{ name: 'docs', desc: 'Local ServiceNow documentation search' },
|
|
141
|
+
{ name: 'diff', desc: 'Compare records across configured profiles' },
|
|
142
|
+
{ name: 'perf', desc: 'Read-only performance capture and comparison' },
|
|
121
143
|
{ name: 'skill', desc: 'AI agent skill file (show, fetch, install)' },
|
|
122
144
|
{ name: 'version', desc: 'Version info (--check for npm updates)' },
|
|
123
145
|
];
|
|
@@ -132,17 +154,50 @@ const COMMAND_GROUPS = {
|
|
|
132
154
|
'DEVELOPER': DEVELOPER_COMMANDS,
|
|
133
155
|
};
|
|
134
156
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
157
|
+
/**
|
|
158
|
+
* Domain separation is an optional plugin — only advertise `domains` when the
|
|
159
|
+
* active profile's capability probe found it installed.
|
|
160
|
+
*/
|
|
161
|
+
function isCapabilityHidden(name, config) {
|
|
162
|
+
if (name !== 'domains') return false;
|
|
163
|
+
const activeName = config?.activeProfile || config?.defaultProfile;
|
|
164
|
+
return !(activeName && config?.profiles?.[activeName]?.domain_separation);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Group rows for one domain: registry first (alias/description from the
|
|
169
|
+
* command's own registration), hint text only as a fallback description.
|
|
170
|
+
*/
|
|
171
|
+
function rowsFor(hint, byName, used, config) {
|
|
172
|
+
const rows = [];
|
|
173
|
+
for (const entry of hint) {
|
|
174
|
+
if (entry.divider) { rows.push(entry); continue; }
|
|
175
|
+
const node = byName.get(entry.name);
|
|
176
|
+
if (!node || isCapabilityHidden(node.name, config)) continue;
|
|
177
|
+
used.add(node.name);
|
|
178
|
+
rows.push({
|
|
179
|
+
name: node.name,
|
|
180
|
+
alias: node.aliases[0] || entry.alias || '',
|
|
181
|
+
desc: node.description || entry.desc || '',
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
return rows;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function renderFlags(options) {
|
|
188
|
+
// Derived from the registered global options, so a new global flag cannot be
|
|
189
|
+
// missing from help (--get and --csv were, along with the csv format choice).
|
|
190
|
+
const lines = [];
|
|
191
|
+
for (const spec of options) {
|
|
192
|
+
if (spec.name === 'help') continue;
|
|
193
|
+
const alias = spec.alias.length ? `-${spec.alias.join(', -')}, ` : '';
|
|
194
|
+
const suffix = spec.name === 'format' && spec.choices.length
|
|
195
|
+
? ` [string: ${spec.choices.join(', ')}]`
|
|
196
|
+
: ` [${spec.type}]`;
|
|
197
|
+
lines.push(` ${`${alias}--${spec.name}`.padEnd(24)}${spec.description}${suffix}`);
|
|
198
|
+
}
|
|
199
|
+
lines.push(` ${'--help'.padEnd(24)}Show help${' [boolean]'}`);
|
|
200
|
+
return `\nFLAGS\n${lines.join('\n')}`;
|
|
146
201
|
}
|
|
147
202
|
|
|
148
203
|
function renderTips() {
|
|
@@ -150,35 +205,45 @@ function renderTips() {
|
|
|
150
205
|
TIPS
|
|
151
206
|
--query is available on every list command (e.g. "incidents list --query priority=1")
|
|
152
207
|
Use "jsn <command> --help" for details, or "jsn <command> list --help" for list options
|
|
208
|
+
"jsn --json" prints the registered command surface as JSON for scripts and agents
|
|
153
209
|
|
|
154
210
|
LEARN MORE
|
|
155
211
|
Use "jsn <command> --help" for more information about a command.`;
|
|
156
212
|
}
|
|
157
213
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
214
|
+
/**
|
|
215
|
+
* Render the grouped root help.
|
|
216
|
+
*
|
|
217
|
+
* @param {object} config — profile config (capability-conditioned hiding)
|
|
218
|
+
* @param {{commands: Array, options: Array}} [registry] — from
|
|
219
|
+
* `cli.commandRegistry()`. Without it only the hint tables render, which is
|
|
220
|
+
* the fallback for callers that have no registry (unit tests).
|
|
221
|
+
*/
|
|
222
|
+
export function renderHelp(config, registry = null) {
|
|
223
|
+
const commands = Array.isArray(registry?.commands) ? registry.commands : [];
|
|
224
|
+
const byName = new Map(commands.map((command) => [command.name, command]));
|
|
225
|
+
const used = new Set();
|
|
226
|
+
|
|
227
|
+
const sections = Object.entries(COMMAND_GROUPS)
|
|
228
|
+
.map(([name, hint]) => [name, rowsFor(hint, byName, used, config)])
|
|
229
|
+
.filter(([, rows]) => rows.some((row) => !row.divider))
|
|
230
|
+
.map(([name, rows]) => renderGroup(name, rows));
|
|
231
|
+
|
|
232
|
+
// Anything registered that the hint tables don't place. This is the safety
|
|
233
|
+
// net that keeps help complete as commands are added at the root.
|
|
234
|
+
const unplaced = commands
|
|
235
|
+
.filter((command) => !used.has(command.name) && !isCapabilityHidden(command.name, config))
|
|
236
|
+
.map((command) => ({ name: command.name, alias: command.aliases[0] || '', desc: command.description || '' }));
|
|
237
|
+
if (unplaced.length) sections.push(renderGroup('MORE', unplaced));
|
|
173
238
|
|
|
174
239
|
return [
|
|
175
240
|
'Usage: jsn <command> [options]',
|
|
176
241
|
'',
|
|
177
|
-
|
|
242
|
+
'Command-line interface for ServiceNow',
|
|
178
243
|
'',
|
|
179
244
|
...sections,
|
|
180
245
|
'',
|
|
181
|
-
renderFlags(),
|
|
246
|
+
renderFlags(Array.isArray(registry?.options) ? registry.options : []),
|
|
182
247
|
renderTips(),
|
|
183
248
|
'',
|
|
184
249
|
].join('\n');
|
package/src/output.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Output formatting: JSON, Markdown, Styled, Quiet
|
|
2
2
|
|
|
3
3
|
import process from 'node:process';
|
|
4
|
+
import { errExtractionNotFound } from './errors.js';
|
|
4
5
|
|
|
5
6
|
export const FormatAuto = 'auto';
|
|
6
7
|
export const FormatJSON = 'json';
|
|
@@ -125,9 +126,15 @@ export class OutputWriter {
|
|
|
125
126
|
// --get extractor: resolve against the JSON envelope and print just that
|
|
126
127
|
// value as JSON. Takes precedence over styling/table formats — a path
|
|
127
128
|
// extract is inherently scriptable, not presentational.
|
|
129
|
+
//
|
|
130
|
+
// A path that does not resolve is an error, not a value. Stringifying
|
|
131
|
+
// `undefined` printed the literal token `undefined` and exited 0, so a
|
|
132
|
+
// typo looked like a successful empty result until the next tool in the
|
|
133
|
+
// pipeline failed to parse it. Present null/false/0/"" still print as-is.
|
|
128
134
|
if (this.jqFilter) {
|
|
129
135
|
const envelope = { ok: true, data, summary: opts.summary || '', breadcrumbs: opts.breadcrumbs || [] };
|
|
130
136
|
const value = resolvePath(envelope, this.jqFilter);
|
|
137
|
+
if (value === undefined) throw errExtractionNotFound(this.jqFilter);
|
|
131
138
|
this.writer.write(JSON.stringify(value, null, 2) + '\n');
|
|
132
139
|
return;
|
|
133
140
|
}
|