@tenonhq/dovetail-servicenow 0.0.36 → 0.0.38
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 +118 -1
- package/dist/cli.js +351 -1
- package/dist/exportApp.d.ts +102 -0
- package/dist/exportApp.js +306 -0
- package/dist/exportUpdateSet.d.ts +133 -0
- package/dist/exportUpdateSet.js +506 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.js +46 -1
- package/dist/mcp/registry.d.ts +1 -1
- package/dist/mcp/registry.js +129 -0
- package/dist/mcp/schemas.d.ts +83 -2
- package/dist/mcp/schemas.js +42 -1
- package/dist/secrets/secretRules.d.ts +112 -0
- package/dist/secrets/secretRules.js +402 -0
- package/dist/secrets/stripSecrets.d.ts +138 -0
- package/dist/secrets/stripSecrets.js +458 -0
- package/dist/table/addIndex.d.ts +130 -0
- package/dist/table/addIndex.js +588 -0
- package/dist/table/formSession.d.ts +8 -0
- package/dist/table/formSession.js +32 -0
- package/dist/table/index.d.ts +3 -1
- package/dist/table/index.js +7 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -367,6 +367,56 @@ adds diagnostics (app-switch status, resolved column key, assigned sys_id) to th
|
|
|
367
367
|
result note. Ground truth (the HAR dissection) lives in the CTO repo's create-table
|
|
368
368
|
docs.
|
|
369
369
|
|
|
370
|
+
### Add a unique index
|
|
371
|
+
|
|
372
|
+
Create a **single-column UNIQUE index** on an existing table, then read it back.
|
|
373
|
+
|
|
374
|
+
```bash
|
|
375
|
+
# Dry-run (the DEFAULT) - prints the plan, writes nothing and reads nothing
|
|
376
|
+
npx dove-sn add-index \
|
|
377
|
+
--table x_cadso_journey_instance --columns occurrence_key --unique \
|
|
378
|
+
--update-set <sys_id> --json
|
|
379
|
+
|
|
380
|
+
# Send it
|
|
381
|
+
npx dove-sn add-index \
|
|
382
|
+
--table x_cadso_journey_instance --columns occurrence_key --unique \
|
|
383
|
+
--update-set <sys_id> --confirm
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
The **only** headless lever for an index is `sys_dictionary.unique`. `sys_index` fails an
|
|
387
|
+
API-LEVEL ACL (HTTP 403) for every identity - and an ACL that refuses `GET` refuses `POST` -
|
|
388
|
+
while `sys_index_column` does not exist at all (HTTP 400 `Invalid table`). So `add-index`
|
|
389
|
+
patches the column's dictionary row through the update-set-aware write path and lets the
|
|
390
|
+
platform build the physical index off that flag.
|
|
391
|
+
|
|
392
|
+
Three consequences, each reported rather than hidden:
|
|
393
|
+
|
|
394
|
+
- **One column, unique only.** `unique` is a per-COLUMN flag, so a composite index has no
|
|
395
|
+
dictionary lever. A multi-column request is **refused**, never narrowed to its first
|
|
396
|
+
column - building a different index than the one asked for is the worst available
|
|
397
|
+
outcome. `--unique` is required for the same reason. Composite and plain indexes stay
|
|
398
|
+
platform-UI work.
|
|
399
|
+
- **Duplicates abort the run BEFORE it writes.** A unique index cannot build over repeated
|
|
400
|
+
values, and ServiceNow fails that ALTER *silently* - leaving a dictionary row claiming
|
|
401
|
+
`unique=true` with no index behind it (which is exactly what
|
|
402
|
+
`x_cadso_core_metric_point.idempotency_key` looks like today). **EMPTY counts as a
|
|
403
|
+
value**: a freshly added column that is empty on every existing row is one collision per
|
|
404
|
+
row. Backfill first, index second. The scan is paged and capped, and a scan that hits
|
|
405
|
+
that cap **aborts the same way** - an UNPROVEN scan is treated exactly like a proven
|
|
406
|
+
collision, because writing on a column that was only read part-way is how this verb
|
|
407
|
+
would manufacture that trap on a table too big for anyone to have checked.
|
|
408
|
+
- **Success is read back; uniqueness never is.** `status` is `created` only when a matching
|
|
409
|
+
row was read back from the `v_db_index` view; a flag with no index is `failed`.
|
|
410
|
+
`verified.indexPresent` is `null` - UNKNOWN, not `false` - when the view could not be
|
|
411
|
+
read, because a blind instrument is not evidence of absence. And `v_db_index` carries no
|
|
412
|
+
uniqueness field (every row reads `btree`, unique or not), so **`uniqueness-enforced` is
|
|
413
|
+
listed in `unverified` on every status, success included**: only a duplicate-insert test
|
|
414
|
+
proves enforcement.
|
|
415
|
+
|
|
416
|
+
`--update-set` is required on the live path and is checked before a client is built or a
|
|
417
|
+
single request goes out. Exit codes: `0` created / skipped / dry-run, `1` bad args, `2`
|
|
418
|
+
failed (the lying-row case included).
|
|
419
|
+
|
|
370
420
|
### Set a field on a record
|
|
371
421
|
|
|
372
422
|
Set scalar field value(s) on an **existing** data record, capture the change into
|
|
@@ -505,6 +555,71 @@ Store `appLink`, and the publish's update-set sys_id where the instance reports
|
|
|
505
555
|
one. Exit codes: `0` published or dry-run, `1` bad args/unconfirmed, `2`
|
|
506
556
|
failed/timeout. Programmatic: `publishApp({ app, version, target, confirm })`.
|
|
507
557
|
|
|
558
|
+
### Export an update set (or a whole app) to importable XML
|
|
559
|
+
|
|
560
|
+
Produce the `<unload>` document the implementation team imports on a customer
|
|
561
|
+
instance — with every secret value replaced by `__SET_DURING_INSTALL__`.
|
|
562
|
+
|
|
563
|
+
```bash
|
|
564
|
+
# An update set. assemble mode is READ-ONLY: nothing on the instance changes.
|
|
565
|
+
npx dove-sn export-update-set --update-set 0123456789abcdef0123456789abcdef \
|
|
566
|
+
--out ./tenon-core.xml
|
|
567
|
+
|
|
568
|
+
# A whole app: publish into a new set, then export it. Dry-run first (default).
|
|
569
|
+
npx dove-sn export-app --app x_cadso_automate --out ./automate.xml
|
|
570
|
+
npx dove-sn export-app --app x_cadso_automate --out ./automate.xml --confirm
|
|
571
|
+
|
|
572
|
+
# A document exported some other way
|
|
573
|
+
npx dove-sn strip-secrets --in ./exported.xml --out ./safe.xml
|
|
574
|
+
npx dove-sn strip-secrets --in ./exported.xml --report # what would be stripped
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
**Secret stripping is not optional.** There is no `--no-strip` flag on any of
|
|
578
|
+
these verbs, and no field in the MCP schemas that disables it. This matters
|
|
579
|
+
because an unload carries field *values*: on tenonworkstudio, completed update
|
|
580
|
+
sets hold filled-in values for `password2` system properties and
|
|
581
|
+
`oauth_entity.client_secret`, several of them in sets that shipped to a
|
|
582
|
+
customer. A field is exempted only by a reviewed entry in the rules file.
|
|
583
|
+
|
|
584
|
+
The rule is enumerable, in four layers:
|
|
585
|
+
|
|
586
|
+
| Layer | Covers |
|
|
587
|
+
|---|---|
|
|
588
|
+
| L1 type | `password` / `password2` fields, restricted to tables an update set can capture (`update_synch=true`), resolved through `super_class`. Refreshed from the live dictionary per run, with a committed baseline as the fallback. |
|
|
589
|
+
| L2 conditional / explicit | `sys_properties.value` when the property's `type` is a password type; named exceptions such as `x_cadso_core.google_translate_api_key`, which holds an API key in a *string* column. |
|
|
590
|
+
| L3 JSON | secrets nested inside a JSON blob field, stripped in place. |
|
|
591
|
+
| L4 heuristic | a field that merely *looks* secret. **Never stripped silently and never assumed safe** — the run fails and names it, until a human records it in the rules file as a strip rule or as `notSecret` with a reason. |
|
|
592
|
+
|
|
593
|
+
Override the rules with `--rules <file>`; the JSON is merged over the built-ins,
|
|
594
|
+
and the only subtractive key is `notSecret`, which requires a reason:
|
|
595
|
+
|
|
596
|
+
```json
|
|
597
|
+
{
|
|
598
|
+
"notSecret": [
|
|
599
|
+
{ "table": "x_cadso_core_thing", "field": "webhook_token", "reason": "public identifier, not a credential" }
|
|
600
|
+
],
|
|
601
|
+
"fieldRules": [
|
|
602
|
+
{ "id": "thing-signing-key", "table": "x_cadso_core_thing", "field": "signing_key", "reason": "inbound webhook HMAC key" }
|
|
603
|
+
]
|
|
604
|
+
}
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
Two more things refuse to produce a file rather than produce a wrong one: a
|
|
608
|
+
record count that does not match the set, and the documented **in-progress
|
|
609
|
+
empty 200** from `export_update_set.do` (the servlet streams a document only for
|
|
610
|
+
a *complete* set, and app-publish leaves the set in progress). After stripping,
|
|
611
|
+
the output is re-read and verified; a secret that somehow survived fails the run.
|
|
612
|
+
|
|
613
|
+
`export-update-set --mode complete` marks the set complete on the instance
|
|
614
|
+
first — a real write, so it needs `--confirm`. `export-app` publishes ~1000+
|
|
615
|
+
records into a new update set and is dry-run by default. Neither is the Store
|
|
616
|
+
publish; that is `publish-app`, which is externally visible.
|
|
617
|
+
|
|
618
|
+
Exit codes: `0` exported or dry-run, `1` bad args/unconfirmed, `2`
|
|
619
|
+
failed/timeout. Programmatic: `exportUpdateSet({ updateSet, mode })`,
|
|
620
|
+
`exportApp({ app, confirm })`, `stripSecrets(xml, rules)`. MCP:
|
|
621
|
+
`update_set_export` (read-only in assemble mode) and `app_export`.
|
|
622
|
+
|
|
508
623
|
`test-flow` defaults to **validate** — a safe pre-flight (published? inputs match
|
|
509
624
|
declared variables?) that never runs the flow; `--execute --confirm` runs it via
|
|
510
625
|
the server-side FlowAPI runner (deploy `resources/runFlow.md` first).
|
|
@@ -576,7 +691,9 @@ console.log(formatLayoutResult("form layout", result));
|
|
|
576
691
|
`dove-sn mcp` runs a self-contained MCP stdio server exposing the tools to
|
|
577
692
|
Claude Code and agents: `create_view`, `set_list_layout`, `set_form_layout`,
|
|
578
693
|
`set_related_lists`, `add_choices_to_field`, the schema verbs `create_table` /
|
|
579
|
-
`add_column
|
|
694
|
+
`add_column` / `add_index` (a single-column unique index via `sys_dictionary.unique`,
|
|
695
|
+
read back from the `v_db_index` view - uniqueness enforcement is always reported
|
|
696
|
+
unverified), the record-write verbs `set_field` (update scalar fields on an
|
|
580
697
|
existing record) and `create_record` (insert one record) — both update-set-captured
|
|
581
698
|
and read-back-verified — `host_assets` (deploy a built dist/), plus the Flow Designer
|
|
582
699
|
tools `flow_view` (read a flow/subflow's step graph), `action_view` (read an action
|
package/dist/cli.js
CHANGED
|
@@ -69,6 +69,10 @@ const formLayout_1 = require("./layout/formLayout");
|
|
|
69
69
|
const relatedLists_1 = require("./layout/relatedLists");
|
|
70
70
|
const formatter_2 = require("./layout/formatter");
|
|
71
71
|
const server_1 = require("./mcp/server");
|
|
72
|
+
const exportUpdateSet_1 = require("./exportUpdateSet");
|
|
73
|
+
const exportApp_1 = require("./exportApp");
|
|
74
|
+
const stripSecrets_1 = require("./secrets/stripSecrets");
|
|
75
|
+
const secretRules_1 = require("./secrets/secretRules");
|
|
72
76
|
const schemas_1 = require("./mcp/schemas");
|
|
73
77
|
const buildFlowOrchestrator_1 = require("./flowDesigner/buildFlowOrchestrator");
|
|
74
78
|
const flowDesigner_formatter_1 = require("./flowDesigner-formatter");
|
|
@@ -977,6 +981,20 @@ function printHelp() {
|
|
|
977
981
|
" [--name <element>] [--max-length <n>] [--reference <table>]\n" +
|
|
978
982
|
" [--mandatory] [--default <v>] [--scope <s>] [--dry-run] [--json])\n" +
|
|
979
983
|
" --update-set is REQUIRED on the live path (not for --dry-run).\n" +
|
|
984
|
+
" add-index Create a single-column UNIQUE index (sys_dictionary.unique), then verify\n" +
|
|
985
|
+
" DRY-RUN BY DEFAULT — nothing is written without --confirm\n" +
|
|
986
|
+
" (--table <name|sys_id> --columns <column> --unique --update-set <sys_id>\n" +
|
|
987
|
+
" [--confirm] [--scope <s>] [--dry-run] [--debug] [--json])\n" +
|
|
988
|
+
" ONE column only: unique is a per-COLUMN dictionary flag, so a\n" +
|
|
989
|
+
" composite index is REFUSED, not narrowed — that stays UI work,\n" +
|
|
990
|
+
" as does a plain (non-unique) index. The run ABORTS before writing\n" +
|
|
991
|
+
" when the column holds duplicate values (EMPTY counts): a unique\n" +
|
|
992
|
+
" index cannot build over them and the platform fails that ALTER\n" +
|
|
993
|
+
" SILENTLY, leaving unique=true with no index behind it. It aborts\n" +
|
|
994
|
+
" the same way when that scan hits its row cap — an UNPROVEN scan\n" +
|
|
995
|
+
" is treated exactly like a proven collision. Success is\n" +
|
|
996
|
+
" read back from v_db_index; that view has no uniqueness field, so\n" +
|
|
997
|
+
" ENFORCEMENT is always reported unverified.\n" +
|
|
980
998
|
" set-column Update an EXISTING column's SCHEMA (label/mandatory/default/read-only/max-length),\n" +
|
|
981
999
|
" into an update set, then verify against the instance\n" +
|
|
982
1000
|
" (--table <t> --column <c> --update-set <sys_id>\n" +
|
|
@@ -1034,6 +1052,22 @@ function printHelp() {
|
|
|
1034
1052
|
" [--dry-run] [--json] [--confirm])\n" +
|
|
1035
1053
|
" Store creds: SN_STORE_USERNAME/SN_STORE_PASSWORD in the --env file;\n" +
|
|
1036
1054
|
" the password is never a flag. Repo publish needs the sn_cicd role.\n" +
|
|
1055
|
+
" export-update-set Export an update set to importable <unload> XML, with secret\n" +
|
|
1056
|
+
" values replaced by __SET_DURING_INSTALL__ (no opt-out).\n" +
|
|
1057
|
+
" assemble mode is READ-ONLY; complete mode marks the set\n" +
|
|
1058
|
+
" complete on the instance and needs --confirm.\n" +
|
|
1059
|
+
" (--update-set <sys_id|name> --out <file>\n" +
|
|
1060
|
+
" [--mode assemble|complete]\n" +
|
|
1061
|
+
" [--rules <file>] [--page-size <n>] [--max-rows <n>]\n" +
|
|
1062
|
+
" [--dry-run] [--json] [--confirm])\n" +
|
|
1063
|
+
" export-app Publish a scoped app into a new update set and export it.\n" +
|
|
1064
|
+
" PUBLISHING IS A REAL INSTANCE WRITE (~1000+ records).\n" +
|
|
1065
|
+
" DRY-RUN BY DEFAULT — nothing is published without --confirm\n" +
|
|
1066
|
+
" (--app <scope|sys_id|name> --out <file> [--version <v>]\n" +
|
|
1067
|
+
" [--description <text>] [--include-data] [--rules <file>]\n" +
|
|
1068
|
+
" [--timeout-ms <n>] [--dry-run] [--json] [--confirm])\n" +
|
|
1069
|
+
" strip-secrets Strip secret values from an unload XML exported elsewhere\n" +
|
|
1070
|
+
" (--in <file> [--out <file>] [--rules <file>] [--report] [--json])\n" +
|
|
1037
1071
|
" mcp Run the MCP stdio server (--smoke lists tools and exits)\n" +
|
|
1038
1072
|
"\nGlobal flags:\n" +
|
|
1039
1073
|
" --env <path> Load credentials from a specific .env file (also --env-file,\n" +
|
|
@@ -1222,6 +1256,82 @@ async function runAddColumn(flags) {
|
|
|
1222
1256
|
return 2;
|
|
1223
1257
|
return 0;
|
|
1224
1258
|
}
|
|
1259
|
+
/**
|
|
1260
|
+
* dove-sn add-index:
|
|
1261
|
+
* --table x_cadso_journey_instance --columns occurrence_key --unique
|
|
1262
|
+
* --update-set <sys_id> [--confirm] [--scope x_cadso_journey] [--debug] [--json]
|
|
1263
|
+
*
|
|
1264
|
+
* DRY-RUN BY DEFAULT — nothing is written without --confirm (--dry-run forces a
|
|
1265
|
+
* dry-run even with it). --update-set is required on the live path and is checked
|
|
1266
|
+
* here, before a client is built or a single request goes out.
|
|
1267
|
+
*
|
|
1268
|
+
* Exit codes: 0 created / skipped / dry-run, 1 bad args, 2 failed (which includes
|
|
1269
|
+
* "the dictionary flag is set but no index was read back" — the lying-row case).
|
|
1270
|
+
*/
|
|
1271
|
+
async function runAddIndex(flags) {
|
|
1272
|
+
var table = flags.table;
|
|
1273
|
+
var columns = (flags.columns || "")
|
|
1274
|
+
.split(",")
|
|
1275
|
+
.map(function (c) {
|
|
1276
|
+
return c.trim();
|
|
1277
|
+
})
|
|
1278
|
+
.filter(function (c) {
|
|
1279
|
+
return c.length > 0;
|
|
1280
|
+
});
|
|
1281
|
+
if (!table || columns.length === 0) {
|
|
1282
|
+
process.stderr.write("add-index: --table and --columns <column> are required " +
|
|
1283
|
+
"(--unique too, and --update-set unless this is a dry-run)\n");
|
|
1284
|
+
return 1;
|
|
1285
|
+
}
|
|
1286
|
+
// The only headless lever is sys_dictionary.unique. Refuse a non-unique request by
|
|
1287
|
+
// name instead of building something else and calling it done.
|
|
1288
|
+
if (flags.unique !== "true") {
|
|
1289
|
+
process.stderr.write("add-index: --unique is required — the only headless lever is " +
|
|
1290
|
+
"sys_dictionary.unique, which has no equivalent for a plain (non-unique) " +
|
|
1291
|
+
"index. Create that one in the platform UI.\n");
|
|
1292
|
+
return 1;
|
|
1293
|
+
}
|
|
1294
|
+
// DRY-RUN BY DEFAULT: --confirm is what sends; --dry-run forces a plan even with it.
|
|
1295
|
+
var dryRun = flags["dry-run"] === "true" || flags.confirm !== "true";
|
|
1296
|
+
if (!dryRun && !flags["update-set"]) {
|
|
1297
|
+
process.stderr.write("add-index: --update-set is required on the live path (only a dry-run works without one)\n");
|
|
1298
|
+
return 1;
|
|
1299
|
+
}
|
|
1300
|
+
var params = {
|
|
1301
|
+
client: (0, client_1.createClient)({}),
|
|
1302
|
+
table: table,
|
|
1303
|
+
columns: columns,
|
|
1304
|
+
unique: true,
|
|
1305
|
+
dryRun: dryRun,
|
|
1306
|
+
};
|
|
1307
|
+
if (flags.scope)
|
|
1308
|
+
params.scope = flags.scope;
|
|
1309
|
+
if (flags["update-set"])
|
|
1310
|
+
params.updateSetSysId = flags["update-set"];
|
|
1311
|
+
if (flags.debug === "true")
|
|
1312
|
+
params.debug = true;
|
|
1313
|
+
var result = await (0, table_1.addIndex)(params);
|
|
1314
|
+
if (flags.json === "true") {
|
|
1315
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
1316
|
+
}
|
|
1317
|
+
else {
|
|
1318
|
+
process.stdout.write("[" +
|
|
1319
|
+
result.status +
|
|
1320
|
+
"] " +
|
|
1321
|
+
result.table +
|
|
1322
|
+
"." +
|
|
1323
|
+
result.columns.join(",") +
|
|
1324
|
+
(result.indexName ? " -> " + result.indexName : "") +
|
|
1325
|
+
"\n" +
|
|
1326
|
+
result.note +
|
|
1327
|
+
"\nUNVERIFIED: " +
|
|
1328
|
+
result.unverified.join(", ") +
|
|
1329
|
+
"\n");
|
|
1330
|
+
}
|
|
1331
|
+
if (result.status === "failed")
|
|
1332
|
+
return 2;
|
|
1333
|
+
return 0;
|
|
1334
|
+
}
|
|
1225
1335
|
/** Parse a CLI boolean flag. Bare `--mandatory` means true; `--mandatory false` means
|
|
1226
1336
|
* false. Anything else is rejected rather than quietly coerced to `true`. */
|
|
1227
1337
|
function parseBoolFlag(name, raw, verb = "set-column") {
|
|
@@ -1349,7 +1459,9 @@ async function runSetTable(flags, bare) {
|
|
|
1349
1459
|
var stringFlags = ["table", "update-set", "updateSetSysId"];
|
|
1350
1460
|
for (var f = 0; f < stringFlags.length; f += 1) {
|
|
1351
1461
|
if (bare[stringFlags[f]]) {
|
|
1352
|
-
process.stderr.write("set-table: --" +
|
|
1462
|
+
process.stderr.write("set-table: --" +
|
|
1463
|
+
stringFlags[f] +
|
|
1464
|
+
" needs a value (it was given none).\n");
|
|
1353
1465
|
return 1;
|
|
1354
1466
|
}
|
|
1355
1467
|
}
|
|
@@ -1812,6 +1924,232 @@ async function runPublishApp(flags) {
|
|
|
1812
1924
|
}
|
|
1813
1925
|
return exitCode;
|
|
1814
1926
|
}
|
|
1927
|
+
/**
|
|
1928
|
+
* dove-sn export-update-set:
|
|
1929
|
+
* --update-set <sys_id|name> Required. The set to export.
|
|
1930
|
+
* [--mode assemble|complete] assemble (default) is READ-ONLY; complete marks
|
|
1931
|
+
* the set complete on the instance first, which is
|
|
1932
|
+
* a real write and needs --confirm.
|
|
1933
|
+
* [--out <file>] Write the XML here (default: stdout is NOT used —
|
|
1934
|
+
* a document this size belongs in a file).
|
|
1935
|
+
* [--rules <file>] JSON overrides for the secret rules.
|
|
1936
|
+
* [--page-size <n>] [--max-rows <n>]
|
|
1937
|
+
* [--dry-run] [--json] [--confirm]
|
|
1938
|
+
*
|
|
1939
|
+
* Secret values are ALWAYS replaced with __SET_DURING_INSTALL__; there is no
|
|
1940
|
+
* opt-out flag. A field that looks secret and is covered by no rule fails the
|
|
1941
|
+
* run, and nothing is written.
|
|
1942
|
+
* Exit codes: 0 exported/dry-run, 1 bad args/unconfirmed, 2 failed.
|
|
1943
|
+
*/
|
|
1944
|
+
async function runExportUpdateSet(flags) {
|
|
1945
|
+
var selector = flags["update-set"];
|
|
1946
|
+
if (!selector) {
|
|
1947
|
+
process.stderr.write("export-update-set: --update-set <sys_id|name> is required\n");
|
|
1948
|
+
return 1;
|
|
1949
|
+
}
|
|
1950
|
+
var mode = flags.mode || "assemble";
|
|
1951
|
+
if (mode !== "assemble" && mode !== "complete") {
|
|
1952
|
+
process.stderr.write("export-update-set: --mode must be assemble or complete\n");
|
|
1953
|
+
return 1;
|
|
1954
|
+
}
|
|
1955
|
+
var outPath = flags.out;
|
|
1956
|
+
if (!outPath && flags["dry-run"] !== "true") {
|
|
1957
|
+
process.stderr.write("export-update-set: --out <file> is required for a real export\n");
|
|
1958
|
+
return 1;
|
|
1959
|
+
}
|
|
1960
|
+
var pageSize = undefined;
|
|
1961
|
+
if (flags["page-size"]) {
|
|
1962
|
+
pageSize = Number(flags["page-size"]);
|
|
1963
|
+
if (!Number.isInteger(pageSize) || pageSize < 1) {
|
|
1964
|
+
process.stderr.write("export-update-set: --page-size must be a positive integer\n");
|
|
1965
|
+
return 1;
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
var maxRows = undefined;
|
|
1969
|
+
if (flags["max-rows"]) {
|
|
1970
|
+
maxRows = Number(flags["max-rows"]);
|
|
1971
|
+
if (!Number.isInteger(maxRows) || maxRows < 1) {
|
|
1972
|
+
process.stderr.write("export-update-set: --max-rows must be a positive integer\n");
|
|
1973
|
+
return 1;
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
var result = await (0, exportUpdateSet_1.exportUpdateSet)({
|
|
1977
|
+
updateSet: selector,
|
|
1978
|
+
mode: mode,
|
|
1979
|
+
confirm: flags.confirm === "true",
|
|
1980
|
+
dryRun: flags["dry-run"] === "true",
|
|
1981
|
+
rulesPath: flags.rules,
|
|
1982
|
+
pageSize: pageSize,
|
|
1983
|
+
maxRows: maxRows,
|
|
1984
|
+
});
|
|
1985
|
+
if (result.status === "exported" && result.xml && outPath) {
|
|
1986
|
+
fs.writeFileSync(path.resolve(outPath), result.xml, "utf8");
|
|
1987
|
+
}
|
|
1988
|
+
writeExportReceipt(flags, result, outPath);
|
|
1989
|
+
if (result.status === "failed") {
|
|
1990
|
+
return 2;
|
|
1991
|
+
}
|
|
1992
|
+
if (result.status === "dry-run" && flags["dry-run"] !== "true") {
|
|
1993
|
+
return 1;
|
|
1994
|
+
}
|
|
1995
|
+
return 0;
|
|
1996
|
+
}
|
|
1997
|
+
/** Shared receipt for both export verbs. */
|
|
1998
|
+
function writeExportReceipt(flags, result, outPath) {
|
|
1999
|
+
if (flags.json === "true") {
|
|
2000
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
2003
|
+
process.stdout.write("[" +
|
|
2004
|
+
result.status +
|
|
2005
|
+
"]" +
|
|
2006
|
+
(outPath && result.status === "exported"
|
|
2007
|
+
? " " + path.resolve(outPath)
|
|
2008
|
+
: "") +
|
|
2009
|
+
"\n" +
|
|
2010
|
+
result.note +
|
|
2011
|
+
"\n");
|
|
2012
|
+
if (result.secretFields.length > 0) {
|
|
2013
|
+
process.stdout.write("Set these after loading the package:\n");
|
|
2014
|
+
for (var i = 0; i < result.secretFields.length; i += 1) {
|
|
2015
|
+
process.stdout.write(" " +
|
|
2016
|
+
result.secretFields[i].table +
|
|
2017
|
+
"." +
|
|
2018
|
+
result.secretFields[i].field +
|
|
2019
|
+
"\n");
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
/**
|
|
2024
|
+
* dove-sn export-app:
|
|
2025
|
+
* --app <scope|sys_id|name> Required. The sys_app to publish and export.
|
|
2026
|
+
* [--version <v>] Publish version (default: the app's current one).
|
|
2027
|
+
* [--description <text>] Recorded on the update set.
|
|
2028
|
+
* [--include-data] Ship table DATA as well as schema (off by default).
|
|
2029
|
+
* --out <file> Where to write the XML.
|
|
2030
|
+
* [--rules <file>] [--timeout-ms <n>]
|
|
2031
|
+
* [--dry-run] [--json] [--confirm]
|
|
2032
|
+
*
|
|
2033
|
+
* PUBLISHING IS A REAL INSTANCE WRITE — a new update set and ~1000+ records.
|
|
2034
|
+
* DRY-RUN BY DEFAULT. Secret values are always stripped before the file lands.
|
|
2035
|
+
* Exit codes: 0 exported/dry-run, 1 bad args/unconfirmed, 2 failed/timeout.
|
|
2036
|
+
*/
|
|
2037
|
+
async function runExportApp(flags) {
|
|
2038
|
+
var app = flags.app;
|
|
2039
|
+
if (!app) {
|
|
2040
|
+
process.stderr.write("export-app: --app <scope|sys_id|name> is required\n");
|
|
2041
|
+
return 1;
|
|
2042
|
+
}
|
|
2043
|
+
var outPath = flags.out;
|
|
2044
|
+
if (!outPath && flags["dry-run"] !== "true") {
|
|
2045
|
+
process.stderr.write("export-app: --out <file> is required for a real export\n");
|
|
2046
|
+
return 1;
|
|
2047
|
+
}
|
|
2048
|
+
var timeoutMs = undefined;
|
|
2049
|
+
if (flags["timeout-ms"]) {
|
|
2050
|
+
timeoutMs = Number(flags["timeout-ms"]);
|
|
2051
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
|
|
2052
|
+
process.stderr.write("export-app: --timeout-ms must be a positive integer\n");
|
|
2053
|
+
return 1;
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
var result = await (0, exportApp_1.exportApp)({
|
|
2057
|
+
app: app,
|
|
2058
|
+
version: flags.version,
|
|
2059
|
+
description: flags.description,
|
|
2060
|
+
includeData: flags["include-data"] === "true",
|
|
2061
|
+
keepSet: flags["keep-set"] !== "false",
|
|
2062
|
+
confirm: flags.confirm === "true",
|
|
2063
|
+
dryRun: flags["dry-run"] === "true",
|
|
2064
|
+
rulesPath: flags.rules,
|
|
2065
|
+
timeoutMs: timeoutMs,
|
|
2066
|
+
});
|
|
2067
|
+
if (result.status === "exported" && result.xml && outPath) {
|
|
2068
|
+
fs.writeFileSync(path.resolve(outPath), result.xml, "utf8");
|
|
2069
|
+
}
|
|
2070
|
+
writeExportReceipt(flags, result, outPath);
|
|
2071
|
+
if (result.status === "failed" || result.status === "timeout") {
|
|
2072
|
+
return 2;
|
|
2073
|
+
}
|
|
2074
|
+
if (result.status === "dry-run" && flags["dry-run"] !== "true") {
|
|
2075
|
+
return 1;
|
|
2076
|
+
}
|
|
2077
|
+
return 0;
|
|
2078
|
+
}
|
|
2079
|
+
/**
|
|
2080
|
+
* dove-sn strip-secrets:
|
|
2081
|
+
* --in <file> Required. An unload XML exported earlier.
|
|
2082
|
+
* --out <file> Required unless --report.
|
|
2083
|
+
* [--rules <file>] JSON overrides for the secret rules.
|
|
2084
|
+
* [--report] List what WOULD be stripped and what needs review;
|
|
2085
|
+
* writes nothing.
|
|
2086
|
+
* [--json]
|
|
2087
|
+
*
|
|
2088
|
+
* Exists for documents produced outside these verbs. Exit codes: 0 clean,
|
|
2089
|
+
* 1 bad args, 2 blocked (a field needs review, or a secret survived).
|
|
2090
|
+
*/
|
|
2091
|
+
async function runStripSecrets(flags) {
|
|
2092
|
+
var inPath = flags.in;
|
|
2093
|
+
if (!inPath) {
|
|
2094
|
+
process.stderr.write("strip-secrets: --in <file> is required\n");
|
|
2095
|
+
return 1;
|
|
2096
|
+
}
|
|
2097
|
+
var report = flags.report === "true";
|
|
2098
|
+
var outPath = flags.out;
|
|
2099
|
+
if (!report && !outPath) {
|
|
2100
|
+
process.stderr.write("strip-secrets: --out <file> is required (or pass --report)\n");
|
|
2101
|
+
return 1;
|
|
2102
|
+
}
|
|
2103
|
+
var xml = "";
|
|
2104
|
+
try {
|
|
2105
|
+
xml = fs.readFileSync(path.resolve(inPath), "utf8");
|
|
2106
|
+
}
|
|
2107
|
+
catch (e) {
|
|
2108
|
+
process.stderr.write("strip-secrets: cannot read " + inPath + "\n");
|
|
2109
|
+
return 1;
|
|
2110
|
+
}
|
|
2111
|
+
var rules = (0, secretRules_1.loadSecretRules)(flags.rules);
|
|
2112
|
+
var result;
|
|
2113
|
+
try {
|
|
2114
|
+
result = (0, stripSecrets_1.stripSecrets)(xml, rules, { allowUnreviewed: report });
|
|
2115
|
+
}
|
|
2116
|
+
catch (e) {
|
|
2117
|
+
process.stderr.write((e instanceof Error ? e.message : String(e)) + "\n");
|
|
2118
|
+
return 2;
|
|
2119
|
+
}
|
|
2120
|
+
if (!report && outPath) {
|
|
2121
|
+
fs.writeFileSync(path.resolve(outPath), result.xml, "utf8");
|
|
2122
|
+
}
|
|
2123
|
+
if (flags.json === "true") {
|
|
2124
|
+
process.stdout.write(JSON.stringify({
|
|
2125
|
+
recordsScanned: result.recordsScanned,
|
|
2126
|
+
secretFields: result.secretFields,
|
|
2127
|
+
reviewFindings: result.reviewFindings,
|
|
2128
|
+
written: report ? null : path.resolve(outPath),
|
|
2129
|
+
}, null, 2) + "\n");
|
|
2130
|
+
}
|
|
2131
|
+
else {
|
|
2132
|
+
process.stdout.write("[" +
|
|
2133
|
+
(report ? "report" : "stripped") +
|
|
2134
|
+
"] " +
|
|
2135
|
+
result.recordsScanned +
|
|
2136
|
+
" record(s), " +
|
|
2137
|
+
result.secretFields.length +
|
|
2138
|
+
" secret value(s)" +
|
|
2139
|
+
(report ? "" : " → " + path.resolve(outPath)) +
|
|
2140
|
+
"\n");
|
|
2141
|
+
for (var i = 0; i < result.reviewFindings.length; i += 1) {
|
|
2142
|
+
process.stdout.write(" NEEDS REVIEW " +
|
|
2143
|
+
result.reviewFindings[i].table +
|
|
2144
|
+
"." +
|
|
2145
|
+
result.reviewFindings[i].field +
|
|
2146
|
+
" (matched '" +
|
|
2147
|
+
result.reviewFindings[i].matched +
|
|
2148
|
+
"')\n");
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
return report && result.reviewFindings.length > 0 ? 2 : 0;
|
|
2152
|
+
}
|
|
1815
2153
|
async function main() {
|
|
1816
2154
|
var parsed = parseArgs(process.argv.slice(2));
|
|
1817
2155
|
// Load credentials before any command runs. `--env`/`--env-file` (or the
|
|
@@ -1851,6 +2189,9 @@ async function main() {
|
|
|
1851
2189
|
if (parsed.command === "add-column") {
|
|
1852
2190
|
return await runAddColumn(parsed.flags);
|
|
1853
2191
|
}
|
|
2192
|
+
if (parsed.command === "add-index") {
|
|
2193
|
+
return await runAddIndex(parsed.flags);
|
|
2194
|
+
}
|
|
1854
2195
|
if (parsed.command === "set-column") {
|
|
1855
2196
|
return await runSetColumn(parsed.flags, parsed.bare);
|
|
1856
2197
|
}
|
|
@@ -1894,6 +2235,15 @@ async function main() {
|
|
|
1894
2235
|
if (parsed.command === "publish-app") {
|
|
1895
2236
|
return await runPublishApp(parsed.flags);
|
|
1896
2237
|
}
|
|
2238
|
+
if (parsed.command === "export-update-set") {
|
|
2239
|
+
return await runExportUpdateSet(parsed.flags);
|
|
2240
|
+
}
|
|
2241
|
+
if (parsed.command === "export-app") {
|
|
2242
|
+
return await runExportApp(parsed.flags);
|
|
2243
|
+
}
|
|
2244
|
+
if (parsed.command === "strip-secrets") {
|
|
2245
|
+
return await runStripSecrets(parsed.flags);
|
|
2246
|
+
}
|
|
1897
2247
|
if (parsed.command === "mcp") {
|
|
1898
2248
|
return await runMcp(parsed.flags);
|
|
1899
2249
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export a scoped application to an importable `<unload>` XML document — the
|
|
3
|
+
* headless equivalent of the classic UI's **Publish to Update Set → Export to
|
|
4
|
+
* XML**, with secret values stripped before the document is returned.
|
|
5
|
+
*
|
|
6
|
+
* Ground truth: `brainstorms/sn-export-app-flow.md` (HAR capture, tenonworkshop
|
|
7
|
+
* 2026-06-08) and the live run recorded there. "Export an app" is two operations
|
|
8
|
+
* chained, with no single button behind it:
|
|
9
|
+
*
|
|
10
|
+
* 1. `POST /xmlhttp.do` com.snc.apps.AppsAjaxProcessor `createUpdateSet` →
|
|
11
|
+
* the new set's sys_id arrives in the answer attribute.
|
|
12
|
+
* 2. `POST /xmlhttp.do` AppsAjaxProcessor `publishToUpdateSet` → a worker id;
|
|
13
|
+
* poll AJAXProgressStatusChecker/getStatus until "Successfully published"
|
|
14
|
+
* (the HAR took 48 polls / ~44s for 1,088 files).
|
|
15
|
+
* 3. export that update set — delegated to exportUpdateSet, which owns the
|
|
16
|
+
* in-progress empty-200 gotcha and the secret stripping.
|
|
17
|
+
*
|
|
18
|
+
* PUBLISHING IS A REAL SHARED-INSTANCE WRITE: it creates an update set and
|
|
19
|
+
* ~1000+ sys_update_xml rows. DRY-RUN BY DEFAULT — without confirm:true this
|
|
20
|
+
* resolves the app and returns the plan, having written nothing.
|
|
21
|
+
*
|
|
22
|
+
* This is NOT the Store publish. That is publishApp, which is externally
|
|
23
|
+
* visible; this one stays inside the instance.
|
|
24
|
+
*
|
|
25
|
+
* ES6 only, no optional chaining.
|
|
26
|
+
*/
|
|
27
|
+
import type { ServiceNowClient } from "./client";
|
|
28
|
+
import type { FormAuth, FormSession, PostResult } from "./table";
|
|
29
|
+
import type { ExportTransport } from "./exportUpdateSet";
|
|
30
|
+
import type { SecretField } from "./secrets/stripSecrets";
|
|
31
|
+
/** Injectable transport so tests never touch the network. */
|
|
32
|
+
export interface ExportAppTransport extends ExportTransport {
|
|
33
|
+
post?: (auth: FormAuth, session: FormSession, path: string, fields: Record<string, string>) => Promise<PostResult>;
|
|
34
|
+
sleep?: (ms: number) => Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
/** Inputs for exportApp. */
|
|
37
|
+
export interface ExportAppParams {
|
|
38
|
+
/** App sys_id, scope, or name. */
|
|
39
|
+
app: string;
|
|
40
|
+
/** Publish version. Defaults to the app's current version. */
|
|
41
|
+
version?: string;
|
|
42
|
+
/** Description recorded on the update set. */
|
|
43
|
+
description?: string;
|
|
44
|
+
/** Include table DATA as well as schema. Off by default. */
|
|
45
|
+
includeData?: boolean;
|
|
46
|
+
/** Leave the published update set behind. Default true — deleting is not ours to do. */
|
|
47
|
+
keepSet?: boolean;
|
|
48
|
+
client?: ServiceNowClient;
|
|
49
|
+
instance?: string;
|
|
50
|
+
user?: string;
|
|
51
|
+
password?: string;
|
|
52
|
+
/** Required: publishing writes to the instance. */
|
|
53
|
+
confirm?: boolean;
|
|
54
|
+
/** Force a plan-only run. Wins over confirm. */
|
|
55
|
+
dryRun?: boolean;
|
|
56
|
+
/** Optional JSON secret-rules file. */
|
|
57
|
+
rulesPath?: string;
|
|
58
|
+
/** Milliseconds to wait for the publish worker. Default 300000. */
|
|
59
|
+
timeoutMs?: number;
|
|
60
|
+
transport?: ExportAppTransport;
|
|
61
|
+
}
|
|
62
|
+
/** Outcome of an app export. */
|
|
63
|
+
export interface ExportAppResult {
|
|
64
|
+
status: "dry-run" | "exported" | "failed" | "timeout";
|
|
65
|
+
appName: string;
|
|
66
|
+
appScope: string;
|
|
67
|
+
appSysId: string;
|
|
68
|
+
version: string;
|
|
69
|
+
/** The update set the publish produced. */
|
|
70
|
+
updateSetSysId: string;
|
|
71
|
+
/** getStatus polls performed. */
|
|
72
|
+
polls: number;
|
|
73
|
+
recordCount: number;
|
|
74
|
+
xml?: string;
|
|
75
|
+
secretFields: Array<SecretField>;
|
|
76
|
+
message?: string;
|
|
77
|
+
note: string;
|
|
78
|
+
}
|
|
79
|
+
/** Publishing a large app is slow — the HAR run took ~44s for 1,088 files. */
|
|
80
|
+
export declare var DEFAULT_EXPORT_APP_TIMEOUT_MS: number;
|
|
81
|
+
interface ResolvedApp {
|
|
82
|
+
sysId: string;
|
|
83
|
+
scope: string;
|
|
84
|
+
name: string;
|
|
85
|
+
version: string;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Fields for the AppsAjaxProcessor createUpdateSet call. The UI sends a single
|
|
89
|
+
* space when the description box is empty — mirrored here so the processor sees
|
|
90
|
+
* what it sees from a browser.
|
|
91
|
+
*/
|
|
92
|
+
export declare function buildCreateSetFields(app: ResolvedApp, description: string): Record<string, string>;
|
|
93
|
+
/** Fields for the AppsAjaxProcessor publishToUpdateSet call. */
|
|
94
|
+
export declare function buildPublishFields(app: ResolvedApp, updateSetSysId: string, version: string, description: string, includeData: boolean): Record<string, string>;
|
|
95
|
+
/**
|
|
96
|
+
* Publish an application into a fresh update set and export that set.
|
|
97
|
+
*
|
|
98
|
+
* Remote failures are RETURNED as a failed/timeout result; only caller errors
|
|
99
|
+
* (bad selector, bad timeout) throw.
|
|
100
|
+
*/
|
|
101
|
+
export declare function exportApp(params: ExportAppParams): Promise<ExportAppResult>;
|
|
102
|
+
export {};
|