@tenonhq/dovetail-core 0.0.97 → 0.0.99

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/dist/appUtils.js CHANGED
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.createAndAssignUpdateSet = exports.swapScope = exports.buildFiles = exports.summarizeRecord = exports.pushFiles = exports.getAppFileList = exports.groupAppFiles = exports.processMissingFiles = exports.refreshAllFiles = exports.findMissingFiles = exports.syncManifest = exports.processManifest = exports.normalizeManifestKeys = exports.toSafeFolderName = void 0;
39
+ exports.createAndAssignUpdateSet = exports.swapScope = exports.buildFiles = exports.summarizeRecord = exports.pushFiles = exports.getAppFileList = exports.groupAppFiles = exports.processMissingFiles = exports.refreshAllFiles = exports.findMissingFiles = exports.syncManifest = exports.processManifest = exports.normalizeManifestKeys = exports.toSafeFolderName = exports.stampMetadataContent = void 0;
40
40
  const path_1 = __importDefault(require("path"));
41
41
  const fs_1 = __importDefault(require("fs"));
42
42
  const progress_1 = __importDefault(require("progress"));
@@ -62,6 +62,14 @@ const getUpdateSetConfig = () => {
62
62
  }
63
63
  return {};
64
64
  };
65
+ // Strip the ServiceNow instance host from an absolute _record_link, leaving the
66
+ // instance-relative path. The CADSO metadata endpoint returns an absolute URL
67
+ // (https://<instance>.service-now.com/<table>.do?sys_id=...); persisting that
68
+ // makes the on-disk artifact instance-bound, so a re-pull from a different
69
+ // instance rewrites every metaData.json with the new host. The relative path
70
+ // (/<table>.do?sys_id=...) resolves against whatever instance the reader is on.
71
+ // Matches any *.service-now.com host so the output is portable across instances.
72
+ const stripRecordLinkHost = (link) => link.replace(/^https?:\/\/[^/]+\.service-now\.com/i, "");
65
73
  // Merge _lastUpdatedOn into the server-provided metadata content. Preserves all
66
74
  // record fields (sys_id, sys_scope, field value/display_value pairs, etc.) so
67
75
  // the local metaData.json is a full snapshot of the record, not just a stub.
@@ -80,6 +88,9 @@ const stampMetadataContent = (file) => {
80
88
  else {
81
89
  metadata._lastUpdatedOn = stamp;
82
90
  }
91
+ if (typeof metadata._record_link === "string") {
92
+ metadata._record_link = stripRecordLinkHost(metadata._record_link);
93
+ }
83
94
  return { ...file, content: JSON.stringify(metadata, null, 2) };
84
95
  }
85
96
  catch (e) {
@@ -87,6 +98,7 @@ const stampMetadataContent = (file) => {
87
98
  return file;
88
99
  }
89
100
  };
101
+ exports.stampMetadataContent = stampMetadataContent;
90
102
  const hasServerMetadata = (files) => files.some((f) => f.name === "metaData" && f.type === "json" && !!f.content);
91
103
  const processFilesInManRec = async (recPath, rec, forceWrite) => {
92
104
  FileLogger_1.fileLogger.debug("Processing record: " + rec.name + " (" + rec.files.length + " files)");
@@ -101,7 +113,7 @@ const processFilesInManRec = async (recPath, rec, forceWrite) => {
101
113
  };
102
114
  await fileWrite(stubMetadata, recPath);
103
115
  }
104
- const writeResults = await (0, genericUtils_1.allSettledBatched)(rec.files, constants_1.CONCURRENCY_FILES, function (file) { return fileWrite(stampMetadataContent(file), recPath); });
116
+ const writeResults = await (0, genericUtils_1.allSettledBatched)(rec.files, constants_1.CONCURRENCY_FILES, function (file) { return fileWrite((0, exports.stampMetadataContent)(file), recPath); });
105
117
  const writeFailures = writeResults.filter((r) => r.status === "rejected");
