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

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 CHANGED
@@ -31,6 +31,19 @@ npm install -g @jacebenson/jsn
31
31
 
32
32
  **Note:** git installs build `better-sqlite3` (docs search) from source, so the machine needs build tools (Python, make, a C++ compiler).
33
33
 
34
+ ## Direct MID installation
35
+
36
+ ```sh
37
+ jsn midservers add --install-method direct --name worker1
38
+ ```
39
+
40
+ Official x86-64 ZIPs for Linux and Windows, selected from the instance's
41
+ `mid.version`. Credentials are prompted and validated before installation.
42
+ Linux supports a user-owned background runtime. Windows currently prepares
43
+ private files and offers a foreground console, not native service registration.
44
+ See [direct MID installation](docs/mid-direct-install.md) for security boundaries,
45
+ lifecycle commands and the untested Windows host phases. Docker remains available.
46
+
34
47
  ## Quick Start
35
48
 
36
49
  ```bash
@@ -97,8 +110,27 @@ jsn logs follow --level error --tail 10
97
110
  ### Flow Designer actions
98
111
 
99
112
  `actions` and `action` are the same command. `list`, `show`, and explicit
100
- `delete` remain available. Create and edit use full Process Flow JSON, not
101
- ordinary table-field updates.
113
+ `delete` remain available. In a human terminal, omit the target to open a
114
+ searchable picker. Create and edit guide you through provider schemas and save
115
+ full Process Flow definitions, not ordinary table-field updates.
116
+
117
+ ```bash
118
+ jsn actions create # Guided scope, name, schemas, inputs and script
119
+ jsn actions edit # Pick an action, edit its saved definition
120
+ jsn actions definition # Pick and inspect a complete definition
121
+ jsn actions test --wait # Pick an action, supply saved-schema inputs, confirm
122
+ jsn actions step-types # Pick the exact transaction scope
123
+ ```
124
+
125
+ Script inputs offer a file path or your `$VISUAL`/`$EDITOR`. New script steps can
126
+ declare outputs and map them to action outputs. Variable declarations reuse
127
+ provider schema templates and receive fresh UI identities. Unknown nested
128
+ schema fields are retained. Unsupported input types stop with instructions to
129
+ use `--data-file`; JSN does not silently omit them or invent schema defaults.
130
+ Creation waits for an explicit final confirmation before allocating a parent.
131
+ It saves a draft only and never publishes or runs it automatically.
132
+
133
+ For automation, keep using explicit identifiers and full JSON:
102
134
 
103
135
  ```bash
104
136
  jsn actions step-types --scope <scope_sys_id> --get data > step-types.json
@@ -177,9 +209,43 @@ These internal endpoints and runtime-table permissions can vary by instance.
177
209
 
178
210
  ### Flow execution fields
179
211
 
212
+ ### Flow Designer terminal workflows
213
+
214
+ ```bash
215
+ jsn flows create # Guided record trigger and published action inputs
216
+ jsn flows edit # Pick an inactive Draft, edit condition or existing inputs
217
+ jsn flows show # Pick a flow to inspect
218
+ jsn flows status # Pick a flow to diagnose
219
+ jsn flows test # Pick flow and a record from its actual trigger table, confirm
220
+ jsn flows publish # Pick a flow, explicitly confirm activation
221
+ # Automation must explicitly confirm publication too:
222
+ jsn flows publish <flow_sys_id> --force --json
223
+ ```
224
+
225
+ Wizards and omitted-target pickers require both a prompt-capable terminal and
226
+ `auto` output. `--json`, explicit formats, `--get`, `--data`, `--data-file`,
227
+ `--output-map`, pipes, `CI`, `NONINTERACTIVE` and `JSN_NO_PROMPTS=1` never open a
228
+ wizard. In those modes provide identifiers and required scope/record/data
229
+ explicitly. Tests, publish and deletion require `--force` or the profile's
230
+ existing explicit skip-confirmations setting when prompting is unavailable.
231
+ Read-only profiles still block mutations before authentication or prompting.
232
+ Press Escape in record pickers or Ctrl+C in any wizard to cancel. Refusing a
233
+ final confirmation leaves the instance untouched.
234
+
235
+ Existing action/flow scopes come from saved raw identifiers. New scopes offer
236
+ the active application preference if available, then a searchable scope picker.
237
+ No scope name is guessed. Input pills are passed through unchanged.
238
+
239
+ Flow edits are limited to the existing manifest contract. They do not rename
240
+ flows, add/remove steps, implement Flow Logic, subflows, variables or error
241
+ handling. Flow deletion remains unsupported. Action deletion retains its
242
+ existing explicit parent-record cleanup and absence readback, not a new native
243
+ builder deletion lifecycle. See [command coverage and local proof](docs/flow-designer-interactive.md).
244
+
180
245
  ### Flow creation manifest
