@tenonhq/dovetail-servicenow 0.0.47 → 0.0.48

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
@@ -110,6 +110,19 @@ file — handy for CI.
110
110
 
111
111
  ## CLI
112
112
 
113
+ ### Getting help
114
+
115
+ ```bash
116
+ npx dove-sn help # verb index
117
+ npx dove-sn help add-choices # one verb: required/optional flags, value formats, write gate, example
118
+ npx dove-sn add-choices --help # same — never loads credentials or touches the instance
119
+ ```
120
+
121
+ Every verb's usage is rendered from one table (`src/cliUsage.ts`), so the help
122
+ and the dispatcher cannot drift — a test fails if a verb is missing an entry. A
123
+ `Missing required flags` error prints that verb's usage block under the error
124
+ line; a mistyped verb lists the closest matches and exits 1.
125
+
113
126
  ```bash
114
127
  # Inline form
115
128
  npx dove-sn add-choices \
@@ -127,8 +140,19 @@ npx dove-sn remove-choices \
127
140
  --column state \
128
141
  --update-set 0083c3bb33d003507b18bc534d5c7b6d \
129
142
  --values "expired,failed"
143
+
144
+ # Preview either verb — reads happen, NOTHING is written
145
+ npx dove-sn add-choices --table ... --column ... --update-set ... --choices "..." --dry-run
146
+ npx dove-sn remove-choices --table ... --column ... --update-set ... --values "..." --dry-run
130
147
  ```
131
148
 
149
+ `--dry-run` still resolves the field and the update set (so a mistyped column or a
150
+ closed update set fails exactly as it would live), then reports what the live run
151
+ would do without sending a single write. Rows are tagged `[would create]` /
152
+ `[would update]` / `[would deactivate]` — never `[created]` — and the header reads
153
+ `(DRY RUN — nothing written)`. The `--json` result carries `dryRun: true` and the
154
+ same `would-*` action values.
155
+
132
156
  JSON payload shape:
133
157
 
134
158
  ```json
@@ -154,6 +178,14 @@ place of `choices`.
154
178
  left alone — retiring values does not un-make the column a choice field.
155
179
  - **Idempotent.** `deactivated` (a live row was flipped) / `unchanged` (already
156
180
  inactive) / `missing` (no such value on the field). Re-running writes nothing.
181
+ - **Case-sensitive, with a hint.** `sys_choice.value` is case-sensitive, so
182
+ `--values DELIVERED` does **not** match a stored `delivered` — it reports
183
+ `missing`. When the field holds the same spelling in a different case, the row
184
+ carries `nearMatches` (the stored spelling(s)) and the CLI prints
185
+ `[missing] DELIVERED — no exact match; did you mean "delivered"? (choice values are case-sensitive)`.
186
+ Matching is never case-folded: a field holding both `delivered` and
187
+ `Delivered` treats them as two distinct values, and a request for either one
188
+ touches only its own row.
157
189
  - **Duplicates.** `sys_choice` has no uniqueness constraint on
158
190
  `(name, element, value, language)`, so a field can hold several live rows for
159
191
  one value. Every live row is deactivated, and `sysIds` lists all of them — a
@@ -181,8 +213,8 @@ var result = await addChoicesToField(client, { /* ... */ });
181
213
 
182
214
  console.log(result.choices);
183
215
  // [
184
- // { value: "delivered", label: "Delivered", sysId: "...", action: "created" },
185
- // { value: "failed", label: "Failed", sysId: "...", action: "created" }
216
+ // { value: "delivered", label: "Delivered", sysId: "...", sysIds: ["..."], action: "created" },
217
+ // { value: "failed", label: "Failed", sysId: "...", sysIds: ["..."], action: "created" }
186
218
  // ]
187
219
 
188
220
  var removed = await removeChoicesFromField(client, {
@@ -192,8 +224,48 @@ var removed = await removeChoicesFromField(client, {
192
224
  values: ["expired"],
193
225
  });
194
226
  // removed.choices[0] -> { value: "expired", sysId: "...", sysIds: ["..."], action: "deactivated" }
227
+
228
+ // Plan without writing — identical reads, zero writes, `would-*` actions
229
+ var plan = await addChoicesToField(client, { /* ... */ dryRun: true });
230
+ // plan.dryRun -> true; plan.choices[0].action -> "would-create" | "would-update" | "unchanged"
231
+ ```
232
+
233
+ ### Result shape
234
+
235
+ Both verbs return the **same `field` envelope**, so one consumer can format either:
236
+
237
+ ```ts
238
+ interface ChoiceFieldRef {
239
+ table: string;
240
+ column: string;
241
+ language: string; // remove: the language matched; add: the default for choices without one
242
+ scope: string; // sys_scope sys_id of the dictionary record
243
+ dictionarySysId: string;
244
+ }
245
+
246
+ interface AddChoicesResult {
247
+ field: ChoiceFieldRef;
248
+ dictionary: { choiceWas: ChoiceType; choiceNow: ChoiceType }; // add-only transition
249
+ updateSet: { sysId: string; name: string };
250
+ dryRun: boolean;
251
+ choices: Array<ChoiceActionResult>; // action: created | updated | unchanged | would-create | would-update
252
+ }
253
+
254
+ interface RemoveChoicesResult {
255
+ field: ChoiceFieldRef;
256
+ updateSet: { sysId: string; name: string };
257
+ dryRun: boolean;
258
+ choices: Array<ChoiceRemovalResult>; // action: deactivated | unchanged | missing | would-deactivate
259
+ } // + nearMatches?: string[] on a case-only "missing"
195
260
  ```
