@jacebenson/jsn 4.3.8-dev.53 → 4.3.8-dev.55

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.8-dev.53",
3
+ "version": "4.3.8-dev.55",
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": {
@@ -12,6 +12,7 @@ const ACTION_COMMANDS = [
12
12
  { name: 'help [identifier]', description: 'Show action input requirements and live usage examples' },
13
13
  { name: 'definition [identifier]', description: 'Read the full JSON definition including executable step_instances' },
14
14
  { name: 'step-types', description: 'Read step schema templates for a target scope' },
15
+ { name: 'action-type-schema', description: 'Read provider metadata for an exact action type ID and scope' },
15
16
  { name: 'create', description: 'Create an action: guided in a terminal, full JSON definition in scripts' },
16
17
  { name: 'update [identifier]', description: 'Save a full definition through Process Flow (alias: edit)' },
17
18
  { name: 'test [identifier]', description: 'Execute an action and optionally wait for actual outputs' },
@@ -29,6 +30,53 @@ async function actionID(argv, app) {
29
30
  return unwrapSysId(await resolveRecord(app.sdk, { table: ACTION_TABLE, identifier: argv.identifier, matchField: 'name', resource: 'action' }));
30
31
  }
31
32
 
33
+ const refID = value => typeof value === 'string' ? value : value?.value ?? value?.sys_id ?? value?.sysId ?? value?.id;
34
+ const asBoolean = value => value === true || value === 1 || (typeof value === 'string' && value.toLowerCase() === 'true');
35
+ const EXAMPLE_TYPES = new Set(['approval_rules', 'conditions', 'schedule_date_time']);
36
+ function collectActionExamples(node, snapshotId, allowedInputs, examples, flowName) {
37
+ if (!node || typeof node !== 'object') return;
38
+ if (Array.isArray(node)) { for (const item of node) collectActionExamples(item, snapshotId, allowedInputs, examples, flowName); return; }
39
+ const typeId = refID(node.actionTypeSysId || node.action_type_sys_id || node.actionType || node.action_type);
40
+ if (typeId === snapshotId && Array.isArray(node.inputs)) {
41
+ const values = Object.fromEntries(node.inputs.filter(input => input?.name && allowedInputs.has(input.name) && input.value !== undefined && input.value !== null)
42
+ .map(input => [input.name, refID(input.value) ?? input.value]));
43
+ if (Object.keys(values).length) examples.push({ flow: flowName, inputs: values });
44
+ }
45
+ for (const [key, value] of Object.entries(node)) if (key !== 'inputs') collectActionExamples(value, snapshotId, allowedInputs, examples, flowName);
46
+ }
47
+ async function findActionExamples(sdk, definition, inputDefinitions) {
48
+ const snapshotId = refID(definition.latest_snapshot);
49
+ if (!/^[a-f0-9]{32}$/i.test(snapshotId || '')) return { examples: [], status: 'unavailable' };
50
+ const allowedInputs = new Set(inputDefinitions.filter(input => EXAMPLE_TYPES.has(input.type)
51
+ && !/(password|secret|token|credential|authorization|cookie|api[_-]?key)/i.test(input.name)).map(input => input.name));
52
+ if (!allowedInputs.size) return { examples: [], status: 'not_applicable' };
53
+ const query = new URLSearchParams({
54
+ sysparm_query: `action_type=${snapshotId}^ORDERBYsys_id`,
55
+ sysparm_fields: 'sys_id,flow,action_type',
56
+ sysparm_limit: '20',
57
+ sysparm_display_value: 'false',
58
+ });
59
+ const rows = await sdk.listStrict('sys_hub_action_instance', query);
60
+ if (!rows.length) return { examples: [], status: 'none' };
61
+ const flowIds = [...new Set(rows.map(row => refID(row.flow)).filter(value => /^[a-f0-9]{32}$/i.test(value || '')))];
62
+ if (!flowIds.length) return { examples: [], status: 'unavailable' };
63
+ const examples = [];
64
+ let unreadable = false;
65
+ for (const flowId of flowIds.slice(0, 10)) {
66
+ try {
67
+ const result = await sdk.inspectFlow(flowId);
68
+ const payloadActions = result?.payload?.actionInstances;
69
+ if (!Array.isArray(payloadActions) || !payloadActions.some(item => item && typeof item === 'object' && !Array.isArray(item))) {
70
+ unreadable = true;
71
+ continue;
72
+ }
73
+ collectActionExamples(result.payload, snapshotId, allowedInputs, examples, result?.flow?.name || flowId);
74
+ } catch { unreadable = true; }
75
+ if (examples.length >= 3) break;
76
+ }
77
+ return { examples: examples.slice(0, 3), status: examples.length ? 'found' : unreadable ? 'unavailable' : 'none' };
78
+ }
79
+
32
80
  export const actionsCmd = wrap => {
33
81
  // Reuse the existing list/show picker and output contract only.
34
82
  const base = buildDevCmd('actions', ACTION_TABLE, ['action'], ['name', 'active', 'sys_scope', 'sys_updated_on'], wrap, {
@@ -56,7 +104,7 @@ export const actionsCmd = wrap => {
56
104
  describe: 'Flow Designer actions: create, edit full definitions, and test with outputs',
57
105
  builder: y => base.builder(y)
58
106
  .command({
59
- command: 'create', describe: `Create parent and executable Process Flow action; verify persistence. --data/--data-file requires a full definition with inputs, outputs, and steps arrays. Variables have name and type; each step requires cid, step_type, and inputs, outputs, extended_inputs, extended_outputs arrays. Use step_types for valid step_type schema. Example (structure only):\n{"name":"Example action","inputs":[{"name":"record_id","type":"string"}],"outputs":[],"steps":[{"cid":"step-1","step_type":"<stepTypeFromSchema>","inputs":[],"outputs":[],"extended_inputs":[],"extended_outputs":[]}]}`,
107
+ command: 'create', describe: 'Create a Process Flow action. Use `jsn actions step-types --scope <sys_id>` for live provider schemas. The required definition structure is code-owned; step-specific fields are provider-defined.',
60
108
  builder: yy => dataOptions(yy).option('scope', { type: 'string', describe: 'Exact target scope sys_id, or global' }),
61
109
  handler: run(async (argv, app) => {
62
110
  const guided = !hasDefinitionData(argv) ? await guidedActionCreate(argv, app) : undefined;
@@ -77,12 +125,13 @@ export const actionsCmd = wrap => {
77
125
  const inputs = (definition.inputs || []).map(input => ({
78
126
  name: input.name,
79
127
  type: input.type || input.parameter?.type || input.parameter?.inputType || input.valueType || 'unknown',
80
- mandatory: input.mandatory ?? input.required ?? input.parameter?.mandatory ?? input.parameter?.required ?? false,
128
+ mandatory: asBoolean(input.mandatory ?? input.required ?? input.parameter?.mandatory ?? input.parameter?.required),
81
129
  default: input.defaultValue ?? input.default ?? input.parameter?.defaultValue ?? '',
82
130
  description: input.description || input.label || input.parameter?.label || '',
83
131
  }));
84
- app.ok({ action: { id, name: definition.name, scope }, inputs, examples: [], examples_status: 'unverified',
85
- examples_note: 'Live examples are not shown yet because this instance’s action-instance to snapshot relationship has not been verified.' },
132
+ const exampleResult = await findActionExamples(app.sdk, definition, inputs);
133
+ app.ok({ action: { id, name: definition.name, scope }, inputs, examples: exampleResult.examples,
134
+ examples_status: exampleResult.status },
86
135
  { summary: `Input help for action ${definition.name || id}` });
87
136
  }),
88
137
  })
@@ -137,13 +186,30 @@ export const actionsCmd = wrap => {
137
186
  }),
138
187
  })
139
188
  .command({
140
- command: 'step-types', describe: 'Read step schema templates for a target scope',
189
+ command: 'step-types', describe: 'Read full live step schemas for a target scope',
141
190
  builder: yy => yy.option('scope', { type: 'string', describe: 'Exact transaction scope sys_id, or global' }),
142
191
  handler: run(async (argv, app) => {
143
192
  const scope = await designerScope(argv, app);
144
193
  app.ok(await app.sdk.getProcessFlowStepTypes(scope), { summary: 'Process Flow step schemas' });
145
194
  }),
146
195
  })
196
+ .command({
197
+ command: 'action-type-schema', describe: 'Read live provider input/output metadata for an exact action type ID and scope',
198
+ builder: yy => yy.option('id', { type: 'string', demandOption: true, describe: 'Exact 32-character action type sys_id' }).option('scope', { type: 'string', demandOption: true, describe: 'Exact transaction scope sys_id, or global' }),
199
+ handler: run(async (argv, app) => {
200
+ if (!/^[a-f\d]{32}$/i.test(argv.id)) throw errUsage('--id must be an exact 32-character action type sys_id');
201
+ if (!/^(?:[a-fA-F0-9]{32}|global)$/.test(argv.scope)) throw errUsage('--scope must be an exact scope sys_id or global');
202
+ app.requireInstance();
203
+ const url = `${app.sdk.baseURL}/api/now/processflow/action/action_types/${encodeURIComponent(argv.id)}?${new URLSearchParams({ sysparm_transaction_scope: argv.scope })}`;
204
+ const response = await app.sdk.request(url, { method: 'GET' });
205
+ const result = response?.result;
206
+ if (response?.error || result?.error || (result?.errorCode !== undefined && result.errorCode !== 0 && result.errorCode !== '0')) {
207
+ throw new Error(result?.errorMessage || response?.error?.message || 'Provider rejected the action type schema request');
208
+ }
209
+ if (!result || typeof result !== 'object') throw new Error('Provider returned no action type metadata');
210
+ app.ok(result, { summary: `Provider action type schema ${argv.id}` });
211
+ }),
212
+ })
147
213
  .command({
148
214
  command: 'delete [identifier]', describe: 'Delete action parent explicitly',
149
215
  builder: yy => scopeOption(yy).option('force', { type: 'boolean', default: false, describe: 'Skip confirmation' }),
@@ -6,7 +6,7 @@ import { inspectFlow } from '../flow-inspection.js';
6
6
  import { publishFlows, publishFlowLifecycle, publishDoctor, flowStatus } from '../flow-publish.js';
7
7
  import { resolveRecord, unwrapSysId } from '../resolve-record.js';
8
8
  import { createFlowFromManifest, updateFlowFromManifest, readCurrentUpdateSet } from '../flow-create.js';
9
- import { EXCLUDED_TRIGGER_TYPES, SUPPORTED_TRIGGER_TYPES } from '../flow-triggers.js';
9
+ import { EXCLUDED_TRIGGER_TYPES, SUPPORTED_TRIGGER_TYPES, readTriggerCatalog, readTriggerInputDefinitions } from '../flow-triggers.js';
10
10
  import { testFlow } from '../flow-test.js';
11
11
  import { errUsage } from '../errors.js';
12
12
  import { cancelled, pickDesignerRecord, guidedFlowCreate, guidedFlowEdit, guidedFlowTestRecord, hasDefinitionData, confirmDesignerMutation } from '../flow-designer-interactive.js';
@@ -63,6 +63,25 @@ export function flowsCmd(wrap) {
63
63
  describe: 'Manage Flow Designer flows',
64
64
  builder: (yargs) => {
65
65
  return yargs
66
+ .command({
67
+ command: 'schemas',
68
+ describe: 'Read live trigger catalog and declared inputs; supported marks verified CLI creation families',
69
+ handler: wrap(async (_argv, app) => {
70
+ app.requireInstance();
71
+ const catalog = await readTriggerCatalog(app.sdk);
72
+ for (const category of catalog.categories) {
73
+ for (const trigger of category.triggers) {
74
+ if (trigger.supported) trigger.inputs = await readTriggerInputDefinitions(app.sdk, trigger.sysId);
75
+ }
76
+ }
77
+ for (const trigger of catalog.ungrouped) {
78
+ if (trigger.supported) trigger.inputs = await readTriggerInputDefinitions(app.sdk, trigger.sysId);
79
+ }
80
+ const data = { ...catalog, structural_contract: 'Manifest keys and persistence shape are code-owned; trigger definitions and input metadata are instance-derived.' };
81
+ data._formatted = [...catalog.categories.flatMap(c => c.triggers.map(t => `${c.name}: ${t.name} [${t.type}] ${t.supported ? 'supported' : 'not supported'}; inputs: ${(t.inputs || []).map(i => i.name + ':' + i.type).join(', ')}`)), ...catalog.ungrouped.map(t => `${t.name} [${t.type}] ungrouped${t.supported ? '; supported' : '; not supported'}; inputs: ${(t.inputs || []).map(i => i.name + ':' + i.type).join(', ')}`)].join('\n');
82
+ app.ok(data, { summary: 'Live Flow trigger schemas' });
83
+ }),
84
+ })
66
85
  .command({
67
86
  command: 'list',
68
87
  aliases: ['ls'],
@@ -294,7 +313,7 @@ export function flowsCmd(wrap) {
294
313
  })
295
314
  .command({
296
315
  command: 'create',
297
- describe: `Create a draft flow. JSON manifest fields: name (required), scope (required), optional description, trigger { type (required), optional table, condition, inputs }, actions (required array of { type, optional inputs }). Trigger type must be supported; trigger inputs and action inputs are objects. Flow Logic, subflows, variables, and errorHandling are unsupported. Example:\n{"name":"Approval flow","scope":"global","trigger":{"type":"record_create_or_update","table":"incident"},"actions":[{"type":"<actionTypeSysId>","inputs":{"message":"Created"}}]}`,
316
+ describe: 'Create a draft flow from a JSON manifest or guided prompts. Use `jsn flows schemas` for instance-derived trigger and input schemas, and `jsn actions action-type-schema --id <actionTypeId> --scope <scopeId>` for provider action inputs. Manifest structure remains code-owned.',
298
317
  builder: (y) => y
299
318
  .option('data', { type: 'string', describe: 'Flow manifest as inline JSON' })
300
319
  .option('data-file', { type: 'string', describe: 'Read the flow manifest from a JSON file' })
@@ -322,7 +341,7 @@ export function flowsCmd(wrap) {
322
341
  .command({
323
342
  command: 'update [identifier]',
324
343
  aliases: ['edit'],
325
- describe: `Safely update an inactive Draft flow. JSON may contain scope, trigger { optional type: "record_create_or_update", table and/or condition }, and/or actions [{ order (positive integer), type, inputs (non-empty object of changed values) }]. Only existing trigger and action inputs are updated; omitted values are preserved. Example:\n{"trigger":{"table":"incident","condition":"active=true"},"actions":[{"order":1,"type":"<existingActionTypeSysId>","inputs":{"message":"Updated"}}]}`,
344
+ describe: 'Safely update an inactive Draft flow from a partial JSON manifest. Use `jsn flows schemas` for live trigger inputs and `jsn actions action-type-schema --id <actionTypeId> --scope <scopeId>` for provider action inputs. Manifest structure remains code-owned; omitted values are preserved.',
326
345
  builder: (y) => y
327
346
  .option('data', { type: 'string', describe: 'Update manifest as inline JSON' })
328
347
  .option('data-file', { type: 'string', describe: 'Read the update manifest from a JSON file' })