@tenonhq/dovetail-servicenow 0.0.38 → 0.0.40

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
@@ -516,11 +516,11 @@ Programmatic: `invokeRest({ method, path, body, confirm })` is exported, and the
516
516
  client gained `now.put` / `now.delete` / `now.invoke` (the latter returns
517
517
  `{ status, body }` verbatim) alongside the existing `now.get` / `now.post`.
518
518
 
519
- ### Publish an app to the Store / application repository
519
+ ### Publish an app to the Store / application repository / an update set
520
520
 
521
521
  Publish a scoped application to the **ServiceNow Store**, the **company
522
- application repository**, or both — headlessly, with the publish's progress
523
- tracker polled to completion.
522
+ application repository**, and/or **into a new update set** — headlessly, with
523
+ each publish's progress tracker polled to completion.
524
524
 
525
525
  ```bash
526
526
  # Dry-run — the DEFAULT: resolves the app, prints the plan, publishes NOTHING
@@ -529,15 +529,21 @@ npx dove-sn publish-app --app x_cadso_filter --version 6.0.20260716 --target bot
529
529
  # Publish for real (store, then repo, same version)
530
530
  npx dove-sn publish-app --app x_cadso_filter --version 6.0.20260716 \
531
531
  --target both --dev-notes "July release" --confirm --json
532
+
533
+ # Release flow on an instance WITHOUT the sn_cicd plugin: publish to the company
534
+ # repository over the UI uploader, then capture the app into a dated update set.
535
+ npx dove-sn publish-app --app x_cadso_filter --version 6.0.20260729 \
536
+ --target repo-ui,update-set --update-set-description 20260729 --confirm --json
532
537
  ```
533
538
 
534
539
  **Store publish is EXTERNALLY VISIBLE on the ServiceNow Store — treat
535
540
  `--target store --confirm` as a release.** `publish-app` is dry-run by default:
536
541
  without `--confirm` it prints the resolved plan and exits `1` (a deliberate
537
- refusal). `--target both` runs store then repo sequentially and short-circuits
538
- if the store leg fails.
542
+ refusal). `--target` takes one target, a comma-separated list, or `both` (an
543
+ alias for `store,repo`); targets run **in the order given** and short-circuit on
544
+ the first failure.
539
545
 
540
- The two targets ride different transports:
546
+ The targets ride different transports:
541
547
 
542
548
  - **store** replays the `sys_app` form's upload flow (`xmlhttp.do` +
543
549
  `sn_appauthor.ScopedAppUploaderAJAX`) over a form-login session — basic auth
@@ -547,13 +553,33 @@ The two targets ride different transports:
547
553
  - **repo** uses the supported CI/CD REST API (`POST /api/sn_cicd/app_repo/publish`
548
554
  + `GET /api/sn_cicd/progress/{id}`) over basic auth. The API user needs the
549
555
  `sn_cicd` role (or admin).
556
+ - **repo-ui** reaches the *same* company repository as `repo`, but over the UI
557
+ uploader (`sysparm_publish_to_store=false`) instead of REST. Use it when the
558
+ instance has no CI/CD plugin — `tenonworkshop`, for instance, has no `sn_cicd`
559
+ scope and no `app_repo` service, so `repo` 404s there while `repo-ui` works.
560
+ It needs no Store credentials.
561
+ - **update-set** publishes the app *into a newly created update set* via the
562
+ two-call `com.snc.apps.AppsAjaxProcessor` flow (`createUpdateSet` →
563
+ `publishToUpdateSet`). There is no REST equivalent. `--update-set-name`
564
+ defaults to the app's name (the dialog's field is readonly, so that is what
565
+ the UI submits); `--update-set-description` is conventionally the release date
566
+ stamp `YYYYMMDD`, which makes a whole release one query
567
+ (`sys_update_set` where `description=20260729`). `--include-data` maps to the
568
+ dialog's "Include demo data" box and defaults **off**, matching the value the
569
+ UI actually puts on the wire.
570
+
571
+ Ordering matters when you combine them: the repo publish is what bumps
572
+ `sys_app.version`, so put it **before** `update-set` if you want the set
573
+ captured at the new version.
550
574
 
551
575
  `--app` accepts a scope name, `sys_app` sys_id, or app name; `--version` must be
552
576
  above the currently published version. The result carries the progress-tracker
553
577
  id, per-step states ("Packaging application", "Uploading application"), the
554
- Store `appLink`, and the publish's update-set sys_id where the instance reports
555
- one. Exit codes: `0` published or dry-run, `1` bad args/unconfirmed, `2`
556
- failed/timeout. Programmatic: `publishApp({ app, version, target, confirm })`.
578
+ Store `appLink`, and the update-set sys_id — for the `update-set` target that is
579
+ recorded as soon as the set is created, so it survives a later failure and you
580
+ can always find (or delete) the set. Exit codes: `0` published or dry-run, `1`
581
+ bad args/unconfirmed, `2` failed/timeout. Programmatic:
582
+ `publishApp({ app, version, target, confirm })`.
557
583
 