196
261
 
262
+ > **Breaking change (0.0.x).** Earlier releases returned
263
+ > `AddChoicesResult.dictionary: { sysId, scope, choiceWas, choiceNow }` and a
264
+ > `RemoveChoicesResult.field` without `scope`. The dictionary sys_id now lives at
265
+ > `field.dictionarySysId` on both verbs and `scope` at `field.scope`;
266
+ > `dictionary` keeps only the choice-type transition. Consumers reading
267
+ > `result.dictionary.sysId` or `result.dictionary.scope` must move to `result.field`.
268
+
197
269
  Both verbs write one value at a time. If a write fails partway through they
198
270
  throw a **`ChoiceWriteError`** carrying `completed` (the values that already
199
271
  landed, in result shape), `failedValue` (the one that threw — its state on the
@@ -769,10 +841,70 @@ already matches a row). Exit codes: `0` created / skipped-in-sync / dry-run, `1`
769
841
  args, `2` write landed unverified (or skipped with drift). To **update** an existing
770
842
  record instead, use `set-field`.
771
843
 
772
- Both verbs are exported for programmatic use:
844
+ #### Field sources (`--fields`, `--from-json`, `--from-stdin`)
845
+
846
+ Both verbs take their field map from any combination of three sources; on a shared
847
+ key the file / stdin value wins over the inline one.
848
+
849
+ | Source | Use it for |
850
+ | --- | --- |
851
+ | `--fields "k=v,k2=v2"` | Short scalar values. Splits on commas and trims, so it cannot carry a comma, newline or `=`. |
852
+ | `--from-json <path>` | A flat JSON object `{ "field": "value" }` — any character, any size (script bodies, HTML, JSON blobs). |
853
+ | `--from-stdin` (alias `--from-json -`) | The same JSON object, piped in: `cat fields.json \| npx dove-sn create-record ... --from-stdin`. |
854
+
855
+ **stdin is read only when you ask for it.** Without `--from-stdin` / `--from-json -`
856
+ no `dove-sn` verb touches stdin at all, so a process launched with an open-but-idle
857
+ stdin pipe (an agent harness running it in the background) returns immediately instead
858
+ of waiting for input that never comes; a missing field source is an immediate usage
859
+ error (exit `1`), never a prompt. `--from-stdin` refuses a terminal stdin and an empty
860
+ stream with an actionable message.
861
+
862
+ #### Read-back verification and the HTML sanitizer
863
+
864
+ After the write both verbs re-query the record and compare every requested value to
865
+ what was stored. ServiceNow's HTML sanitizer rewrites characters in `html` /
866
+ `translated_html` / `wiki` fields on save (for example `@` becomes `&#64;`), which used
867
+ to surface as a false **mismatch** on a record that was in fact correct. The compare is
868
+ now strict byte-for-byte first, and only when the **stored** value contains an HTML
869
+ entity reference are both sides entity-decoded and compared again; a match found that
870
+ way is reported as verified with a note naming the normalized field(s). A genuinely
871
+ different value still decodes to something different and still reports `failed`
872
+ (exit `2`), and the failure note names the mismatched field(s).
873
+
874
+ ### Delete a record
875
+
876
+ Delete **one** existing data record by table + sys_id, with the record read back **before** (so the dry-run shows exactly what would go,
877
+ and a missing record is an error rather than a "successful" delete of nothing) and
878
+ **after** (success is never reported until the record is confirmed gone).
879
+
880
+ ```bash
881
+ # Dry-run (the default) — prints the record snapshot, deletes nothing
882
+ npx dove-sn delete-record \
883
+ --table x_cadso_core_metric_point_type --sys-id <32-hex sys_id> \
884
+ --update-set <sys_id>
885
+
886
+ # Apply — deletes, then reads back and verifies the record is gone
887
+ npx dove-sn delete-record \
888
+ --table x_cadso_core_metric_point_type --sys-id <32-hex sys_id> \
889
+ --update-set <sys_id> --apply --json
890
+ ```
891
+
892
+ `delete-record` wraps the core `deleteRecord` op. It is **dry-run by default** —
893
+ nothing is deleted without `--apply` (`--dry-run` wins if both are given). `--sys-id`
894
+ must be a 32-character lowercase hex id and `--table` a plain table name; both are
895
+ validated before any network call. `--update-set` is **required** and sent with the
896
+ delete, but **the capture is not pinned yet**: until
897
+ [#297](https://github.com/TenonHQ/Dovetail/issues/297) ships server-side, the op ignores
898
+ `update_set_sys_id` and captures the delete into the session's **current** update set —
899
+ make that the set you want before `--apply`. Every result note repeats this caveat; the
900
+ client keeps sending the field so it takes effect the moment the server honours it. Like its siblings it **refuses** schema tables (`sys_db_object` /
901
+ `sys_dictionary`). Exit codes: `0` deleted / dry-run, `1` bad args or no such record,
902
+ `2` the delete returned but the record is **still present** on read-back.
903
+
904
+ All three verbs are exported for programmatic use:
773
905
 
774
906
  ```ts
775
- import { createClient, setField, createRecord } from "@tenonhq/dovetail-servicenow";
907
+ import { createClient, setField, createRecord, deleteRecord } from "@tenonhq/dovetail-servicenow";
776
908
 
777
909
  var client = createClient({});
778
910
  var r = await setField({
@@ -1027,8 +1159,13 @@ does not exist) / `index_create` (create an index, composite and non-unique incl
1027
1159
  replaying the platform index-creator form; dry-run by default, idempotent, read back from
1028
1160
  `v_db_index` - and **not** captured in an update set, because a database index is a
1029
1161
  physical per-instance change), the record-write verbs `set_field` (update scalar fields on an
1030
- existing record) and `create_record` (insert one record) — both update-set-captured
1031
- and read-back-verified — `host_assets` (deploy a built dist/), plus the Flow Designer
1162
+ existing record), `create_record` (insert one record) and `delete_record` (delete one
1163
+ record — dry-run by default, `confirm:true` to apply, `updateSetSysId` required, the
1164
+ record read back before AND after so success is only reported once it is confirmed
1165
+ gone) — all read-back-verified; `set_field` / `create_record` are captured in the
1166
+ update set you pass, while `delete_record` captures into the session's current set
1167
+ until [#297](https://github.com/TenonHQ/Dovetail/issues/297) ships — `host_assets` (deploy a built
1168
+ dist/), plus the Flow Designer
1032
1169
  tools `flow_view` (read a flow/subflow's step graph), `action_view` (read an action
1033
1170
  type's model), `action_edit` (structurally edit a published action type — per-step
1034
1171
  scripts, step-level inputs/outputs, data-pill wiring — dry-run by default, and the
@@ -1060,6 +1197,10 @@ npx dove-sn mcp # run the stdio server (wire into .mcp.json)
1060
1197
 
1061
1198
  This server is separate from `@tenonhq/dovetail-mcp` (the read-only cross-system
1062
1199
  aggregator) — `dovetail-servicenow`'s server is the ServiceNow **write** surface.
1200
+ `dovetail-mcp` intentionally does **not** get `delete_record` (or any other ServiceNow
1201
+ write): record writes — create, set, delete — live on `dove-sn mcp` only, where every
1202
+ one takes an update set, previews before writing and is read-back-verified (the delete's
1203
+ update set is honoured server-side once #297 ships).
1063
1204
 
1064
1205
  ## Publishing a Custom Action Type
1065
1206
 
package/dist/choices.d.ts CHANGED
@@ -36,6 +36,11 @@ export declare function encodeQueryValue(v: string): string;
36
36
  * Upsert choices for a field and (optionally) toggle sys_dictionary.choice.
37
37
  * Idempotent: re-running with the same inputs returns `action: "unchanged"`
38
38
  * for every row and skips the dictionary write when no change is required.
39
+ *
40
+ * `params.dryRun` plans instead of writing: every read still happens (so a missing field
41
+ * or a closed update set still fails loudly), but no pushWithUpdateSet / createRecord
42
+ * call is made, and rows report "would-create" / "would-update" in place of
43
+ * "created" / "updated". The result's `dryRun` flag is true.
39
44
  */
40
45
  export declare function addChoicesToField(client: ServiceNowClient, params: AddChoicesParams): Promise<AddChoicesResult>;
41
46
  /**
@@ -48,7 +53,13 @@ export declare function addChoicesToField(client: ServiceNowClient, params: AddC
48
53
  * - active value -> "deactivated" (every live row for it flipped to inactive —
49
54
  * usually one write, but more when the field holds duplicates)
50
55
  * - already inactive -> "unchanged" (no write)
51
- * - value not found -> "missing" (no write)
56
+ * - value not found -> "missing" (no write; `nearMatches` lists any value on the
57
+ * field that differs by letter case alone — matching is strictly
58
+ * case-sensitive, this is a hint so the caller can tell a typo'd
59
+ * casing from a genuinely absent value)
60
+ *
61
+ * `params.dryRun` plans instead of writing: reads still happen, no pushWithUpdateSet is
62
+ * sent, and a live row reports "would-deactivate" in place of "deactivated".
52
63
  *
53
64
  * The dictionary row is fetched only to PROVE the field exists — without that guard a
54
65
  * mistyped column reports every value as "missing", the silent-failure this family
package/dist/choices.js CHANGED
@@ -231,6 +231,40 @@ function dedupe(values) {
231
231
  });
232
232
  return out;
233
233
  }
234
+ /**
235
+ * Values on the field (same language) that differ from `value` by letter case ALONE.
236
+ *
237
+ * sys_choice.value is case-sensitive, so "DELIVERED" is NOT "delivered" and the lookup
238
+ * must not fold case — but the two outcomes "that value is absent" and "that value is
239
+ * present, spelled with different casing" are otherwise indistinguishable to the caller.
240
+ * This surfaces the second so it can be reported as a hint next to "missing".
241
+ *
242
+ * Best-effort by design: it inspects the rows the scoped read already returned, and
243
+ * sends no extra request. ServiceNow's `IN` / `=` on string columns are case-insensitive
244
+ * at the database layer, which is exactly how a differently-cased row comes back for a
245
+ * `valueIN DELIVERED` query in the first place.
246
+ *
247
+ * Distinct values only, in first-seen order; a value that exactly equals `value` is
248
+ * excluded — that one is a real match and is handled by the caller before this runs.
249
+ */
250
+ function findCaseOnlyMatches(rows, language, value) {
251
+ var wanted = value.toLowerCase();
252
+ var out = [];
253
+ rows.forEach(function (row) {
254
+ if ((row.language || "en") !== language)
255
+ return;
256
+ if (typeof row.value !== "string")
257
+ return;
258
+ if (row.value === value)
259
+ return;
260
+ if (row.value.toLowerCase() !== wanted)
261
+ return;
262
+ if (out.indexOf(row.value) !== -1)
263
+ return;
264
+ out.push(row.value);
265
+ });
266
+ return out;
267
+ }
234
268
  function buildChoiceFields(table, column, choice, scope) {
235
269
  var fields = {
236
270
  name: table,
@@ -260,6 +294,11 @@ function isUnchanged(existing, choice) {
260
294
  * Upsert choices for a field and (optionally) toggle sys_dictionary.choice.
261
295
  * Idempotent: re-running with the same inputs returns `action: "unchanged"`
262
296
  * for every row and skips the dictionary write when no change is required.
297
+ *
298
+ * `params.dryRun` plans instead of writing: every read still happens (so a missing field
299
+ * or a closed update set still fails loudly), but no pushWithUpdateSet / createRecord
300
+ * call is made, and rows report "would-create" / "would-update" in place of
301
+ * "created" / "updated". The result's `dryRun` flag is true.
263
302
  */
264
303
  async function addChoicesToField(client, params) {
265
304
  if (!params.updateSetSysId) {
@@ -268,6 +307,9 @@ async function addChoicesToField(client, params) {
268
307
  if (!params.choices || params.choices.length === 0) {
269
308
  throw new Error("choices must be a non-empty array.");
270
309
  }
310
+ // Strict equality on purpose: a truthy-but-not-true value (the string "false" from a
311
+ // mis-parsed flag, say) must not silently turn a planned run into a live write.
312
+ var dryRun = params.dryRun === true;
271
313
  var dict = await fetchDictionary(client, params.table, params.column);
272
314
  var updateSet = await fetchUpdateSet(client, params.updateSetSysId);
273
315
  var scopeName = await resolveScopeName(client, dict.sys_scope);
@@ -279,12 +321,14 @@ async function addChoicesToField(client, params) {
279
321
  var choiceWas = Number(dict.choice);
280
322
  var choiceNow = choiceWas;
281
323
  if (params.choiceType !== null && Number(dict.choice) !== targetChoiceType) {
282
- await client.claude.pushWithUpdateSet({
283
- update_set_sys_id: params.updateSetSysId,
284
- table: "sys_dictionary",
285
- record_sys_id: dict.sys_id,
286
- fields: { choice: String(targetChoiceType) },
287
- });
324
+ if (!dryRun) {
325
+ await client.claude.pushWithUpdateSet({
326
+ update_set_sys_id: params.updateSetSysId,
327
+ table: "sys_dictionary",
328
+ record_sys_id: dict.sys_id,
329
+ fields: { choice: String(targetChoiceType) },
330
+ });
331
+ }
288
332
  choiceNow = targetChoiceType;
289
333
  }
290
334
  // Collapse repeats on language::value, last spec wins. Without this a value listed
@@ -329,6 +373,16 @@ async function addChoicesToField(client, params) {
329
373
  if (choice.sequence != null) {
330
374
  updFields.sequence = String(choice.sequence);
331
375
  }
376
+ if (dryRun) {
377
+ results.push({
378
+ value: choice.value,
379
+ label: choice.label,
380
+ sysId: matchSysIds[0],
381
+ sysIds: matchSysIds,
382
+ action: "would-update",
383
+ });
384
+ continue;
385
+ }
332
386
  try {
333
387
  for (var s = 0; s < stale.length; s += 1) {
334
388
  await client.claude.pushWithUpdateSet({
@@ -351,6 +405,18 @@ async function addChoicesToField(client, params) {
351
405
  });
352
406
  continue;
353
407
  }
408
+ if (dryRun) {
409
+ // No row exists yet, so there is no sys_id to report — "" mirrors the layout
410
+ // verbs' convention for a create that was only planned.
411
+ results.push({
412
+ value: choice.value,
413
+ label: choice.label,
414
+ sysId: "",
415
+ sysIds: [],
416
+ action: "would-create",
417
+ });
418
+ continue;
419
+ }
354
420
  var created;
355
421
  try {
356
422
  created = await client.claude.createRecord({
@@ -372,13 +438,19 @@ async function addChoicesToField(client, params) {
372
438
  });
373
439
  }
374
440
  return {
375
- dictionary: {
376
- sysId: dict.sys_id,
441
+ field: {
442
+ table: params.table,
443
+ column: params.column,
444
+ language: "en",
377
445
  scope: dict.sys_scope,
446
+ dictionarySysId: dict.sys_id,
447
+ },
448
+ dictionary: {
378
449
  choiceWas: choiceWas,
379
450
  choiceNow: choiceNow,
380
451
  },
381
452
  updateSet: { sysId: updateSet.sys_id, name: updateSet.name },
453
+ dryRun: dryRun,
382
454
  choices: results,
383
455
  };
384
456
  }
@@ -392,7 +464,13 @@ async function addChoicesToField(client, params) {
392
464
  * - active value -> "deactivated" (every live row for it flipped to inactive —
393
465
  * usually one write, but more when the field holds duplicates)
394
466
  * - already inactive -> "unchanged" (no write)
395
- * - value not found -> "missing" (no write)
467
+ * - value not found -> "missing" (no write; `nearMatches` lists any value on the
468
+ * field that differs by letter case alone — matching is strictly
469
+ * case-sensitive, this is a hint so the caller can tell a typo'd
470
+ * casing from a genuinely absent value)
471
+ *
472
+ * `params.dryRun` plans instead of writing: reads still happen, no pushWithUpdateSet is
473
+ * sent, and a live row reports "would-deactivate" in place of "deactivated".
396
474
  *
397
475
  * The dictionary row is fetched only to PROVE the field exists — without that guard a
398
476
  * mistyped column reports every value as "missing", the silent-failure this family
@@ -419,6 +497,8 @@ async function removeChoicesFromField(client, params) {
419
497
  throw new Error("values must be a non-empty array.");
420
498
  }
421
499
  var language = params.language || "en";
500
+ // Strict equality on purpose — see addChoicesToField.
501
+ var dryRun = params.dryRun === true;
422
502
  // fetchDictionary throws a clear error when the field does not exist; fetchUpdateSet
423
503
  // throws unless the set is in progress. Both mirror the add path's guards.
424
504
  var dict = await fetchDictionary(client, params.table, params.column);
@@ -435,7 +515,19 @@ async function removeChoicesFromField(client, params) {
435
515
  var value = values[i];
436
516
  var matches = existingByValue[language + "::" + value] || [];
437
517
  if (matches.length === 0) {
438
- results.push({ value: value, sysId: "", sysIds: [], action: "missing" });
518
+ // Exact match first, hint second: nearMatches is only ever computed once the
519
+ // strict lookup has come up empty, so it can never shadow a real match.
520
+ var missing = {
521
+ value: value,
522
+ sysId: "",
523
+ sysIds: [],
524
+ action: "missing",
525
+ };
526
+ var near = findCaseOnlyMatches(existing, language, value);
527
+ if (near.length > 0) {
528
+ missing.nearMatches = near;
529
+ }
530
+ results.push(missing);
439
531
  continue;
440
532
  }
441
533
  var sysIds = matches.map(function (row) {
@@ -455,6 +547,15 @@ async function removeChoicesFromField(client, params) {
455
547
  });
456
548
  continue;
457
549
  }
550
+ if (dryRun) {
551
+ results.push({
552
+ value: value,
553
+ sysId: sysIds[0],
554
+ sysIds: sysIds,
555
+ action: "would-deactivate",
556
+ });
557
+ continue;
558
+ }
458
559
  try {
459
560
  for (var a = 0; a < active.length; a += 1) {
460
561
  await client.claude.pushWithUpdateSet({
@@ -480,9 +581,11 @@ async function removeChoicesFromField(client, params) {
480
581
  table: params.table,
481
582
  column: params.column,
482
583
  language: language,
584
+ scope: dict.sys_scope,
483
585
  dictionarySysId: dict.sys_id,
484
586
  },
485
587
  updateSet: { sysId: updateSet.sys_id, name: updateSet.name },
588
+ dryRun: dryRun,
486
589
  choices: results,
487
590
  };
488
591
  }
package/dist/cli.d.ts CHANGED
@@ -3,22 +3,30 @@
3
3
  * dove-sn — thin CLI adapter for @tenonhq/dovetail-servicenow.
4
4
  *
5
5
  * Usage:
6
- * dove-sn add-choices \
7
- * --table x_cadso_core_event \
8
- * --column state \
9
- * --update-set <sys_id> \
10
- * --choices 'delivered=Delivered,failed=Failed,...' \
11
- * [--choice-type 3] [--json]
6
+ * dove-sn help verb index
7
+ * dove-sn help <verb> one verb's flags, value formats, write gate, example
8
+ * dove-sn <verb> --help same — never loads an env file or builds a client
9
+ * dove-sn <verb> [flags]
12
10
  *
13
- * dove-sn add-choices --from-json path/to/choices.json
14
- *
15
- * JSON payload shape:
16
- * {
17
- * "table": "x_cadso_core_event",
18
- * "column": "state",
19
- * "updateSetSysId": "...",
20
- * "choiceType": 3,
21
- * "choices": [{ "value": "delivered", "label": "Delivered" }, ...]
22
- * }
11
+ * Every verb's usage is rendered from VERB_USAGE in ./cliUsage — add the entry there
12
+ * when you add a dispatch site below; cliHelp.test.ts fails on a missing one.
23
13
  */
14
+ interface ParsedArgs {
15
+ command: string;
16
+ flags: Record<string, string>;
17
+ /**
18
+ * Flags that arrived with no value at all (`--label` followed by another flag, or by
19
+ * nothing). They land in `flags` as the string "true", which is right for a boolean and
20
+ * a trap for a string: `--label` with a forgotten value would rename a column to "true".
21
+ * Recorded here so a verb can tell the two apart and refuse.
22
+ */
23
+ bare: Record<string, boolean>;
24
+ /**
25
+ * Non-flag tokens after the command that no flag consumed as its value —
26
+ * `dove-sn help add-choices` carries the verb here.
27
+ */
28
+ positional: Array<string>;
29
+ }
30
+ export declare function parseArgs(argv: Array<string>): ParsedArgs;
31
+ export declare function main(argv: Array<string>): Promise<number>;
24
32
  export {};