@tenonhq/dovetail-servicenow 0.0.29 → 0.0.31

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
@@ -406,6 +406,45 @@ Programmatic: `invokeRest({ method, path, body, confirm })` is exported, and the
406
406
  client gained `now.put` / `now.delete` / `now.invoke` (the latter returns
407
407
  `{ status, body }` verbatim) alongside the existing `now.get` / `now.post`.
408
408
 
409
+ ### Publish an app to the Store / application repository
410
+
411
+ Publish a scoped application to the **ServiceNow Store**, the **company
412
+ application repository**, or both — headlessly, with the publish's progress
413
+ tracker polled to completion.
414
+
415
+ ```bash
416
+ # Dry-run — the DEFAULT: resolves the app, prints the plan, publishes NOTHING
417
+ npx dove-sn publish-app --app x_cadso_filter --version 6.0.20260716 --target both
418
+
419
+ # Publish for real (store, then repo, same version)
420
+ npx dove-sn publish-app --app x_cadso_filter --version 6.0.20260716 \
421
+ --target both --dev-notes "July release" --confirm --json
422
+ ```
423
+
424
+ **Store publish is EXTERNALLY VISIBLE on the ServiceNow Store — treat
425
+ `--target store --confirm` as a release.** `publish-app` is dry-run by default:
426
+ without `--confirm` it prints the resolved plan and exits `1` (a deliberate
427
+ refusal). `--target both` runs store then repo sequentially and short-circuits
428
+ if the store leg fails.
429
+
430
+ The two targets ride different transports:
431
+
432
+ - **store** replays the `sys_app` form's upload flow (`xmlhttp.do` +
433
+ `sn_appauthor.ScopedAppUploaderAJAX`) over a form-login session — basic auth
434
+ alone no-ops there. It needs the Store account credentials in the loaded env
435
+ file as `SN_STORE_USERNAME` / `SN_STORE_PASSWORD`. The password is **never
436
+ accepted as a flag** and never appears in output, dry-run previews included.
437
+ - **repo** uses the supported CI/CD REST API (`POST /api/sn_cicd/app_repo/publish`
438
+ + `GET /api/sn_cicd/progress/{id}`) over basic auth. The API user needs the
439
+ `sn_cicd` role (or admin).
440
+
441
+ `--app` accepts a scope name, `sys_app` sys_id, or app name; `--version` must be
442
+ above the currently published version. The result carries the progress-tracker
443
+ id, per-step states ("Packaging application", "Uploading application"), the
444
+ Store `appLink`, and the publish's update-set sys_id where the instance reports
445
+ one. Exit codes: `0` published or dry-run, `1` bad args/unconfirmed, `2`
446
+ failed/timeout. Programmatic: `publishApp({ app, version, target, confirm })`.
447
+
409
448
  `test-flow` defaults to **validate** — a safe pre-flight (published? inputs match
410
449
  declared variables?) that never runs the flow; `--execute --confirm` runs it via
411
450
  the server-side FlowAPI runner (deploy `resources/runFlow.md` first).
@@ -488,8 +527,10 @@ publish is read back and verified), `flow_publish` (compile a flow/subflow snaps
488
527
  publish, grafting a template), `flow_test` (validate or run a flow), and
489
528
  `flow_edit` (patch a flow), plus `invoke_rest` (invoke an arbitrary authenticated
490
529
  REST operation — Scripted REST included — with GET/POST/PUT/DELETE; dry-run by
491
- default, response passed through verbatim, bodies never logged). It reads
492
- ServiceNow credentials from the same env vars as the CLI.
530
+ default, response passed through verbatim, bodies never logged) and `app_publish`
531
+ (publish a scoped app to the ServiceNow Store and/or the application repository;
532
+ dry-run by default, Store credentials env-only — they never transit tool
533
+ arguments). It reads ServiceNow credentials from the same env vars as the CLI.
493
534
 