558
584
  ### Export an update set (or a whole app) to importable XML
559
585
 
package/dist/cli.js CHANGED
@@ -1043,15 +1043,18 @@ function printHelp() {
1043
1043
  " [--script-input <name>] [--update-set <sys_id>] [--apply] [--json])\n" +
1044
1044
  " edit-flow Patch a flow/subflow (rename, description, step inputs)\n" +
1045
1045
  " (--sys-id <sys_id> --from-json <ops.json> [--apply] [--update-set <sys_id>] [--scope <sys_id>] [--json])\n" +
1046
- " publish-app Publish a scoped app to the ServiceNow Store and/or the company\n" +
1047
- " application repository, then poll the publish to completion.\n" +
1046
+ " publish-app Publish a scoped app to the ServiceNow Store, the company application\n" +
1047
+ " repository, and/or a new update set, then poll each to completion.\n" +
1048
1048
  " STORE PUBLISH IS EXTERNALLY VISIBLE on the ServiceNow Store.\n" +
1049
1049
  " DRY-RUN BY DEFAULT — nothing is published without --confirm\n" +
1050
- " (--app <scope|sys_id|name> --version <v> --target store|repo|both\n" +
1050
+ " (--app <scope|sys_id|name> --version <v>\n" +
1051
+ " --target store|repo|repo-ui|update-set|both (comma-separated ok)\n" +
1051
1052
  " [--dev-notes <text>] [--store-user <email>] [--timeout-ms <n>]\n" +
1052
- " [--dry-run] [--json] [--confirm])\n" +
1053
+ " [--update-set-name <name>] [--update-set-description <text>]\n" +
1054
+ " [--include-data] [--dry-run] [--json] [--confirm])\n" +
1053
1055
  " Store creds: SN_STORE_USERNAME/SN_STORE_PASSWORD in the --env file;\n" +
1054
- " the password is never a flag. Repo publish needs the sn_cicd role.\n" +
1056
+ " the password is never a flag. 'repo' needs the sn_cicd plugin+role;\n" +
1057
+ " 'repo-ui' reaches the same repository over the UI uploader instead.\n" +
1055
1058
  " export-update-set Export an update set to importable <unload> XML, with secret\n" +
1056
1059
  " values replaced by __SET_DURING_INSTALL__ (no opt-out).\n" +
1057
1060
  " assemble mode is READ-ONLY; complete mode marks the set\n" +
@@ -1070,8 +1073,11 @@ function printHelp() {
1070
1073
  " (--in <file> [--out <file>] [--rules <file>] [--report] [--json])\n" +
1071
1074
  " mcp Run the MCP stdio server (--smoke lists tools and exits)\n" +
1072
1075
  "\nGlobal flags:\n" +
1073
- " --env <path> Load credentials from a specific .env file (also --env-file,\n" +
1074
- " or the DOVETAIL_ENV_FILE env var). Default: .env in the cwd.\n");
1076
+ " --env <name|path> Load credentials from a specific env file (also --env-file,\n" +
1077
+ " or the DOVETAIL_ENV_FILE env var). A bare name like 'prod'\n" +
1078
+ " resolves to .env.prod in the cwd. The file's SN_* connection\n" +
1079
+ " vars replace any already exported; a missing or incomplete\n" +
1080
+ " file is an error (no fallback). Default: .env in the cwd.\n");
1075
1081
  }
1076
1082
  /** Parse inline `--columns "Label:type:max, Other:choice, ..."` into ColumnSpec[]. */