106
118
  if (writeFailures.length > 0) {
107
119
  writeFailures.forEach((f) => {
@@ -572,7 +584,7 @@ const refreshAllFiles = async (newManifest, sourcePath, options = {}) => {
572
584
  if (anyChanged || forceWrite) {
573
585
  let metadataFile;
574
586
  if (metadataFiles.length > 0 && metadataFiles[0].content) {
575
- metadataFile = stampMetadataContent(metadataFiles[0]);
587
+ metadataFile = (0, exports.stampMetadataContent)(metadataFiles[0]);
576
588
  }
577
589
  else {
578
590
  metadataFile = {
package/dist/bootstrap.js CHANGED
@@ -40,7 +40,15 @@ exports.init = init;
40
40
  const dotenv_1 = __importDefault(require("dotenv"));
41
41
  const ConfigManager = __importStar(require("./config"));
42
42
  const Logger_1 = require("./Logger");
43
+ const envArg_1 = require("./envArg");
43
44
  async function init() {
45
+ // Honor a per-command `--env <path>` override before config/env load so
46
+ // dotenv reads the requested file. Resolved here (not in loadConfigs) because
47
+ // the override must be set before loadEnvPath runs inside loadConfigs.
48
+ const envArg = (0, envArg_1.parseEnvArg)(process.argv.slice(2));
49
+ if (envArg) {
50
+ ConfigManager.setEnvPathOverride((0, envArg_1.resolveEnvArgPath)(envArg));
51
+ }
44
52
  let configLoaded = false;
45
53
  try {
46
54
  await ConfigManager.loadConfigs();
package/dist/commander.js CHANGED
@@ -24,6 +24,12 @@ async function initCommands() {
24
24
  },
25
25
  };
26
26
  yargs_1.default
27
+ .option("env", {
28
+ alias: "e",
29
+ type: "string",
30
+ describe: "Path to a .env file to load for this command (default: .env in the project root). Lets one checkout target multiple instances.",
31
+ })
32
+ .global("env")
27
33
  .command(["watch", "w", "watchAllScopes"], "Watch all scopes for file changes and sync to ServiceNow", (cmdArgs) => {
28
34
  cmdArgs.options({
29
35
  ...sharedOptions,
package/dist/config.js CHANGED
@@ -48,6 +48,7 @@ exports.getSourcePath = getSourcePath;
48
48
  exports.getSourcePathForScope = getSourcePathForScope;
49
49
  exports.getBuildPath = getBuildPath;
50
50
  exports.getEnvPath = getEnvPath;
51
+ exports.setEnvPathOverride = setEnvPathOverride;
51
52
  exports.getDiffPath = getDiffPath;
52
53
  exports.getDiffFile = getDiffFile;
53
54
  exports.getRefresh = getRefresh;
@@ -96,6 +97,7 @@ let config_path;
96
97
  let source_path;
97
98
  let build_path;
98
99
  let env_path;
100
+ let env_path_override;
99
101
  let manifest_path;
100
102
  let diff_path;
101
103
  let diff_file;
@@ -188,6 +190,16 @@ function getEnvPath() {
188
190
  return env_path;
189
191
  throw new Error("Error getting env path");
190
192
  }
193
+ /**
194
+ * @description Overrides the .env file the CLI loads for this invocation.
195
+ * Set from the per-command `--env` flag before loadConfigs() runs (see
196
+ * bootstrap.ts) so `loadEnvPath` resolves to the requested file. Pass an
197
+ * absolute path; relative resolution is handled by the caller (resolveEnvArgPath).
198
+ * @param {string} absolutePath - Absolute path to the .env file to load.
199
+ */
200
+ function setEnvPathOverride(absolutePath) {
201
+ env_path_override = absolutePath;
202
+ }
191
203
  function getDiffPath() {
192
204
  if (diff_path)
193
205
  return diff_path;
@@ -535,6 +547,13 @@ async function loadBuildPath() {
535
547
  }
536
548
  async function loadEnvPath() {
537
549
  let rootDir = getRootDir();
550
+ // A per-command `--env` override wins over the project-root .env so a single
551
+ // checkout can target multiple instances by pointing at different credential
552
+ // files. The override is already resolved to an absolute path by the caller.
553
+ if (env_path_override) {
554
+ env_path = env_path_override;
555
+ return;
556
+ }
538
557
  env_path = path_1.default.join(rootDir, ".env");
539
558
  }
540
559
  async function loadManifestPath() {
package/dist/envArg.js ADDED
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.parseEnvArg = parseEnvArg;
7
+ exports.resolveEnvArgPath = resolveEnvArgPath;
8
+ const path_1 = __importDefault(require("path"));
9
+ /**
10
+ * Per-invocation .env selection for the `dove` CLI.
11
+ *
12
+ * Every command accepts `--env <path>` (alias `-e`, also `--env-file` /
13
+ * `--envFile`) so a single checkout can target multiple instances by pointing
14
+ * at different credential files, e.g.:
15
+ *
16
+ * npx dove push --env .env.prod
17
+ * npx dove status -e ../envs/workshop.env
18
+ *
19
+ * The flag is parsed from raw argv before config/env load (see bootstrap.ts)
20
+ * and also registered as a global yargs option so it appears in `--help`.
21
+ */
22
+ // Flag spellings that select an env file. `-e` is the short alias.
23
+ var ENV_FLAGS = ["--env", "--env-file", "--envFile", "-e"];
24
+ /**
25
+ * @description Extracts the env-file path from a raw argv slice. Supports both
26
+ * `--env <path>` and `--env=<path>` (and the `-e` / `--env-file` spellings).
27
+ * Returns the last occurrence so a later flag overrides an earlier one.
28
+ * @param {string[]} argv - Arguments after the node + script entries (process.argv.slice(2)).
29
+ * @returns {string|undefined} The raw path string, or undefined when absent.
30
+ */
31
+ function parseEnvArg(argv) {
32
+ var found;
33
+ for (var i = 0; i < argv.length; i++) {
34
+ var arg = argv[i];
35
+ // `--env=path` / `-e=path`
36
+ var eq = arg.match(/^(--env|--env-file|--envFile|-e)=(.*)$/);
37
+ if (eq) {
38
+ found = eq[2];
39
+ continue;
40
+ }
41
+ // `--env path` / `-e path`
42
+ if (ENV_FLAGS.indexOf(arg) !== -1) {
43
+ var next = argv[i + 1];
44
+ if (typeof next === "string" && next.charAt(0) !== "-") {
45
+ found = next;
46
+ i++;
47
+ }
48
+ }
49
+ }
50
+ if (found === undefined || found === "")
51
+ return undefined;
52
+ return found;
53
+ }
54
+ /**
55
+ * @description Resolves a raw env-file argument to an absolute path. Relative
56
+ * paths resolve against the directory the command was run from (cwd), matching
57
+ * where the user typed the path.
58
+ * @param {string} rawPath - The value returned by parseEnvArg.
59
+ * @param {string} [cwd] - Base directory for relative paths (defaults to process.cwd()).
60
+ * @returns {string} An absolute path to the requested .env file.
61
+ */
62
+ function resolveEnvArgPath(rawPath, cwd) {
63
+ if (path_1.default.isAbsolute(rawPath))
64
+ return rawPath;
65
+ return path_1.default.resolve(cwd || process.cwd(), rawPath);
66
+ }
@@ -1,4 +1,37 @@
1
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
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
@@ -12,8 +45,24 @@ const fs_1 = __importDefault(require("fs"));
12
45
  const path_1 = __importDefault(require("path"));
13
46
  const Logger_1 = require("../Logger");
14
47
  const FileUtils_1 = require("../FileUtils");
48
+ const ConfigManager = __importStar(require("../config"));
15
49
  const discovery_1 = require("./discovery");
16
50
  const corePlugin_1 = require("./corePlugin");
51
+ /**
52
+ * @description Resolves the .env file login should read from and write to,
53
+ * honoring a per-command `--env` override when one is set. Falls back to
54
+ * <rootDir>/.env when config hasn't loaded an override.
55
+ * @param {string} rootDir - Project root used for the default .env location.
56
+ * @returns {string} Absolute path to the target .env file.
57
+ */
58
+ function resolveLoginEnvPath(rootDir) {
59
+ try {
60
+ return ConfigManager.getEnvPath();
61
+ }
62
+ catch (e) {
63
+ return path_1.default.resolve(rootDir, ".env");
64
+ }
65
+ }
17
66
  // ============================================================================
18
67
  // Init Context
19
68
  // ============================================================================
@@ -24,7 +73,7 @@ const corePlugin_1 = require("./corePlugin");
24
73
  */
25
74
  function buildInitContext(plugins) {
26
75
  const rootDir = process.cwd();
27
- const envPath = path_1.default.resolve(rootDir, ".env");
76
+ const envPath = resolveLoginEnvPath(rootDir);
28
77
  const env = {};
29
78
  // Load existing .env values
30
79
  try {
@@ -254,10 +303,11 @@ function saveEnvVars(context, plugins) {
254
303
  .filter(key => context.env[key])
255
304
  .map(key => ({ key, value: context.env[key] }));
256
305
  if (vars.length > 0) {
257
- (0, FileUtils_1.writeEnvVars)({ vars });
306
+ const envPath = resolveLoginEnvPath(process.cwd());
307
+ (0, FileUtils_1.writeEnvVars)({ vars, envPath });
258
308
  // Also set on process.env for immediate use
259
309
  vars.forEach(({ key, value }) => { process.env[key] = value; });
260
- Logger_1.logger.success(chalk_1.default.green("✓ Saved to .env (existing values preserved)"));
310
+ Logger_1.logger.success(chalk_1.default.green("✓ Saved to " + path_1.default.basename(envPath) + " (existing values preserved)"));
261
311
  }
262
312
  }
263
313
  async function runInit(options) {
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ // Tests for the per-command `--env` flag parser (envArg.ts). Covers the flag
3
+ // spellings (--env, --env-file, --envFile, -e), `=` and space-separated forms,
4
+ // last-wins precedence, absence, and absolute/relative path resolution.
5
+ var __importDefault = (this && this.__importDefault) || function (mod) {
6
+ return (mod && mod.__esModule) ? mod : { "default": mod };
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ const path_1 = __importDefault(require("path"));
10
+ const envArg_1 = require("../envArg");
11
+ describe("parseEnvArg", function () {
12
+ it("returns undefined when no env flag is present", function () {
13
+ expect((0, envArg_1.parseEnvArg)(["push", "--diff", "main"])).toBeUndefined();
14
+ });
15
+ it("parses --env <path> (space-separated)", function () {
16
+ expect((0, envArg_1.parseEnvArg)(["push", "--env", ".env.prod"])).toBe(".env.prod");
17
+ });
18
+ it("parses --env=<path>", function () {
19
+ expect((0, envArg_1.parseEnvArg)(["status", "--env=../envs/workshop.env"])).toBe("../envs/workshop.env");
20
+ });
21
+ it("parses the -e short alias (space and = forms)", function () {
22
+ expect((0, envArg_1.parseEnvArg)(["status", "-e", ".env.dev"])).toBe(".env.dev");
23
+ expect((0, envArg_1.parseEnvArg)(["status", "-e=.env.dev"])).toBe(".env.dev");
24
+ });
25
+ it("parses --env-file and --envFile spellings", function () {
26
+ expect((0, envArg_1.parseEnvArg)(["push", "--env-file", "a.env"])).toBe("a.env");
27
+ expect((0, envArg_1.parseEnvArg)(["push", "--envFile", "b.env"])).toBe("b.env");
28
+ });
29
+ it("takes the last occurrence when the flag is repeated", function () {
30
+ expect((0, envArg_1.parseEnvArg)(["push", "--env", "first.env", "--env", "second.env"])).toBe("second.env");
31
+ });
32
+ it("ignores a bare --env with no following value", function () {
33
+ expect((0, envArg_1.parseEnvArg)(["push", "--env"])).toBeUndefined();
34
+ });
35
+ it("does not consume the next token when it looks like another flag", function () {
36
+ expect((0, envArg_1.parseEnvArg)(["push", "--env", "--ci"])).toBeUndefined();
37
+ });
38
+ it("treats an empty --env= as absent", function () {
39
+ expect((0, envArg_1.parseEnvArg)(["push", "--env="])).toBeUndefined();
40
+ });
41
+ });
42
+ describe("resolveEnvArgPath", function () {
43
+ it("returns absolute paths unchanged", function () {
44
+ expect((0, envArg_1.resolveEnvArgPath)("/abs/path/.env", "/cwd")).toBe("/abs/path/.env");
45
+ });
46
+ it("resolves relative paths against the provided cwd", function () {
47
+ expect((0, envArg_1.resolveEnvArgPath)(".env.prod", "/projects/sn")).toBe(path_1.default.join("/projects/sn", ".env.prod"));
48
+ });
49
+ });
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ // Tests for stampMetadataContent — the writer that finalizes each record's
3
+ // metaData.json before it lands on disk.
4
+ //
5
+ // Two on-disk-stability concerns are covered:
6
+ // 1. _lastUpdatedOn is sourced from the record's sys_updated_on (stable across
7
+ // re-pulls) rather than a fresh wall-clock stamp.
8
+ // 2. _record_link is rewritten to an instance-relative path. The CADSO metadata
9
+ // endpoint returns an absolute URL (https://<instance>.service-now.com/...);
10
+ // persisting the host makes the artifact instance-bound and dirties every
11
+ // file on a re-pull from a different instance. Stripping any *.service-now.com
12
+ // host keeps the diff to genuine schema changes.
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ const appUtils_1 = require("../appUtils");
15
+ const metaFile = (content) => ({
16
+ name: "metaData",
17
+ type: "json",
18
+ content: JSON.stringify(content),
19
+ });
20
+ const parse = (file) => JSON.parse(file.content);
21
+ describe("stampMetadataContent — _record_link host stripping", () => {
22
+ it("strips the configured workstudio host, leaving the relative path", () => {
23
+ const out = (0, appUtils_1.stampMetadataContent)(metaFile({
24
+ sys_updated_on: { value: "2025-08-21 17:09:02" },
25
+ _record_link: "https://tenonworkstudio.service-now.com/sys_hub_action_input.do?sys_id=85de623c33ef2a107b18bc534d5c7b92",
26
+ }));
27
+ expect(parse(out)._record_link).toBe("/sys_hub_action_input.do?sys_id=85de623c33ef2a107b18bc534d5c7b92");
28
+ });
29
+ it("strips any *.service-now.com host (instance-portable)", () => {
30
+ for (const host of ["tenonworkshop", "tenon", "tenonworkbench"]) {
31
+ const out = (0, appUtils_1.stampMetadataContent)(metaFile({
32
+ _record_link: `https://${host}.service-now.com/x_cadso_core_setting.do?sys_id=abc`,
33
+ }));
34
+ expect(parse(out)._record_link).toBe("/x_cadso_core_setting.do?sys_id=abc");
35
+ }
36
+ });
37
+ it("leaves an already-relative _record_link untouched (idempotent)", () => {
38
+ const out = (0, appUtils_1.stampMetadataContent)(metaFile({ _record_link: "/sys_hub_action_input.do?sys_id=abc" }));
39
+ expect(parse(out)._record_link).toBe("/sys_hub_action_input.do?sys_id=abc");
40
+ });
41
+ it("leaves records without a _record_link unchanged", () => {
42
+ const out = (0, appUtils_1.stampMetadataContent)(metaFile({ sys_updated_on: { value: "2025-08-21 17:09:02" } }));
43
+ expect(parse(out)._record_link).toBeUndefined();
44
+ });
45
+ it("ignores a non-string _record_link rather than throwing", () => {
46
+ const out = (0, appUtils_1.stampMetadataContent)(metaFile({ _record_link: null }));
47
+ expect(parse(out)._record_link).toBeNull();
48
+ });
49
+ it("still sources _lastUpdatedOn from sys_updated_on while stripping the host", () => {
50
+ const out = (0, appUtils_1.stampMetadataContent)(metaFile({
51
+ sys_updated_on: { value: "2025-08-21 17:09:02" },
52
+ _record_link: "https://tenonworkstudio.service-now.com/incident.do?sys_id=1",
53
+ }));
54
+ const parsed = parse(out);
55
+ expect(parsed._lastUpdatedOn).toBe("2025-08-21 17:09:02");
56
+ expect(parsed._record_link).toBe("/incident.do?sys_id=1");
57
+ });
58
+ it("passes through non-metaData files verbatim", () => {
59
+ const script = { name: "script", type: "js", content: "var x = 1;" };
60
+ expect((0, appUtils_1.stampMetadataContent)(script)).toEqual(script);
61
+ });
62
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenonhq/dovetail-core",
3
- "version": "0.0.97",
3
+ "version": "0.0.99",
4
4
  "description": "Next-gen file syncer",
5
5
  "license": "GPL-3.0",
6
6
  "main": "./dist/index.js",
@@ -1,7 +1,111 @@
1
1
  {
2
2
  "$schema": "dovetail-manifest/v1",
3
- "generated_at": "2026-06-01T21:28:08.741Z",
3
+ "generated_at": "2026-06-02T17:01:03.752Z",
4
4
  "events": [
5
+ {
6
+ "$schema": "dovetail-release-event/v1",
7
+ "event_id": "dovetail-servicenow@0.0.13",
8
+ "package": "@tenonhq/dovetail-servicenow",
9
+ "version": "0.0.13",
10
+ "prev_version": "0.0.12",
11
+ "published_at": "2026-06-02T17:01:03.745Z",
12
+ "git": {
13
+ "sha": "db8fddb963948f013aacfc6f67b26773234ef39d",
14
+ "tag": "dovetail-servicenow@0.0.13"
15
+ },
16
+ "semver_bump": "patch",
17
+ "commits": [
18
+ {
19
+ "sha": "db8fddb96",
20
+ "subject": "feat(servicenow): create-flow — mint + publish a whole flow from scratch (#138)",
21
+ "pr": 138
22
+ },
23
+ {
24
+ "sha": "bd127c726",
25
+ "subject": "chore(release): @tenonhq/dovetail-servicenow@0.0.12 [skip ci]"
26
+ },
27
+ {
28
+ "sha": "448696846",
29
+ "subject": "chore(release): publish 3 packages [skip ci]"
30
+ }
31
+ ],
32
+ "exports_delta": null,
33
+ "narrative": null
34
+ },
35
+ {
36
+ "$schema": "dovetail-release-event/v1",
37
+ "event_id": "dovetail-servicenow@0.0.12",
38
+ "package": "@tenonhq/dovetail-servicenow",
39
+ "version": "0.0.12",
40
+ "prev_version": "0.0.11",
41
+ "published_at": "2026-06-02T16:06:43.535Z",
42
+ "git": {
43
+ "sha": "73ad931dbda5cdf85d4bc91307a17eb4d08de3d2",
44
+ "tag": "dovetail-servicenow@0.0.12"
45
+ },
46
+ "semver_bump": "patch",
47
+ "commits": [
48
+ {
49
+ "sha": "73ad931db",
50
+ "subject": "docs: point SN env-var references at committed .env.example (#137)",
51
+ "pr": 137
52
+ }
53
+ ],
54
+ "exports_delta": null,
55
+ "narrative": null
56
+ },
57
+ {
58
+ "$schema": "dovetail-release-event/v1",
59
+ "event_id": "dovetail-schema@0.0.10",
60
+ "package": "@tenonhq/dovetail-schema",
61
+ "version": "0.0.10",
62
+ "prev_version": "0.0.9",
63
+ "published_at": "2026-06-02T14:03:06.699Z",
64
+ "git": {
65
+ "sha": "9a243fd936caa778def575de19c79d978b65f5ac",
66
+ "tag": "dovetail-schema@0.0.10"
67
+ },
68
+ "semver_bump": "patch",
69
+ "commits": [
70
+ {
71
+ "sha": "9a243fd93",
72
+ "subject": "fix(schema): stop stamping created_at/generated_at into schema pull output (#135)",
73
+ "pr": 135
74
+ },
75
+ {
76
+ "sha": "407d6d75e",
77
+ "subject": "chore(release): publish 2 packages [skip ci]"
78
+ }
79
+ ],
80
+ "exports_delta": null,
81
+ "narrative": null
82
+ },
83
+ {
84
+ "$schema": "dovetail-release-event/v1",
85
+ "event_id": "dovetail-core@0.0.97",
86
+ "package": "@tenonhq/dovetail-core",
87
+ "version": "0.0.97",
88
+ "prev_version": "0.0.96",
89
+ "published_at": "2026-06-01T23:57:01.670Z",
90
+ "git": {
91
+ "sha": "a0c364c7070a82f70c5dc9eed01c29c61b2f8b56",
92
+ "tag": "dovetail-core@0.0.97"
93
+ },
94
+ "semver_bump": "patch",
95
+ "commits": [
96
+ {
97
+ "sha": "a0c364c70",
98
+ "subject": "fix: route refresh folder writes through toSafeFolderName (Windows-safe names) (#133)",
99
+ "pr": 133
100
+ },
101
+ {
102
+ "sha": "958f4fcd7",
103
+ "subject": "chore(release): publish 3 packages [skip ci]"
104
+ }
105
+ ],
106
+ "exports_delta": null,
107
+ "narrative": null
108
+ },
5
109
  {
6
110
  "$schema": "dovetail-release-event/v1",
7
111
  "event_id": "dovetail-core@0.0.96",
@@ -56,6 +56,7 @@ SN_INSTANCE=your-instance.service-now.com
56
56
  - Instance should NOT have `https://` prefix or trailing slash
57
57
  - Optional: `DASHBOARD_PORT=3456`
58
58
  - **Never commit `.env` to git**
59
+ - **Multiple instances?** Keep one file per instance (`.env.dev`, `.env.prod`) and pass `--env <path>` (alias `-e`) on any command: `npx dove push --env .env.prod`. `dove login --env .env.prod` writes credentials to that file too. Git-ignore the whole pattern (`.env*`).
59
60
 
60
61
  #### Set up `.gitignore`
61
62
  ```
@@ -49,10 +49,12 @@ Ask the user which symptom they are experiencing (if not already clear):
49
49
  - Instance should NOT have `https://` prefix or trailing slash
50
50
  - Credentials must have admin or developer role
51
51
 
52
- 2. **Test connection:** `npx dove status`
52
+ 2. **Test connection:** `npx dove status` (add `--env <path>` to test a specific credential file, e.g. `npx dove status --env .env.prod`)
53
53
 
54
54
  3. **Check the Dovetail server scoped app** is installed on the instance. Without it, API endpoints will 404.
55
55
 
56
+ - **Hitting the wrong instance?** A stray `.env` in the working directory — or a shell-exported `SN_INSTANCE` — can override the file you expect. Pass `--env <path>` to load credentials from a specific file (every `dove` command accepts it; alias `-e`). Already-set environment variables still win, so unset conflicting `SN_*` vars if needed.
57
+
56
58
  4. **Check for MFA/SSO:** If the instance uses MFA or SSO, basic auth may not work. You may need a local ServiceNow account.
57
59
 
58
60
  ### Diagnostic C: Scope Mismatch