@tenonhq/dovetail-servicenow 0.0.27 → 0.0.29

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
@@ -201,8 +201,62 @@ npx dove-sn edit-action --sys-id <action_type_sys_id> --scope <scope_sys_id> \
201
201
  --patch-script "grabHashData::grabRecipients" # dry-run (diff)
202
202
  npx dove-sn edit-action --sys-id <id> --scope <scope> --set-script ./script.js \
203
203
  --merge-outputs ./output-var.json --apply --update-set <id> # persist + publish
204
+
205
+ # Edit it STRUCTURALLY — several steps' scripts, step-level IO, pill wiring — in one publish
206
+ npx dove-sn edit-action --sys-id <id> --scope <scope> --from-json ops.json # dry-run
207
+ npx dove-sn edit-action --sys-id <id> --scope <scope> --from-json ops.json \
208
+ --apply --update-set <id> # publish + verify
209
+ ```
210
+
211
+ ### Editing an action type's steps (`--from-json`)
212
+
213
+ The flag form above patches the one auto-detected script. When you need to touch
214
+ more than one step — or the step's own inputs and outputs — pass an ops file:
215
+
216
+ ```json
217
+ {
218
+ "patchStepScripts": [
219
+ { "step": "Parse Response", "scriptFile": "./parse-response.js" },
220
+ { "step": "Handle Error", "patchScript": { "find": "gs.error", "replace": "gs.warn" } }
221
+ ],
222
+ "addStepOutputs": [
223
+ { "step": "Parse Response", "name": "isRetryable", "label": "Is Retryable", "type": "boolean" }
224
+ ],
225
+ "addStepInputs": [
226
+ {
227
+ "step": "Handle Error",
228
+ "name": "isRetryable",
229
+ "type": "boolean",
230
+ "pillFrom": { "step": "Parse Response", "output": "isRetryable" }
231
+ }
232
+ ]
233
+ }
204
234
  ```
205
235
 
236
+ - **`step`** is a step's `cid` **or** its label — an unknown ref fails with the list of steps that do exist.
237
+ - **`scriptFile`** is sugar for `setScript`, resolved **relative to the ops file**, so scripts can live beside it.
238
+ - **`addStepInputs[].pillFrom`** wires the input to another step's output. You never write the pill
239
+ yourself — the correct format is `{{step[<source_cid>].<output>}}`, and getting it wrong does not
240
+ fail the publish, it compiles a dead reference that reads `undefined` at runtime.
241
+ - Ops are **order-independent**: an input may pill from an output added in the same call. Everything
242
+ lands in a **single** `/snapshot` POST.
243
+ - Adding IO is **idempotent** — a name that is already present is skipped with a warning, not duplicated.
244
+
245
+ Two behaviours worth knowing before you rely on this:
246
+
247
+ **It refuses to guess an entry shape.** A new `extended_inputs` / `extended_outputs` entry is built by
248
+ mirroring an existing sibling entry on the same step, because those entries carry more keys than the
249
+ four you supply and some are wrapped as `{value: x}` inconsistently. If the step has *no* existing
250
+ entry in that list, there is nothing to mirror and the command **errors out** rather than hand-author
251
+ an object that would corrupt the action. Author one entry in the Designer first, then re-run.
252
+
253
+ **It verifies the publish.** A `201` from `/snapshot` means the snapshot compiled — not that your edit
254
+ landed as intended. With `--apply`, the steps are read back from the instance and compared against what
255
+ was sent: script **content** (hashed, so a same-length-but-different script can't pass) and each IO
256
+ entry's **name, type and value** — so an entry that landed with a mis-wired pill is caught, not just a
257
+ missing one. A mismatch prints the diff and exits **2**. When every op was a no-op, there is nothing to
258
+ read back and the round-trip is skipped.
259
+
206
260
  `copy-flow` calls the Designer's own `POST /processflow/flow/{id}/copy` — a
207
261
  complete, faithful clone created as an **inactive draft**. (Don't publish +
208
262
  activate a copy of a triggered production flow unless you intend it to fire.)
@@ -427,7 +481,9 @@ Claude Code and agents: `create_view`, `set_list_layout`, `set_form_layout`,
427
481
  existing record) and `create_record` (insert one record) — both update-set-captured
428
482
  and read-back-verified — `host_assets` (deploy a built dist/), plus the Flow Designer
429
483
  tools `flow_view` (read a flow/subflow's step graph), `action_view` (read an action
430
- type's model), `flow_publish` (compile a flow/subflow snapshot), `flow_copy`
484
+ type's model), `action_edit` (structurally edit a published action type — per-step
485
+ scripts, step-level inputs/outputs, data-pill wiring — dry-run by default, and the
486
+ publish is read back and verified), `flow_publish` (compile a flow/subflow snapshot), `flow_copy`
431
487
  (copy a flow as an inactive draft), `flow_create` (create a NEW flow from scratch +
432
488
  publish, grafting a template), `flow_test` (validate or run a flow), and
433
489
  `flow_edit` (patch a flow), plus `invoke_rest` (invoke an arbitrary authenticated
package/dist/choices.d.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import type { ServiceNowClient } from "./client";
11
11
  import type { AddChoicesParams, AddChoicesResult } from "./types";
12
+ export declare function encodeQueryValue(v: string): string;
12
13
  /**
13
14
  * Upsert choices for a field and (optionally) toggle sys_dictionary.choice.
14
15
  * Idempotent: re-running with the same inputs returns `action: "unchanged"`
package/dist/choices.js CHANGED
@@ -9,6 +9,7 @@
9
9
  * so choices stay in the same application as the field.
10
10
  */
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.encodeQueryValue = encodeQueryValue;
12
13
  exports.addChoicesToField = addChoicesToField;
13
14
  function encodeQueryValue(v) {
14
15
  // ServiceNow encoded-query values: commas/carets/equals are special. We
@@ -39,7 +40,10 @@ async function resolveScopeName(client, scopeSysId) {
39
40
  async function fetchDictionary(client, table, column) {
40
41
  var rows = await client.table.query("sys_dictionary", "name=" + encodeQueryValue(table) + "^element=" + encodeQueryValue(column), 1);
41
42
  if (rows.length === 0) {
42
- throw new Error("sys_dictionary record not found for " + table + "." + column +
43
+ throw new Error("sys_dictionary record not found for " +
44
+ table +
45
+ "." +
46
+ column +
43
47
  " — verify the field exists and your user has read access.");
44
48
  }
45
49
  var row = rows[0];
@@ -53,7 +57,7 @@ async function fetchDictionary(client, table, column) {
53
57
  name: row.name,
54
58
  element: row.element,
55
59
  choice: String(row.choice || "0"),
56
- sys_scope: scope || ""
60
+ sys_scope: scope || "",
57
61
  };
58
62
  }
59
63
  async function fetchUpdateSet(client, sysId) {
@@ -63,14 +67,16 @@ async function fetchUpdateSet(client, sysId) {
63
67
  }
64
68
  var row = rows[0];
65
69
  if (row.state && row.state !== "in progress" && row.state !== "in_progress") {
66
- throw new Error("Update set " + row.name + " is in state '" + row.state +
70
+ throw new Error("Update set " +
71
+ row.name +
72
+ " is in state '" +
73
+ row.state +
67
74
  "' — only 'in progress' update sets can capture new changes.");
68
75
  }
69
76
  return row;
70
77
  }
71
78
  async function fetchExistingChoices(client, table, column) {
72
- return client.table.query("sys_choice", "name=" + encodeQueryValue(table) +
73
- "^element=" + encodeQueryValue(column), 1000);
79
+ return client.table.query("sys_choice", "name=" + encodeQueryValue(table) + "^element=" + encodeQueryValue(column), 1000);
74
80
  }
75
81
  function buildChoiceFields(table, column, choice, scope) {
76
82
  var fields = {
@@ -79,7 +85,7 @@ function buildChoiceFields(table, column, choice, scope) {
79
85
  value: choice.value,
80
86
  label: choice.label,
81
87
  language: choice.language || "en",
82
- inactive: "false"
88
+ inactive: "false",
83
89
  };
84
90
  if (choice.sequence != null) {
85
91
  fields.sequence = String(choice.sequence);
@@ -114,7 +120,9 @@ async function addChoicesToField(client, params) {
114
120
  var scopeName = await resolveScopeName(client, dict.sys_scope);
115
121
  var targetChoiceType = params.choiceType === null
116
122
  ? Number(dict.choice)
117
- : (params.choiceType != null ? params.choiceType : 3);
123
+ : params.choiceType != null
124
+ ? params.choiceType
125
+ : 3;
118
126
  var choiceWas = Number(dict.choice);
119
127
  var choiceNow = choiceWas;
120
128
  if (params.choiceType !== null && Number(dict.choice) !== targetChoiceType) {
@@ -122,7 +130,7 @@ async function addChoicesToField(client, params) {
122
130
  update_set_sys_id: params.updateSetSysId,
123
131
  table: "sys_dictionary",
124
132
  record_sys_id: dict.sys_id,
125
- fields: { choice: String(targetChoiceType) }
133
+ fields: { choice: String(targetChoiceType) },
126
134
  });
127
135
  choiceNow = targetChoiceType;
128
136
  }
@@ -138,14 +146,19 @@ async function addChoicesToField(client, params) {
138
146
  var key = (choice.language || "en") + "::" + choice.value;
139
147
  var match = existingByValue[key];
140
148
  if (match && isUnchanged(match, choice)) {
141
- results.push({ value: choice.value, label: choice.label, sysId: match.sys_id, action: "unchanged" });
149
+ results.push({
150
+ value: choice.value,
151
+ label: choice.label,
152
+ sysId: match.sys_id,
153
+ action: "unchanged",
154
+ });
142
155
  continue;
143
156
  }
144
157
  if (match) {
145
158
  var updFields = {
146
159
  label: choice.label,
147
160
  language: choice.language || "en",
148
- inactive: "false"
161
+ inactive: "false",
149
162
  };
150
163
  if (choice.sequence != null) {
151
164
  updFields.sequence = String(choice.sequence);
@@ -154,22 +167,27 @@ async function addChoicesToField(client, params) {
154
167
  update_set_sys_id: params.updateSetSysId,
155
168
  table: "sys_choice",
156
169
  record_sys_id: match.sys_id,
157
- fields: updFields
170
+ fields: updFields,
171
+ });
172
+ results.push({
173
+ value: choice.value,
174
+ label: choice.label,
175
+ sysId: match.sys_id,
176
+ action: "updated",
158
177
  });
159
- results.push({ value: choice.value, label: choice.label, sysId: match.sys_id, action: "updated" });
160
178
  continue;
161
179
  }
162
180
  var created = await client.claude.createRecord({
163
181
  table: "sys_choice",
164
182
  fields: buildChoiceFields(params.table, params.column, choice, dict.sys_scope),
165
183
  scope: scopeName,
166
- update_set_sys_id: params.updateSetSysId
184
+ update_set_sys_id: params.updateSetSysId,
167
185
  });
168
186
  results.push({
169
187
  value: choice.value,
170
188
  label: choice.label,
171
189
  sysId: created.sys_id,
172
- action: "created"
190
+ action: "created",
173
191
  });
174
192
  }
175
193
  return {
@@ -177,9 +195,9 @@ async function addChoicesToField(client, params) {
177
195
  sysId: dict.sys_id,
178
196
  scope: dict.sys_scope,
179
197
  choiceWas: choiceWas,
180
- choiceNow: choiceNow
198
+ choiceNow: choiceNow,
181
199
  },
182
200
  updateSet: { sysId: updateSet.sys_id, name: updateSet.name },
183
- choices: results
201
+ choices: results,
184
202
  };
185
203
  }
package/dist/cli.js CHANGED
@@ -87,24 +87,30 @@ const flowDesigner_formatter_2 = require("./flowDesigner-formatter");
87
87
  function parseArgs(argv) {
88
88
  var command = argv[0] || "";
89
89
  var flags = {};
90
+ var bare = {};
90
91
  for (var i = 1; i < argv.length; i += 1) {
91
92
  var arg = argv[i];
92
93
  if (arg.indexOf("--") !== 0)
93
94
  continue;
94
95
  var key = arg.slice(2);
95
96
  var value = "true";
97
+ var isBare = true;
96
98
  var eq = key.indexOf("=");
97
99
  if (eq !== -1) {
98
100
  value = key.slice(eq + 1);
99
101
  key = key.slice(0, eq);
102
+ isBare = false;
100
103
  }
101
104
  else if (i + 1 < argv.length && argv[i + 1].indexOf("--") !== 0) {
102
105
  value = argv[i + 1];
103
106
  i += 1;
107
+ isBare = false;
104
108
  }
105
109
  flags[key] = value;
110
+ if (isBare)
111
+ bare[key] = true;
106
112
  }
107
- return { command: command, flags: flags };
113
+ return { command: command, flags: flags, bare: bare };
108
114
  }
109
115
  function parseChoicesInline(input) {
110
116
  return input.split(",").map(function (pair) {
@@ -666,17 +672,54 @@ async function runEditFlow(flags) {
666
672
  * dove-sn edit-action:
667
673
  * --sys-id <sys_id> Required. sys_hub_action_type_definition sys_id.
668
674
  * --scope <sys_id> Required. sysparm_transaction_scope (app scope sys_id).
669
- * --patch-script "<find>::<replace>" Optional. Find/replace in the script step value.
670
- * --set-script <path> Optional. Replace the script step value from a file.
675
+ * --from-json <path> Optional. JSON EditActionTypeOps — the full surface, incl.
676
+ * per-step ops: patchStepScripts / addStepOutputs / addStepInputs.
677
+ * --patch-script "<find>::<replace>" Optional. Find/replace in the auto-detected script step value.
678
+ * --set-script <path> Optional. Replace the auto-detected script step value from a file.
671
679
  * --merge-outputs <path> Optional. JSON file: an output-variable object/array to merge by name.
672
680
  * --script-input <name> Optional. Input name holding the script (default: auto-detect).
673
681
  * --update-set <sys_id> Optional. Capture the republish into this update set.
674
682
  * --apply Optional. Republish (POST /snapshot). Omit for dry-run.
675
683
  * --json Optional. Emit the structured EditActionTypeResult.
676
684
  *
677
- * Edits a published Custom Action Type's script and/or output variables and
678
- * republishes through the snapshot POST. Dry-run (read-only) by default; --apply writes.
685
+ * Edits a published Custom Action Type and republishes through the snapshot POST.
686
+ * Dry-run (read-only) by default; --apply writes.
687
+ *
688
+ * The flag form handles the single-script case. For anything structural — patching
689
+ * several steps' scripts, adding a step-level output, adding a step-level input
690
+ * pill-wired to another step's output — use --from-json:
691
+ *
692
+ * {
693
+ * "patchStepScripts": [{ "step": "Parse Response", "scriptFile": "./parse.js" }],
694
+ * "addStepOutputs": [{ "step": "Parse Response", "name": "isRetryable", "type": "boolean" }],
695
+ * "addStepInputs": [{ "step": "Handle Error", "name": "isRetryable", "type": "boolean",
696
+ * "pillFrom": { "step": "Parse Response", "output": "isRetryable" } }]
697
+ * }
698
+ *
699
+ * `step` is a step cid or label. `scriptFile` is sugar for `setScript` and is
700
+ * resolved RELATIVE TO THE OPS FILE, so an ops file can sit next to its scripts.
679
701
  */
702
+ /** Resolve `scriptFile` sugar in patchStepScripts, relative to the ops file's own dir. */
703
+ function resolveScriptFiles(ops, opsPath) {
704
+ var stepScripts = ops.patchStepScripts;
705
+ if (!Array.isArray(stepScripts)) {
706
+ return;
707
+ }
708
+ var opsDir = path.dirname(path.resolve(opsPath));
709
+ for (var i = 0; i < stepScripts.length; i += 1) {
710
+ var op = stepScripts[i];
711
+ if (!op || typeof op !== "object" || typeof op.scriptFile !== "string") {
712
+ continue;
713
+ }
714
+ if (typeof op.setScript === "string") {
715
+ throw new Error("edit-action: step '" +
716
+ String(op.step) +
717
+ "' sets both scriptFile and setScript — pick one.");
718
+ }
719
+ op.setScript = fs.readFileSync(path.resolve(opsDir, op.scriptFile), "utf8");
720
+ delete op.scriptFile;
721
+ }
722
+ }
680
723
  async function runEditAction(flags) {
681
724
  var sysId = flags["sys-id"] || flags.sysId;
682
725
  var scope = flags.scope || flags.scopeSysId;
@@ -685,6 +728,14 @@ async function runEditAction(flags) {
685
728
  return 1;
686
729
  }
687
730
  var ops = {};
731
+ if (flags["from-json"]) {
732
+ ops = JSON.parse(fs.readFileSync(flags["from-json"], "utf8"));
733
+ if (!ops || typeof ops !== "object" || Array.isArray(ops)) {
734
+ process.stderr.write("edit-action: --from-json must contain an EditActionTypeOps object\n");
735
+ return 1;
736
+ }
737
+ resolveScriptFiles(ops, flags["from-json"]);
738
+ }
688
739
  if (flags["patch-script"]) {
689
740
  var parts = String(flags["patch-script"]).split("::");
690
741
  if (parts.length !== 2) {
@@ -730,6 +781,51 @@ async function runEditAction(flags) {
730
781
  for (var wi = 0; wi < result.warnings.length; wi += 1) {
731
782
  process.stdout.write(" ! " + result.warnings[wi] + "\n");
732
783
  }
784
+ // Per-step before/after — the dry-run's whole job is to make this inspectable.
785
+ if (result.stepsBefore && result.stepsAfter) {
786
+ process.stdout.write("\n--- steps (before -> after) ---\n");
787
+ for (var si = 0; si < result.stepsAfter.length; si += 1) {
788
+ var after = result.stepsAfter[si];
789
+ var before = result.stepsBefore[si];
790
+ var io = function (label, list) {
791
+ if (list.length === 0) {
792
+ return "";
793
+ }
794
+ var rendered = list
795
+ .map(function (e) {
796
+ return e.name + (e.value ? "=" + e.value : "");
797
+ })
798
+ .join(", ");
799
+ return "\n " + label + ": " + rendered;
800
+ };
801
+ process.stdout.write(" " +
802
+ after.label +
803
+ " (" +
804
+ after.cid +
805
+ ")\n" +
806
+ " script: " +
807
+ String(before ? before.scriptChars : "?") +
808
+ " -> " +
809
+ String(after.scriptChars) +
810
+ " chars" +
811
+ io("in ", after.extendedInputs) +
812
+ io("out", after.extendedOutputs) +
813
+ "\n");
814
+ }
815
+ }
816
+ if (result.verified) {
817
+ process.stdout.write("\n--- verify (read back from the instance) ---\n");
818
+ process.stdout.write(" " + (result.verified.ok ? "OK" : "FAILED") + "\n");
819
+ for (var vi = 0; vi < result.verified.notes.length; vi += 1) {
820
+ process.stdout.write(" " +
821
+ (result.verified.ok ? "+ " : "! ") +
822
+ result.verified.notes[vi] +
823
+ "\n");
824
+ }
825
+ if (!result.verified.ok) {
826
+ return 2;
827
+ }
828
+ }
733
829
  if (result.status === "preview" &&
734
830
  result.scriptAfter !== undefined &&
735
831
  result.scriptAfter !== result.scriptBefore) {
@@ -786,6 +882,17 @@ function printHelp() {
786
882
  " [--name <element>] [--max-length <n>] [--reference <table>]\n" +
787
883
  " [--mandatory] [--default <v>] [--scope <s>] [--dry-run] [--json])\n" +
788
884
  " --update-set is REQUIRED on the live path (not for --dry-run).\n" +
885
+ " set-column Update an EXISTING column's SCHEMA (label/mandatory/default/read-only/max-length),\n" +
886
+ " into an update set, then verify against the instance\n" +
887
+ " (--table <t> --column <c> --update-set <sys_id>\n" +
888
+ " [--label <l>] [--mandatory true|false] [--default <v>]\n" +
889
+ " [--read-only true|false] [--max-length <n>]\n" +
890
+ " [--dry-run] [--json])\n" +
891
+ " A max-length SHRINK is REFUSED while rows hold longer values —\n" +
892
+ " ServiceNow silently ignores such a shrink (200 OK, no change).\n" +
893
+ " Shorten or clear those values first, then re-run.\n" +
894
+ " --element / --internal-type are REFUSED with an explanation:\n" +
895
+ " ServiceNow silently ignores both on an existing column.\n" +
789
896
  " invoke-rest Invoke an arbitrary authenticated REST operation (Scripted REST incl.)\n" +
790
897
  " DRY-RUN BY DEFAULT — nothing is sent without --confirm\n" +
791
898
  " (--method <GET|POST|PUT|DELETE> --path /api/<scope>/<service>/<resource>\n" +
@@ -801,6 +908,12 @@ function printHelp() {
801
908
  " [--update-set <sys_id>] [--max-bytes <n>] [--allow-oversize] [--dry-run] [--json])\n" +
802
909
  " test-flow Validate (default) or run a flow/subflow\n" +
803
910
  " (--sys-id <sys_id> [--execute --confirm] [--inputs <json>] [--json])\n" +
911
+ " edit-action Patch a published Custom Action Type and republish (snapshot)\n" +
912
+ " (--sys-id <sys_id> --scope <sys_id>\n" +
913
+ " --from-json <ops.json> ops: patchStepScripts / addStepOutputs / addStepInputs\n" +
914
+ " (per-step scripts + step IO + data-pill wiring)\n" +
915
+ ' | --patch-script "<find>::<replace>" | --set-script <path> | --merge-outputs <path>\n' +
916
+ " [--script-input <name>] [--update-set <sys_id>] [--apply] [--json])\n" +
804
917
  " edit-flow Patch a flow/subflow (rename, description, step inputs)\n" +
805
918
  " (--sys-id <sys_id> --from-json <ops.json> [--apply] [--update-set <sys_id>] [--scope <sys_id>] [--json])\n" +
806
919
  " mcp Run the MCP stdio server (--smoke lists tools and exits)\n" +
@@ -991,6 +1104,111 @@ async function runAddColumn(flags) {
991
1104
  return 2;
992
1105
  return 0;
993
1106
  }
1107
+ /** Parse a CLI boolean flag. Bare `--mandatory` means true; `--mandatory false` means
1108
+ * false. Anything else is rejected rather than quietly coerced to `true`. */
1109
+ function parseBoolFlag(name, raw) {
1110
+ if (raw === "true")
1111
+ return true;
1112
+ if (raw === "false")
1113
+ return false;
1114
+ throw new Error("set-column: --" + name + " must be true or false (got '" + raw + "').");
1115
+ }
1116
+ /**
1117
+ * dove-sn set-column:
1118
+ * --table x_cadso_journey --column description --update-set <sys_id>
1119
+ * [--label "Description"] [--mandatory true|false] [--default <v>]
1120
+ * [--read-only true|false] [--max-length 4000] [--dry-run] [--json]
1121
+ *
1122
+ * Updates an EXISTING column's schema. `internal_type` and a rename are refused —
1123
+ * ServiceNow silently ignores both on an existing column. To CREATE one, use add-column;
1124
+ * to set a RECORD's value, use set-field.
1125
+ */
1126
+ async function runSetColumn(flags, bare) {
1127
+ // A string flag whose value was forgotten arrives as the literal "true" — `--label`
1128
+ // with nothing after it would rename the column to "true". Booleans legitimately do
1129
+ // that, strings never do, so refuse rather than silently write nonsense.
1130
+ var stringFlags = ["label", "default", "table", "column", "update-set"];
1131
+ for (var f = 0; f < stringFlags.length; f += 1) {
1132
+ if (bare[stringFlags[f]]) {
1133
+ process.stderr.write("set-column: --" +
1134
+ stringFlags[f] +
1135
+ " needs a value (it was given none).\n");
1136
+ return 1;
1137
+ }
1138
+ }
1139
+ var table = flags.table;
1140
+ var column = flags.column;
1141
+ if (!table || !column) {
1142
+ process.stderr.write("set-column: --table and --column are required " +
1143
+ "(--update-set is required too, unless --dry-run)\n");
1144
+ return 1;
1145
+ }
1146
+ var attributes = {};
1147
+ // Accepted so that setColumn can REFUSE them by name with the reason. Dropping them
1148
+ // silently would leave someone who asked for a rename believing it happened.
1149
+ if (flags.element !== undefined)
1150
+ attributes.element = flags.element;
1151
+ if (flags["internal-type"] !== undefined) {
1152
+ attributes.internalType = flags["internal-type"];
1153
+ }
1154
+ if (flags.label !== undefined)
1155
+ attributes.label = flags.label;
1156
+ if (flags.default !== undefined)
1157
+ attributes.default = flags.default;
1158
+ if (flags.mandatory !== undefined) {
1159
+ attributes.mandatory = parseBoolFlag("mandatory", flags.mandatory);
1160
+ }
1161
+ if (flags["read-only"] !== undefined) {
1162
+ attributes.readOnly = parseBoolFlag("read-only", flags["read-only"]);
1163
+ }
1164
+ if (flags["max-length"] !== undefined) {
1165
+ var len = Number(flags["max-length"]);
1166
+ // sys_dictionary.max_length is an integer; the MCP schema enforces int() too.
1167
+ if (!Number.isInteger(len) || len < 1) {
1168
+ process.stderr.write("set-column: --max-length must be a positive integer\n");
1169
+ return 1;
1170
+ }
1171
+ attributes.maxLength = len;
1172
+ }
1173
+ var params = {
1174
+ client: (0, client_1.createClient)({}),
1175
+ table: table,
1176
+ column: column,
1177
+ attributes: attributes,
1178
+ };
1179
+ // Accept the same alias pair as the other verbs (create-view, set-list-layout, …).
1180
+ var setColumnUs = flags["update-set"] || flags.updateSetSysId;
1181
+ if (setColumnUs)
1182
+ params.updateSetSysId = setColumnUs;
1183
+ if (flags["dry-run"] === "true")
1184
+ params.dryRun = true;
1185
+ var result = await (0, table_1.setColumn)(params);
1186
+ if (flags.json === "true") {
1187
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
1188
+ }
1189
+ else {
1190
+ process.stdout.write("[" +
1191
+ result.status +
1192
+ "] " +
1193
+ result.table +
1194
+ "." +
1195
+ result.column +
1196
+ (result.verified && result.status === "applied" ? " — verified" : "") +
1197
+ (result.status === "applied" && !result.capturedInUpdateSet
1198
+ ? " — NOT CAPTURED"
1199
+ : "") +
1200
+ "\n" +
1201
+ result.note +
1202
+ "\n");
1203
+ }
1204
+ // 2 = the write landed but the instance does not reflect it (or it was not captured),
1205
+ // which must not read as success to a script.
1206
+ if (result.status === "failed")
1207
+ return 2;
1208
+ if (result.status === "applied" && !result.capturedInUpdateSet)
1209
+ return 2;
1210
+ return 0;
1211
+ }
994
1212
  /** Parse inline `--fields "k=v, k2=v2"` into a field map. */
995
1213
  function parseFieldsInline(input) {
996
1214
  var out = {};
@@ -1292,6 +1510,9 @@ async function main() {
1292
1510
  if (parsed.command === "add-column") {
1293
1511
  return await runAddColumn(parsed.flags);
1294
1512
  }
1513
+ if (parsed.command === "set-column") {
1514
+ return await runSetColumn(parsed.flags, parsed.bare);
1515
+ }
1295
1516
  if (parsed.command === "set-field") {
1296
1517
  return await runSetField(parsed.flags);
1297
1518
  }
@@ -23,6 +23,7 @@
23
23
  * Full write-up: docs/servicenow-flow-designer-headless-authoring.md.
24
24
  */
25
25
  import type { ServiceNowClient } from "../client";
26
+ import type { AddStepInputOp, AddStepOutputOp, PatchStepScriptOp, StepSummary, VerifyStepsResult } from "./stepOps";
26
27
  export interface EditActionTypeOps {
27
28
  /** Replace every occurrence of `find` with `replace` inside the script step value. */
28
29
  patchScript?: {
@@ -31,6 +32,25 @@ export interface EditActionTypeOps {
31
32
  };
32
33
  /** Replace the script step value outright (wins over patchScript). */
33
34
  setScript?: string;
35
+ /**
36
+ * Per-step script edits, addressing each step by `cid` or `label`. Use this
37
+ * instead of patchScript/setScript when the action has more than one scripted
38
+ * step, or when you need to target a specific one rather than the auto-detected
39
+ * first match.
40
+ */
41
+ patchStepScripts?: Array<PatchStepScriptOp>;
42
+ /**
43
+ * Step-level outputs (`extended_outputs`) to add — the values one step exposes
44
+ * to the steps after it. Idempotent: an output whose name is already present is
45
+ * skipped, not duplicated.
46
+ */
47
+ addStepOutputs?: Array<AddStepOutputOp>;
48
+ /**
49
+ * Step-level inputs (`extended_inputs`) to add, each wired by data pill to
50
+ * another step's output via `pillFrom: { step, output }`. Outputs added in the
51
+ * same call are visible to these — it all lands in one snapshot.
52
+ */
53
+ addStepInputs?: Array<AddStepInputOp>;
34
54
  /**
35
55
  * Output-variable definition objects to merge into `model.outputs`, matched by
36
56
  * `name` (replaced in place, else appended). Supply the modeled output JSON —
@@ -63,6 +83,15 @@ export interface EditActionTypeResult {
63
83
  scriptBefore?: string;
64
84
  scriptAfter?: string;
65
85
  outputsMerged: Array<string>;
86
+ /** Per-step scripts + step-level IO, as read (before) and as sent (after). */
87
+ stepsBefore?: Array<StepSummary>;
88
+ stepsAfter?: Array<StepSummary>;
89
+ /**
90
+ * Post-publish read-back of /step_instances for the steps we touched. A 201 from
91
+ * /snapshot means "compiled", not "your edit landed" — only set when applied and
92
+ * step ops were supplied.
93
+ */
94
+ verified?: VerifyStepsResult;
66
95
  /** HTTP status of the snapshot POST (201 on success); only set when applied. */
67
96
  httpStatus?: number;
68
97
  snapshotSysId?: string;