1077
1083
  function parseColumnsInline(input) {
@@ -1830,8 +1836,19 @@ async function runInvokeRest(flags) {
1830
1836
  * dove-sn publish-app:
1831
1837
  * --app <scope|sys_id|name> Required. The sys_app to publish.
1832
1838
  * --version <v> Required. Version to publish (e.g. 6.0.20260716).
1833
- * --target store|repo|both Required. STORE PUBLISH IS EXTERNALLY VISIBLE.
1834
- * [--dev-notes <text>] Optional developer notes.
1839
+ * --target <t[,t...]> Required. store | repo | repo-ui | update-set |
1840
+ * both (= store,repo). STORE IS EXTERNALLY VISIBLE.
1841
+ * repo = CI/CD REST API (needs the sn_cicd plugin)
1842
+ * repo-ui= same destination over the UI uploader,
1843
+ * for instances without sn_cicd
1844
+ * update-set = publish the app INTO a new update set
1845
+ * [--dev-notes <text>] Optional developer notes (uploader targets).
1846
+ * [--update-set-name <name>] update-set only. Defaults to the app's name.
1847
+ * [--update-set-description <text>]
1848
+ * update-set only. Tenon convention is the release
1849
+ * date stamp (YYYYMMDD) so a release is one query.
1850
+ * [--include-data] update-set only. Include demo data (default off,
1851
+ * matching the observed wire value).
1835
1852
  * [--store-user <email>] Store account email (else SN_STORE_USERNAME).
1836
1853
  * The password comes ONLY from SN_STORE_PASSWORD —
1837
1854
  * there is no flag for it, ever.
@@ -1839,9 +1856,9 @@ async function runInvokeRest(flags) {
1839
1856
  * [--dry-run] [--json] [--confirm]
1840
1857
  *
1841
1858
  * DRY-RUN unless --confirm: without it the resolved plan is printed and the
1842
- * command exits 1 (a deliberate refusal, not success). --target both publishes
1843
- * store then repo sequentially with the same version and short-circuits if the
1844
- * store leg fails; --json emits an array of per-target results.
1859
+ * command exits 1 (a deliberate refusal, not success). Multiple targets run
1860
+ * sequentially with the same version and short-circuit on the first failure;
1861
+ * --json emits an array of per-target results.
1845
1862
  * Exit codes: 0 published/dry-run, 1 bad args/unconfirmed, 2 failed/timeout.
1846
1863
  */
1847
1864
  async function runPublishApp(flags) {
@@ -1849,16 +1866,17 @@ async function runPublishApp(flags) {
1849
1866
  var version = flags.version;
1850
1867
  var target = flags.target;
1851
1868
  if (!app || !version || !target) {
1852
- process.stderr.write("publish-app: --app, --version and --target store|repo|both are required\n");
1869
+ process.stderr.write("publish-app: --app, --version and --target <" +
1870
+ publishApp_1.PUBLISH_TARGETS.join("|") +
1871
+ "|both> are required\n");
1853
1872
  return 1;
1854
1873
  }
1855
- if (target !== "store" && target !== "repo" && target !== "both") {
1856
- process.stderr.write("publish-app: --target must be store, repo or both (got '" +
1857
- target +
1858
- "')\n");
1874
+ var parsedTargets = (0, publishApp_1.parsePublishTargets)(target);
1875
+ if (parsedTargets.error) {
1876
+ process.stderr.write("publish-app: " + parsedTargets.error + "\n");
1859
1877
  return 1;
1860
1878
  }
1861
- var targets = target === "both" ? ["store", "repo"] : [target];
1879
+ var targets = parsedTargets.targets;
1862
1880
  var dryRun = flags["dry-run"] === "true";
1863
1881
  var confirmed = flags.confirm === "true";
1864
1882
  var client = (0, client_1.createClient)({});
@@ -1877,6 +1895,16 @@ async function runPublishApp(flags) {
1877
1895
  params.devNotes = flags["dev-notes"];
1878
1896
  if (flags["store-user"])
1879
1897
  params.storeUsername = flags["store-user"];
1898
+ if (flags["update-set-name"]) {
1899
+ params.updateSetName = flags["update-set-name"];
1900
+ }
1901
+ // Read with !== undefined, not truthiness: an intentionally empty
1902
+ // description must stay empty rather than silently fall back.
1903
+ if (flags["update-set-description"] !== undefined) {
1904
+ params.updateSetDescription = flags["update-set-description"];
1905
+ }
1906
+ if (flags["include-data"] === "true")
1907
+ params.includeData = true;
1880
1908
  if (flags["timeout-ms"]) {
1881
1909
  // A NaN timeout would make the poll-loop budget check always false —
1882
1910
  // an infinite loop. Validate here, exit 1 on garbage.
package/dist/index.d.ts CHANGED
@@ -29,7 +29,7 @@ export type { RecordWriteResult, SetFieldParams, SetFieldResult, } from "./setFi
29
29
  export type { CreateRecordParams, CreateRecordResult } from "./createRecord";
30
30
  export { invokeRest, INVOKE_REST_METHODS } from "./invokeRest";
31
31
  export type { InvokeRestParams, InvokeRestResult } from "./invokeRest";
32
- export { publishApp, buildStartFields, parseXmlAnswer, parseProgressTree, classifyProgress, flattenSteps, harvestProgressResults, parseCicdPublishResponse, parseCicdProgress, DEFAULT_PUBLISH_TIMEOUT_MS, PUBLISH_POLL_DELAYS_MS, } from "./publishApp";
32
+ export { publishApp, buildStartFields, buildCreateUpdateSetFields, buildPublishToUpdateSetFields, resolveUpdateSetNaming, describeTarget, parsePublishTargets, PUBLISH_TARGETS, parseXmlAnswer, parseProgressTree, classifyProgress, flattenSteps, harvestProgressResults, parseCicdPublishResponse, parseCicdProgress, DEFAULT_PUBLISH_TIMEOUT_MS, PUBLISH_POLL_DELAYS_MS, } from "./publishApp";
33
33
  export type { PublishAppParams, PublishAppResult, PublishTarget, PublishStep, ProgressNode, CicdProgress, PublishTransport, } from "./publishApp";
34
34
  export { exportUpdateSet, renderUpdateXmlRow, renderRemoteUpdateSet, renderUnload, countUnloadRecords, countUpdateXml, fetchUpdateXmlRows, refreshTypeFields, parseStatsCount, formatUnloadDate, xmlEscape, UPDATE_XML_FIELDS, DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE, DEFAULT_MAX_ROWS, } from "./exportUpdateSet";
35
35
  export type { ExportUpdateSetParams, ExportUpdateSetResult, ExportMode, ExportTransport, } from "./exportUpdateSet";
package/dist/index.js CHANGED
@@ -7,8 +7,8 @@
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  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.formatRemoveChoicesResult = exports.formatAddChoicesResult = exports.formatHostAssetsResult = exports.classifyChunks = exports.hostAssets = exports.ChoiceWriteError = exports.removeChoicesFromField = exports.addChoicesToField = exports.resolveConfigFromEnvFile = exports.createClientFromEnvFile = exports.assertWriteAllowed = exports.evaluateWriteGate = exports.isCiEnvironment = exports.resolveExecutionContext = exports.createClient = void 0;
10
- exports.DEFAULT_EXPORT_APP_TIMEOUT_MS = exports.buildPublishFields = exports.buildCreateSetFields = exports.exportApp = exports.DEFAULT_MAX_ROWS = exports.MAX_PAGE_SIZE = exports.DEFAULT_PAGE_SIZE = exports.UPDATE_XML_FIELDS = exports.xmlEscape = exports.formatUnloadDate = exports.parseStatsCount = exports.refreshTypeFields = exports.fetchUpdateXmlRows = exports.countUpdateXml = exports.countUnloadRecords = exports.renderUnload = exports.renderRemoteUpdateSet = exports.renderUpdateXmlRow = exports.exportUpdateSet = 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.resolveTableAttributes = exports.setTable = exports.toStoredValue = exports.resolveAttributes = exports.setColumn = exports.indexMatchesColumns = exports.parseIndexColumns = exports.addIndex = exports.deriveElement = exports.addColumn = exports.DEFAULT_SAVE_ACTION = exports.DEFAULT_SUPER_CLASS = exports.TYPE_MAP = exports.defaultAccessFlags = exports.applyTableSaveOverlay = exports.resolveType = void 0;
11
- exports.SECRET_INTERNAL_TYPES = exports.SENTINEL = exports.isCapturable = exports.secretFieldsFromDictionary = exports.mergeSecretRules = exports.loadSecretRules = exports.defaultSecretRules = exports.escapeRegExp = exports.encodeXmlText = exports.encodeXmlEntities = exports.stripJsonValue = exports.stripField = exports.plannedStrips = exports.recordFieldNames = exports.readRecordTable = exports.readField = exports.verifyStripped = exports.stripSecrets = void 0;
10
+ exports.DEFAULT_PAGE_SIZE = exports.UPDATE_XML_FIELDS = exports.xmlEscape = exports.formatUnloadDate = exports.parseStatsCount = exports.refreshTypeFields = exports.fetchUpdateXmlRows = exports.countUpdateXml = exports.countUnloadRecords = exports.renderUnload = exports.renderRemoteUpdateSet = exports.renderUpdateXmlRow = exports.exportUpdateSet = 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.PUBLISH_TARGETS = exports.parsePublishTargets = exports.describeTarget = exports.resolveUpdateSetNaming = exports.buildPublishToUpdateSetFields = exports.buildCreateUpdateSetFields = exports.buildStartFields = exports.publishApp = exports.INVOKE_REST_METHODS = exports.invokeRest = exports.createRecord = exports.setField = exports.resolveTableAttributes = exports.setTable = exports.toStoredValue = exports.resolveAttributes = exports.setColumn = exports.indexMatchesColumns = exports.parseIndexColumns = exports.addIndex = exports.deriveElement = exports.addColumn = exports.DEFAULT_SAVE_ACTION = exports.DEFAULT_SUPER_CLASS = exports.TYPE_MAP = exports.defaultAccessFlags = exports.applyTableSaveOverlay = exports.resolveType = void 0;
11
+ exports.SECRET_INTERNAL_TYPES = exports.SENTINEL = exports.isCapturable = exports.secretFieldsFromDictionary = exports.mergeSecretRules = exports.loadSecretRules = exports.defaultSecretRules = exports.escapeRegExp = exports.encodeXmlText = exports.encodeXmlEntities = exports.stripJsonValue = exports.stripField = exports.plannedStrips = exports.recordFieldNames = exports.readRecordTable = exports.readField = exports.verifyStripped = exports.stripSecrets = exports.DEFAULT_EXPORT_APP_TIMEOUT_MS = exports.buildPublishFields = exports.buildCreateSetFields = exports.exportApp = exports.DEFAULT_MAX_ROWS = exports.MAX_PAGE_SIZE = void 0;
12
12
  var client_1 = require("./client");
13
13
  Object.defineProperty(exports, "createClient", { enumerable: true, get: function () { return client_1.createClient; } });
14
14
  var executionContext_1 = require("./executionContext");
@@ -99,6 +99,12 @@ Object.defineProperty(exports, "INVOKE_REST_METHODS", { enumerable: true, get: f
99
99
  var publishApp_1 = require("./publishApp");
100
100
  Object.defineProperty(exports, "publishApp", { enumerable: true, get: function () { return publishApp_1.publishApp; } });
101
101
  Object.defineProperty(exports, "buildStartFields", { enumerable: true, get: function () { return publishApp_1.buildStartFields; } });
102
+ Object.defineProperty(exports, "buildCreateUpdateSetFields", { enumerable: true, get: function () { return publishApp_1.buildCreateUpdateSetFields; } });
103
+ Object.defineProperty(exports, "buildPublishToUpdateSetFields", { enumerable: true, get: function () { return publishApp_1.buildPublishToUpdateSetFields; } });
104
+ Object.defineProperty(exports, "resolveUpdateSetNaming", { enumerable: true, get: function () { return publishApp_1.resolveUpdateSetNaming; } });
105
+ Object.defineProperty(exports, "describeTarget", { enumerable: true, get: function () { return publishApp_1.describeTarget; } });
106
+ Object.defineProperty(exports, "parsePublishTargets", { enumerable: true, get: function () { return publishApp_1.parsePublishTargets; } });
107
+ Object.defineProperty(exports, "PUBLISH_TARGETS", { enumerable: true, get: function () { return publishApp_1.PUBLISH_TARGETS; } });
102
108
  Object.defineProperty(exports, "parseXmlAnswer", { enumerable: true, get: function () { return publishApp_1.parseXmlAnswer; } });
103
109
  Object.defineProperty(exports, "parseProgressTree", { enumerable: true, get: function () { return publishApp_1.parseProgressTree; } });
104
110
  Object.defineProperty(exports, "classifyProgress", { enumerable: true, get: function () { return publishApp_1.classifyProgress; } });
package/dist/loadEnv.d.ts CHANGED
@@ -1,15 +1,45 @@
1
+ /**
2
+ * ServiceNow connection variables. When an env file is selected explicitly,
3
+ * these are taken from the file ONLY — any value already in process.env is
4
+ * cleared first — so the selected file fully determines the target instance
5
+ * and auth mode. (Without this, a shell/session-exported SN_INSTANCE or
6
+ * SN_API_KEY silently won over the file and the command hit the wrong
7
+ * instance.)
8
+ */
9
+ export declare var SN_CONNECTION_KEYS: string[];
10
+ /**
11
+ * Resolve an `--env` selector to an absolute env-file path.
12
+ *
13
+ * Accepts, in order:
14
+ * - an absolute path, or any value containing a path separator → used as a path
15
+ * (relative paths resolve against cwd) — unchanged legacy behavior;
16
+ * - a bare name that exists as a file in cwd (e.g. `--env my.env`) → that file;
17
+ * - a bare name like `loft` → `<cwd>/.env.loft`, matching the MCP tool's
18
+ * per-call `env` resolution; a `.env`-prefixed basename is used as-is.
19
+ *
20
+ * Returns the resolved path; does not check existence (loadEnvFile does).
21
+ */
22
+ export declare function resolveEnvSelection(raw: string, cwd?: string): string;
1
23
  /**
2
24
  * Loads ServiceNow credentials for the `dove-sn` CLI and its MCP server.
3
25
  *
4
26
  * Resolution order for the env file:
5
- * 1. An explicit `--env <path>` / `--env-file <path>` flag (passed in here).
6
- * 2. The `DOVETAIL_ENV_FILE` environment variable — lets an MCP host point
7
- * `dove-sn mcp` at a specific credential file without a CLI flag.
27
+ * 1. An explicit `--env <name|path>` / `--env-file <name|path>` flag.
28
+ * 2. The `DOVETAIL_ENV_FILE` environment variable.
8
29
  * 3. The default `.env` in the current working directory.
9
30
  *
10
- * dotenv does not override variables already present in process.env, so an
11
- * explicit file augments (never clobbers) credentials the parent shell exported.
31
+ * For an explicit selection (1 or 2) this FAILS CLOSED: a missing file, or a
32
+ * file that doesn't define an instance plus credentials, throws instead of
33
+ * silently falling back to whatever instance the surrounding environment
34
+ * points at. The file's ServiceNow connection variables (SN_CONNECTION_KEYS)
35
+ * replace any already in process.env; every other variable keeps dotenv's
36
+ * never-override semantics.
37
+ *
38
+ * The default cwd `.env` (3) keeps its historical behavior: optional, and
39
+ * never overrides already-exported variables.
12
40
  *
13
- * @param {string} [explicitPath] - Path from the `--env` / `--env-file` flag.
41
+ * @param {string} [explicitSelection] - Value of the `--env` / `--env-file` flag.
42
+ * @returns {string|undefined} The absolute path of the explicitly selected
43
+ * file, or undefined when the default `.env` path was used.
14
44
  */
15
- export declare function loadEnvFile(explicitPath?: string): void;
45
+ export declare function loadEnvFile(explicitSelection?: string): string | undefined;
package/dist/loadEnv.js CHANGED
@@ -3,30 +3,110 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SN_CONNECTION_KEYS = void 0;
7
+ exports.resolveEnvSelection = resolveEnvSelection;
6
8
  exports.loadEnvFile = loadEnvFile;
9
+ const fs_1 = __importDefault(require("fs"));
7
10
  const dotenv_1 = __importDefault(require("dotenv"));
8
11
  const path_1 = __importDefault(require("path"));
12
+ const createClientFromEnvFile_1 = require("./createClientFromEnvFile");
13
+ /**
14
+ * ServiceNow connection variables. When an env file is selected explicitly,
15
+ * these are taken from the file ONLY — any value already in process.env is
16
+ * cleared first — so the selected file fully determines the target instance
17
+ * and auth mode. (Without this, a shell/session-exported SN_INSTANCE or
18
+ * SN_API_KEY silently won over the file and the command hit the wrong
19
+ * instance.)
20
+ */
21
+ exports.SN_CONNECTION_KEYS = [
22
+ "SN_INSTANCE",
23
+ "SN_DEV_INSTANCE",
24
+ "SN_PROD_INSTANCE",
25
+ "SN_API_KEY",
26
+ "SN_DEV_API_KEY",
27
+ "SN_PROD_API_KEY",
28
+ "SN_USER",
29
+ "SN_PASSWORD",
30
+ "SN_DEV_USERNAME",
31
+ "SN_DEV_PASSWORD",
32
+ "SN_PROD_USERNAME",
33
+ "SN_PROD_PASSWORD",
34
+ ];
35
+ /**
36
+ * Resolve an `--env` selector to an absolute env-file path.
37
+ *
38
+ * Accepts, in order:
39
+ * - an absolute path, or any value containing a path separator → used as a path
40
+ * (relative paths resolve against cwd) — unchanged legacy behavior;
41
+ * - a bare name that exists as a file in cwd (e.g. `--env my.env`) → that file;
42
+ * - a bare name like `loft` → `<cwd>/.env.loft`, matching the MCP tool's
43
+ * per-call `env` resolution; a `.env`-prefixed basename is used as-is.
44
+ *
45
+ * Returns the resolved path; does not check existence (loadEnvFile does).
46
+ */
47
+ function resolveEnvSelection(raw, cwd) {
48
+ if (typeof raw !== "string" || raw.trim().length === 0) {
49
+ throw new Error("--env must be a non-empty env-file name or path.");
50
+ }
51
+ var value = raw.trim();
52
+ var base = cwd || process.cwd();
53
+ if (path_1.default.isAbsolute(value) || /[\\/]/.test(value)) {
54
+ return path_1.default.resolve(base, value);
55
+ }
56
+ var literal = path_1.default.resolve(base, value);
57
+ if (fs_1.default.existsSync(literal)) {
58
+ return literal;
59
+ }
60
+ if (value.indexOf(".env") === 0) {
61
+ return literal;
62
+ }
63
+ return path_1.default.resolve(base, ".env." + value);
64
+ }
9
65
  /**
10
66
  * Loads ServiceNow credentials for the `dove-sn` CLI and its MCP server.
11
67
  *
12
68
  * Resolution order for the env file:
13
- * 1. An explicit `--env <path>` / `--env-file <path>` flag (passed in here).
14
- * 2. The `DOVETAIL_ENV_FILE` environment variable — lets an MCP host point
15
- * `dove-sn mcp` at a specific credential file without a CLI flag.
69
+ * 1. An explicit `--env <name|path>` / `--env-file <name|path>` flag.
70
+ * 2. The `DOVETAIL_ENV_FILE` environment variable.
16
71
  * 3. The default `.env` in the current working directory.
17
72
  *
18
- * dotenv does not override variables already present in process.env, so an
19
- * explicit file augments (never clobbers) credentials the parent shell exported.
73
+ * For an explicit selection (1 or 2) this FAILS CLOSED: a missing file, or a
74
+ * file that doesn't define an instance plus credentials, throws instead of
75
+ * silently falling back to whatever instance the surrounding environment
76
+ * points at. The file's ServiceNow connection variables (SN_CONNECTION_KEYS)
77
+ * replace any already in process.env; every other variable keeps dotenv's
78
+ * never-override semantics.
79
+ *
80
+ * The default cwd `.env` (3) keeps its historical behavior: optional, and
81
+ * never overrides already-exported variables.
20
82
  *
21
- * @param {string} [explicitPath] - Path from the `--env` / `--env-file` flag.
83
+ * @param {string} [explicitSelection] - Value of the `--env` / `--env-file` flag.
84
+ * @returns {string|undefined} The absolute path of the explicitly selected
85
+ * file, or undefined when the default `.env` path was used.
22
86
  */
23
- function loadEnvFile(explicitPath) {
24
- var raw = explicitPath || process.env.DOVETAIL_ENV_FILE;
25
- if (raw) {
26
- var resolved = path_1.default.isAbsolute(raw) ? raw : path_1.default.resolve(process.cwd(), raw);
27
- dotenv_1.default.config({ path: resolved });
28
- return;
87
+ function loadEnvFile(explicitSelection) {
88
+ var raw = explicitSelection || process.env.DOVETAIL_ENV_FILE;
89
+ if (!raw) {
90
+ // No explicit selection — load .env from cwd if it exists (no-op otherwise).
91
+ dotenv_1.default.config();
92
+ return undefined;
93
+ }
94
+ var resolved = resolveEnvSelection(raw);
95
+ if (!fs_1.default.existsSync(resolved)) {
96
+ throw new Error("env file not found: '" + resolved + "' (from --env '" + raw + "'). " +
97
+ "Refusing to fall back to the default environment — pass an existing env-file " +
98
+ "name (e.g. 'prod' → .env.prod in the current directory) or path.");
29
99
  }
30
- // No explicit selection — load .env from cwd if it exists (no-op otherwise).
31
- dotenv_1.default.config();
100
+ // Validates instance + credentials and throws loudly if either is missing.
101
+ (0, createClientFromEnvFile_1.resolveConfigFromEnvFile)(resolved);
102
+ var parsed = dotenv_1.default.parse(fs_1.default.readFileSync(resolved, "utf8"));
103
+ exports.SN_CONNECTION_KEYS.forEach(function (key) {
104
+ delete process.env[key];
105
+ if (Object.prototype.hasOwnProperty.call(parsed, key) && parsed[key] !== "") {
106
+ process.env[key] = parsed[key];
107
+ }
108
+ });
109
+ // Everything else in the file: dotenv's normal never-override semantics.
110
+ dotenv_1.default.config({ path: resolved });
111
+ return resolved;
32
112
  }
@@ -624,18 +624,24 @@ function buildDescriptors(deps = {}) {
624
624
  {
625
625
  name: "app_publish",
626
626
  annotations: dovetail_mcp_kit_1.WRITE_EXECUTE,
627
- description: "Publish a scoped ServiceNow application to ONE target per call — the ServiceNow Store " +
628
- "(target 'store') or the company Application Repository (target 'repo'); call twice to " +
629
- "publish to both — then poll the publish to completion. STORE PUBLISH IS EXTERNALLY " +
630
- "VISIBLE on the ServiceNow Store — treat it as a release. DRY-RUN BY DEFAULT: without " +
631
- "confirm:true the resolved plan (app, current version, target) is returned and nothing is " +
632
- "published. target 'store' replays the sys_app form's upload flow over a form-login session " +
633
- "and requires SN_STORE_USERNAME/SN_STORE_PASSWORD in the server's env file — credentials " +
634
- "never transit tool arguments. target 'repo' uses the supported CI/CD REST API " +
635
- "(/api/sn_cicd/app_repo/publish) and requires the sn_cicd role. app is a scope name, " +
627
+ description: "Publish a scoped ServiceNow application to ONE target per call, then poll the publish to " +
628
+ "completion; call repeatedly to hit several targets. STORE PUBLISH IS EXTERNALLY VISIBLE " +
629
+ "on the ServiceNow Store — treat it as a release. DRY-RUN BY DEFAULT: without confirm:true " +
630
+ "the resolved plan (app, current version, target) is returned and nothing is published. " +
631
+ "Targets: 'store' replays the sys_app form's upload flow over a form-login session and " +
632
+ "requires SN_STORE_USERNAME/SN_STORE_PASSWORD in the server's env file — credentials never " +
633
+ "transit tool arguments. 'repo' publishes to the company Application Repository via the " +
634
+ "supported CI/CD REST API (/api/sn_cicd/app_repo/publish) and requires the sn_cicd plugin " +
635
+ "and role. 'repo-ui' reaches the SAME company repository over the UI uploader instead — use " +
636
+ "it on instances without sn_cicd, where 'repo' 404s. 'update-set' publishes the app INTO a " +
637
+ "newly created update set via the two-call AppsAjaxProcessor flow (no REST equivalent " +
638
+ "exists); updateSetName defaults to the app's name and updateSetDescription is conventionally " +
639
+ "the release date stamp (YYYYMMDD) so a whole release is one sys_update_set query. " +
640
+ "includeData (default false) is the dialog's 'Include demo data' box. app is a scope name, " +
636
641
  "sys_app sys_id, or app name; version must be above the currently published version. The " +
637
642
  "result carries the progress-tracker id, per-step states, the Store appLink, and the " +
638
- "publish's update-set sys_id where the instance reports one.",
643
+ "update-set sys_id (for 'update-set' this is set as soon as the set is created, so it " +
644
+ "survives a later failure).",
639
645
  shape: schemas_1.publishAppSchema.shape,
640
646
  handler: async function (args) {
641
647
  var p = schemas_1.publishAppSchema.parse(args);
@@ -644,6 +650,9 @@ function buildDescriptors(deps = {}) {
644
650
  version: p.version,
645
651
  target: p.target,
646
652
  devNotes: p.devNotes,
653
+ updateSetName: p.updateSetName,
654
+ updateSetDescription: p.updateSetDescription,
655
+ includeData: p.includeData,
647
656
  confirm: p.confirm,
648
657
  dryRun: p.dryRun,
649
658
  timeoutMs: p.timeoutMs,
@@ -1099,26 +1099,35 @@ export declare var publishAppSchema: z.ZodObject<{
1099
1099
  app: z.ZodString;
1100
1100
  version: z.ZodString;
1101
1101
  devNotes: z.ZodOptional<z.ZodString>;
1102
- target: z.ZodUnion<[z.ZodLiteral<"store">, z.ZodLiteral<"repo">]>;
1102
+ target: z.ZodUnion<[z.ZodLiteral<"store">, z.ZodLiteral<"repo">, z.ZodLiteral<"repo-ui">, z.ZodLiteral<"update-set">]>;
1103
+ updateSetName: z.ZodOptional<z.ZodString>;
1104
+ updateSetDescription: z.ZodOptional<z.ZodString>;
1105
+ includeData: z.ZodOptional<z.ZodBoolean>;
1103
1106
  confirm: z.ZodOptional<z.ZodBoolean>;
1104
1107
  dryRun: z.ZodOptional<z.ZodBoolean>;
1105
1108
  timeoutMs: z.ZodOptional<z.ZodNumber>;
1106
1109
  }, "strip", z.ZodTypeAny, {
1107
- target: "store" | "repo";
1110
+ target: "store" | "repo" | "repo-ui" | "update-set";
1108
1111
  version: string;
1109
1112
  app: string;
1110
1113
  confirm?: boolean | undefined;
1111
1114
  dryRun?: boolean | undefined;
1115
+ includeData?: boolean | undefined;
1112
1116
  timeoutMs?: number | undefined;
1113
1117
  devNotes?: string | undefined;
1118
+ updateSetName?: string | undefined;
1119
+ updateSetDescription?: string | undefined;
1114
1120
  }, {
1115
- target: "store" | "repo";
1121
+ target: "store" | "repo" | "repo-ui" | "update-set";
1116
1122
  version: string;
1117
1123
  app: string;
1118
1124
  confirm?: boolean | undefined;
1119
1125
  dryRun?: boolean | undefined;
1126
+ includeData?: boolean | undefined;
1120
1127
  timeoutMs?: number | undefined;
1121
1128
  devNotes?: string | undefined;
1129
+ updateSetName?: string | undefined;
1130
+ updateSetDescription?: string | undefined;
1122
1131
  }>;
1123
1132
  export declare var invokeRestSchema: z.ZodObject<{
1124
1133
  method: z.ZodEffects<z.ZodEnum<["GET", "POST", "PUT", "DELETE"]>, "GET" | "DELETE" | "POST" | "PUT", unknown>;
@@ -339,7 +339,15 @@ exports.publishAppSchema = zod_1.z.object({
339
339
  app: zod_1.z.string().min(1),
340
340
  version: zod_1.z.string().min(1),
341
341
  devNotes: zod_1.z.string().optional(),
342
- target: zod_1.z.union([zod_1.z.literal("store"), zod_1.z.literal("repo")]),
342
+ target: zod_1.z.union([
343
+ zod_1.z.literal("store"),
344
+ zod_1.z.literal("repo"),
345
+ zod_1.z.literal("repo-ui"),
346
+ zod_1.z.literal("update-set"),
347
+ ]),
348
+ updateSetName: zod_1.z.string().optional(),
349
+ updateSetDescription: zod_1.z.string().optional(),
350
+ includeData: zod_1.z.boolean().optional(),
343
351
  confirm: zod_1.z.boolean().optional(),
344
352
  dryRun: zod_1.z.boolean().optional(),
345
353
  timeoutMs: zod_1.z.number().int().positive().optional(),