@tenonhq/dovetail-servicenow 0.0.7 → 0.0.9
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 +52 -3
- package/dist/cli.js +251 -0
- package/dist/flowDesigner/buildFlowOrchestrator.d.ts +9 -0
- package/dist/flowDesigner/buildFlowOrchestrator.js +52 -19
- package/dist/flowDesigner/copyFlow.d.ts +48 -0
- package/dist/flowDesigner/copyFlow.js +106 -0
- package/dist/flowDesigner/editFlow.d.ts +71 -0
- package/dist/flowDesigner/editFlow.js +186 -0
- package/dist/flowDesigner/index.d.ts +12 -0
- package/dist/flowDesigner/index.js +14 -1
- package/dist/flowDesigner/publishFlow.d.ts +50 -0
- package/dist/flowDesigner/publishFlow.js +96 -0
- package/dist/flowDesigner/readActionType.d.ts +45 -0
- package/dist/flowDesigner/readActionType.js +84 -0
- package/dist/flowDesigner/readFlow.d.ts +75 -0
- package/dist/flowDesigner/readFlow.js +154 -0
- package/dist/flowDesigner/testFlow.d.ts +50 -0
- package/dist/flowDesigner/testFlow.js +113 -0
- package/dist/flowDesigner-formatter.d.ts +6 -0
- package/dist/flowDesigner-formatter.js +58 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +8 -1
- package/dist/mcp/registry.d.ts +1 -1
- package/dist/mcp/registry.js +102 -1
- package/dist/mcp/schemas.d.ts +166 -0
- package/dist/mcp/schemas.js +45 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -154,8 +154,53 @@ npx dove-sn set-related-lists \
|
|
|
154
154
|
--table x_cadso_automate_audience \
|
|
155
155
|
--related-lists "x_cadso_automate_audience_member.audience" \
|
|
156
156
|
--update-set 0083c3bb33d003507b18bc534d5c7b6d
|
|
157
|
+
|
|
158
|
+
# View a flow / subflow's compiled step graph (read-only, headless)
|
|
159
|
+
npx dove-sn view-flow --sys-id 327c53bfc33e3250d4ddf1db05013135
|
|
160
|
+
npx dove-sn view-flow --sys-id <sys_id> --json --raw # structured + full model
|
|
161
|
+
|
|
162
|
+
# View a Custom Action Type's model (inputs/outputs)
|
|
163
|
+
npx dove-sn view-action --sys-id <action_type_sys_id> --scope <scope_sys_id>
|
|
164
|
+
|
|
165
|
+
# Copy a flow / subflow (creates an INACTIVE DRAFT) via the Designer's Copy endpoint
|
|
166
|
+
npx dove-sn copy-flow --sys-id <sys_id> --name "My Copy" # scope defaults to source's
|
|
167
|
+
|
|
168
|
+
# Publish (compile the snapshot of) a flow / subflow after editing in the Designer
|
|
169
|
+
npx dove-sn publish-flow --sys-id <sys_id> # scope defaults to the flow's
|
|
170
|
+
|
|
171
|
+
# Test a flow: validate (default, read-only) or actually run it
|
|
172
|
+
npx dove-sn test-flow --sys-id <sys_id> --inputs '{"phone":"+1555..."}'
|
|
173
|
+
npx dove-sn test-flow --sys-id <sys_id> --execute --confirm --inputs '{...}' # runs it
|
|
174
|
+
|
|
175
|
+
# Edit a flow in place (rename / description / step inputs)
|
|
176
|
+
echo '{"rename":{"name":"New Name"},"patchStepInputs":[{"step":"Calculate SMS Send At","input":"send_rate","value":"5"}]}' > ops.json
|
|
177
|
+
npx dove-sn edit-flow --sys-id <sys_id> --from-json ops.json # dry-run (diff)
|
|
178
|
+
npx dove-sn edit-flow --sys-id <sys_id> --from-json ops.json --apply --update-set <id> # persist
|
|
157
179
|
```
|
|
158
180
|
|
|
181
|
+
`copy-flow` calls the Designer's own `POST /processflow/flow/{id}/copy` — a
|
|
182
|
+
complete, faithful clone created as an **inactive draft**. (Don't publish +
|
|
183
|
+
activate a copy of a triggered production flow unless you intend it to fire.)
|
|
184
|
+
`test-flow` defaults to **validate** — a safe pre-flight (published? inputs match
|
|
185
|
+
declared variables?) that never runs the flow; `--execute --confirm` runs it via
|
|
186
|
+
the server-side FlowAPI runner (deploy `resources/runFlow.md` first).
|
|
187
|
+
|
|
188
|
+
`edit-flow` defaults to a **dry-run** diff. With `--apply`: rename/description are
|
|
189
|
+
written to `sys_hub_flow` through the update-set-aware API (so `--update-set` is
|
|
190
|
+
**required** for those), while `patchStepInputs` ride a snapshot recompile (the
|
|
191
|
+
`/snapshot` POST persists step input values but NOT top-level flow fields).
|
|
192
|
+
Step-input persistence via the snapshot POST is verified for action types but is
|
|
193
|
+
**best-effort for flows** — after the recompile, `edit-flow` reads the model back
|
|
194
|
+
and **warns** if a value didn't actually persist, so a no-op never reports as a
|
|
195
|
+
silent success.
|
|
196
|
+
|
|
197
|
+
`view-flow` reads `GET /api/now/processflow/flow/{id}` — the Designer's own model
|
|
198
|
+
endpoint — and prints the ordered, nesting-aware action + flow-logic step graph
|
|
199
|
+
plus the flow variables. This works for the integration user with plain basic
|
|
200
|
+
auth; the raw `sys_hub_flow_snapshot` Table API 404 is a row-level restriction on
|
|
201
|
+
the working snapshot, not a barrier. `publish-flow` POSTs the model back to
|
|
202
|
+
`.../flow/{id}/snapshot`, recompiling the current design (a write).
|
|
203
|
+
|
|
159
204
|
`set-form-layout` JSON payload shape:
|
|
160
205
|
|
|
161
206
|
```json
|
|
@@ -193,10 +238,14 @@ console.log(formatLayoutResult("form layout", result));
|
|
|
193
238
|
|
|
194
239
|
## MCP server
|
|
195
240
|
|
|
196
|
-
`dove-sn mcp` runs a self-contained MCP stdio server exposing the
|
|
241
|
+
`dove-sn mcp` runs a self-contained MCP stdio server exposing the tools to
|
|
197
242
|
Claude Code and agents: `create_view`, `set_list_layout`, `set_form_layout`,
|
|
198
|
-
`set_related_lists`,
|
|
199
|
-
|
|
243
|
+
`set_related_lists`, `add_choices_to_field`, plus the Flow Designer tools
|
|
244
|
+
`flow_view` (read a flow/subflow's step graph), `action_view` (read an action
|
|
245
|
+
type's model), `flow_publish` (compile a flow/subflow snapshot), `flow_copy`
|
|
246
|
+
(copy a flow as an inactive draft), `flow_test` (validate or run a flow), and
|
|
247
|
+
`flow_edit` (patch a flow). It reads ServiceNow credentials from the same env
|
|
248
|
+
vars as the CLI.
|
|
200
249
|
|
|
201
250
|
```bash
|
|
202
251
|
npx dove-sn mcp --smoke # list the registered tools and exit
|
package/dist/cli.js
CHANGED
|
@@ -68,6 +68,13 @@ const formatter_2 = require("./layout/formatter");
|
|
|
68
68
|
const server_1 = require("./mcp/server");
|
|
69
69
|
const buildFlowOrchestrator_1 = require("./flowDesigner/buildFlowOrchestrator");
|
|
70
70
|
const flowDesigner_formatter_1 = require("./flowDesigner-formatter");
|
|
71
|
+
const readFlow_1 = require("./flowDesigner/readFlow");
|
|
72
|
+
const readActionType_1 = require("./flowDesigner/readActionType");
|
|
73
|
+
const publishFlow_1 = require("./flowDesigner/publishFlow");
|
|
74
|
+
const copyFlow_1 = require("./flowDesigner/copyFlow");
|
|
75
|
+
const editFlow_1 = require("./flowDesigner/editFlow");
|
|
76
|
+
const testFlow_1 = require("./flowDesigner/testFlow");
|
|
77
|
+
const flowDesigner_formatter_2 = require("./flowDesigner-formatter");
|
|
71
78
|
function parseArgs(argv) {
|
|
72
79
|
var command = argv[0] || "";
|
|
73
80
|
var flags = {};
|
|
@@ -299,6 +306,220 @@ async function runSetRelatedLists(flags) {
|
|
|
299
306
|
* dove-sn mcp — run the MCP stdio server. With --smoke, list the registered
|
|
300
307
|
* tools and exit. Otherwise the process stays alive until the transport closes.
|
|
301
308
|
*/
|
|
309
|
+
/**
|
|
310
|
+
* dove-sn view-flow:
|
|
311
|
+
* --sys-id <sys_id> Required. sys_hub_flow sys_id (flow or subflow).
|
|
312
|
+
* --json Optional. Emit the structured ReadFlowResult.
|
|
313
|
+
* --raw Optional (with --json). Include the full processflow model.
|
|
314
|
+
*
|
|
315
|
+
* Reads the compiled flow headlessly via GET /api/now/processflow/flow/{id} and
|
|
316
|
+
* prints the ordered, nesting-aware step graph + flow variables. Read-only.
|
|
317
|
+
*/
|
|
318
|
+
async function runViewFlow(flags) {
|
|
319
|
+
var sysId = flags["sys-id"] || flags.sysId;
|
|
320
|
+
if (!sysId) {
|
|
321
|
+
process.stderr.write("view-flow: --sys-id <sys_id> is required\n");
|
|
322
|
+
return 1;
|
|
323
|
+
}
|
|
324
|
+
var client = (0, client_1.createClient)({});
|
|
325
|
+
var result = await (0, readFlow_1.readFlow)({ client: client, sysId: sysId, raw: flags.raw === "true" });
|
|
326
|
+
if (flags.json === "true") {
|
|
327
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
328
|
+
return 0;
|
|
329
|
+
}
|
|
330
|
+
process.stdout.write((0, flowDesigner_formatter_2.formatReadFlowResult)(result) + "\n");
|
|
331
|
+
return 0;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* dove-sn view-action:
|
|
335
|
+
* --sys-id <sys_id> Required. sys_hub_action_type_definition sys_id.
|
|
336
|
+
* --scope <sys_id> Required. Application scope (sysparm_transaction_scope).
|
|
337
|
+
* --json [--raw] Optional. Structured ReadActionTypeResult / full model.
|
|
338
|
+
*
|
|
339
|
+
* Reads a Custom Action Type's compiled model (identity, inputs, outputs). Read-only.
|
|
340
|
+
*/
|
|
341
|
+
async function runViewAction(flags) {
|
|
342
|
+
var sysId = flags["sys-id"] || flags.sysId;
|
|
343
|
+
var scope = flags.scope || flags.scopeSysId;
|
|
344
|
+
if (!sysId || !scope) {
|
|
345
|
+
process.stderr.write("view-action: --sys-id <sys_id> and --scope <sys_id> are required\n");
|
|
346
|
+
return 1;
|
|
347
|
+
}
|
|
348
|
+
var client = (0, client_1.createClient)({});
|
|
349
|
+
var result = await (0, readActionType_1.readActionType)({
|
|
350
|
+
client: client,
|
|
351
|
+
sysId: sysId,
|
|
352
|
+
scopeSysId: scope,
|
|
353
|
+
raw: flags.raw === "true"
|
|
354
|
+
});
|
|
355
|
+
if (flags.json === "true") {
|
|
356
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
357
|
+
return 0;
|
|
358
|
+
}
|
|
359
|
+
process.stdout.write((0, flowDesigner_formatter_2.formatReadActionTypeResult)(result) + "\n");
|
|
360
|
+
return 0;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* dove-sn publish-flow:
|
|
364
|
+
* --sys-id <sys_id> Required. sys_hub_flow sys_id (flow or subflow) to publish.
|
|
365
|
+
* --scope <sys_id> Optional. sysparm_transaction_scope (defaults to the model's scope).
|
|
366
|
+
* --json Optional. Emit the structured PublishFlowResult.
|
|
367
|
+
*
|
|
368
|
+
* Compiles the flow's snapshot via POST /api/now/processflow/flow/{id}/snapshot —
|
|
369
|
+
* a WRITE that recompiles the current design. Use the Designer to edit, then this
|
|
370
|
+
* to publish. (For edited content, the library publishFlow accepts a model.)
|
|
371
|
+
*/
|
|
372
|
+
async function runPublishFlow(flags) {
|
|
373
|
+
var sysId = flags["sys-id"] || flags.sysId;
|
|
374
|
+
if (!sysId) {
|
|
375
|
+
process.stderr.write("publish-flow: --sys-id <sys_id> is required\n");
|
|
376
|
+
return 1;
|
|
377
|
+
}
|
|
378
|
+
var params = {
|
|
379
|
+
client: (0, client_1.createClient)({}),
|
|
380
|
+
sysId: sysId
|
|
381
|
+
};
|
|
382
|
+
if (flags.scope || flags.scopeSysId) {
|
|
383
|
+
params.scopeSysId = flags.scope || flags.scopeSysId;
|
|
384
|
+
}
|
|
385
|
+
var result = await (0, publishFlow_1.publishFlow)(params);
|
|
386
|
+
if (flags.json === "true") {
|
|
387
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
388
|
+
return 0;
|
|
389
|
+
}
|
|
390
|
+
process.stdout.write("Published flow " + sysId + " (HTTP " + result.httpStatus + ")"
|
|
391
|
+
+ (result.snapshotSysId ? " — snapshot " + result.snapshotSysId : "") + "\n");
|
|
392
|
+
return 0;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* dove-sn copy-flow:
|
|
396
|
+
* --sys-id <sys_id> Required. Source sys_hub_flow sys_id (flow or subflow).
|
|
397
|
+
* --name <name> Required. Name for the copy.
|
|
398
|
+
* --scope <sys_id> Optional. Target scope (defaults to the source's scope).
|
|
399
|
+
* --json Optional. Emit the structured CopyFlowResult.
|
|
400
|
+
*
|
|
401
|
+
* Copies the flow via the Designer's own Copy endpoint — a complete, faithful
|
|
402
|
+
* clone created as an INACTIVE DRAFT. Publish it with publish-flow when ready.
|
|
403
|
+
* (Do NOT publish + activate a copy of a triggered production flow unless you
|
|
404
|
+
* intend it to fire.)
|
|
405
|
+
*/
|
|
406
|
+
async function runCopyFlow(flags) {
|
|
407
|
+
var sysId = flags["sys-id"] || flags.sysId;
|
|
408
|
+
var name = flags.name;
|
|
409
|
+
if (!sysId || !name) {
|
|
410
|
+
process.stderr.write("copy-flow: --sys-id <sys_id> and --name <name> are required\n");
|
|
411
|
+
return 1;
|
|
412
|
+
}
|
|
413
|
+
var params = { client: (0, client_1.createClient)({}), sourceSysId: sysId, newName: name };
|
|
414
|
+
if (flags.scope || flags.scopeSysId) {
|
|
415
|
+
params.scopeSysId = flags.scope || flags.scopeSysId;
|
|
416
|
+
}
|
|
417
|
+
var result = await (0, copyFlow_1.copyFlow)(params);
|
|
418
|
+
if (flags.json === "true") {
|
|
419
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
420
|
+
return 0;
|
|
421
|
+
}
|
|
422
|
+
process.stdout.write("Copied to '" + result.name + "' (sys_id " + result.sysId + ", scope " + result.scopeSysId
|
|
423
|
+
+ ") — inactive draft. Publish with: dove-sn publish-flow --sys-id " + result.sysId + "\n");
|
|
424
|
+
return 0;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* dove-sn test-flow:
|
|
428
|
+
* --sys-id <sys_id> Required. sys_hub_flow sys_id (flow or subflow).
|
|
429
|
+
* --execute Optional. Actually run it (default is validate-only).
|
|
430
|
+
* --confirm Required with --execute. A deliberate run-for-real gate.
|
|
431
|
+
* --inputs <json> Optional. JSON object of inputs (or --inputs-json <path>).
|
|
432
|
+
* --json Optional. Emit the structured TestFlowResult.
|
|
433
|
+
*
|
|
434
|
+
* Default (no --execute) is a safe pre-flight: published? readable? inputs match
|
|
435
|
+
* declared variables? --execute POSTs the FlowAPI runner endpoint (see
|
|
436
|
+
* resources/runFlow.md). Executing a flow can cause real side effects.
|
|
437
|
+
*/
|
|
438
|
+
async function runTestFlow(flags) {
|
|
439
|
+
var sysId = flags["sys-id"] || flags.sysId;
|
|
440
|
+
if (!sysId) {
|
|
441
|
+
process.stderr.write("test-flow: --sys-id <sys_id> is required\n");
|
|
442
|
+
return 1;
|
|
443
|
+
}
|
|
444
|
+
var inputs = {};
|
|
445
|
+
if (flags["inputs-json"]) {
|
|
446
|
+
inputs = JSON.parse(fs.readFileSync(flags["inputs-json"], "utf8"));
|
|
447
|
+
}
|
|
448
|
+
else if (flags.inputs) {
|
|
449
|
+
inputs = JSON.parse(flags.inputs);
|
|
450
|
+
}
|
|
451
|
+
var params = {
|
|
452
|
+
client: (0, client_1.createClient)({}),
|
|
453
|
+
sysId: sysId,
|
|
454
|
+
mode: flags.execute === "true" ? "execute" : "validate",
|
|
455
|
+
inputs: inputs,
|
|
456
|
+
confirm: flags.confirm === "true"
|
|
457
|
+
};
|
|
458
|
+
if (flags.runner) {
|
|
459
|
+
params.runnerPath = flags.runner;
|
|
460
|
+
}
|
|
461
|
+
var result = await (0, testFlow_1.testFlow)(params);
|
|
462
|
+
if (flags.json === "true") {
|
|
463
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
464
|
+
return 0;
|
|
465
|
+
}
|
|
466
|
+
process.stdout.write("[" + result.mode + "] ok=" + result.ok + "\n");
|
|
467
|
+
for (var i = 0; i < result.notes.length; i += 1) {
|
|
468
|
+
process.stdout.write(" " + result.notes[i] + "\n");
|
|
469
|
+
}
|
|
470
|
+
return result.ok ? 0 : 2;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* dove-sn edit-flow:
|
|
474
|
+
* --sys-id <sys_id> Required. sys_hub_flow sys_id (flow or subflow).
|
|
475
|
+
* --from-json <path> Required. JSON EditFlowOps { rename?, description?, patchStepInputs? }.
|
|
476
|
+
* --apply Optional. Persist the edit (default is a dry-run diff).
|
|
477
|
+
* --scope <sys_id> Optional. sysparm_transaction_scope for the publish.
|
|
478
|
+
* --update-set <sys_id> Required with --apply when ops include rename/description.
|
|
479
|
+
* --json Optional. Emit the structured EditFlowResult.
|
|
480
|
+
*
|
|
481
|
+
* Reads the model, applies the declarative edits, and (with --apply) persists them:
|
|
482
|
+
* rename/description via the update-set-aware record write, step inputs via a
|
|
483
|
+
* snapshot recompile. Without --apply it prints the would-be changes.
|
|
484
|
+
*/
|
|
485
|
+
async function runEditFlow(flags) {
|
|
486
|
+
var sysId = flags["sys-id"] || flags.sysId;
|
|
487
|
+
if (!sysId) {
|
|
488
|
+
process.stderr.write("edit-flow: --sys-id <sys_id> is required\n");
|
|
489
|
+
return 1;
|
|
490
|
+
}
|
|
491
|
+
if (!flags["from-json"]) {
|
|
492
|
+
process.stderr.write("edit-flow: --from-json <path> (EditFlowOps) is required\n");
|
|
493
|
+
return 1;
|
|
494
|
+
}
|
|
495
|
+
var ops = JSON.parse(fs.readFileSync(flags["from-json"], "utf8"));
|
|
496
|
+
var params = {
|
|
497
|
+
client: (0, client_1.createClient)({}),
|
|
498
|
+
sysId: sysId,
|
|
499
|
+
ops: ops,
|
|
500
|
+
apply: flags.apply === "true"
|
|
501
|
+
};
|
|
502
|
+
if (flags.scope || flags.scopeSysId) {
|
|
503
|
+
params.scopeSysId = flags.scope || flags.scopeSysId;
|
|
504
|
+
}
|
|
505
|
+
if (flags["update-set"] || flags.updateSetSysId) {
|
|
506
|
+
params.updateSetSysId = flags["update-set"] || flags.updateSetSysId;
|
|
507
|
+
}
|
|
508
|
+
var result = await (0, editFlow_1.editFlow)(params);
|
|
509
|
+
if (flags.json === "true") {
|
|
510
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
511
|
+
return 0;
|
|
512
|
+
}
|
|
513
|
+
process.stdout.write("[" + result.status + "] " + result.changes.length + " change(s)"
|
|
514
|
+
+ (result.snapshotSysId ? " — snapshot " + result.snapshotSysId : "") + "\n");
|
|
515
|
+
for (var i = 0; i < result.changes.length; i += 1) {
|
|
516
|
+
process.stdout.write(" + " + result.changes[i] + "\n");
|
|
517
|
+
}
|
|
518
|
+
for (var w = 0; w < result.warnings.length; w += 1) {
|
|
519
|
+
process.stdout.write(" ! " + result.warnings[w] + "\n");
|
|
520
|
+
}
|
|
521
|
+
return 0;
|
|
522
|
+
}
|
|
302
523
|
async function runMcp(flags) {
|
|
303
524
|
if (flags.smoke === "true") {
|
|
304
525
|
await (0, server_1.runSmoke)();
|
|
@@ -324,6 +545,18 @@ function printHelp() {
|
|
|
324
545
|
" [--view <v>] [--scope <s>] [--prune false] [--dry-run] [--json])\n" +
|
|
325
546
|
" build-flow Author Custom Action Types and Subflows from a JSON spec\n" +
|
|
326
547
|
" (--from-json <path> [--update-set <sys_id>] [--dry-run] [--skip-publish] [--json])\n" +
|
|
548
|
+
" view-flow Read a flow/subflow's compiled step graph (read-only)\n" +
|
|
549
|
+
" (--sys-id <sys_id> [--json] [--raw])\n" +
|
|
550
|
+
" view-action Read a Custom Action Type's model — inputs/outputs (read-only)\n" +
|
|
551
|
+
" (--sys-id <sys_id> --scope <sys_id> [--json] [--raw])\n" +
|
|
552
|
+
" publish-flow Compile a flow/subflow snapshot (write)\n" +
|
|
553
|
+
" (--sys-id <sys_id> [--scope <sys_id>] [--json])\n" +
|
|
554
|
+
" copy-flow Copy a flow/subflow (inactive draft) via the Designer Copy API\n" +
|
|
555
|
+
" (--sys-id <sys_id> --name <name> [--scope <sys_id>] [--json])\n" +
|
|
556
|
+
" test-flow Validate (default) or run a flow/subflow\n" +
|
|
557
|
+
" (--sys-id <sys_id> [--execute --confirm] [--inputs <json>] [--json])\n" +
|
|
558
|
+
" edit-flow Patch a flow/subflow (rename, description, step inputs)\n" +
|
|
559
|
+
" (--sys-id <sys_id> --from-json <ops.json> [--apply] [--update-set <sys_id>] [--scope <sys_id>] [--json])\n" +
|
|
327
560
|
" mcp Run the MCP stdio server (--smoke lists tools and exits)\n");
|
|
328
561
|
}
|
|
329
562
|
async function main() {
|
|
@@ -335,6 +568,24 @@ async function main() {
|
|
|
335
568
|
if (parsed.command === "build-flow") {
|
|
336
569
|
return await runBuildFlowCmd(parsed.flags);
|
|
337
570
|
}
|
|
571
|
+
if (parsed.command === "view-flow") {
|
|
572
|
+
return await runViewFlow(parsed.flags);
|
|
573
|
+
}
|
|
574
|
+
if (parsed.command === "view-action") {
|
|
575
|
+
return await runViewAction(parsed.flags);
|
|
576
|
+
}
|
|
577
|
+
if (parsed.command === "publish-flow") {
|
|
578
|
+
return await runPublishFlow(parsed.flags);
|
|
579
|
+
}
|
|
580
|
+
if (parsed.command === "copy-flow") {
|
|
581
|
+
return await runCopyFlow(parsed.flags);
|
|
582
|
+
}
|
|
583
|
+
if (parsed.command === "test-flow") {
|
|
584
|
+
return await runTestFlow(parsed.flags);
|
|
585
|
+
}
|
|
586
|
+
if (parsed.command === "edit-flow") {
|
|
587
|
+
return await runEditFlow(parsed.flags);
|
|
588
|
+
}
|
|
338
589
|
if (parsed.command === "create-view") {
|
|
339
590
|
await runCreateView(parsed.flags);
|
|
340
591
|
return 0;
|
|
@@ -29,6 +29,15 @@ export interface BuildFlowOptions {
|
|
|
29
29
|
skipPublish?: boolean;
|
|
30
30
|
/** Forwarded to triggerPublication. Useful for tight tests; defaults to its own 15s. */
|
|
31
31
|
snapshotTimeoutMs?: number;
|
|
32
|
+
/**
|
|
33
|
+
* Steps fixture for publishing an action-type clone. The action-type model GET
|
|
34
|
+
* returns `steps: null`, so the real publisher (publishActionType) needs a
|
|
35
|
+
* steps array. When provided for kind="actionType", the orchestrator uses the
|
|
36
|
+
* real processflow publisher; without it, action-type publish falls back to
|
|
37
|
+
* the degraded triggerPublication. Ignored for subflows (publishFlow re-posts
|
|
38
|
+
* the full model, which already carries the instance graph).
|
|
39
|
+
*/
|
|
40
|
+
steps?: Array<Record<string, any>>;
|
|
32
41
|
}
|
|
33
42
|
/**
|
|
34
43
|
* Outcome categories. Maps directly to CLI exit codes:
|
|
@@ -14,6 +14,8 @@ const cloneSubflow_1 = require("./cloneSubflow");
|
|
|
14
14
|
const cloneActionType_1 = require("./cloneActionType");
|
|
15
15
|
const verifyArtifact_1 = require("./verifyArtifact");
|
|
16
16
|
const triggerPublication_1 = require("./triggerPublication");
|
|
17
|
+
const publishFlow_1 = require("./publishFlow");
|
|
18
|
+
const publishActionType_1 = require("./publishActionType");
|
|
17
19
|
const RX_SYS_ID = /^[0-9a-f]{32}$/;
|
|
18
20
|
function unrecoverable(spec, stage, message) {
|
|
19
21
|
return {
|
|
@@ -23,6 +25,50 @@ function unrecoverable(spec, stage, message) {
|
|
|
23
25
|
error: { stage: stage, message: message },
|
|
24
26
|
};
|
|
25
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Publish a freshly cloned artifact. Tries the real processflow publisher first
|
|
30
|
+
* (publishFlow for subflows, publishActionType for action types when a steps
|
|
31
|
+
* fixture is supplied), and falls back to the degraded triggerPublication if the
|
|
32
|
+
* real path is unavailable or throws. Never throws — always returns a
|
|
33
|
+
* TriggerPublicationResult-compatible object so the caller's outcome mapping is
|
|
34
|
+
* unchanged (status "published" => done; anything else => needs-ui-publish).
|
|
35
|
+
*/
|
|
36
|
+
async function publishArtifact(client, spec, sysId, opts) {
|
|
37
|
+
try {
|
|
38
|
+
if (spec.kind === "subflow") {
|
|
39
|
+
var pf = await (0, publishFlow_1.publishFlow)({ client: client, sysId: sysId, scopeSysId: spec.newScope });
|
|
40
|
+
return { status: "published", snapshotSysId: pf.snapshotSysId, pushSucceeded: true };
|
|
41
|
+
}
|
|
42
|
+
// actionType: the real publisher needs a steps fixture (model GET => steps:null).
|
|
43
|
+
if (spec.kind === "actionType" && opts.steps && opts.steps.length > 0) {
|
|
44
|
+
var pa = await (0, publishActionType_1.publishActionType)({
|
|
45
|
+
client: client,
|
|
46
|
+
sysId: sysId,
|
|
47
|
+
scopeSysId: spec.newScope,
|
|
48
|
+
steps: opts.steps,
|
|
49
|
+
});
|
|
50
|
+
return { status: "published", snapshotSysId: pa.snapshotSysId, pushSucceeded: true };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
// Real publish failed — fall through to the degraded path below, which is
|
|
55
|
+
// engineered to surface a UI publish URL rather than throw.
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
return await (0, triggerPublication_1.triggerPublication)({
|
|
59
|
+
client: client,
|
|
60
|
+
sysId: sysId,
|
|
61
|
+
kind: spec.kind,
|
|
62
|
+
updateSetSysId: spec.updateSetSysId,
|
|
63
|
+
snapshotTimeoutMs: opts.snapshotTimeoutMs,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
// triggerPublication is engineered to never throw on the happy path, but
|
|
68
|
+
// defensive: treat unexpected throws as needs-ui-publish.
|
|
69
|
+
return { status: "needs-ui-publish", pushSucceeded: false, uiPublishUrl: undefined };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
26
72
|
function validateSpec(spec) {
|
|
27
73
|
if (!spec || typeof spec !== "object")
|
|
28
74
|
throw new Error("spec must be a JSON object");
|
|
@@ -163,27 +209,14 @@ async function runBuildFlow(client, rawSpec, opts = {}) {
|
|
|
163
209
|
verify: verify,
|
|
164
210
|
};
|
|
165
211
|
}
|
|
166
|
-
// Phase 4: publication
|
|
212
|
+
// Phase 4: publication. Try the real processflow publisher first (compiles
|
|
213
|
+
// the snapshot via the Designer's own Publish endpoint — basic auth, 2xx),
|
|
214
|
+
// and fall back to the degraded triggerPublication (status flip + poll, UI
|
|
215
|
+
// fallback) only if the real path throws. This is what replaces the long-
|
|
216
|
+
// standing "needs-ui-publish" default for the common case.
|
|
167
217
|
var publish;
|
|
168
218
|
if (!opts.skipPublish) {
|
|
169
|
-
|
|
170
|
-
publish = await (0, triggerPublication_1.triggerPublication)({
|
|
171
|
-
client: client,
|
|
172
|
-
sysId: cloneResult.sysId,
|
|
173
|
-
kind: spec.kind,
|
|
174
|
-
updateSetSysId: spec.updateSetSysId,
|
|
175
|
-
snapshotTimeoutMs: opts.snapshotTimeoutMs,
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
catch (err) {
|
|
179
|
-
// triggerPublication is engineered to never throw on the happy path,
|
|
180
|
-
// but defensive: treat unexpected throws as needs-ui-publish.
|
|
181
|
-
publish = {
|
|
182
|
-
status: "needs-ui-publish",
|
|
183
|
-
pushSucceeded: false,
|
|
184
|
-
uiPublishUrl: undefined,
|
|
185
|
-
};
|
|
186
|
-
}
|
|
219
|
+
publish = await publishArtifact(client, spec, cloneResult.sysId, opts);
|
|
187
220
|
}
|
|
188
221
|
var artifact = {
|
|
189
222
|
sysId: cloneResult.sysId,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copy a Flow Designer flow or subflow — the Designer's own "Copy flow" action,
|
|
3
|
+
* headless.
|
|
4
|
+
*
|
|
5
|
+
* The Workflow Studio "Copy flow" button calls a clean REST endpoint (captured
|
|
6
|
+
* from a HAR, same family as the publish/snapshot endpoint), which works for the
|
|
7
|
+
* integration user with basic auth:
|
|
8
|
+
*
|
|
9
|
+
* POST /api/now/processflow/flow/{sourceSysId}/copy?sysparm_transaction_scope={scope}
|
|
10
|
+
* body: { name: "<new flow name>", scope: "<target scope sys_id>" }
|
|
11
|
+
* -> 200, { result: { data: "<new flow sys_id>" } } — the copy is a complete,
|
|
12
|
+
* faithful clone (trigger + all actions + variables + properties), created
|
|
13
|
+
* as an INACTIVE DRAFT.
|
|
14
|
+
*
|
|
15
|
+
* This supersedes the record-graph clone (cloneSubflow), which can't work for
|
|
16
|
+
* the integration user: the design tables (sys_hub_flow_input/action_instance)
|
|
17
|
+
* ignore sysparm_query on the plain Table API, and the build-agent endpoint that
|
|
18
|
+
* honors the filter returns 401 without the Build Agent role — so a record-graph
|
|
19
|
+
* read either 401s or scans the whole table. The platform copy endpoint sidesteps
|
|
20
|
+
* all of that: ServiceNow assembles the copy server-side.
|
|
21
|
+
*
|
|
22
|
+
* The copy lands in `scopeSysId` (sysparm_transaction_scope) — which may differ
|
|
23
|
+
* from the source's scope — as an inactive draft. Publish it with publishFlow
|
|
24
|
+
* when ready. Do NOT publish + activate a copy of a triggered production flow
|
|
25
|
+
* unless you intend it to fire (e.g. a Send-SMS flow would duplicate sends).
|
|
26
|
+
*/
|
|
27
|
+
import type { ServiceNowClient } from "../client";
|
|
28
|
+
export interface CopyFlowParams {
|
|
29
|
+
client: ServiceNowClient;
|
|
30
|
+
/** sys_id of the sys_hub_flow (flow or subflow) to copy. */
|
|
31
|
+
sourceSysId: string;
|
|
32
|
+
/** Name for the new copy. */
|
|
33
|
+
newName: string;
|
|
34
|
+
/**
|
|
35
|
+
* Target application scope sys_id (sysparm_transaction_scope) the copy is
|
|
36
|
+
* created in. Optional — defaults to the source flow's own scope (read first).
|
|
37
|
+
*/
|
|
38
|
+
scopeSysId?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface CopyFlowResult {
|
|
41
|
+
/** sys_id of the newly created copy. */
|
|
42
|
+
sysId: string;
|
|
43
|
+
/** Name of the copy (echoes newName). */
|
|
44
|
+
name: string;
|
|
45
|
+
/** The scope the copy was created in. */
|
|
46
|
+
scopeSysId: string;
|
|
47
|
+
}
|
|
48
|
+
export declare function copyFlow(params: CopyFlowParams): Promise<CopyFlowResult>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copy a Flow Designer flow or subflow — the Designer's own "Copy flow" action,
|
|
4
|
+
* headless.
|
|
5
|
+
*
|
|
6
|
+
* The Workflow Studio "Copy flow" button calls a clean REST endpoint (captured
|
|
7
|
+
* from a HAR, same family as the publish/snapshot endpoint), which works for the
|
|
8
|
+
* integration user with basic auth:
|
|
9
|
+
*
|
|
10
|
+
* POST /api/now/processflow/flow/{sourceSysId}/copy?sysparm_transaction_scope={scope}
|
|
11
|
+
* body: { name: "<new flow name>", scope: "<target scope sys_id>" }
|
|
12
|
+
* -> 200, { result: { data: "<new flow sys_id>" } } — the copy is a complete,
|
|
13
|
+
* faithful clone (trigger + all actions + variables + properties), created
|
|
14
|
+
* as an INACTIVE DRAFT.
|
|
15
|
+
*
|
|
16
|
+
* This supersedes the record-graph clone (cloneSubflow), which can't work for
|
|
17
|
+
* the integration user: the design tables (sys_hub_flow_input/action_instance)
|
|
18
|
+
* ignore sysparm_query on the plain Table API, and the build-agent endpoint that
|
|
19
|
+
* honors the filter returns 401 without the Build Agent role — so a record-graph
|
|
20
|
+
* read either 401s or scans the whole table. The platform copy endpoint sidesteps
|
|
21
|
+
* all of that: ServiceNow assembles the copy server-side.
|
|
22
|
+
*
|
|
23
|
+
* The copy lands in `scopeSysId` (sysparm_transaction_scope) — which may differ
|
|
24
|
+
* from the source's scope — as an inactive draft. Publish it with publishFlow
|
|
25
|
+
* when ready. Do NOT publish + activate a copy of a triggered production flow
|
|
26
|
+
* unless you intend it to fire (e.g. a Send-SMS flow would duplicate sends).
|
|
27
|
+
*/
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.copyFlow = copyFlow;
|
|
30
|
+
/**
|
|
31
|
+
* Pull the new flow's sys_id from the copy response. The endpoint returns the
|
|
32
|
+
* new sys_id as a bare string under `result.data`; older/alternate shapes may
|
|
33
|
+
* nest it under a model object's `id`/`sys_id`.
|
|
34
|
+
*/
|
|
35
|
+
function extractCopiedSysId(resp) {
|
|
36
|
+
if (resp && typeof resp === "object" && resp.result && typeof resp.result === "object") {
|
|
37
|
+
var d = resp.result.data;
|
|
38
|
+
if (typeof d === "string" && d.length >= 32) {
|
|
39
|
+
return d;
|
|
40
|
+
}
|
|
41
|
+
if (d && typeof d === "object") {
|
|
42
|
+
if (typeof d.id === "string")
|
|
43
|
+
return d.id;
|
|
44
|
+
if (typeof d.sys_id === "string")
|
|
45
|
+
return d.sys_id;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (resp && typeof resp === "object") {
|
|
49
|
+
if (typeof resp.id === "string")
|
|
50
|
+
return resp.id;
|
|
51
|
+
if (typeof resp.sys_id === "string")
|
|
52
|
+
return resp.sys_id;
|
|
53
|
+
}
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
function flowGetPath(sysId) {
|
|
57
|
+
return "/api/now/processflow/flow/" + encodeURIComponent(sysId);
|
|
58
|
+
}
|
|
59
|
+
function copyPath(sourceSysId, scopeSysId) {
|
|
60
|
+
return "/api/now/processflow/flow/" + encodeURIComponent(sourceSysId) + "/copy"
|
|
61
|
+
+ "?sysparm_transaction_scope=" + encodeURIComponent(scopeSysId);
|
|
62
|
+
}
|
|
63
|
+
function unwrapModel(data) {
|
|
64
|
+
if (data && typeof data === "object" && data.result && typeof data.result === "object") {
|
|
65
|
+
if (data.result.data && typeof data.result.data === "object") {
|
|
66
|
+
return data.result.data;
|
|
67
|
+
}
|
|
68
|
+
return data.result;
|
|
69
|
+
}
|
|
70
|
+
if (data && typeof data === "object" && data.data && typeof data.data === "object") {
|
|
71
|
+
return data.data;
|
|
72
|
+
}
|
|
73
|
+
return data;
|
|
74
|
+
}
|
|
75
|
+
async function copyFlow(params) {
|
|
76
|
+
var client = params.client;
|
|
77
|
+
var sourceSysId = params.sourceSysId;
|
|
78
|
+
if (!sourceSysId) {
|
|
79
|
+
throw new Error("copyFlow: sourceSysId is required.");
|
|
80
|
+
}
|
|
81
|
+
if (typeof params.newName !== "string" || params.newName.trim().length === 0) {
|
|
82
|
+
throw new Error("copyFlow: newName is required.");
|
|
83
|
+
}
|
|
84
|
+
// Resolve the target scope: explicit param, else the source flow's own scope.
|
|
85
|
+
var scopeSysId = params.scopeSysId;
|
|
86
|
+
if (!scopeSysId) {
|
|
87
|
+
var src = unwrapModel(await client.now.get(flowGetPath(sourceSysId)));
|
|
88
|
+
if (src && typeof src.scope === "string") {
|
|
89
|
+
scopeSysId = src.scope;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (!scopeSysId) {
|
|
93
|
+
throw new Error("copyFlow: scopeSysId is required and the source flow carried no `scope` to default from.");
|
|
94
|
+
}
|
|
95
|
+
var resp = await client.now.post(copyPath(sourceSysId, scopeSysId), { name: params.newName, scope: scopeSysId });
|
|
96
|
+
var newSysId = extractCopiedSysId(resp);
|
|
97
|
+
if (!newSysId) {
|
|
98
|
+
throw new Error("copyFlow: the copy succeeded but no new flow sys_id was found in the response: "
|
|
99
|
+
+ JSON.stringify(resp).substring(0, 300));
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
sysId: newSysId,
|
|
103
|
+
name: params.newName,
|
|
104
|
+
scopeSysId: scopeSysId
|
|
105
|
+
};
|
|
106
|
+
}
|