181
246
 
182
- `jsn flows create` accepts `--data-file` or inline `--data`. The first slice
247
+ `jsn flows create` accepts `--data-file` or inline `--data`, or guides human
248
+ terminal users without those options. The supported slice
183
249
  supports one `record_create_or_update` trigger and published Action types whose
184
250
  input schemas are available from the instance. JSN resolves action metadata,
185
251
  defaults, and typed inputs from ServiceNow; callers do not provide GraphQL or
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacebenson/jsn",
3
- "version": "4.3.7-dev.33",
3
+ "version": "4.3.7-dev.35",
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": {
@@ -181,11 +181,12 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
181
181
  }),
182
182
  })
183
183
  .command({
184
- command: 'show <identifier>',
184
+ command: opts.resolveIdentifier ? 'show [identifier]' : 'show <identifier>',
185
185
  aliases: ['get'],
186
186
  describe: `Show ${vowelArticle(singular)} ${singular} by name or sys_id`,
187
187
  handler: wrap(async (argv, app) => {
188
- const id = argv.identifier;
188
+ const id = opts.resolveIdentifier ? await opts.resolveIdentifier(argv, app) : argv.identifier;
189
+ if (id === undefined) return;
189
190
  // Only restrict sysparm_fields if showFields is explicitly set.
190
191
  // Go version fetches all fields for show unless explicitly restricted.
191
192
  const fields = (showFields && showFields.length > 0)
@@ -1,15 +1,19 @@
1
1
  import { buildDevCmd } from './_generic.js';
2
- import { parseDataArg, confirmDelete } from '../helpers.js';
2
+ import { parseDataArg } from '../helpers.js';
3
3
  import { resolveRecord, unwrapSysId } from '../resolve-record.js';
4
4
  import { declareCapabilities } from '../capabilities.js';
5
- import { ACTION_TABLE, actionScope, validateScope } from '../processflow-actions.js';
5
+ import { ACTION_TABLE, actionScope } from '../processflow-actions.js';
6
6
  import { errUsage } from '../errors.js';
7
+ import { cancelled, pickDesignerRecord, designerScope, guidedActionCreate, guidedActionEdit, guidedActionTestInputs, interactiveDesigner, hasDefinitionData, confirmDesignerMutation } from '../flow-designer-interactive.js';
7
8
 
8
9
  const scopeOption = y => y.option('scope', { type: 'string', describe: 'Exact transaction scope sys_id; inferred from existing action when omitted' });
9
10
  const dataOptions = y => y
10
11
  .option('data-file', { type: 'string', describe: 'Full action definition JSON file, not table fields' })
11
12
  .option('data', { type: 'string', describe: 'Full action definition as JSON' });
12
13
  async function actionID(argv, app) {
14
+ const identifier = await pickDesignerRecord(argv, app, ACTION_TABLE, 'action');
15
+ if (identifier === undefined) return undefined;
16
+ argv = { ...argv, identifier };
13
17
  return unwrapSysId(await resolveRecord(app.sdk, { table: ACTION_TABLE, identifier: argv.identifier, matchField: 'name', resource: 'action' }));
14
18
  }
15
19
 
@@ -17,6 +21,7 @@ export const actionsCmd = wrap => {
17
21
  // Reuse the existing list/show picker and output contract only.
18
22
  const base = buildDevCmd('actions', ACTION_TABLE, ['action'], ['name', 'active', 'sys_scope', 'sys_updated_on'], wrap, {
19
23
  singular: 'action', readOnly: true,
24
+ resolveIdentifier: (argv, app) => cancelled(() => pickDesignerRecord(argv, app, ACTION_TABLE, 'action')),
20
25
  showBreadcrumbs: (_record, id) => [
21
26
  { action: 'definition', cmd: `jsn actions definition ${id}`, description: 'Read complete executable definition' },
22
27
  { action: 'list', cmd: 'jsn actions list', description: 'Back to actions' },
@@ -26,7 +31,7 @@ export const actionsCmd = wrap => {
26
31
  // Retain the common wrap and output controller; domain failures get a
27
32
  // structured error envelope and nonzero exit, never a success-shaped dispatch.
28
33
  const run = handler => wrap(async (argv, app) => {
29
- try { await handler(argv, app); }
34
+ try { await cancelled(() => handler(argv, app)); }
30
35
  catch (error) {
31
36
  if (!error.code?.startsWith('action_')) throw error;
32
37
  if (error.details) error.hint = JSON.stringify(error.details);
@@ -40,37 +45,43 @@ export const actionsCmd = wrap => {
40
45
  builder: y => base.builder(y)
41
46
  .command({
42
47
  command: 'create', describe: 'Create parent and executable Process Flow action; verify persistence',
43
- builder: yy => dataOptions(yy).option('scope', { type: 'string', demandOption: true, describe: 'Exact target scope sys_id, or global' }),
48
+ builder: yy => dataOptions(yy).option('scope', { type: 'string', describe: 'Exact target scope sys_id, or global' }),
44
49
  handler: run(async (argv, app) => {
45
- const definition = parseDataArg(argv);
46
- const result = await app.sdk.createProcessFlowAction(argv.scope, definition);
50
+ const guided = !hasDefinitionData(argv) ? await guidedActionCreate(argv, app) : undefined;
51
+ const definition = guided?.definition ?? parseDataArg(argv);
52
+ const scope = guided?.scope ?? await designerScope(argv, app);
53
+ const result = await app.sdk.createProcessFlowAction(scope, definition);
47
54
  app.ok(result, { summary: `Created and verified action ${result.sys_id}` });
48
55
  }),
49
56
  })
50
57
  .command({
51
- command: 'definition <identifier>', describe: 'Read full JSON definition including executable step_instances',
58
+ command: 'definition [identifier]', describe: 'Read full JSON definition including executable step_instances',
52
59
  builder: scopeOption,
53
60
  handler: run(async (argv, app) => {
54
61
  const id = await actionID(argv, app);
62
+ if (id === undefined) return;
55
63
  const scope = await actionScope(app.sdk, id, argv.scope);
56
64
  app.ok(await app.sdk.getProcessFlowAction(id, scope), { summary: `Read action definition ${id}` });
57
65
  }),
58
66
  })
59
67
  .command({
60
- command: 'update <identifier>', aliases: ['edit'], describe: 'Save a full definition through Process Flow PUT and verify readback',
68
+ command: 'update [identifier]', aliases: ['edit'], describe: 'Save a full definition through Process Flow PUT and verify readback',
61
69
  builder: yy => dataOptions(scopeOption(yy)),
62
70
  handler: run(async (argv, app) => {
63
- const definition = parseDataArg(argv);
71
+ if (!hasDefinitionData(argv) && !interactiveDesigner(argv, app)) throw errUsage('Provide --data-file or --data for the full action definition in noninteractive mode');
64
72
  const id = await actionID(argv, app);
73
+ if (id === undefined) return;
74
+ const definition = hasDefinitionData(argv) ? parseDataArg(argv)
75
+ : await guidedActionEdit(argv, app, await app.sdk.getProcessFlowAction(id, await actionScope(app.sdk, id, argv.scope)));
65
76
  const result = await app.sdk.updateProcessFlowAction(id, argv.scope, definition);
66
77
  app.ok(result, { summary: `Saved and verified action ${id}` });
67
78
  }),
68
79
  })
69
80
  .command({
70
- command: 'test <identifier>', describe: 'Execute saved or supplied definition; optionally wait for actual outputs',
81
+ command: 'test [identifier]', describe: 'Execute saved or supplied definition; optionally wait for actual outputs',
71
82
  builder: yy => dataOptions(scopeOption(yy))
72
83
  .option('force', { type: 'boolean', default: false, describe: 'Confirm execution of action scripts, which may have side effects' })
73
- .option('output-map', { type: 'string', default: '{}', describe: 'JSON action input names to values; sent as outputMap' })
84
+ .option('output-map', { type: 'string', describe: 'JSON action input names to values; sent as outputMap' })
74
85
  .option('run-on-thread', { type: 'boolean', default: true, describe: 'Run test on a worker thread' })
75
86
  .option('tracing-enabled', { type: 'boolean', default: false, describe: 'Enable Flow Designer tracing' })
76
87
  .option('wait', { type: 'boolean', default: false, describe: 'Wait for COMPLETE context and actual runtime outputs' })
@@ -79,9 +90,14 @@ export const actionsCmd = wrap => {
79
90
  let outputMap;
80
91
  try { outputMap = JSON.parse(argv['output-map'] ?? '{}'); }
81
92
  catch { throw errUsage('--output-map must be valid JSON'); }
82
- const definition = argv['data-file'] || argv.data ? parseDataArg(argv) : undefined;
93
+ let definition = hasDefinitionData(argv) ? parseDataArg(argv) : undefined;
83
94
  const id = await actionID(argv, app);
84
- await confirmDelete(app, argv, `Execute action '${argv.identifier}' scripts, which may have side effects`);
95
+ if (id === undefined) return;
96
+ if (argv['output-map'] === undefined && interactiveDesigner(argv, app)) {
97
+ definition = await app.sdk.getProcessFlowAction(id, await actionScope(app.sdk, id, argv.scope));
98
+ outputMap = await guidedActionTestInputs(argv, app, definition);
99
+ }
100
+ await confirmDesignerMutation(argv, app, `Execute action '${id}' scripts, which may have side effects`);
85
101
  const result = await app.sdk.testProcessFlowAction(id, argv.scope, definition, outputMap, {
86
102
  wait: argv.wait, timeout: argv.timeout, runOnThread: argv['run-on-thread'], tracingEnabled: argv['tracing-enabled'],
87
103
  });
@@ -90,19 +106,20 @@ export const actionsCmd = wrap => {
90
106
  })
91
107
  .command({
92
108
  command: 'step-types', describe: 'Read step schema templates for a target scope',
93
- builder: yy => yy.option('scope', { type: 'string', demandOption: true, describe: 'Exact transaction scope sys_id, or global' }),
109
+ builder: yy => yy.option('scope', { type: 'string', describe: 'Exact transaction scope sys_id, or global' }),
94
110
  handler: run(async (argv, app) => {
95
- await validateScope(app.sdk, argv.scope);
96
- app.ok(await app.sdk.getProcessFlowStepTypes(argv.scope), { summary: 'Process Flow step schemas' });
111
+ const scope = await designerScope(argv, app);
112
+ app.ok(await app.sdk.getProcessFlowStepTypes(scope), { summary: 'Process Flow step schemas' });
97
113
  }),
98
114
  })
99
115
  .command({
100
- command: 'delete <identifier>', describe: 'Delete action parent explicitly',
116
+ command: 'delete [identifier]', describe: 'Delete action parent explicitly',
101
117
  builder: yy => scopeOption(yy).option('force', { type: 'boolean', default: false, describe: 'Skip confirmation' }),
102
118
  handler: run(async (argv, app) => {
103
119
  const id = await actionID(argv, app);
120
+ if (id === undefined) return;
104
121
  await actionScope(app.sdk, id, argv.scope);
105
- await confirmDelete(app, argv, `Delete action '${argv.identifier}'`);
122
+ await confirmDesignerMutation(argv, app, `Delete action '${id}'`);
106
123
  await app.sdk.delete(ACTION_TABLE, id);
107
124
  // Verify the exact target is absent. Do not interpret a 403 as deletion.
108
125
  const query = new URLSearchParams({ sysparm_query: `sys_id=${id}`, sysparm_limit: '1', sysparm_fields: 'sys_id', sysparm_display_value: 'false' });
@@ -115,7 +132,7 @@ export const actionsCmd = wrap => {
115
132
  }),
116
133
  handler: wrap(async (_argv, app) => {
117
134
  app.ok({ commands: ['list', 'show', 'definition', 'step-types', 'create', 'update', 'test', 'delete'] }, {
118
- summary: 'Use jsn actions <command> --help. Create/update require full JSON definitions, not table fields.',
135
+ summary: 'Use jsn actions <command> --help. Create/update guide terminal users; automation uses full JSON definitions, not table fields.',
119
136
  });
120
137
  }),
121
138
  };
@@ -1,5 +1,5 @@
1
1
  import fs from 'node:fs';
2
- import { formatRecordForDisplay, getStringField, interactiveList, confirmDelete } from '../helpers.js';
2
+ import { formatRecordForDisplay, getStringField, interactiveList } from '../helpers.js';
3
3
  import { discoverFlowContextFields, normalizeFlowContext, summarizeFlowContexts, formatFlowContextSummary, aggregateFlowContextMappings, mergeFlowContextStats, buildFlowContextQuery } from '../flow-context.js';
4
4
  import { declareCapabilities } from '../capabilities.js';
5
5
  import { inspectFlow } from '../flow-inspection.js';
@@ -7,6 +7,10 @@ import { publishFlows, publishFlowLifecycle, publishDoctor, flowStatus } from '.
7
7
  import { resolveRecord, unwrapSysId } from '../resolve-record.js';
8
8
  import { createFlowFromManifest, updateFlowFromManifest, readCurrentUpdateSet } from '../flow-create.js';
9
9
  import { testFlow } from '../flow-test.js';
10
+ import { errUsage } from '../errors.js';
11
+ import { cancelled, pickDesignerRecord, guidedFlowCreate, guidedFlowEdit, guidedFlowTestRecord, hasDefinitionData, confirmDesignerMutation } from '../flow-designer-interactive.js';
12
+
13
+ const pickFlow = (argv, app, query = '') => pickDesignerRecord(argv, app, argv.action ? 'sys_hub_action_type_definition' : 'sys_hub_flow', argv.action ? 'action' : 'flow', query);
10
14
 
11
15
  function flowInspectionAdapter(app) {
12
16
  return {
@@ -15,7 +19,7 @@ function flowInspectionAdapter(app) {
15
19
  };
16
20
  }
17
21
 
18
- declareCapabilities('flows', { mutationSubcommands: ['create', 'update', 'delete', 'publish', 'test'] });
22
+ declareCapabilities('flows', { mutationSubcommands: ['create', 'update', 'edit', 'delete', 'publish', 'test'] });
19
23
 
20
24
  function renderChecks(checks) {
21
25
  return checks.map((c) => {
@@ -25,6 +29,8 @@ function renderChecks(checks) {
25
29
  }
26
30
 
27
31
  export function flowsCmd(wrap) {
32
+ const originalWrap = wrap;
33
+ wrap = handler => originalWrap((argv, app) => cancelled(() => handler(argv, app)));
28
34
  return {
29
35
  command: 'flows [subcommand]',
30
36
  aliases: ['flow'],
@@ -143,7 +149,7 @@ export function flowsCmd(wrap) {
143
149
  }),
144
150
  })
145
151
  .command({
146
- command: 'show <identifier>',
152
+ command: 'show [identifier]',
147
153
  aliases: ['get'],
148
154
  describe: 'Show flow details by name or sys_id',
149
155
  builder: (y) => y
@@ -153,9 +159,11 @@ export function flowsCmd(wrap) {
153
159
  describe: 'Expand nested subflows to this depth (1 = subflow names only, 2 = one level, 3+ = deeper)',
154
160
  }),
155
161
  handler: wrap(async (argv, app) => {
162
+ const identifier = await pickFlow(argv, app);
163
+ if (identifier === undefined) return;
156
164
  const inspection = await inspectFlow({
157
165
  adapter: flowInspectionAdapter(app),
158
- identifier: argv.identifier,
166
+ identifier,
159
167
  instanceURL: app.getEffectiveInstance(),
160
168
  depth: argv.depth,
161
169
  });
@@ -168,20 +176,24 @@ export function flowsCmd(wrap) {
168
176
  }),
169
177
  })
170
178
  .command({
171
- command: 'publish <identifier>',
179
+ command: 'publish [identifier]',
172
180
  aliases: ['activate'],
173
181
  describe: 'Publish (activate) a flow so it can actually run',
174
182
  builder: (y) => y
175
- .option('action', { type: 'boolean', default: false, describe: 'Treat the identifier as a custom action rather than a flow' }),
183
+ .option('action', { type: 'boolean', default: false, describe: 'Treat the identifier as a custom action rather than a flow' })
184
+ .option('force', { type: 'boolean', default: false, describe: 'Confirm publication and activation' }),
176
185
  handler: wrap(async (argv, app) => {
177
186
  app.requireInstance();
187
+ const identifier = await pickFlow(argv, app);
188
+ if (identifier === undefined) return;
178
189
  const sysID = unwrapSysId(await resolveRecord(app.sdk, {
179
190
  table: argv.action ? 'sys_hub_action_type_definition' : 'sys_hub_flow',
180
- identifier: argv.identifier,
191
+ identifier,
181
192
  matchField: 'name',
182
193
  resource: argv.action ? 'Custom action' : 'Flow',
183
194
  }));
184
195
 
196
+ await confirmDesignerMutation(argv, app, `Publish and activate ${argv.action ? 'action' : 'flow'} ${sysID}?`);
185
197
  if (!argv.action) {
186
198
  const lifecycle = await publishFlowLifecycle(app.sdk, app.getEffectiveInstance(), sysID);
187
199
  return app.ok(lifecycle, {
@@ -201,12 +213,14 @@ export function flowsCmd(wrap) {
201
213
  }),
202
214
  })
203
215
  .command({
204
- command: 'status <identifier>',
216
+ command: 'status [identifier]',
205
217
  describe: 'Check whether a flow is actually able to run',
206
218
  handler: wrap(async (argv, app) => {
207
219
  app.requireInstance();
220
+ const identifier = await pickFlow(argv, app);
221
+ if (identifier === undefined) return;
208
222
  const sysID = unwrapSysId(await resolveRecord(app.sdk, {
209
- table: 'sys_hub_flow', identifier: argv.identifier, matchField: 'name', resource: 'Flow',
223
+ table: 'sys_hub_flow', identifier, matchField: 'name', resource: 'Flow',
210
224
  }));
211
225
  const status = await flowStatus(app.sdk, sysID);
212
226
 
@@ -217,15 +231,20 @@ export function flowsCmd(wrap) {
217
231
  }),
218
232
  })
219
233
  .command({
220
- command: 'test <identifier>',
234
+ command: 'test [identifier]',
221
235
  describe: 'Execute a record-triggered flow against one record (may cause side effects)',
222
236
  builder: (y) => y
223
- .option('record', { type: 'string', demandOption: true, describe: 'Trigger record sys_id to test against' })
237
+ .option('record', { type: 'string', describe: 'Trigger record sys_id to test against' })
224
238
  .option('force', { type: 'boolean', default: false, describe: 'Skip confirmation' }),
225
239
  handler: wrap(async (argv, app) => {
226
240
  app.requireInstance();
227
- await confirmDelete(app, argv, `Execute flow test for ${argv.identifier} against record ${argv.record}; actions may cause side effects`);
228
- const result = await testFlow(app.sdk, app.getEffectiveInstance(), argv.identifier, argv.record);
241
+ const identifier = await pickFlow(argv, app);
242
+ if (identifier === undefined) return;
243
+ argv = { ...argv, identifier };
244
+ const record = await guidedFlowTestRecord(argv, app);
245
+ if (record === undefined) return;
246
+ await confirmDesignerMutation(argv, app, `Execute flow test for ${identifier} against record ${record}; actions may cause side effects`);
247
+ const result = await testFlow(app.sdk, app.getEffectiveInstance(), identifier, record);
229
248
  return app.ok(result, {
230
249
  summary: result.status === 'completed'
231
250
  ? `Flow test completed (${result.success ? 'success' : 'failed'})`
@@ -249,16 +268,16 @@ export function flowsCmd(wrap) {
249
268
  })
250
269
  .command({
251
270
  command: 'create',
252
- describe: 'Create a draft flow from a schema-driven JSON manifest',
271
+ describe: 'Guide draft flow creation in a terminal, or use a schema-driven JSON manifest',
253
272
  builder: (y) => y
254
273
  .option('data', { type: 'string', describe: 'Flow manifest as inline JSON' })
255
274
  .option('data-file', { type: 'string', describe: 'Read the flow manifest from a JSON file' }),
256
275
  handler: wrap(async (argv, app) => {
257
276
  app.requireInstance();
258
277
  const dataFile = argv.dataFile ?? argv['data-file'];
259
- if (!dataFile && argv.data === undefined) throw new Error('Provide --data-file or --data for the flow manifest');
260
278
  let manifest;
261
- try {
279
+ if (!hasDefinitionData(argv)) manifest = await guidedFlowCreate(argv, app);
280
+ else try {
262
281
  const raw = dataFile ? fs.readFileSync(dataFile, 'utf8') : argv.data;
263
282
  manifest = JSON.parse(raw.charCodeAt?.(0) === 0xFEFF ? raw.slice(1) : raw);
264
283
  } catch (error) {
@@ -274,7 +293,8 @@ export function flowsCmd(wrap) {
274
293
  }),
275
294
  })
276
295
  .command({
277
- command: 'update <identifier>',
296
+ command: 'update [identifier]',
297
+ aliases: ['edit'],
278
298
  describe: 'Safely update supported fields of an existing inactive Draft flow',
279
299
  builder: (y) => y
280
300
  .option('data', { type: 'string', describe: 'Update manifest as inline JSON' })
@@ -283,15 +303,18 @@ export function flowsCmd(wrap) {
283
303
  handler: wrap(async (argv, app) => {
284
304
  app.requireInstance();
285
305
  const dataFile = argv.dataFile ?? argv['data-file'];
286
- if (!dataFile && argv.data === undefined) throw new Error('Provide --data-file or --data for the flow update manifest');
306
+ const identifier = await pickFlow(argv, app, !hasDefinitionData(argv) ? 'active=false^status=draft' : '');
307
+ if (identifier === undefined) return;
308
+ argv = { ...argv, identifier };
287
309
  let manifest;
288
- try {
310
+ if (!hasDefinitionData(argv)) manifest = await guidedFlowEdit(argv, app);
311
+ else try {
289
312
  const raw = dataFile ? fs.readFileSync(dataFile, 'utf8') : argv.data;
290
313
  manifest = JSON.parse(raw.charCodeAt?.(0) === 0xFEFF ? raw.slice(1) : raw);
291
314
  } catch (error) {
292
315
  throw new Error(`Invalid flow update manifest JSON: ${error.message}`, { cause: error });
293
316
  }
294
- await confirmDelete(app, argv, `Update draft flow ${argv.identifier}; unspecified components and inputs are preserved`);
317
+ await confirmDesignerMutation(argv, app, `Update draft flow ${identifier}; unspecified components and inputs are preserved`);
295
318
  const result = await updateFlowFromManifest(app.sdk, app.getEffectiveInstance(), argv.identifier, manifest, {
296
319
  readUpdateSet: () => readCurrentUpdateSet(app.sdk),
297
320
  });
@@ -302,34 +325,20 @@ export function flowsCmd(wrap) {
302
325
  }),
303
326
  })
304
327
  .command({
305
- command: 'delete <identifier>',
328
+ command: 'delete [identifier]',
306
329
  describe: 'Delete a flow (not yet implemented)',
307
- handler: wrap(async (_argv, _app) => {
308
- throw new Error('Flow deletion requires the Flow Designer GraphQL API - not yet implemented.\n'
330
+ handler: wrap(async (argv, app) => {
331
+ const identifier = await pickFlow(argv, app);
332
+ if (identifier === undefined) return;
333
+ throw errUsage('Flow deletion requires the Flow Designer GraphQL API - not yet implemented.\n'
309
334
  + 'Use the ServiceNow web UI to delete flows.');
310
335
  }),
311
336
  });
312
337
  },
313
- handler: () => {
314
- console.log('Manage Flow Designer flows from the sys_hub_flow table.');
315
- console.log('');
316
- console.log('Available subcommands:');
317
- console.log(' list List flows');
318
- console.log(' executions Show flow executions from sys_flow_context');
319
- console.log(' show <identifier> Show flow details by name or sys_id');
320
- console.log(' status <identifier> Check whether a flow is actually able to run');
321
- console.log(' publish <identifier> Publish (activate) a flow');
322
- console.log(' test <identifier> Execute a record-triggered flow against one record');
323
- console.log(' doctor Check whether this instance can publish flows');
324
- console.log(' create Create a draft flow from a JSON manifest');
325
- console.log(' update <identifier> Safely update supported fields of an existing Draft flow');
326
- console.log(' delete <identifier> Delete a flow (not yet implemented)');
327
- console.log('');
328
- console.log('Run "jsn flows <command> --help" for details.');
329
- console.log('');
330
- console.log('Note: Flow structure comes from the ProcessFlow API (the Flow');
331
- console.log('Designer UI source), so V1, V2, and subflow definitions all');
332
- console.log('render with full action inputs and conditions.');
333
- },
338
+ handler: wrap(async (_argv, app) => {
339
+ app.ok({ commands: ['list', 'executions', 'show', 'status', 'publish', 'test', 'doctor', 'create', 'update', 'delete'] }, {
340
+ summary: 'Use jsn flows <command> --help. Human terminals can omit targets and guide create/edit; automation uses explicit identifiers and manifests.',
341
+ });
342
+ }),
334
343
  };
335
344
  }