@tenonhq/dovetail-core 0.0.96 → 0.0.98
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 +14 -3
- package/dist/bootstrap.js +8 -0
- package/dist/commander.js +6 -0
- package/dist/config.js +19 -0
- package/dist/envArg.js +66 -0
- package/dist/initSystem/orchestrator.js +53 -3
- package/dist/tests/envArg.test.js +49 -0
- package/dist/tests/refreshSafeFolderNames.test.js +216 -0
- package/package.json +1 -1
- package/release-manifest.json +155 -1
- package/skills/dove-setup-project.md +1 -0
- package/skills/dove-troubleshoot-sync.md +3 -1
package/dist/appUtils.js
CHANGED
|
@@ -118,7 +118,13 @@ const processFilesInManRec = async (recPath, rec, forceWrite) => {
|
|
|
118
118
|
const processRecsInManTable = async (tablePath, table, forceWrite, onRecordProcessed) => {
|
|
119
119
|
const { records } = table;
|
|
120
120
|
const recKeys = Object.keys(records);
|
|
121
|
-
|
|
121
|
+
// Derive the folder name via toSafeFolderName, not the raw record.name. The
|
|
122
|
+
// download path normalizes manifest keys upstream (processManifest), but the
|
|
123
|
+
// refresh path (processMissingFiles) feeds this writer the raw bulkDownload
|
|
124
|
+
// response, whose .name is the unmodified ServiceNow display name — which can
|
|
125
|
+
// be filesystem-unsafe (e.g. a `<table>.*` ACL). toSafeFolderName is
|
|
126
|
+
// idempotent on already-safe names, so re-applying it here is harmless.
|
|
127
|
+
const recKeyToPath = (key) => path_1.default.join(tablePath, (0, exports.toSafeFolderName)(records[key]));
|
|
122
128
|
const recPathPromises = recKeys
|
|
123
129
|
.map(recKeyToPath)
|
|
124
130
|
.map(fUtils.createDirRecursively);
|
|
@@ -511,11 +517,16 @@ const refreshAllFiles = async (newManifest, sourcePath, options = {}) => {
|
|
|
511
517
|
var recs = filesToProcess[tableName].records;
|
|
512
518
|
var recKeys = Object.keys(recs);
|
|
513
519
|
await Promise.all(recKeys.map(function (k) {
|
|
514
|
-
|
|
520
|
+
// toSafeFolderName, not raw recs[k].name: the bulkDownload response
|
|
521
|
+
// carries the unmodified ServiceNow display name, which may be
|
|
522
|
+
// filesystem-unsafe (e.g. a `<table>.*` ACL). Without this, refresh
|
|
523
|
+
// recreates Windows-illegal folders that normalizeManifestKeys
|
|
524
|
+
// already removed from the manifest.
|
|
525
|
+
return fUtils.createDirRecursively(path_1.default.join(tablePath, (0, exports.toSafeFolderName)(recs[k])));
|
|
515
526
|
}));
|
|
516
527
|
await (0, genericUtils_1.processBatched)(recKeys, constants_1.CONCURRENCY_RECORDS, async function (recKey) {
|
|
517
528
|
var rec = recs[recKey];
|
|
518
|
-
var recPath = path_1.default.join(tablePath,
|
|
529
|
+
var recPath = path_1.default.join(tablePath, (0, exports.toSafeFolderName)(rec));
|
|
519
530
|
// Split server-provided metadata off from the regular files so we can
|
|
520
531
|
// track whether any regular file actually changed — metaData shouldn't
|
|
521
532
|
// be the trigger for "this record changed" since we stamp it on every
|
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 =
|
|
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
|
-
(
|
|
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 .
|
|
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,216 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Regression: the refresh path must write filesystem-SAFE folder names.
|
|
4
|
+
*
|
|
5
|
+
* normalizeManifestKeys() (PR #117) re-keys the manifest object so a record
|
|
6
|
+
* with a Windows-illegal display name (e.g. a table-wildcard ACL named
|
|
7
|
+
* `x_cadso_work_campaign.*`) maps to its sys_id. But the refresh writer
|
|
8
|
+
* (`refreshAllFiles`) builds folders from the raw `getMissingFiles`
|
|
9
|
+
* (bulkDownload) server response, whose `.name` is the unmodified display
|
|
10
|
+
* name — so it recreated the illegal `<table>.*` folders on every refresh,
|
|
11
|
+
* silently breaking the folder ≡ manifest-key invariant and aborting
|
|
12
|
+
* `git clone` on Windows.
|
|
13
|
+
*
|
|
14
|
+
* These tests assert the writer routes through toSafeFolderName: a `.*` ACL
|
|
15
|
+
* record lands in a sys_id folder, while a safe name stays human-readable.
|
|
16
|
+
*/
|
|
17
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
20
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
21
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
22
|
+
}
|
|
23
|
+
Object.defineProperty(o, k2, desc);
|
|
24
|
+
}) : (function(o, m, k, k2) {
|
|
25
|
+
if (k2 === undefined) k2 = k;
|
|
26
|
+
o[k2] = m[k];
|
|
27
|
+
}));
|
|
28
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
29
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
30
|
+
}) : function(o, v) {
|
|
31
|
+
o["default"] = v;
|
|
32
|
+
});
|
|
33
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
34
|
+
var ownKeys = function(o) {
|
|
35
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
36
|
+
var ar = [];
|
|
37
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
38
|
+
return ar;
|
|
39
|
+
};
|
|
40
|
+
return ownKeys(o);
|
|
41
|
+
};
|
|
42
|
+
return function (mod) {
|
|
43
|
+
if (mod && mod.__esModule) return mod;
|
|
44
|
+
var result = {};
|
|
45
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
46
|
+
__setModuleDefault(result, mod);
|
|
47
|
+
return result;
|
|
48
|
+
};
|
|
49
|
+
})();
|
|
50
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
|
+
const fs = __importStar(require("fs"));
|
|
52
|
+
const os = __importStar(require("os"));
|
|
53
|
+
const path = __importStar(require("path"));
|
|
54
|
+
// ---------- mocks (must come before importing the module under test) ----------
|
|
55
|
+
var mockLogger = {
|
|
56
|
+
info: jest.fn(),
|
|
57
|
+
debug: jest.fn(),
|
|
58
|
+
warn: jest.fn(),
|
|
59
|
+
error: jest.fn(),
|
|
60
|
+
success: jest.fn(),
|
|
61
|
+
getLogLevel: function () { return "warn"; },
|
|
62
|
+
};
|
|
63
|
+
var mockFileLogger = {
|
|
64
|
+
debug: jest.fn(),
|
|
65
|
+
info: jest.fn(),
|
|
66
|
+
warn: jest.fn(),
|
|
67
|
+
error: jest.fn(),
|
|
68
|
+
};
|
|
69
|
+
var mockClient = {
|
|
70
|
+
getManifest: jest.fn(),
|
|
71
|
+
getMissingFiles: jest.fn(),
|
|
72
|
+
};
|
|
73
|
+
jest.mock("../Logger", function () { return { logger: mockLogger }; });
|
|
74
|
+
jest.mock("../FileLogger", function () { return { fileLogger: mockFileLogger }; });
|
|
75
|
+
jest.mock("../snClient", function () {
|
|
76
|
+
return {
|
|
77
|
+
defaultClient: function () { return mockClient; },
|
|
78
|
+
unwrapSNResponse: function (p) { return Promise.resolve(p).then(function (r) { return r; }); },
|
|
79
|
+
processPushResponse: jest.fn(),
|
|
80
|
+
retryOnErr: jest.fn(),
|
|
81
|
+
retryOnHttpErr: jest.fn(),
|
|
82
|
+
unwrapTableAPIFirstItem: jest.fn(),
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
var mockConfig = {
|
|
86
|
+
getConfig: jest.fn().mockReturnValue({
|
|
87
|
+
scopes: { x_cadso_work: {} },
|
|
88
|
+
tableOptions: {},
|
|
89
|
+
}),
|
|
90
|
+
getManifest: jest.fn().mockResolvedValue({
|
|
91
|
+
x_cadso_work: { scope: "x_cadso_work", tables: {} },
|
|
92
|
+
}),
|
|
93
|
+
getSourcePathForScope: jest.fn(),
|
|
94
|
+
getSourcePath: jest.fn(),
|
|
95
|
+
getManifestPath: jest.fn().mockReturnValue("/tmp/dove.manifest.json"),
|
|
96
|
+
resolveConfigForScope: jest.fn().mockImplementation(function () {
|
|
97
|
+
return {
|
|
98
|
+
tables: ["sys_security_acl"],
|
|
99
|
+
fieldOverrides: {},
|
|
100
|
+
apiIncludes: {},
|
|
101
|
+
apiExcludes: {},
|
|
102
|
+
};
|
|
103
|
+
}),
|
|
104
|
+
isMultiScopeManifest: jest.fn().mockReturnValue(true),
|
|
105
|
+
updateManifest: jest.fn(),
|
|
106
|
+
};
|
|
107
|
+
jest.mock("../config", function () { return mockConfig; });
|
|
108
|
+
jest.mock("progress", function () {
|
|
109
|
+
return jest.fn().mockImplementation(function () {
|
|
110
|
+
return { tick: jest.fn() };
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
// Keep real FileUtils so we assert against the actual file system; stub only
|
|
114
|
+
// writeScopeManifest so no manifest file is written.
|
|
115
|
+
jest.mock("../FileUtils", function () {
|
|
116
|
+
var actual = jest.requireActual("../FileUtils");
|
|
117
|
+
return Object.assign({}, actual, {
|
|
118
|
+
writeScopeManifest: jest.fn().mockResolvedValue(undefined),
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
const AppUtils = __importStar(require("../appUtils"));
|
|
122
|
+
// ---------- helpers ----------
|
|
123
|
+
var tmpRoot;
|
|
124
|
+
var ACL_SYS_ID = "bc91c2a5f334621048dff31590812771";
|
|
125
|
+
function listAclFolders() {
|
|
126
|
+
var dir = path.join(tmpRoot, "sys_security_acl");
|
|
127
|
+
try {
|
|
128
|
+
return fs.readdirSync(dir).sort();
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
describe("refresh — folder names are filesystem-safe (sys_id fallback)", function () {
|
|
135
|
+
beforeEach(function () {
|
|
136
|
+
jest.clearAllMocks();
|
|
137
|
+
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "dove-safe-folder-"));
|
|
138
|
+
mockConfig.getSourcePathForScope.mockReturnValue(tmpRoot);
|
|
139
|
+
mockConfig.getSourcePath.mockReturnValue(tmpRoot);
|
|
140
|
+
});
|
|
141
|
+
afterEach(function () {
|
|
142
|
+
try {
|
|
143
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
144
|
+
}
|
|
145
|
+
catch (e) { }
|
|
146
|
+
});
|
|
147
|
+
test("a `<table>.*` ACL record is written to its sys_id folder, never `.*`", async function () {
|
|
148
|
+
// Manifest as the server returns it: keyed by the illegal display name.
|
|
149
|
+
// normalizeManifestKeys re-keys this to sys_id inside syncManifest.
|
|
150
|
+
mockClient.getManifest.mockResolvedValue({
|
|
151
|
+
scope: "x_cadso_work",
|
|
152
|
+
tables: {
|
|
153
|
+
sys_security_acl: {
|
|
154
|
+
records: {
|
|
155
|
+
"x_cadso_work_campaign.*": {
|
|
156
|
+
name: "x_cadso_work_campaign.*",
|
|
157
|
+
sys_id: ACL_SYS_ID,
|
|
158
|
+
files: [{ name: "script", type: "js" }],
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
// bulkDownload echoes the RAW display name back — the bug source. The
|
|
165
|
+
// writer must not trust it for the folder path.
|
|
166
|
+
mockClient.getMissingFiles.mockResolvedValue({
|
|
167
|
+
sys_security_acl: {
|
|
168
|
+
records: {
|
|
169
|
+
"x_cadso_work_campaign.*": {
|
|
170
|
+
name: "x_cadso_work_campaign.*",
|
|
171
|
+
sys_id: ACL_SYS_ID,
|
|
172
|
+
files: [{ name: "script", type: "js", content: "// acl" }],
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
await AppUtils.syncManifest("x_cadso_work");
|
|
178
|
+
var folders = listAclFolders();
|
|
179
|
+
expect(folders).toContain(ACL_SYS_ID);
|
|
180
|
+
expect(folders).not.toContain("x_cadso_work_campaign.*");
|
|
181
|
+
// The file landed inside the sys_id folder.
|
|
182
|
+
expect(fs.readFileSync(path.join(tmpRoot, "sys_security_acl", ACL_SYS_ID, "script.js"), "utf8")).toBe("// acl");
|
|
183
|
+
});
|
|
184
|
+
test("a filesystem-safe ACL name stays human-readable (no needless sys_id fallback)", async function () {
|
|
185
|
+
var SAFE_SYS_ID = "aa11bb22cc33dd44ee55ff6600112233";
|
|
186
|
+
mockClient.getManifest.mockResolvedValue({
|
|
187
|
+
scope: "x_cadso_work",
|
|
188
|
+
tables: {
|
|
189
|
+
sys_security_acl: {
|
|
190
|
+
records: {
|
|
191
|
+
x_cadso_work_campaign: {
|
|
192
|
+
name: "x_cadso_work_campaign",
|
|
193
|
+
sys_id: SAFE_SYS_ID,
|
|
194
|
+
files: [{ name: "script", type: "js" }],
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
mockClient.getMissingFiles.mockResolvedValue({
|
|
201
|
+
sys_security_acl: {
|
|
202
|
+
records: {
|
|
203
|
+
x_cadso_work_campaign: {
|
|
204
|
+
name: "x_cadso_work_campaign",
|
|
205
|
+
sys_id: SAFE_SYS_ID,
|
|
206
|
+
files: [{ name: "script", type: "js", content: "// safe" }],
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
await AppUtils.syncManifest("x_cadso_work");
|
|
212
|
+
var folders = listAclFolders();
|
|
213
|
+
expect(folders).toContain("x_cadso_work_campaign");
|
|
214
|
+
expect(folders).not.toContain(SAFE_SYS_ID);
|
|
215
|
+
});
|
|
216
|
+
});
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,7 +1,161 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "dovetail-manifest/v1",
|
|
3
|
-
"generated_at": "2026-
|
|
3
|
+
"generated_at": "2026-06-02T14:03:06.707Z",
|
|
4
4
|
"events": [
|
|
5
|
+
{
|
|
6
|
+
"$schema": "dovetail-release-event/v1",
|
|
7
|
+
"event_id": "dovetail-schema@0.0.10",
|
|
8
|
+
"package": "@tenonhq/dovetail-schema",
|
|
9
|
+
"version": "0.0.10",
|
|
10
|
+
"prev_version": "0.0.9",
|
|
11
|
+
"published_at": "2026-06-02T14:03:06.699Z",
|
|
12
|
+
"git": {
|
|
13
|
+
"sha": "9a243fd936caa778def575de19c79d978b65f5ac",
|
|
14
|
+
"tag": "dovetail-schema@0.0.10"
|
|
15
|
+
},
|
|
16
|
+
"semver_bump": "patch",
|
|
17
|
+
"commits": [
|
|
18
|
+
{
|
|
19
|
+
"sha": "9a243fd93",
|
|
20
|
+
"subject": "fix(schema): stop stamping created_at/generated_at into schema pull output (#135)",
|
|
21
|
+
"pr": 135
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"sha": "407d6d75e",
|
|
25
|
+
"subject": "chore(release): publish 2 packages [skip ci]"
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"exports_delta": null,
|
|
29
|
+
"narrative": null
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"$schema": "dovetail-release-event/v1",
|
|
33
|
+
"event_id": "dovetail-core@0.0.97",
|
|
34
|
+
"package": "@tenonhq/dovetail-core",
|
|
35
|
+
"version": "0.0.97",
|
|
36
|
+
"prev_version": "0.0.96",
|
|
37
|
+
"published_at": "2026-06-01T23:57:01.670Z",
|
|
38
|
+
"git": {
|
|
39
|
+
"sha": "a0c364c7070a82f70c5dc9eed01c29c61b2f8b56",
|
|
40
|
+
"tag": "dovetail-core@0.0.97"
|
|
41
|
+
},
|
|
42
|
+
"semver_bump": "patch",
|
|
43
|
+
"commits": [
|
|
44
|
+
{
|
|
45
|
+
"sha": "a0c364c70",
|
|
46
|
+
"subject": "fix: route refresh folder writes through toSafeFolderName (Windows-safe names) (#133)",
|
|
47
|
+
"pr": 133
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"sha": "958f4fcd7",
|
|
51
|
+
"subject": "chore(release): publish 3 packages [skip ci]"
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"exports_delta": null,
|
|
55
|
+
"narrative": null
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"$schema": "dovetail-release-event/v1",
|
|
59
|
+
"event_id": "dovetail-core@0.0.96",
|
|
60
|
+
"package": "@tenonhq/dovetail-core",
|
|
61
|
+
"version": "0.0.96",
|
|
62
|
+
"prev_version": "0.0.95",
|
|
63
|
+
"published_at": "2026-06-01T21:28:08.724Z",
|
|
64
|
+
"git": {
|
|
65
|
+
"sha": "a3ea5ead48b0bd4c324e0847e7e768a43520c7ec",
|
|
66
|
+
"tag": "dovetail-core@0.0.96"
|
|
67
|
+
},
|
|
68
|
+
"semver_bump": "patch",
|
|
69
|
+
"commits": [
|
|
70
|
+
{
|
|
71
|
+
"sha": "a3ea5ead4",
|
|
72
|
+
"subject": "feat(client): point Dovetail clients at the scoped dovetail_* APIs (#132)",
|
|
73
|
+
"pr": 132
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"sha": "93d180eef",
|
|
77
|
+
"subject": "chore(release): @tenonhq/dovetail-servicenow@0.0.9 [skip ci]"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"sha": "d32523991",
|
|
81
|
+
"subject": "chore(release): @tenonhq/dovetail-servicenow@0.0.8 [skip ci]"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"sha": "ba1c67775",
|
|
85
|
+
"subject": "chore(release): @tenonhq/dovetail-dashboard@0.0.36 [skip ci]"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"sha": "9901285c6",
|
|
89
|
+
"subject": "chore(release): @tenonhq/dovetail-dashboard@0.0.35 [skip ci]"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"sha": "55de31527",
|
|
93
|
+
"subject": "chore(release): @tenonhq/dovetail-dashboard@0.0.34 [skip ci]"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"sha": "776f1fc53",
|
|
97
|
+
"subject": "chore(release): @tenonhq/dovetail-sawmill@0.0.7 [skip ci]"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"sha": "407d6d75e",
|
|
101
|
+
"subject": "chore(release): publish 2 packages [skip ci]"
|
|
102
|
+
}
|
|
103
|
+
],
|
|
104
|
+
"exports_delta": null,
|
|
105
|
+
"narrative": null
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"$schema": "dovetail-release-event/v1",
|
|
109
|
+
"event_id": "dovetail-servicenow@0.0.10",
|
|
110
|
+
"package": "@tenonhq/dovetail-servicenow",
|
|
111
|
+
"version": "0.0.10",
|
|
112
|
+
"prev_version": "0.0.9",
|
|
113
|
+
"published_at": "2026-06-01T21:28:08.724Z",
|
|
114
|
+
"git": {
|
|
115
|
+
"sha": "a3ea5ead48b0bd4c324e0847e7e768a43520c7ec",
|
|
116
|
+
"tag": "dovetail-servicenow@0.0.10"
|
|
117
|
+
},
|
|
118
|
+
"semver_bump": "patch",
|
|
119
|
+
"commits": [
|
|
120
|
+
{
|
|
121
|
+
"sha": "a3ea5ead4",
|
|
122
|
+
"subject": "feat(client): point Dovetail clients at the scoped dovetail_* APIs (#132)",
|
|
123
|
+
"pr": 132
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"sha": "93d180eef",
|
|
127
|
+
"subject": "chore(release): @tenonhq/dovetail-servicenow@0.0.9 [skip ci]"
|
|
128
|
+
}
|
|
129
|
+
],
|
|
130
|
+
"exports_delta": null,
|
|
131
|
+
"narrative": null
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"$schema": "dovetail-release-event/v1",
|
|
135
|
+
"event_id": "dovetail-sawmill@0.0.8",
|
|
136
|
+
"package": "@tenonhq/dovetail-sawmill",
|
|
137
|
+
"version": "0.0.8",
|
|
138
|
+
"prev_version": "0.0.7",
|
|
139
|
+
"published_at": "2026-06-01T21:28:08.724Z",
|
|
140
|
+
"git": {
|
|
141
|
+
"sha": "a3ea5ead48b0bd4c324e0847e7e768a43520c7ec",
|
|
142
|
+
"tag": "dovetail-sawmill@0.0.8"
|
|
143
|
+
},
|
|
144
|
+
"semver_bump": "patch",
|
|
145
|
+
"commits": [
|
|
146
|
+
{
|
|
147
|
+
"sha": "a3ea5ead4",
|
|
148
|
+
"subject": "feat(client): point Dovetail clients at the scoped dovetail_* APIs (#132)",
|
|
149
|
+
"pr": 132
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"sha": "776f1fc53",
|
|
153
|
+
"subject": "chore(release): @tenonhq/dovetail-sawmill@0.0.7 [skip ci]"
|
|
154
|
+
}
|
|
155
|
+
],
|
|
156
|
+
"exports_delta": null,
|
|
157
|
+
"narrative": null
|
|
158
|
+
},
|
|
5
159
|
{
|
|
6
160
|
"$schema": "dovetail-release-event/v1",
|
|
7
161
|
"event_id": "dovetail-servicenow@0.0.9",
|
|
@@ -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
|