494
535
  ```bash
495
536
  npx dove-sn mcp --smoke # list the registered tools and exit
package/dist/cli.js CHANGED
@@ -60,6 +60,7 @@ const fs = __importStar(require("fs"));
60
60
  const path = __importStar(require("path"));
61
61
  const loadEnv_1 = require("./loadEnv");
62
62
  const client_1 = require("./client");
63
+ const fieldsFromJson_1 = require("./fieldsFromJson");
63
64
  const choices_1 = require("./choices");
64
65
  const formatter_1 = require("./formatter");
65
66
  const views_1 = require("./layout/views");
@@ -82,6 +83,7 @@ const table_1 = require("./table");
82
83
  const setField_1 = require("./setField");
83
84
  const createRecord_1 = require("./createRecord");
84
85
  const invokeRest_1 = require("./invokeRest");
86
+ const publishApp_1 = require("./publishApp");
85
87
  const hostAssets_1 = require("./hostAssets");
86
88
  const flowDesigner_formatter_2 = require("./flowDesigner-formatter");
87
89
  function parseArgs(argv) {
@@ -897,11 +899,12 @@ function printHelp() {
897
899
  " DRY-RUN BY DEFAULT — nothing is sent without --confirm\n" +
898
900
  " (--method <GET|POST|PUT|DELETE> --path /api/<scope>/<service>/<resource>\n" +
899
901
  " [--body '<json>' | --body-json <path>] [--confirm] [--dry-run] [--json])\n" +
900
- " set-field Set scalar field value(s) on an EXISTING record, into an update set, then verify\n" +
901
- ' (--table <t> --sys-id <id>|--query <q> --fields "k=v,k2=v2"\n' +
902
- " --update-set <sys_id> [--dry-run] [--json])\n" +
902
+ " set-field Set field value(s) on an EXISTING record, into an update set, then verify\n" +
903
+ " (--table <t> --sys-id <id>|--query <q> --update-set <sys_id>\n" +
904
+ ' (--fields "k=v,k2=v2" | --from-json <path>) [--dry-run] [--json])\n' +
903
905
  " create-record Create ONE NEW record in a data table, into an update set, then verify\n" +
904
- ' (--table <t> --fields "k=v,k2=v2" --scope <s> --update-set <sys_id>\n' +
906
+ " (--table <t> --scope <s> --update-set <sys_id>\n" +
907
+ ' (--fields "k=v,k2=v2" | --from-json <path>)\n' +
905
908
  " [--if-absent <encoded-query>] [--dry-run] [--json])\n" +
906
909
  " host-assets Deploy a built dist/ to ServiceNow (carrier sys_ui_script + attachment + m2m)\n" +
907
910
  " (--dir <dist> --app <sys_id> --scope <namespace>\n" +
@@ -916,6 +919,15 @@ function printHelp() {
916
919
  " [--script-input <name>] [--update-set <sys_id>] [--apply] [--json])\n" +
917
920
  " edit-flow Patch a flow/subflow (rename, description, step inputs)\n" +
918
921
  " (--sys-id <sys_id> --from-json <ops.json> [--apply] [--update-set <sys_id>] [--scope <sys_id>] [--json])\n" +
922
+ " publish-app Publish a scoped app to the ServiceNow Store and/or the company\n" +
923
+ " application repository, then poll the publish to completion.\n" +
924
+ " STORE PUBLISH IS EXTERNALLY VISIBLE on the ServiceNow Store.\n" +
925
+ " DRY-RUN BY DEFAULT — nothing is published without --confirm\n" +
926
+ " (--app <scope|sys_id|name> --version <v> --target store|repo|both\n" +
927
+ " [--dev-notes <text>] [--store-user <email>] [--timeout-ms <n>]\n" +
928
+ " [--dry-run] [--json] [--confirm])\n" +
929
+ " Store creds: SN_STORE_USERNAME/SN_STORE_PASSWORD in the --env file;\n" +
930
+ " the password is never a flag. Repo publish needs the sn_cicd role.\n" +
919
931
  " mcp Run the MCP stdio server (--smoke lists tools and exits)\n" +
920
932
  "\nGlobal flags:\n" +
921
933
  " --env <path> Load credentials from a specific .env file (also --env-file,\n" +
@@ -1228,24 +1240,48 @@ function parseFieldsInline(input) {
1228
1240
  }
1229
1241
  return out;
1230
1242
  }
1243
+ /**
1244
+ * Merge the two field sources for the record-write verbs: inline `--fields "k=v,k2=v2"`
1245
+ * and `--from-json <path>` (a JSON object — the only form that can carry a large or
1246
+ * multiline value, since the inline form splits on commas and trims). On a key present in
1247
+ * both, `--from-json` wins: it is the explicit spec file. Throws on an unreadable file,
1248
+ * malformed JSON, or a non-scalar value (surfaced by the caller as a bad-args error).
1249
+ */
1250
+ function mergeFields(flags) {
1251
+ var inline = parseFieldsInline(flags.fields || "");
1252
+ if (!flags["from-json"])
1253
+ return inline;
1254
+ return Object.assign(inline, (0, fieldsFromJson_1.readFieldsFromJsonFile)(flags["from-json"]));
1255
+ }
1231
1256
  /**
1232
1257
  * dove-sn set-field:
1233
1258
  * --table x_cadso_core_metric_point_type
1234
1259
  * --sys-id <id> | --query "name=send_size" (query must resolve to exactly 1 row)
1235
- * --fields "order=20" (comma-separated key=value pairs)
1260
+ * [--fields "order=20"] (comma-separated key=value pairs)
1261
+ * [--from-json <path>] (JSON { field: value }; carries large or
1262
+ * multiline values the inline form can't;
1263
+ * overrides --fields on a shared key)
1264
+ * — at least one of --fields / --from-json is required
1236
1265
  * --update-set <sys_id> (required — the change is captured here)
1237
1266
  * [--dry-run] [--json]
1238
1267
  * Exit codes: 0 applied/dry-run, 1 bad args, 2 write landed but read-back unverified.
1239
1268
  */
1240
1269
  async function runSetField(flags) {
1241
1270
  var table = flags.table;
1242
- var fields = parseFieldsInline(flags.fields || "");
1271
+ var fields;
1272
+ try {
1273
+ fields = mergeFields(flags);
1274
+ }
1275
+ catch (err) {
1276
+ process.stderr.write("set-field: " + (err instanceof Error ? err.message : String(err)) + "\n");
1277
+ return 1;
1278
+ }
1243
1279
  var hasTarget = Boolean(flags["sys-id"] || flags.query);
1244
1280
  if (!table ||
1245
1281
  Object.keys(fields).length === 0 ||
1246
1282
  !hasTarget ||
1247
1283
  !flags["update-set"]) {
1248
- process.stderr.write('set-field: --table, --fields "k=v", one of --sys-id/--query, and --update-set are required\n');
1284
+ process.stderr.write('set-field: --table, one of --sys-id/--query, --update-set, and at least one field (--fields "k=v" or --from-json <path>) are required\n');
1249
1285
  return 1;
1250
1286
  }
1251
1287
  var params = {
@@ -1285,7 +1321,10 @@ async function runSetField(flags) {
1285
1321
  /**
1286
1322
  * dove-sn create-record:
1287
1323
  * --table x_cadso_core_metric_point_type
1288
- * --fields "name=avg_message_parts,label=Avg. Message Parts,order=35"
1324
+ * [--fields "name=avg_message_parts,label=Avg. Message Parts,order=35"]
1325
+ * [--from-json <path>] (JSON { field: value }; carries large or
1326
+ * multiline values the inline form can't)
1327
+ * — at least one of --fields / --from-json is required
1289
1328
  * --scope x_cadso_core (the app that owns the new record)
1290
1329
  * --update-set <sys_id> (required — the insert is captured here)
1291
1330
  * [--if-absent "name=avg_message_parts"] (skip the insert when this query already matches)
@@ -1295,12 +1334,21 @@ async function runSetField(flags) {
1295
1334
  */
1296
1335
  async function runCreateRecord(flags) {
1297
1336
  var table = flags.table;
1298
- var fields = parseFieldsInline(flags.fields || "");
1337
+ var fields;
1338
+ try {
1339
+ fields = mergeFields(flags);
1340
+ }
1341
+ catch (err) {
1342
+ process.stderr.write("create-record: " +
1343
+ (err instanceof Error ? err.message : String(err)) +
1344
+ "\n");
1345
+ return 1;
1346
+ }
1299
1347
  if (!table ||
1300
1348
  Object.keys(fields).length === 0 ||
1301
1349
  !flags.scope ||
1302
1350
  !flags["update-set"]) {
1303
- process.stderr.write('create-record: --table, --fields "k=v", --scope and --update-set are required\n');
1351
+ process.stderr.write('create-record: --table, --scope, --update-set, and at least one field (--fields "k=v" or --from-json <path>) are required\n');
1304
1352
  return 1;
1305
1353
  }
1306
1354
  var params = {
@@ -1473,6 +1521,104 @@ async function runInvokeRest(flags) {
1473
1521
  }
1474
1522
  return 0;
1475
1523
  }
1524
+ /**
1525
+ * dove-sn publish-app:
1526
+ * --app <scope|sys_id|name> Required. The sys_app to publish.
1527
+ * --version <v> Required. Version to publish (e.g. 6.0.20260716).
1528
+ * --target store|repo|both Required. STORE PUBLISH IS EXTERNALLY VISIBLE.
1529
+ * [--dev-notes <text>] Optional developer notes.
1530
+ * [--store-user <email>] Store account email (else SN_STORE_USERNAME).
1531
+ * The password comes ONLY from SN_STORE_PASSWORD —
1532
+ * there is no flag for it, ever.
1533
+ * [--timeout-ms <n>] Progress-poll budget (default 120000).
1534
+ * [--dry-run] [--json] [--confirm]
1535
+ *
1536
+ * DRY-RUN unless --confirm: without it the resolved plan is printed and the
1537
+ * command exits 1 (a deliberate refusal, not success). --target both publishes
1538
+ * store then repo sequentially with the same version and short-circuits if the
1539
+ * store leg fails; --json emits an array of per-target results.
1540
+ * Exit codes: 0 published/dry-run, 1 bad args/unconfirmed, 2 failed/timeout.
1541
+ */
1542
+ async function runPublishApp(flags) {
1543
+ var app = flags.app;
1544
+ var version = flags.version;
1545
+ var target = flags.target;
1546
+ if (!app || !version || !target) {
1547
+ process.stderr.write("publish-app: --app, --version and --target store|repo|both are required\n");
1548
+ return 1;
1549
+ }
1550
+ if (target !== "store" && target !== "repo" && target !== "both") {
1551
+ process.stderr.write("publish-app: --target must be store, repo or both (got '" +
1552
+ target +
1553
+ "')\n");
1554
+ return 1;
1555
+ }
1556
+ var targets = target === "both" ? ["store", "repo"] : [target];
1557
+ var dryRun = flags["dry-run"] === "true";
1558
+ var confirmed = flags.confirm === "true";
1559
+ var client = (0, client_1.createClient)({});
1560
+ var results = [];
1561
+ var exitCode = 0;
1562
+ for (var i = 0; i < targets.length; i += 1) {
1563
+ var params = {
1564
+ client: client,
1565
+ app: app,
1566
+ version: version,
1567
+ target: targets[i],
1568
+ confirm: confirmed,
1569
+ dryRun: dryRun,
1570
+ };
1571
+ if (flags["dev-notes"])
1572
+ params.devNotes = flags["dev-notes"];
1573
+ if (flags["store-user"])
1574
+ params.storeUsername = flags["store-user"];
1575
+ if (flags["timeout-ms"]) {
1576
+ // A NaN timeout would make the poll-loop budget check always false —
1577
+ // an infinite loop. Validate here, exit 1 on garbage.
1578
+ var timeoutMs = Number(flags["timeout-ms"]);
1579
+ if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
1580
+ process.stderr.write("publish-app: --timeout-ms must be a positive integer (got '" +
1581
+ flags["timeout-ms"] +
1582
+ "')\n");
1583
+ return 1;
1584
+ }
1585
+ params.timeoutMs = timeoutMs;
1586
+ }
1587
+ var result = await (0, publishApp_1.publishApp)(params);
1588
+ results.push(result);
1589
+ if (flags.json !== "true") {
1590
+ process.stdout.write("[" +
1591
+ result.status +
1592
+ "] " +
1593
+ result.target +
1594
+ " — " +
1595
+ result.appName +
1596
+ " (" +
1597
+ result.appScope +
1598
+ ") v" +
1599
+ result.version +
1600
+ (result.appLink ? " — " + result.appLink : "") +
1601
+ (result.updateSetSysId
1602
+ ? " — update set " + result.updateSetSysId
1603
+ : "") +
1604
+ "\n" +
1605
+ result.note +
1606
+ "\n");
1607
+ }
1608
+ if (result.status === "failed" || result.status === "timeout") {
1609
+ exitCode = 2;
1610
+ break; // Short-circuit: never repo-publish after a failed store leg.
1611
+ }
1612
+ }
1613
+ if (flags.json === "true") {
1614
+ process.stdout.write(JSON.stringify(results, null, 2) + "\n");
1615
+ }
1616
+ if (exitCode === 0 && !dryRun && !confirmed) {
1617
+ process.stderr.write("publish-app: refusing to publish without --confirm (the plan above is a dry-run).\n");
1618
+ return 1;
1619
+ }
1620
+ return exitCode;
1621
+ }
1476
1622
  async function main() {
1477
1623
  var parsed = parseArgs(process.argv.slice(2));
1478
1624
  // Load credentials before any command runs. `--env`/`--env-file` (or the
@@ -1547,6 +1693,9 @@ async function main() {
1547
1693
  await runSetRelatedLists(parsed.flags);
1548
1694
  return 0;
1549
1695
  }
1696
+ if (parsed.command === "publish-app") {
1697
+ return await runPublishApp(parsed.flags);
1698
+ }
1550
1699
  if (parsed.command === "mcp") {
1551
1700
  return await runMcp(parsed.flags);
1552
1701
  }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * `--from-json` field-map loader for the record-write verbs (set-field, create-record).
3
+ *
4
+ * The inline `--fields "k=v,k2=v2"` form splits on commas and trims each piece — so it
5
+ * cannot carry a value that contains a comma, a newline, an `=`, or leading/trailing
6
+ * whitespace. That rules out every large field: a script body, an HTML/XML/CSS field, a
7
+ * JSON blob. `--from-json <path>` closes that gap: the file is a flat JSON object of
8
+ * `field → value`, and JSON quoting carries any character faithfully.
9
+ *
10
+ * Values may be string | number | boolean. Numbers/booleans are stringified — matching
11
+ * the inline parser and the wire format, where ServiceNow scalar fields are strings.
12
+ * `null`/`undefined` values are skipped. Nested objects and arrays are rejected: a scalar
13
+ * field cannot take one, and silently JSON-encoding it would write garbage into the record.
14
+ */
15
+ export declare function coerceFieldsFromJson(parsed: unknown): Record<string, string>;
16
+ /**
17
+ * Read + parse a `--from-json` file into a validated field map. The path is resolved
18
+ * against the process cwd, matching the other `--from-json` consumers in the CLI.
19
+ * Throws a clean, prefixed message on a missing file or malformed JSON so the caller can
20
+ * surface it as a bad-args error rather than an uncaught stack trace.
21
+ */
22
+ export declare function readFieldsFromJsonFile(filePath: string): Record<string, string>;
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.coerceFieldsFromJson = coerceFieldsFromJson;
37
+ exports.readFieldsFromJsonFile = readFieldsFromJsonFile;
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ /**
41
+ * `--from-json` field-map loader for the record-write verbs (set-field, create-record).
42
+ *
43
+ * The inline `--fields "k=v,k2=v2"` form splits on commas and trims each piece — so it
44
+ * cannot carry a value that contains a comma, a newline, an `=`, or leading/trailing
45
+ * whitespace. That rules out every large field: a script body, an HTML/XML/CSS field, a
46
+ * JSON blob. `--from-json <path>` closes that gap: the file is a flat JSON object of
47
+ * `field → value`, and JSON quoting carries any character faithfully.
48
+ *
49
+ * Values may be string | number | boolean. Numbers/booleans are stringified — matching
50
+ * the inline parser and the wire format, where ServiceNow scalar fields are strings.
51
+ * `null`/`undefined` values are skipped. Nested objects and arrays are rejected: a scalar
52
+ * field cannot take one, and silently JSON-encoding it would write garbage into the record.
53
+ */
54
+ function coerceFieldsFromJson(parsed) {
55
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
56
+ throw new Error("--from-json must contain a JSON object of field → value pairs");
57
+ }
58
+ var obj = parsed;
59
+ var out = {};
60
+ var keys = Object.keys(obj);
61
+ for (var i = 0; i < keys.length; i += 1) {
62
+ var key = keys[i];
63
+ var value = obj[key];
64
+ if (value === null || value === undefined)
65
+ continue;
66
+ var valueType = typeof value;
67
+ if (valueType === "string") {
68
+ out[key] = value;
69
+ }
70
+ else if (valueType === "number" || valueType === "boolean") {
71
+ out[key] = String(value);
72
+ }
73
+ else {
74
+ throw new Error("--from-json field '" +
75
+ key +
76
+ "' must be a string, number, or boolean (got " +
77
+ valueType +
78
+ ")");
79
+ }
80
+ }
81
+ return out;
82
+ }
83
+ /**
84
+ * Read + parse a `--from-json` file into a validated field map. The path is resolved
85
+ * against the process cwd, matching the other `--from-json` consumers in the CLI.
86
+ * Throws a clean, prefixed message on a missing file or malformed JSON so the caller can
87
+ * surface it as a bad-args error rather than an uncaught stack trace.
88
+ */
89
+ function readFieldsFromJsonFile(filePath) {
90
+ var raw;
91
+ try {
92
+ raw = fs.readFileSync(path.resolve(filePath), "utf8");
93
+ }
94
+ catch (err) {
95
+ var readMsg = err instanceof Error ? err.message : String(err);
96
+ throw new Error("--from-json: cannot read '" + filePath + "': " + readMsg);
97
+ }
98
+ var parsed;
99
+ try {
100
+ parsed = JSON.parse(raw);
101
+ }
102
+ catch (err) {
103
+ var parseMsg = err instanceof Error ? err.message : String(err);
104
+ throw new Error("--from-json: '" + filePath + "' is not valid JSON: " + parseMsg);
105
+ }
106
+ return coerceFieldsFromJson(parsed);
107
+ }
package/dist/index.d.ts CHANGED
@@ -27,3 +27,5 @@ export type { RecordWriteResult, SetFieldParams, SetFieldResult, } from "./setFi
27
27
  export type { CreateRecordParams, CreateRecordResult } from "./createRecord";
28
28
  export { invokeRest, INVOKE_REST_METHODS } from "./invokeRest";
29
29
  export type { InvokeRestParams, InvokeRestResult } from "./invokeRest";
30
+ export { publishApp, buildStartFields, parseXmlAnswer, parseProgressTree, classifyProgress, flattenSteps, harvestProgressResults, parseCicdPublishResponse, parseCicdProgress, DEFAULT_PUBLISH_TIMEOUT_MS, PUBLISH_POLL_DELAYS_MS, } from "./publishApp";
31
+ export type { PublishAppParams, PublishAppResult, PublishTarget, PublishStep, ProgressNode, CicdProgress, PublishTransport, } from "./publishApp";
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.addColumn = exports.DEFAULT_SAVE_ACTION = exports.DEFAULT_SUPER_CLASS = exports.TYPE_MAP = exports.defaultAccessFlags = exports.applyTableSaveOverlay = exports.resolveType = exports.normalizeColumns = exports.buildColumnXml = exports.projectTableGraph = exports.createTable = exports.WriteOrderError = exports.executeWritePlan = exports.topoSort = exports.generateSysId = exports.DEFAULT_RUN_FLOW_PATH = exports.testFlow = exports.editFlow = exports.buildPublishModel = exports.createFlow = exports.copyFlow = exports.publishFlow = exports.readActionType = exports.readFlow = exports.formatStepPill = exports.summarizeSteps = exports.verifySteps = exports.applyStepOps = exports.editActionType = exports.publishActionType = exports.triggerPublication = exports.cloneActionType = exports.cloneSubflow = exports.verifyArtifact = exports.listTemplates = exports.sincPlugin = exports.formatCreateViewResult = exports.formatLayoutResult = exports.setRelatedLists = exports.setFormLayout = exports.setListLayout = exports.createView = exports.formatAddChoicesResult = exports.formatHostAssetsResult = exports.classifyChunks = exports.hostAssets = exports.addChoicesToField = exports.resolveConfigFromEnvFile = exports.createClientFromEnvFile = exports.createClient = void 0;
10
- exports.INVOKE_REST_METHODS = exports.invokeRest = exports.createRecord = exports.setField = exports.toStoredValue = exports.resolveAttributes = exports.setColumn = exports.deriveElement = void 0;
10
+ exports.PUBLISH_POLL_DELAYS_MS = exports.DEFAULT_PUBLISH_TIMEOUT_MS = exports.parseCicdProgress = exports.parseCicdPublishResponse = exports.harvestProgressResults = exports.flattenSteps = exports.classifyProgress = exports.parseProgressTree = exports.parseXmlAnswer = exports.buildStartFields = exports.publishApp = exports.INVOKE_REST_METHODS = exports.invokeRest = exports.createRecord = exports.setField = exports.toStoredValue = exports.resolveAttributes = exports.setColumn = exports.deriveElement = void 0;
11
11
  var client_1 = require("./client");
12
12
  Object.defineProperty(exports, "createClient", { enumerable: true, get: function () { return client_1.createClient; } });
13
13
  var createClientFromEnvFile_1 = require("./createClientFromEnvFile");
@@ -82,3 +82,15 @@ Object.defineProperty(exports, "createRecord", { enumerable: true, get: function
82
82
  var invokeRest_1 = require("./invokeRest");
83
83
  Object.defineProperty(exports, "invokeRest", { enumerable: true, get: function () { return invokeRest_1.invokeRest; } });
84
84
  Object.defineProperty(exports, "INVOKE_REST_METHODS", { enumerable: true, get: function () { return invokeRest_1.INVOKE_REST_METHODS; } });
85
+ var publishApp_1 = require("./publishApp");
86
+ Object.defineProperty(exports, "publishApp", { enumerable: true, get: function () { return publishApp_1.publishApp; } });
87
+ Object.defineProperty(exports, "buildStartFields", { enumerable: true, get: function () { return publishApp_1.buildStartFields; } });
88
+ Object.defineProperty(exports, "parseXmlAnswer", { enumerable: true, get: function () { return publishApp_1.parseXmlAnswer; } });
89
+ Object.defineProperty(exports, "parseProgressTree", { enumerable: true, get: function () { return publishApp_1.parseProgressTree; } });
90
+ Object.defineProperty(exports, "classifyProgress", { enumerable: true, get: function () { return publishApp_1.classifyProgress; } });
91
+ Object.defineProperty(exports, "flattenSteps", { enumerable: true, get: function () { return publishApp_1.flattenSteps; } });
92
+ Object.defineProperty(exports, "harvestProgressResults", { enumerable: true, get: function () { return publishApp_1.harvestProgressResults; } });
93
+ Object.defineProperty(exports, "parseCicdPublishResponse", { enumerable: true, get: function () { return publishApp_1.parseCicdPublishResponse; } });
94
+ Object.defineProperty(exports, "parseCicdProgress", { enumerable: true, get: function () { return publishApp_1.parseCicdProgress; } });
95
+ Object.defineProperty(exports, "DEFAULT_PUBLISH_TIMEOUT_MS", { enumerable: true, get: function () { return publishApp_1.DEFAULT_PUBLISH_TIMEOUT_MS; } });
96
+ Object.defineProperty(exports, "PUBLISH_POLL_DELAYS_MS", { enumerable: true, get: function () { return publishApp_1.PUBLISH_POLL_DELAYS_MS; } });
@@ -10,7 +10,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
10
  import { z } from "zod";
11
11
  import type { ToolAnnotations } from "@tenonhq/dovetail-mcp-kit";
12
12
  import type { ServiceNowClient } from "../client";
13
- export declare var TOOL_NAMES: readonly ["create_view", "set_list_layout", "set_form_layout", "set_related_lists", "add_choices_to_field", "flow_view", "action_view", "action_edit", "flow_publish", "flow_copy", "flow_create", "flow_test", "flow_edit", "create_table", "add_column", "set_column", "set_field", "create_record", "host_assets", "invoke_rest"];
13
+ export declare var TOOL_NAMES: readonly ["create_view", "set_list_layout", "set_form_layout", "set_related_lists", "add_choices_to_field", "flow_view", "action_view", "action_edit", "flow_publish", "flow_copy", "flow_create", "flow_test", "flow_edit", "create_table", "add_column", "set_column", "set_field", "create_record", "host_assets", "invoke_rest", "app_publish"];
14
14
  export type ToolName = (typeof TOOL_NAMES)[number];
15
15
  export interface RegistryDeps {
16
16
  /** Optional client injection for tests; defaults to createClient({}). */
@@ -31,6 +31,7 @@ const hostAssets_1 = require("../hostAssets");
31
31
  const setField_1 = require("../setField");
32
32
  const createRecord_1 = require("../createRecord");
33
33
  const invokeRest_1 = require("../invokeRest");
34
+ const publishApp_1 = require("../publishApp");
34
35
  const schemas_1 = require("./schemas");
35
36
  exports.TOOL_NAMES = [
36
37
  "create_view",
@@ -53,6 +54,7 @@ exports.TOOL_NAMES = [
53
54
  "create_record",
54
55
  "host_assets",
55
56
  "invoke_rest",
57
+ "app_publish",
56
58
  ];
57
59
  // Annotation presets (READ_ONLY / WRITE_ADDITIVE_IDEMPOTENT / WRITE_CREATE /
58
60
  // WRITE_OVERWRITE / WRITE_EXECUTE) come from @tenonhq/dovetail-mcp-kit.
@@ -491,6 +493,39 @@ function buildDescriptors(deps = {}) {
491
493
  return (0, invokeRest_1.invokeRest)(params);
492
494
  },
493
495
  },
496
+ {
497
+ name: "app_publish",
498
+ annotations: dovetail_mcp_kit_1.WRITE_EXECUTE,
499
+ description: "Publish a scoped ServiceNow application to ONE target per call — the ServiceNow Store " +
500
+ "(target 'store') or the company Application Repository (target 'repo'); call twice to " +
501
+ "publish to both — then poll the publish to completion. STORE PUBLISH IS EXTERNALLY " +
502
+ "VISIBLE on the ServiceNow Store — treat it as a release. DRY-RUN BY DEFAULT: without " +
503
+ "confirm:true the resolved plan (app, current version, target) is returned and nothing is " +
504
+ "published. target 'store' replays the sys_app form's upload flow over a form-login session " +
505
+ "and requires SN_STORE_USERNAME/SN_STORE_PASSWORD in the server's env file — credentials " +
506
+ "never transit tool arguments. target 'repo' uses the supported CI/CD REST API " +
507
+ "(/api/sn_cicd/app_repo/publish) and requires the sn_cicd role. app is a scope name, " +
508
+ "sys_app sys_id, or app name; version must be above the currently published version. The " +
509
+ "result carries the progress-tracker id, per-step states, the Store appLink, and the " +
510
+ "publish's update-set sys_id where the instance reports one.",
511
+ shape: schemas_1.publishAppSchema.shape,
512
+ handler: async function (args) {
513
+ var p = schemas_1.publishAppSchema.parse(args);
514
+ var params = {
515
+ app: p.app,
516
+ version: p.version,
517
+ target: p.target,
518
+ devNotes: p.devNotes,
519
+ confirm: p.confirm,
520
+ dryRun: p.dryRun,
521
+ timeoutMs: p.timeoutMs,
522
+ };
523
+ if (deps.client) {
524
+ params.client = deps.client;
525
+ }
526
+ return (0, publishApp_1.publishApp)(params);
527
+ },
528
+ },
494
529
  ];
495
530
  }
496
531
  function registerAllTools(server, deps = {}) {
@@ -958,6 +958,31 @@ export declare var createRecordSchema: z.ZodObject<{
958
958
  dryRun?: boolean | undefined;
959
959
  ifAbsentQuery?: string | undefined;
960
960
  }>;
961
+ export declare var publishAppSchema: z.ZodObject<{
962
+ app: z.ZodString;
963
+ version: z.ZodString;
964
+ devNotes: z.ZodOptional<z.ZodString>;
965
+ target: z.ZodUnion<[z.ZodLiteral<"store">, z.ZodLiteral<"repo">]>;
966
+ confirm: z.ZodOptional<z.ZodBoolean>;
967
+ dryRun: z.ZodOptional<z.ZodBoolean>;
968
+ timeoutMs: z.ZodOptional<z.ZodNumber>;
969
+ }, "strip", z.ZodTypeAny, {
970
+ target: "store" | "repo";
971
+ version: string;
972
+ app: string;
973
+ confirm?: boolean | undefined;
974
+ dryRun?: boolean | undefined;
975
+ devNotes?: string | undefined;
976
+ timeoutMs?: number | undefined;
977
+ }, {
978
+ target: "store" | "repo";
979
+ version: string;
980
+ app: string;
981
+ confirm?: boolean | undefined;
982
+ dryRun?: boolean | undefined;
983
+ devNotes?: string | undefined;
984
+ timeoutMs?: number | undefined;
985
+ }>;
961
986
  export declare var invokeRestSchema: z.ZodObject<{
962
987
  method: z.ZodEffects<z.ZodEnum<["GET", "POST", "PUT", "DELETE"]>, "GET" | "DELETE" | "POST" | "PUT", unknown>;
963
988
  path: z.ZodString;
@@ -4,7 +4,7 @@
4
4
  * own file so registry.ts stays focused on wiring.
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.invokeRestSchema = exports.createRecordSchema = exports.setFieldSchema = exports.setColumnSchema = exports.columnAttributesSchema = exports.addColumnSchema = exports.createTableSchema = exports.columnSpecSchema = exports.hostAssetsSchema = exports.editFlowSchema = exports.editActionSchema = exports.stepInputPatchSchema = exports.testFlowSchema = exports.createFlowSchema = exports.copyFlowSchema = exports.publishFlowSchema = exports.viewActionSchema = exports.viewFlowSchema = exports.addChoicesToFieldSchema = exports.choiceValueSchema = exports.setRelatedListsSchema = exports.setFormLayoutSchema = exports.formSectionSchema = exports.setListLayoutSchema = exports.createViewSchema = void 0;
7
+ exports.invokeRestSchema = exports.publishAppSchema = exports.createRecordSchema = exports.setFieldSchema = exports.setColumnSchema = exports.columnAttributesSchema = exports.addColumnSchema = exports.createTableSchema = exports.columnSpecSchema = exports.hostAssetsSchema = exports.editFlowSchema = exports.editActionSchema = exports.stepInputPatchSchema = exports.testFlowSchema = exports.createFlowSchema = exports.copyFlowSchema = exports.publishFlowSchema = exports.viewActionSchema = exports.viewFlowSchema = exports.addChoicesToFieldSchema = exports.choiceValueSchema = exports.setRelatedListsSchema = exports.setFormLayoutSchema = exports.formSectionSchema = exports.setListLayoutSchema = exports.createViewSchema = void 0;
8
8
  const zod_1 = require("zod");
9
9
  exports.createViewSchema = zod_1.z.object({
10
10
  name: zod_1.z.string().min(1),
@@ -261,6 +261,19 @@ exports.createRecordSchema = zod_1.z.object({
261
261
  ifAbsentQuery: zod_1.z.string().optional(),
262
262
  dryRun: zod_1.z.boolean().optional(),
263
263
  });
264
+ // app_publish: publish a scoped app to the ServiceNow Store or the company
265
+ // application repository. Deliberately NO credential fields — the Store
266
+ // account resolves from SN_STORE_USERNAME/SN_STORE_PASSWORD inside the verb,
267
+ // so credentials never transit MCP arguments or telemetry.
268
+ exports.publishAppSchema = zod_1.z.object({
269
+ app: zod_1.z.string().min(1),
270
+ version: zod_1.z.string().min(1),
271
+ devNotes: zod_1.z.string().optional(),
272
+ target: zod_1.z.union([zod_1.z.literal("store"), zod_1.z.literal("repo")]),
273
+ confirm: zod_1.z.boolean().optional(),
274
+ dryRun: zod_1.z.boolean().optional(),
275
+ timeoutMs: zod_1.z.number().int().positive().optional(),
276
+ });
264
277
  // invoke_rest: transport primitive for arbitrary authenticated REST operations
265
278
  // (Scripted REST included). The dry-run-unless-confirm gate lives in invokeRest
266
279
  // itself; the regex here rejects absolute URLs and non-/api/ paths early.