@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 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 both — headlessly, with the publish's progress
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 both` runs store then repo sequentially and short-circuits
538
- if the store leg fails.
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 two targets ride different transports:
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 publish's update-set sys_id where the instance reports
555
- one. Exit codes: `0` published or dry-run, `1` bad args/unconfirmed, `2`
556
- failed/timeout. Programmatic: `publishApp({ app, version, target, confirm })`.
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), `flow_publish` (compile a flow/subflow snapshot), `flow_copy`
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 and/or the company\n" +
1047
- " application repository, then poll the publish to completion.\n" +
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> --target store|repo|both\n" +
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
- " [--dry-run] [--json] [--confirm])\n" +
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. Repo publish needs the sn_cicd role.\n" +
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|both Required. STORE PUBLISH IS EXTERNALLY VISIBLE.
1837
- * [--dev-notes <text>] Optional developer notes.
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). --target both publishes
1846
- * store then repo sequentially with the same version and short-circuits if the
1847
- * store leg fails; --json emits an array of per-target results.
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 store|repo|both are required\n");
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
- if (target !== "store" && target !== "repo" && target !== "both") {
1859
- process.stderr.write("publish-app: --target must be store, repo or both (got '" +
1860
- target +
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 = target === "both" ? ["store", "repo"] : [target];
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
- * Basic auth, same credentials/retry/throttle as the rest of the client.
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
  */