@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 +69 -3
- package/package.json +1 -1
- package/src/commands/_generic.js +3 -2
- package/src/commands/actions.js +36 -19
- package/src/commands/flows.js +53 -44
- package/src/commands/midservers.js +105 -23
- package/src/errors.js +5 -0
- package/src/flow-create.js +6 -5
- package/src/flow-designer-interactive.js +327 -0
- package/src/midserver-direct.js +252 -0
- package/src/midserver-zip.js +107 -0
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.
|
|
101
|
-
|
|
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
|
|
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
package/src/commands/_generic.js
CHANGED
|
@@ -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)
|
package/src/commands/actions.js
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { buildDevCmd } from './_generic.js';
|
|
2
|
-
import { parseDataArg
|
|
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
|
|
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',
|
|
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
|
|
46
|
-
const
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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',
|
|
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
|
-
|
|
93
|
+
let definition = hasDefinitionData(argv) ? parseDataArg(argv) : undefined;
|
|
83
94
|
const id = await actionID(argv, app);
|
|
84
|
-
|
|
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',
|
|
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
|
|
96
|
-
app.ok(await app.sdk.getProcessFlowStepTypes(
|
|
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
|
|
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
|
|
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
|
|
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
|
};
|
package/src/commands/flows.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import { formatRecordForDisplay, getStringField, interactiveList
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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',
|
|
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
|
-
|
|
228
|
-
|
|
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: '
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
328
|
+
command: 'delete [identifier]',
|
|
306
329
|
describe: 'Delete a flow (not yet implemented)',
|
|
307
|
-
handler: wrap(async (
|
|
308
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
}
|