@tenonhq/dovetail-servicenow 0.0.39 → 0.0.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +124 -10
- package/dist/cli.js +222 -17
- package/dist/client.d.ts +30 -1
- package/dist/client.js +94 -2
- package/dist/createClientFromEnvFile.js +18 -2
- package/dist/flowDesigner/actionTypeApi.d.ts +23 -0
- package/dist/flowDesigner/actionTypeApi.js +47 -0
- package/dist/flowDesigner/buildFlowOrchestrator.d.ts +1 -1
- package/dist/flowDesigner/buildFlowOrchestrator.js +16 -1
- package/dist/flowDesigner/cloneActionType.d.ts +98 -19
- package/dist/flowDesigner/cloneActionType.js +400 -87
- package/dist/flowDesigner/editActionType.d.ts +6 -1
- package/dist/flowDesigner/editActionType.js +5 -5
- package/dist/flowDesigner/index.d.ts +5 -4
- package/dist/flowDesigner/index.js +8 -1
- package/dist/flowDesigner/stepOps.d.ts +21 -0
- package/dist/flowDesigner/stepOps.js +57 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.js +16 -3
- package/dist/loadEnv.js +7 -0
- package/dist/mcp/registry.d.ts +1 -1
- package/dist/mcp/registry.js +54 -10
- package/dist/mcp/schemas.d.ts +304 -3
- package/dist/mcp/schemas.js +45 -2
- package/dist/publishApp.d.ts +128 -16
- package/dist/publishApp.js +463 -100
- package/dist/types.d.ts +13 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -55,6 +55,33 @@ Reads ServiceNow credentials from env vars in this order of precedence:
|
|
|
55
55
|
| User | `SN_USER` | `SN_DEV_USERNAME` | `SN_PROD_USERNAME` |
|
|
56
56
|
| Password | `SN_PASSWORD` | `SN_DEV_PASSWORD` | `SN_PROD_PASSWORD` |
|
|
57
57
|
|
|
58
|
+
### Flow Designer identity (`SN_FLOW_*`)
|
|
59
|
+
|
|
60
|
+
`/api/now/processflow/*` — every Flow Designer authoring call (view/edit/clone/
|
|
61
|
+
publish an action, create/copy/publish a flow) — **cannot carry a REST API access
|
|
62
|
+
policy** on ServiceNow, so under API-key auth (`SN_API_KEY`) those calls 401. They
|
|
63
|
+
authenticate instead with a dedicated basic-auth identity used **only** for
|
|
64
|
+
processflow paths; every other path keeps the main identity:
|
|
65
|
+
|
|
66
|
+
| Field | Preferred | Dev fallback | Prod fallback |
|
|
67
|
+
|----------|--------------------|------------------------|-------------------------|
|
|
68
|
+
| User | `SN_FLOW_USER` | `SN_DEV_FLOW_USER` | `SN_PROD_FLOW_USER` |
|
|
69
|
+
| Password | `SN_FLOW_PASSWORD` | `SN_DEV_FLOW_PASSWORD` | `SN_PROD_FLOW_PASSWORD` |
|
|
70
|
+
|
|
71
|
+
- **Key mode + flow identity** → processflow requests go out as basic auth with the
|
|
72
|
+
flow identity and **no** `x-sn-apikey` header; table/Dovetail requests keep the key.
|
|
73
|
+
- **Basic mode, no flow identity** → unchanged: processflow uses the main `SN_USER`.
|
|
74
|
+
- **Key mode, no flow identity** → a processflow call **throws before sending**, naming
|
|
75
|
+
`SN_FLOW_USER` / `SN_FLOW_PASSWORD` (it would only 401). A half-set pair also throws.
|
|
76
|
+
- Programmatic: `createClient({ apiKey, flowUser, flowPassword })` — explicit config beats
|
|
77
|
+
env. A config that pins the main identity (apiKey or user/password — e.g. one resolved
|
|
78
|
+
from an `--env` file) takes the flow identity from the config only, never from
|
|
79
|
+
`process.env`, so a per-call retarget can't borrow another instance's flow creds.
|
|
80
|
+
- `--env <file>` fully determines both identities: the `SN_FLOW_*` / `SN_DEV_FLOW_*` /
|
|
81
|
+
`SN_PROD_FLOW_*` keys are connection keys, replaced (or cleared) from the file.
|
|
82
|
+
|
|
83
|
+
The flow password is never logged; errors name the variables, never their values.
|
|
84
|
+
|
|
58
85
|
The dev/prod fallbacks match the names documented in the committed
|
|
59
86
|
`Craftsman/.env.example`, so existing developer setups work out of the box.
|
|
60
87
|
Bare instance names (e.g. `TenonWorkStudio`) get `.service-now.com` appended
|
|
@@ -266,8 +293,67 @@ npx dove-sn edit-action --sys-id <id> --scope <scope> --set-script ./script.js \
|
|
|
266
293
|
npx dove-sn edit-action --sys-id <id> --scope <scope> --from-json ops.json # dry-run
|
|
267
294
|
npx dove-sn edit-action --sys-id <id> --scope <scope> --from-json ops.json \
|
|
268
295
|
--apply --update-set <id> # publish + verify
|
|
296
|
+
|
|
297
|
+
# Clone a Custom Action Type (every step + its step IO) into a scope and publish it
|
|
298
|
+
npx dove-sn clone-action --from <source_sys_id> --name "Send REST (Spoke)" \
|
|
299
|
+
--scope x_cadso_email_spok --ops ops.json # dry-run (plan)
|
|
300
|
+
npx dove-sn clone-action --from <source_sys_id> --name "Send REST (Spoke)" \
|
|
301
|
+
--scope x_cadso_email_spok --ops ops.json --update-set <id> --confirm # write + publish + verify
|
|
269
302
|
```
|
|
270
303
|
|
|
304
|
+
### Cloning an action type (`clone-action` / `action_clone`)
|
|
305
|
+
|
|
306
|
+
`clone-action` copies a Custom Action Type headlessly — **multi-step capable** — and
|
|
307
|
+
publishes the copy:
|
|
308
|
+
|
|
309
|
+
1. **Reads** (Table API only — `sn_build_agent` is never used) the parent
|
|
310
|
+
`sys_hub_action_type_definition`, its `sys_hub_action_input` / `sys_hub_action_output`
|
|
311
|
+
(`model_id` → parent), every `sys_hub_step_instance` (**`action`** → parent), and each
|
|
312
|
+
step's `sys_hub_step_ext_input` / `sys_hub_step_ext_output` (`model_id` → step).
|
|
313
|
+
2. **Plans** fresh sys_ids for every record (old→new step map), the target scope,
|
|
314
|
+
`name` = `--name`, `internal_name` = `--internal-name` or the slug of the name
|
|
315
|
+
(lowercase, non-alphanumerics → `_`), `state = draft`, and strips system/snapshot
|
|
316
|
+
fields (`master_snapshot`, `latest_snapshot`, `sys_update_name`, audit fields, …).
|
|
317
|
+
3. **Writes** the graph through Dovetail `createRecord`, pinned to `--update-set`, scope
|
|
318
|
+
set per record.
|
|
319
|
+
4. **Publishes**: the SOURCE action's steps are fetched from
|
|
320
|
+
`/processflow/action/action_types/{source}/step_instances`, each step's `action` and
|
|
321
|
+
`sys_id` remapped onto the clone, `--ops` applied, then grafted onto the clone's model
|
|
322
|
+
and POSTed to `/snapshot` — no steps fixture needed.
|
|
323
|
+
5. **Verifies** by reading the clone's steps back (script hash + step IO per step, plus
|
|
324
|
+
the step count). A mismatch exits `1`.
|
|
325
|
+
|
|
326
|
+
`--scope` takes a scope **name** (resolved via `sys_scope`) or a 32-hex sys_id. The
|
|
327
|
+
clone is **idempotent** on `(name, scope)`: an existing match returns `unchanged` and
|
|
328
|
+
writes nothing. **Dry-run by default** — without `--confirm` it prints the plan (records
|
|
329
|
+
per table, step summary, the effect of every op) and writes nothing; `--update-set` is
|
|
330
|
+
required with `--confirm`. Exit `0` on success (incl. dry-run / unchanged), `1` on error.
|
|
331
|
+
|
|
332
|
+
`--ops` takes the same step ops as `edit-action --from-json`, plus **`setStepInputs`** —
|
|
333
|
+
set an **existing** step input's value (and its `display_value` when present), e.g. a
|
|
334
|
+
REST step's HTTP method. An unknown input fails with the list of inputs on that step:
|
|
335
|
+
|
|
336
|
+
```json
|
|
337
|
+
{
|
|
338
|
+
"setStepInputs": [
|
|
339
|
+
{ "step": "REST Step", "input": "http_method", "value": "post" }
|
|
340
|
+
],
|
|
341
|
+
"patchStepScripts": [
|
|
342
|
+
{ "step": "Parse Response", "patchScript": { "find": "v1", "replace": "v2" } }
|
|
343
|
+
],
|
|
344
|
+
"addStepOutputs": [{ "step": "Parse Response", "name": "isRetryable", "type": "boolean" }],
|
|
345
|
+
"addStepInputs": [
|
|
346
|
+
{ "step": "Handle Error", "name": "isRetryable", "type": "boolean",
|
|
347
|
+
"pillFrom": { "step": "Parse Response", "output": "isRetryable" } }
|
|
348
|
+
]
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
The MCP tool **`action_clone`** takes the same inputs — `from`, `name`, `scope`,
|
|
353
|
+
`internalName`, `description`, `updateSetSysId`, `ops` (inline object), `confirm`,
|
|
354
|
+
`dryRun` — with the same dry-run-unless-`confirm:true` gate. `setStepInputs` is also
|
|
355
|
+
accepted by `edit-action` / `action_edit`.
|
|
356
|
+
|
|
271
357
|
### Editing an action type's steps (`--from-json`)
|
|
272
358
|
|
|
273
359
|
The flag form above patches the one auto-detected script. When you need to touch
|
|
@@ -516,11 +602,11 @@ Programmatic: `invokeRest({ method, path, body, confirm })` is exported, and the
|
|
|
516
602
|
client gained `now.put` / `now.delete` / `now.invoke` (the latter returns
|
|
517
603
|
`{ status, body }` verbatim) alongside the existing `now.get` / `now.post`.
|
|
518
604
|
|
|
519
|
-
### Publish an app to the Store / application repository
|
|
605
|
+
### Publish an app to the Store / application repository / an update set
|
|
520
606
|
|
|
521
607
|
Publish a scoped application to the **ServiceNow Store**, the **company
|
|
522
|
-
application repository**, or
|
|
523
|
-
tracker polled to completion.
|
|
608
|
+
application repository**, and/or **into a new update set** — headlessly, with
|
|
609
|
+
each publish's progress tracker polled to completion.
|
|
524
610
|
|
|
525
611
|
```bash
|
|
526
612
|
# Dry-run — the DEFAULT: resolves the app, prints the plan, publishes NOTHING
|
|
@@ -529,15 +615,21 @@ npx dove-sn publish-app --app x_cadso_filter --version 6.0.20260716 --target bot
|
|
|
529
615
|
# Publish for real (store, then repo, same version)
|
|
530
616
|
npx dove-sn publish-app --app x_cadso_filter --version 6.0.20260716 \
|
|
531
617
|
--target both --dev-notes "July release" --confirm --json
|
|
618
|
+
|
|
619
|
+
# Release flow on an instance WITHOUT the sn_cicd plugin: publish to the company
|
|
620
|
+
# repository over the UI uploader, then capture the app into a dated update set.
|
|
621
|
+
npx dove-sn publish-app --app x_cadso_filter --version 6.0.20260729 \
|
|
622
|
+
--target repo-ui,update-set --update-set-description 20260729 --confirm --json
|
|
532
623
|
```
|
|
533
624
|
|
|
534
625
|
**Store publish is EXTERNALLY VISIBLE on the ServiceNow Store — treat
|
|
535
626
|
`--target store --confirm` as a release.** `publish-app` is dry-run by default:
|
|
536
627
|
without `--confirm` it prints the resolved plan and exits `1` (a deliberate
|
|
537
|
-
refusal). `--target
|
|
538
|
-
|
|
628
|
+
refusal). `--target` takes one target, a comma-separated list, or `both` (an
|
|
629
|
+
alias for `store,repo`); targets run **in the order given** and short-circuit on
|
|
630
|
+
the first failure.
|
|
539
631
|
|
|
540
|
-
The
|
|
632
|
+
The targets ride different transports:
|
|
541
633
|
|
|
542
634
|
- **store** replays the `sys_app` form's upload flow (`xmlhttp.do` +
|
|
543
635
|
`sn_appauthor.ScopedAppUploaderAJAX`) over a form-login session — basic auth
|
|
@@ -547,13 +639,33 @@ The two targets ride different transports:
|
|
|
547
639
|
- **repo** uses the supported CI/CD REST API (`POST /api/sn_cicd/app_repo/publish`
|
|
548
640
|
+ `GET /api/sn_cicd/progress/{id}`) over basic auth. The API user needs the
|
|
549
641
|
`sn_cicd` role (or admin).
|
|
642
|
+
- **repo-ui** reaches the *same* company repository as `repo`, but over the UI
|
|
643
|
+
uploader (`sysparm_publish_to_store=false`) instead of REST. Use it when the
|
|
644
|
+
instance has no CI/CD plugin — `tenonworkshop`, for instance, has no `sn_cicd`
|
|
645
|
+
scope and no `app_repo` service, so `repo` 404s there while `repo-ui` works.
|
|
646
|
+
It needs no Store credentials.
|
|
647
|
+
- **update-set** publishes the app *into a newly created update set* via the
|
|
648
|
+
two-call `com.snc.apps.AppsAjaxProcessor` flow (`createUpdateSet` →
|
|
649
|
+
`publishToUpdateSet`). There is no REST equivalent. `--update-set-name`
|
|
650
|
+
defaults to the app's name (the dialog's field is readonly, so that is what
|
|
651
|
+
the UI submits); `--update-set-description` is conventionally the release date
|
|
652
|
+
stamp `YYYYMMDD`, which makes a whole release one query
|
|
653
|
+
(`sys_update_set` where `description=20260729`). `--include-data` maps to the
|
|
654
|
+
dialog's "Include demo data" box and defaults **off**, matching the value the
|
|
655
|
+
UI actually puts on the wire.
|
|
656
|
+
|
|
657
|
+
Ordering matters when you combine them: the repo publish is what bumps
|
|
658
|
+
`sys_app.version`, so put it **before** `update-set` if you want the set
|
|
659
|
+
captured at the new version.
|
|
550
660
|
|
|
551
661
|
`--app` accepts a scope name, `sys_app` sys_id, or app name; `--version` must be
|
|
552
662
|
above the currently published version. The result carries the progress-tracker
|
|
553
663
|
id, per-step states ("Packaging application", "Uploading application"), the
|
|
554
|
-
Store `appLink`, and the
|
|
555
|
-
|
|
556
|
-
|
|
664
|
+
Store `appLink`, and the update-set sys_id — for the `update-set` target that is
|
|
665
|
+
recorded as soon as the set is created, so it survives a later failure and you
|
|
666
|
+
can always find (or delete) the set. Exit codes: `0` published or dry-run, `1`
|
|
667
|
+
bad args/unconfirmed, `2` failed/timeout. Programmatic:
|
|
668
|
+
`publishApp({ app, version, target, confirm })`.
|
|
557
669
|
|
|
558
670
|
### Export an update set (or a whole app) to importable XML
|
|
559
671
|
|
|
@@ -699,7 +811,9 @@ and read-back-verified — `host_assets` (deploy a built dist/), plus the Flow D
|
|
|
699
811
|
tools `flow_view` (read a flow/subflow's step graph), `action_view` (read an action
|
|
700
812
|
type's model), `action_edit` (structurally edit a published action type — per-step
|
|
701
813
|
scripts, step-level inputs/outputs, data-pill wiring — dry-run by default, and the
|
|
702
|
-
publish is read back and verified), `
|
|
814
|
+
publish is read back and verified), `action_clone` (clone an action type — every step
|
|
815
|
+
and its step IO — into a scope and publish + verify it; dry-run by default),
|
|
816
|
+
`flow_publish` (compile a flow/subflow snapshot), `flow_copy`
|
|
703
817
|
(copy a flow as an inactive draft), `flow_create` (create a NEW flow from scratch +
|
|
704
818
|
publish, grafting a template), `flow_test` (validate or run a flow), and
|
|
705
819
|
`flow_edit` (patch a flow), plus `invoke_rest` (invoke an arbitrary authenticated
|
package/dist/cli.js
CHANGED
|
@@ -83,6 +83,7 @@ const copyFlow_1 = require("./flowDesigner/copyFlow");
|
|
|
83
83
|
const createFlow_1 = require("./flowDesigner/createFlow");
|
|
84
84
|
const editFlow_1 = require("./flowDesigner/editFlow");
|
|
85
85
|
const editActionType_1 = require("./flowDesigner/editActionType");
|
|
86
|
+
const cloneActionType_1 = require("./flowDesigner/cloneActionType");
|
|
86
87
|
const testFlow_1 = require("./flowDesigner/testFlow");
|
|
87
88
|
const table_1 = require("./table");
|
|
88
89
|
const setField_1 = require("./setField");
|
|
@@ -929,6 +930,170 @@ async function runEditAction(flags) {
|
|
|
929
930
|
}
|
|
930
931
|
return 0;
|
|
931
932
|
}
|
|
933
|
+
/**
|
|
934
|
+
* dove-sn clone-action:
|
|
935
|
+
* --from <sys_id> Required. Source sys_hub_action_type_definition sys_id.
|
|
936
|
+
* --name <name> Required. Display name of the clone (idempotency key with --scope).
|
|
937
|
+
* --scope <name|sys_id> Required. Target scope — a scope name (x_cadso_email_spok) or 32-hex sys_id.
|
|
938
|
+
* --internal-name <name> Optional. Default: slug of --name.
|
|
939
|
+
* --description <text> Optional.
|
|
940
|
+
* --ops <path> Optional. JSON StepOps applied to the cloned steps before publish:
|
|
941
|
+
* patchStepScripts / setStepInputs / addStepOutputs / addStepInputs.
|
|
942
|
+
* --update-set <sys_id> Required with --confirm. Every write + the publish land here.
|
|
943
|
+
* --confirm Execute (write the graph, publish, verify). WITHOUT it: dry-run.
|
|
944
|
+
* --dry-run Force a dry-run even with --confirm.
|
|
945
|
+
* --json Emit the structured CloneActionTypeResult.
|
|
946
|
+
*
|
|
947
|
+
* Clones a Custom Action Type — parent, inputs, outputs, every step instance and
|
|
948
|
+
* its step-level ext inputs/outputs — into the target scope, then publishes it
|
|
949
|
+
* headlessly through the snapshot path (multi-step capable) and reads the steps
|
|
950
|
+
* back to verify. Idempotent on (name, scope). DRY-RUN BY DEFAULT.
|
|
951
|
+
*
|
|
952
|
+
* --ops shape:
|
|
953
|
+
* {
|
|
954
|
+
* "setStepInputs": [{ "step": "REST Step", "input": "http_method", "value": "post" }],
|
|
955
|
+
* "patchStepScripts": [{ "step": "Parse", "patchScript": { "find": "a", "replace": "b" } }],
|
|
956
|
+
* "addStepOutputs": [{ "step": "Parse", "name": "isRetryable", "type": "boolean" }],
|
|
957
|
+
* "addStepInputs": [{ "step": "Handle", "name": "isRetryable", "type": "boolean",
|
|
958
|
+
* "pillFrom": { "step": "Parse", "output": "isRetryable" } }]
|
|
959
|
+
* }
|
|
960
|
+
* `step` is a step cid or label; `scriptFile` (resolved relative to the ops file)
|
|
961
|
+
* is sugar for `setScript` in patchStepScripts.
|
|
962
|
+
*/
|
|
963
|
+
var CLONE_OPS_KEYS = ["patchStepScripts", "setStepInputs", "addStepOutputs", "addStepInputs"];
|
|
964
|
+
async function runCloneAction(flags, bare) {
|
|
965
|
+
var bareErr = bareStringFlagError("clone-action", bare, [
|
|
966
|
+
"from",
|
|
967
|
+
"name",
|
|
968
|
+
"scope",
|
|
969
|
+
"internal-name",
|
|
970
|
+
"description",
|
|
971
|
+
"ops",
|
|
972
|
+
"update-set",
|
|
973
|
+
]);
|
|
974
|
+
if (bareErr) {
|
|
975
|
+
process.stderr.write(bareErr);
|
|
976
|
+
return 1;
|
|
977
|
+
}
|
|
978
|
+
var from = flags.from;
|
|
979
|
+
var name = flags.name;
|
|
980
|
+
var scope = flags.scope;
|
|
981
|
+
if (!from || !name || !scope) {
|
|
982
|
+
process.stderr.write("clone-action: --from <sys_id>, --name <name> and --scope <scope name|sys_id> are required\n");
|
|
983
|
+
return 1;
|
|
984
|
+
}
|
|
985
|
+
var confirm = flags.confirm === "true";
|
|
986
|
+
var dryRun = flags["dry-run"] === "true";
|
|
987
|
+
var updateSet = flags["update-set"] || flags.updateSetSysId;
|
|
988
|
+
if (confirm && !dryRun && !updateSet) {
|
|
989
|
+
process.stderr.write("clone-action: --update-set <sys_id> is required with --confirm\n");
|
|
990
|
+
return 1;
|
|
991
|
+
}
|
|
992
|
+
var stepOps;
|
|
993
|
+
if (flags.ops) {
|
|
994
|
+
var parsedOps = JSON.parse(fs.readFileSync(flags.ops, "utf8"));
|
|
995
|
+
if (!parsedOps || typeof parsedOps !== "object" || Array.isArray(parsedOps)) {
|
|
996
|
+
process.stderr.write("clone-action: --ops must contain a StepOps object\n");
|
|
997
|
+
return 1;
|
|
998
|
+
}
|
|
999
|
+
var opsObj = parsedOps;
|
|
1000
|
+
var opsKeys = Object.keys(opsObj);
|
|
1001
|
+
for (var k = 0; k < opsKeys.length; k += 1) {
|
|
1002
|
+
if (CLONE_OPS_KEYS.indexOf(opsKeys[k]) === -1) {
|
|
1003
|
+
process.stderr.write("clone-action: unknown --ops key '" +
|
|
1004
|
+
opsKeys[k] +
|
|
1005
|
+
"' (allowed: " +
|
|
1006
|
+
CLONE_OPS_KEYS.join(", ") +
|
|
1007
|
+
")\n");
|
|
1008
|
+
return 1;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
resolveScriptFiles(opsObj, flags.ops);
|
|
1012
|
+
stepOps = opsObj;
|
|
1013
|
+
}
|
|
1014
|
+
var result = await (0, cloneActionType_1.cloneActionType)({
|
|
1015
|
+
client: (0, client_1.createClient)({}),
|
|
1016
|
+
sourceSysId: from,
|
|
1017
|
+
newName: name,
|
|
1018
|
+
internalName: flags["internal-name"],
|
|
1019
|
+
newScope: scope,
|
|
1020
|
+
updateSetSysId: updateSet,
|
|
1021
|
+
description: flags.description,
|
|
1022
|
+
stepOps: stepOps,
|
|
1023
|
+
confirm: confirm,
|
|
1024
|
+
dryRun: dryRun,
|
|
1025
|
+
});
|
|
1026
|
+
if (flags.json === "true") {
|
|
1027
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
1028
|
+
return result.verify && !result.verify.ok ? 1 : 0;
|
|
1029
|
+
}
|
|
1030
|
+
process.stdout.write("[" + result.action + "] " + name + " (" + result.internalName + ") -> " + result.sysId + "\n");
|
|
1031
|
+
if (result.action === "unchanged") {
|
|
1032
|
+
process.stdout.write(" an action with this name already exists in the target scope — nothing written\n");
|
|
1033
|
+
return 0;
|
|
1034
|
+
}
|
|
1035
|
+
if (result.plan) {
|
|
1036
|
+
process.stdout.write(" scope: " +
|
|
1037
|
+
result.plan.scope.name +
|
|
1038
|
+
" (" +
|
|
1039
|
+
result.plan.scope.sysId +
|
|
1040
|
+
") source scope: " +
|
|
1041
|
+
result.plan.sourceScopeSysId +
|
|
1042
|
+
"\n records: " +
|
|
1043
|
+
result.plan.total +
|
|
1044
|
+
"\n");
|
|
1045
|
+
var tables = Object.keys(result.plan.counts);
|
|
1046
|
+
for (var t = 0; t < tables.length; t += 1) {
|
|
1047
|
+
process.stdout.write(" " + tables[t] + ": " + result.plan.counts[tables[t]] + "\n");
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
if (result.steps) {
|
|
1051
|
+
process.stdout.write("\n--- steps (as published) ---\n");
|
|
1052
|
+
for (var si = 0; si < result.steps.after.length; si += 1) {
|
|
1053
|
+
var step = result.steps.after[si];
|
|
1054
|
+
process.stdout.write(" " +
|
|
1055
|
+
step.label +
|
|
1056
|
+
" (" +
|
|
1057
|
+
step.cid +
|
|
1058
|
+
")" +
|
|
1059
|
+
(step.scriptChars !== null ? " script " + step.scriptChars + " chars" : "") +
|
|
1060
|
+
(step.extendedInputs.length ? " in:" + step.extendedInputs.length : "") +
|
|
1061
|
+
(step.extendedOutputs.length ? " out:" + step.extendedOutputs.length : "") +
|
|
1062
|
+
"\n");
|
|
1063
|
+
}
|
|
1064
|
+
for (var ci = 0; ci < result.steps.changes.length; ci += 1) {
|
|
1065
|
+
process.stdout.write(" + " + result.steps.changes[ci] + "\n");
|
|
1066
|
+
}
|
|
1067
|
+
for (var wi = 0; wi < result.steps.warnings.length; wi += 1) {
|
|
1068
|
+
process.stdout.write(" ! " + result.steps.warnings[wi] + "\n");
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
if (result.action === "planned") {
|
|
1072
|
+
process.stdout.write("\nDRY RUN — nothing written. Re-run with --confirm --update-set <sys_id> to clone + publish.\n");
|
|
1073
|
+
return 0;
|
|
1074
|
+
}
|
|
1075
|
+
process.stdout.write("\nwritten: " +
|
|
1076
|
+
result.written.length +
|
|
1077
|
+
" record(s)" +
|
|
1078
|
+
(result.publish
|
|
1079
|
+
? "; published (HTTP " +
|
|
1080
|
+
result.publish.httpStatus +
|
|
1081
|
+
(result.publish.snapshotSysId ? ", snapshot " + result.publish.snapshotSysId : "") +
|
|
1082
|
+
")"
|
|
1083
|
+
: "") +
|
|
1084
|
+
"\n");
|
|
1085
|
+
if (result.verify) {
|
|
1086
|
+
process.stdout.write("\n--- verify (read back from the instance) ---\n");
|
|
1087
|
+
process.stdout.write(" " + (result.verify.ok ? "OK" : "FAILED") + "\n");
|
|
1088
|
+
for (var vi = 0; vi < result.verify.notes.length; vi += 1) {
|
|
1089
|
+
process.stdout.write(" " + (result.verify.ok ? "+ " : "! ") + result.verify.notes[vi] + "\n");
|
|
1090
|
+
}
|
|
1091
|
+
if (!result.verify.ok) {
|
|
1092
|
+
return 1;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return 0;
|
|
1096
|
+
}
|
|
932
1097
|
async function runMcp(flags) {
|
|
933
1098
|
if (flags.smoke === "true") {
|
|
934
1099
|
await (0, server_1.runSmoke)();
|
|
@@ -1041,17 +1206,29 @@ function printHelp() {
|
|
|
1041
1206
|
" (per-step scripts + step IO + data-pill wiring)\n" +
|
|
1042
1207
|
' | --patch-script "<find>::<replace>" | --set-script <path> | --merge-outputs <path>\n' +
|
|
1043
1208
|
" [--script-input <name>] [--update-set <sys_id>] [--apply] [--json])\n" +
|
|
1209
|
+
" clone-action Clone a Custom Action Type (all steps + step IO) into a scope and publish it\n" +
|
|
1210
|
+
" DRY-RUN BY DEFAULT — nothing is written without --confirm\n" +
|
|
1211
|
+
" (--from <sys_id> --name <n> --scope <scope name|sys_id>\n" +
|
|
1212
|
+
" [--internal-name <n>] [--description <d>]\n" +
|
|
1213
|
+
" [--ops <ops.json>] ops: setStepInputs / patchStepScripts /\n" +
|
|
1214
|
+
" addStepOutputs / addStepInputs\n" +
|
|
1215
|
+
" [--update-set <sys_id> (required with --confirm)] [--confirm] [--dry-run] [--json])\n" +
|
|
1216
|
+
" Idempotent on (name, scope). Publishes via the snapshot path and\n" +
|
|
1217
|
+
" reads the steps back to verify.\n" +
|
|
1044
1218
|
" edit-flow Patch a flow/subflow (rename, description, step inputs)\n" +
|
|
1045
1219
|
" (--sys-id <sys_id> --from-json <ops.json> [--apply] [--update-set <sys_id>] [--scope <sys_id>] [--json])\n" +
|
|
1046
|
-
" publish-app Publish a scoped app to the ServiceNow Store
|
|
1047
|
-
"
|
|
1220
|
+
" publish-app Publish a scoped app to the ServiceNow Store, the company application\n" +
|
|
1221
|
+
" repository, and/or a new update set, then poll each to completion.\n" +
|
|
1048
1222
|
" STORE PUBLISH IS EXTERNALLY VISIBLE on the ServiceNow Store.\n" +
|
|
1049
1223
|
" DRY-RUN BY DEFAULT — nothing is published without --confirm\n" +
|
|
1050
|
-
" (--app <scope|sys_id|name> --version <v
|
|
1224
|
+
" (--app <scope|sys_id|name> --version <v>\n" +
|
|
1225
|
+
" --target store|repo|repo-ui|update-set|both (comma-separated ok)\n" +
|
|
1051
1226
|
" [--dev-notes <text>] [--store-user <email>] [--timeout-ms <n>]\n" +
|
|
1052
|
-
" [--
|
|
1227
|
+
" [--update-set-name <name>] [--update-set-description <text>]\n" +
|
|
1228
|
+
" [--include-data] [--dry-run] [--json] [--confirm])\n" +
|
|
1053
1229
|
" Store creds: SN_STORE_USERNAME/SN_STORE_PASSWORD in the --env file;\n" +
|
|
1054
|
-
" the password is never a flag.
|
|
1230
|
+
" the password is never a flag. 'repo' needs the sn_cicd plugin+role;\n" +
|
|
1231
|
+
" 'repo-ui' reaches the same repository over the UI uploader instead.\n" +
|
|
1055
1232
|
" export-update-set Export an update set to importable <unload> XML, with secret\n" +
|
|
1056
1233
|
" values replaced by __SET_DURING_INSTALL__ (no opt-out).\n" +
|
|
1057
1234
|
" assemble mode is READ-ONLY; complete mode marks the set\n" +
|
|
@@ -1074,7 +1251,10 @@ function printHelp() {
|
|
|
1074
1251
|
" or the DOVETAIL_ENV_FILE env var). A bare name like 'prod'\n" +
|
|
1075
1252
|
" resolves to .env.prod in the cwd. The file's SN_* connection\n" +
|
|
1076
1253
|
" vars replace any already exported; a missing or incomplete\n" +
|
|
1077
|
-
" file is an error (no fallback). Default: .env in the cwd.\n"
|
|
1254
|
+
" file is an error (no fallback). Default: .env in the cwd.\n" +
|
|
1255
|
+
" Flow Designer auth /api/now/processflow/* can't carry an API access policy, so\n" +
|
|
1256
|
+
" under SN_API_KEY those calls use a dedicated basic-auth identity:\n" +
|
|
1257
|
+
" SN_FLOW_USER / SN_FLOW_PASSWORD (or SN_DEV_FLOW_* / SN_PROD_FLOW_*).\n");
|
|
1078
1258
|
}
|
|
1079
1259
|
/** Parse inline `--columns "Label:type:max, Other:choice, ..."` into ColumnSpec[]. */
|
|
1080
1260
|
function parseColumnsInline(input) {
|
|
@@ -1833,8 +2013,19 @@ async function runInvokeRest(flags) {
|
|
|
1833
2013
|
* dove-sn publish-app:
|
|
1834
2014
|
* --app <scope|sys_id|name> Required. The sys_app to publish.
|
|
1835
2015
|
* --version <v> Required. Version to publish (e.g. 6.0.20260716).
|
|
1836
|
-
* --target store|repo|
|
|
1837
|
-
*
|
|
2016
|
+
* --target <t[,t...]> Required. store | repo | repo-ui | update-set |
|
|
2017
|
+
* both (= store,repo). STORE IS EXTERNALLY VISIBLE.
|
|
2018
|
+
* repo = CI/CD REST API (needs the sn_cicd plugin)
|
|
2019
|
+
* repo-ui= same destination over the UI uploader,
|
|
2020
|
+
* for instances without sn_cicd
|
|
2021
|
+
* update-set = publish the app INTO a new update set
|
|
2022
|
+
* [--dev-notes <text>] Optional developer notes (uploader targets).
|
|
2023
|
+
* [--update-set-name <name>] update-set only. Defaults to the app's name.
|
|
2024
|
+
* [--update-set-description <text>]
|
|
2025
|
+
* update-set only. Tenon convention is the release
|
|
2026
|
+
* date stamp (YYYYMMDD) so a release is one query.
|
|
2027
|
+
* [--include-data] update-set only. Include demo data (default off,
|
|
2028
|
+
* matching the observed wire value).
|
|
1838
2029
|
* [--store-user <email>] Store account email (else SN_STORE_USERNAME).
|
|
1839
2030
|
* The password comes ONLY from SN_STORE_PASSWORD —
|
|
1840
2031
|
* there is no flag for it, ever.
|
|
@@ -1842,9 +2033,9 @@ async function runInvokeRest(flags) {
|
|
|
1842
2033
|
* [--dry-run] [--json] [--confirm]
|
|
1843
2034
|
*
|
|
1844
2035
|
* DRY-RUN unless --confirm: without it the resolved plan is printed and the
|
|
1845
|
-
* command exits 1 (a deliberate refusal, not success).
|
|
1846
|
-
*
|
|
1847
|
-
*
|
|
2036
|
+
* command exits 1 (a deliberate refusal, not success). Multiple targets run
|
|
2037
|
+
* sequentially with the same version and short-circuit on the first failure;
|
|
2038
|
+
* --json emits an array of per-target results.
|
|
1848
2039
|
* Exit codes: 0 published/dry-run, 1 bad args/unconfirmed, 2 failed/timeout.
|
|
1849
2040
|
*/
|
|
1850
2041
|
async function runPublishApp(flags) {
|
|
@@ -1852,16 +2043,17 @@ async function runPublishApp(flags) {
|
|
|
1852
2043
|
var version = flags.version;
|
|
1853
2044
|
var target = flags.target;
|
|
1854
2045
|
if (!app || !version || !target) {
|
|
1855
|
-
process.stderr.write("publish-app: --app, --version and --target
|
|
2046
|
+
process.stderr.write("publish-app: --app, --version and --target <" +
|
|
2047
|
+
publishApp_1.PUBLISH_TARGETS.join("|") +
|
|
2048
|
+
"|both> are required\n");
|
|
1856
2049
|
return 1;
|
|
1857
2050
|
}
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
"')\n");
|
|
2051
|
+
var parsedTargets = (0, publishApp_1.parsePublishTargets)(target);
|
|
2052
|
+
if (parsedTargets.error) {
|
|
2053
|
+
process.stderr.write("publish-app: " + parsedTargets.error + "\n");
|
|
1862
2054
|
return 1;
|
|
1863
2055
|
}
|
|
1864
|
-
var targets =
|
|
2056
|
+
var targets = parsedTargets.targets;
|
|
1865
2057
|
var dryRun = flags["dry-run"] === "true";
|
|
1866
2058
|
var confirmed = flags.confirm === "true";
|
|
1867
2059
|
var client = (0, client_1.createClient)({});
|
|
@@ -1880,6 +2072,16 @@ async function runPublishApp(flags) {
|
|
|
1880
2072
|
params.devNotes = flags["dev-notes"];
|
|
1881
2073
|
if (flags["store-user"])
|
|
1882
2074
|
params.storeUsername = flags["store-user"];
|
|
2075
|
+
if (flags["update-set-name"]) {
|
|
2076
|
+
params.updateSetName = flags["update-set-name"];
|
|
2077
|
+
}
|
|
2078
|
+
// Read with !== undefined, not truthiness: an intentionally empty
|
|
2079
|
+
// description must stay empty rather than silently fall back.
|
|
2080
|
+
if (flags["update-set-description"] !== undefined) {
|
|
2081
|
+
params.updateSetDescription = flags["update-set-description"];
|
|
2082
|
+
}
|
|
2083
|
+
if (flags["include-data"] === "true")
|
|
2084
|
+
params.includeData = true;
|
|
1883
2085
|
if (flags["timeout-ms"]) {
|
|
1884
2086
|
// A NaN timeout would make the poll-loop budget check always false —
|
|
1885
2087
|
// an infinite loop. Validate here, exit 1 on garbage.
|
|
@@ -2219,6 +2421,9 @@ async function main() {
|
|
|
2219
2421
|
if (parsed.command === "edit-action") {
|
|
2220
2422
|
return await runEditAction(parsed.flags);
|
|
2221
2423
|
}
|
|
2424
|
+
if (parsed.command === "clone-action") {
|
|
2425
|
+
return await runCloneAction(parsed.flags, parsed.bare);
|
|
2426
|
+
}
|
|
2222
2427
|
if (parsed.command === "create-view") {
|
|
2223
2428
|
await runCreateView(parsed.flags);
|
|
2224
2429
|
return 0;
|
package/dist/client.d.ts
CHANGED
|
@@ -29,6 +29,33 @@ export type ResolvedAuth = {
|
|
|
29
29
|
user: string;
|
|
30
30
|
password: string;
|
|
31
31
|
};
|
|
32
|
+
/** Path prefix of the Flow Designer authoring API — the only paths the flow identity is used for. */
|
|
33
|
+
export declare var PROCESSFLOW_PATH_PREFIX: string;
|
|
34
|
+
/**
|
|
35
|
+
* Resolved Flow Designer (processflow) identity:
|
|
36
|
+
* - "basic" — a complete SN_FLOW_USER / SN_FLOW_PASSWORD pair;
|
|
37
|
+
* - "partial" — only one half was set (reported, never sent);
|
|
38
|
+
* - "none" — no flow identity configured.
|
|
39
|
+
*/
|
|
40
|
+
export type ResolvedFlowAuth = {
|
|
41
|
+
mode: "basic";
|
|
42
|
+
user: string;
|
|
43
|
+
password: string;
|
|
44
|
+
} | {
|
|
45
|
+
mode: "partial";
|
|
46
|
+
missing: string;
|
|
47
|
+
} | {
|
|
48
|
+
mode: "none";
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Explicit config beats env. When the config pins the MAIN identity (apiKey or
|
|
52
|
+
* user/password — e.g. a config resolved from an --env file), the flow identity
|
|
53
|
+
* is taken from the config ONLY, so a per-call retarget can never borrow the
|
|
54
|
+
* process's flow credentials for a different instance.
|
|
55
|
+
*/
|
|
56
|
+
export declare function resolveFlowAuth(cfg: ServiceNowClientConfig): ResolvedFlowAuth;
|
|
57
|
+
/** True when an instance-relative request URL targets the processflow API. */
|
|
58
|
+
export declare function isProcessflowPath(url: unknown): boolean;
|
|
32
59
|
export interface TableQueryOptions {
|
|
33
60
|
limit?: number;
|
|
34
61
|
fields?: string[];
|
|
@@ -137,7 +164,9 @@ export interface ServiceNowClient {
|
|
|
137
164
|
now: {
|
|
138
165
|
/**
|
|
139
166
|
* GET an arbitrary native ServiceNow REST path (e.g. /api/now/processflow/...).
|
|
140
|
-
*
|
|
167
|
+
* Same credentials/retry/throttle as the rest of the client, except that
|
|
168
|
+
* /api/now/processflow/* paths authenticate with the dedicated Flow Designer
|
|
169
|
+
* identity (SN_FLOW_USER / SN_FLOW_PASSWORD) when one is configured.
|
|
141
170
|
* Use for endpoints that aren't the Table API or the Dovetail Scripted REST API
|
|
142
171
|
* — currently the Flow Designer processflow endpoints. Returns the raw response body.
|
|
143
172
|
*/
|