@tenonhq/dovetail-core 0.0.106 → 0.0.108
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/FileLogger.js +52 -0
- package/dist/appUtils.js +22 -16
- package/dist/commander.js +6 -0
- package/dist/snClient.js +10 -0
- package/dist/tests/fileLoggerEnableGate.test.js +140 -0
- package/dist/tests/updateSetConfig.test.js +126 -0
- package/dist/updateSetCommands.js +61 -0
- package/dist/updateSetConfig.js +55 -0
- package/package.json +3 -3
- package/release-manifest.json +605 -709
- package/skills/dove-create-plugin.md +1 -1
- package/skills/dove-manage-update-sets.md +6 -0
- package/skills/dove-troubleshoot-sync.md +2 -2
package/dist/FileLogger.js
CHANGED
|
@@ -37,6 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.fileLogger = void 0;
|
|
40
|
+
exports.shouldEnableFileLogging = shouldEnableFileLogging;
|
|
40
41
|
exports.enableFileLogging = enableFileLogging;
|
|
41
42
|
const fs = __importStar(require("fs"));
|
|
42
43
|
const path = __importStar(require("path"));
|
|
@@ -45,14 +46,58 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
45
46
|
// Long-running sessions (e.g. `dove watch`) would otherwise grow a single
|
|
46
47
|
// debug log unbounded; capping at 200 writes per file keeps each one small.
|
|
47
48
|
const ROTATION_WRITE_LIMIT = 200;
|
|
49
|
+
// Truthy spellings accepted for the DOVETAIL_DEBUG env var.
|
|
50
|
+
const TRUTHY = /^(1|true|yes|on)$/i;
|
|
51
|
+
/**
|
|
52
|
+
* @description Decide whether the file logger should write `dovetail-debug-*.log`
|
|
53
|
+
* files to disk. File logging is OFF by default and only turns on when the user
|
|
54
|
+
* explicitly opts in — either the `--debug` CLI flag (`--debug` or
|
|
55
|
+
* `--debug=<truthy>`) or the `DOVETAIL_DEBUG` env var set to a truthy value.
|
|
56
|
+
* Pure and side-effect free so it can be unit-tested with injected inputs.
|
|
57
|
+
* @param {string[]} argv - CLI args after the node + script entries (process.argv.slice(2)).
|
|
58
|
+
* @param {NodeJS.ProcessEnv} env - Environment map (defaults to an empty object).
|
|
59
|
+
* @returns {boolean} true when file logging should be enabled.
|
|
60
|
+
*/
|
|
61
|
+
function shouldEnableFileLogging(argv = [], env = {}) {
|
|
62
|
+
const raw = typeof env.DOVETAIL_DEBUG === "string" ? env.DOVETAIL_DEBUG.trim() : "";
|
|
63
|
+
if (TRUTHY.test(raw))
|
|
64
|
+
return true;
|
|
65
|
+
const args = Array.isArray(argv) ? argv : [];
|
|
66
|
+
for (let i = 0; i < args.length; i++) {
|
|
67
|
+
const arg = args[i];
|
|
68
|
+
if (arg === "--debug")
|
|
69
|
+
return true;
|
|
70
|
+
const eq = typeof arg === "string" ? arg.match(/^--debug=(.*)$/) : null;
|
|
71
|
+
if (eq)
|
|
72
|
+
return TRUTHY.test(eq[1].trim());
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
48
76
|
class FileLogger {
|
|
49
77
|
logFilePath;
|
|
50
78
|
logStream = null;
|
|
51
79
|
initialized = false;
|
|
52
80
|
writeCount = 0;
|
|
81
|
+
// File logging is opt-in — nothing touches disk unless this is true. Seeded
|
|
82
|
+
// from `--debug` / DOVETAIL_DEBUG so a stray log file is never created just by
|
|
83
|
+
// running a command; can also be flipped on at runtime via enable().
|
|
84
|
+
enabled;
|
|
53
85
|
constructor() {
|
|
54
86
|
// Initialize on first use
|
|
55
87
|
this.logFilePath = "";
|
|
88
|
+
this.enabled = shouldEnableFileLogging(Array.isArray(process.argv) ? process.argv.slice(2) : [], process.env);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Turn file logging on for this process (programmatic opt-in). Idempotent.
|
|
92
|
+
*/
|
|
93
|
+
enable() {
|
|
94
|
+
this.enabled = true;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Whether file logging is currently active.
|
|
98
|
+
*/
|
|
99
|
+
isEnabled() {
|
|
100
|
+
return this.enabled;
|
|
56
101
|
}
|
|
57
102
|
/**
|
|
58
103
|
* Initialize the file logger in the current working directory
|
|
@@ -109,6 +154,11 @@ class FileLogger {
|
|
|
109
154
|
* Write a message to the log file
|
|
110
155
|
*/
|
|
111
156
|
writeToFile(message) {
|
|
157
|
+
// Opt-in gate: without --debug / DOVETAIL_DEBUG we never create or write a
|
|
158
|
+
// log file. Console output (info/warn/error/success) still happens because
|
|
159
|
+
// those methods log to the console before calling writeToFile.
|
|
160
|
+
if (!this.enabled)
|
|
161
|
+
return;
|
|
112
162
|
if (!this.initialized) {
|
|
113
163
|
this.initialize();
|
|
114
164
|
}
|
|
@@ -215,6 +265,8 @@ const fileLogger = new FileLogger();
|
|
|
215
265
|
exports.fileLogger = fileLogger;
|
|
216
266
|
// Also export a function to replace console.log globally
|
|
217
267
|
function enableFileLogging() {
|
|
268
|
+
// Opt in to file logging even if neither --debug nor DOVETAIL_DEBUG was set.
|
|
269
|
+
fileLogger.enable();
|
|
218
270
|
// Store original console.log
|
|
219
271
|
const originalConsoleLog = console.log;
|
|
220
272
|
// Override console.log to also write to file
|
package/dist/appUtils.js
CHANGED
|
@@ -38,30 +38,20 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
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
|
-
const fs_1 = __importDefault(require("fs"));
|
|
42
41
|
const progress_1 = __importDefault(require("progress"));
|
|
43
42
|
const fUtils = __importStar(require("./FileUtils"));
|
|
44
43
|
const ConfigManager = __importStar(require("./config"));
|
|
45
44
|
const constants_1 = require("./constants");
|
|
46
45
|
const PluginManager_1 = __importDefault(require("./PluginManager"));
|
|
47
46
|
const FileLogger_1 = require("./FileLogger");
|
|
48
|
-
const
|
|
47
|
+
const updateSetConfig_1 = require("./updateSetConfig");
|
|
49
48
|
const snClient_1 = require("./snClient");
|
|
50
49
|
const benchmark_1 = require("./benchmark");
|
|
51
50
|
const Logger_1 = require("./Logger");
|
|
52
51
|
const genericUtils_1 = require("./genericUtils");
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (fs_1.default.existsSync(configPath)) {
|
|
57
|
-
return JSON.parse(fs_1.default.readFileSync(configPath, "utf8"));
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
catch (e) {
|
|
61
|
-
Logger_1.logger.warn(`Failed to parse update set config at ${configPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
62
|
-
}
|
|
63
|
-
return {};
|
|
64
|
-
};
|
|
52
|
+
// Delegate to the shared reader so the push hot-path and the
|
|
53
|
+
// createUpdateSet/switchUpdateSet writers stay on one parse contract.
|
|
54
|
+
const getUpdateSetConfig = () => (0, updateSetConfig_1.readUpdateSetConfig)();
|
|
65
55
|
// Strip the ServiceNow instance host from an absolute _record_link, leaving the
|
|
66
56
|
// instance-relative path. The CADSO metadata endpoint returns an absolute URL
|
|
67
57
|
// (https://<instance>.service-now.com/<table>.do?sys_id=...); persisting that
|
|
@@ -762,9 +752,11 @@ const pushFiles = async (recs) => {
|
|
|
762
752
|
const hasUpdateSets = Object.keys(updateSetConfig).length > 0;
|
|
763
753
|
if (hasUpdateSets) {
|
|
764
754
|
const activeScopes = Object.entries(updateSetConfig)
|
|
765
|
-
.map(([scope, us]) => `${scope} -> ${us.name}`)
|
|
755
|
+
.map(([scope, us]) => `${scope} -> ${us.name} (${us.sys_id})`)
|
|
766
756
|
.join(", ");
|
|
767
|
-
|
|
757
|
+
// Name the source file and show the sys_id so the operator can see exactly
|
|
758
|
+
// where captures land — push routes by this file, not the active set (#182).
|
|
759
|
+
Logger_1.logger.info(`Update set routing (from .dove-update-sets.json): ${activeScopes}`);
|
|
768
760
|
}
|
|
769
761
|
// Pre-resolve read-only table sets for every scope present in this batch.
|
|
770
762
|
// Cheaper than re-resolving per record. Tables flagged in _readOnlyTables
|
|
@@ -971,6 +963,20 @@ const createAndAssignUpdateSet = async (updateSetName = "", scope) => {
|
|
|
971
963
|
else {
|
|
972
964
|
await client.createCurrentUpdateSetUserPref(updateSetSysId, userSysId);
|
|
973
965
|
}
|
|
966
|
+
// Route this scope's captures to the new set. pushFiles() routes by
|
|
967
|
+
// .dove-update-sets.json, not the active set, so without this a `dove push
|
|
968
|
+
// --updateSet` could create+activate a set yet capture into a stale one
|
|
969
|
+
// (TenonHQ/Dovetail#182). Only possible when the scope name is known.
|
|
970
|
+
if (scope) {
|
|
971
|
+
const routed = (0, updateSetConfig_1.writeUpdateSetRouting)({
|
|
972
|
+
scope,
|
|
973
|
+
sysId: updateSetSysId,
|
|
974
|
+
name: updateSetName,
|
|
975
|
+
});
|
|
976
|
+
if (routed) {
|
|
977
|
+
Logger_1.logger.info(`Push routing updated: ${scope} -> ${updateSetName} (${updateSetSysId})`);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
974
980
|
return {
|
|
975
981
|
name: updateSetName,
|
|
976
982
|
id: updateSetSysId,
|
package/dist/commander.js
CHANGED
|
@@ -31,6 +31,12 @@ async function initCommands() {
|
|
|
31
31
|
describe: "Path to a .env file to load for this command (default: .env in the project root). Lets one checkout target multiple instances.",
|
|
32
32
|
})
|
|
33
33
|
.global("env")
|
|
34
|
+
.option("debug", {
|
|
35
|
+
type: "boolean",
|
|
36
|
+
default: false,
|
|
37
|
+
describe: "Write a dovetail-debug-*.log file for this command (off by default; also enabled by DOVETAIL_DEBUG=1).",
|
|
38
|
+
})
|
|
39
|
+
.global("debug")
|
|
34
40
|
.command(["watch", "w", "watchAllScopes"], "Watch all scopes for file changes and sync to ServiceNow", (cmdArgs) => {
|
|
35
41
|
cmdArgs.options({
|
|
36
42
|
...sharedOptions,
|
package/dist/snClient.js
CHANGED
|
@@ -334,6 +334,15 @@ const snClient = (baseURL, username, password) => {
|
|
|
334
334
|
},
|
|
335
335
|
});
|
|
336
336
|
};
|
|
337
|
+
const getScopeById = (scopeSysId) => {
|
|
338
|
+
const endpoint = "api/now/table/sys_scope";
|
|
339
|
+
return client.get(endpoint, {
|
|
340
|
+
params: {
|
|
341
|
+
sysparm_query: `sys_id=${scopeSysId}`,
|
|
342
|
+
sysparm_fields: "scope",
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
};
|
|
337
346
|
const getUserSysId = (userName = process.env.SN_USER) => {
|
|
338
347
|
const endpoint = "api/now/table/sys_user";
|
|
339
348
|
return client.get(endpoint, {
|
|
@@ -504,6 +513,7 @@ const snClient = (baseURL, username, password) => {
|
|
|
504
513
|
getAppList,
|
|
505
514
|
updateRecord,
|
|
506
515
|
getScopeId,
|
|
516
|
+
getScopeById,
|
|
507
517
|
getUserSysId,
|
|
508
518
|
getCurrentAppUserPrefSysId,
|
|
509
519
|
updateCurrentAppUserPref,
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Regression: `dove` used to drop a `dovetail-debug-*.log` file into the cwd on
|
|
4
|
+
* EVERY invocation, because the file logger lazily created its file on the first
|
|
5
|
+
* write and `index.ts` logs "Starting Dovetail..." (plus scattered debug traces)
|
|
6
|
+
* on every command. The file logger is now opt-in — it only touches disk when
|
|
7
|
+
* the user asks for it via `--debug` or `DOVETAIL_DEBUG`.
|
|
8
|
+
*
|
|
9
|
+
* These tests cover:
|
|
10
|
+
* - the pure decision helper (`shouldEnableFileLogging`), and
|
|
11
|
+
* - the singleton's disk behavior: writes no file by default, writes once
|
|
12
|
+
* enabled, while console output is unaffected in both states.
|
|
13
|
+
*/
|
|
14
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
17
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
18
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
19
|
+
}
|
|
20
|
+
Object.defineProperty(o, k2, desc);
|
|
21
|
+
}) : (function(o, m, k, k2) {
|
|
22
|
+
if (k2 === undefined) k2 = k;
|
|
23
|
+
o[k2] = m[k];
|
|
24
|
+
}));
|
|
25
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
26
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
27
|
+
}) : function(o, v) {
|
|
28
|
+
o["default"] = v;
|
|
29
|
+
});
|
|
30
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
31
|
+
var ownKeys = function(o) {
|
|
32
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
33
|
+
var ar = [];
|
|
34
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
35
|
+
return ar;
|
|
36
|
+
};
|
|
37
|
+
return ownKeys(o);
|
|
38
|
+
};
|
|
39
|
+
return function (mod) {
|
|
40
|
+
if (mod && mod.__esModule) return mod;
|
|
41
|
+
var result = {};
|
|
42
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
43
|
+
__setModuleDefault(result, mod);
|
|
44
|
+
return result;
|
|
45
|
+
};
|
|
46
|
+
})();
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
const fs = __importStar(require("fs"));
|
|
49
|
+
const os = __importStar(require("os"));
|
|
50
|
+
const path = __importStar(require("path"));
|
|
51
|
+
const FileLogger_1 = require("../FileLogger");
|
|
52
|
+
function countDebugLogs(dir) {
|
|
53
|
+
return fs.readdirSync(dir).filter((f) => /^dovetail-debug-.*\.log$/.test(f))
|
|
54
|
+
.length;
|
|
55
|
+
}
|
|
56
|
+
// The file logger opens its stream asynchronously; poll until a predicate holds.
|
|
57
|
+
async function waitFor(predicate, timeoutMs = 2000) {
|
|
58
|
+
const start = Date.now();
|
|
59
|
+
while (!predicate()) {
|
|
60
|
+
if (Date.now() - start > timeoutMs) {
|
|
61
|
+
throw new Error("waitFor: condition not met before timeout");
|
|
62
|
+
}
|
|
63
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
describe("shouldEnableFileLogging (pure)", () => {
|
|
67
|
+
it("is OFF by default — no flag, no env", () => {
|
|
68
|
+
expect((0, FileLogger_1.shouldEnableFileLogging)([], {})).toBe(false);
|
|
69
|
+
expect((0, FileLogger_1.shouldEnableFileLogging)(["push", "--diff", "main"], {})).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
it("turns ON with the --debug flag", () => {
|
|
72
|
+
expect((0, FileLogger_1.shouldEnableFileLogging)(["push", "--debug"], {})).toBe(true);
|
|
73
|
+
expect((0, FileLogger_1.shouldEnableFileLogging)(["--debug", "status"], {})).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
it("honors --debug=<value>", () => {
|
|
76
|
+
expect((0, FileLogger_1.shouldEnableFileLogging)(["--debug=true"], {})).toBe(true);
|
|
77
|
+
expect((0, FileLogger_1.shouldEnableFileLogging)(["--debug=1"], {})).toBe(true);
|
|
78
|
+
expect((0, FileLogger_1.shouldEnableFileLogging)(["--debug=false"], {})).toBe(false);
|
|
79
|
+
expect((0, FileLogger_1.shouldEnableFileLogging)(["--debug=0"], {})).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
it("turns ON with a truthy DOVETAIL_DEBUG env var", () => {
|
|
82
|
+
for (const v of ["1", "true", "TRUE", "yes", "on", " on "]) {
|
|
83
|
+
expect((0, FileLogger_1.shouldEnableFileLogging)([], { DOVETAIL_DEBUG: v })).toBe(true);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
it("stays OFF for a falsy/empty DOVETAIL_DEBUG env var", () => {
|
|
87
|
+
for (const v of ["", "0", "false", "no", "off"]) {
|
|
88
|
+
expect((0, FileLogger_1.shouldEnableFileLogging)([], { DOVETAIL_DEBUG: v })).toBe(false);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
it("tolerates non-array argv defensively", () => {
|
|
92
|
+
expect((0, FileLogger_1.shouldEnableFileLogging)(undefined, {})).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
describe("fileLogger disk gate", () => {
|
|
96
|
+
let tmpDir;
|
|
97
|
+
let cwdSpy;
|
|
98
|
+
let logSpy;
|
|
99
|
+
let errSpy;
|
|
100
|
+
beforeEach(() => {
|
|
101
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "dovetail-log-gate-"));
|
|
102
|
+
cwdSpy = jest.spyOn(process, "cwd").mockReturnValue(tmpDir);
|
|
103
|
+
logSpy = jest.spyOn(console, "log").mockImplementation(() => undefined);
|
|
104
|
+
errSpy = jest.spyOn(console, "error").mockImplementation(() => undefined);
|
|
105
|
+
});
|
|
106
|
+
afterEach(async () => {
|
|
107
|
+
FileLogger_1.fileLogger.close();
|
|
108
|
+
// Let any in-flight stream close settle before removing the directory.
|
|
109
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
110
|
+
cwdSpy.mockRestore();
|
|
111
|
+
logSpy.mockRestore();
|
|
112
|
+
errSpy.mockRestore();
|
|
113
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
114
|
+
});
|
|
115
|
+
it("writes NO log file when disabled (jest runs with no --debug / env)", () => {
|
|
116
|
+
expect(FileLogger_1.fileLogger.isEnabled()).toBe(false);
|
|
117
|
+
FileLogger_1.fileLogger.debug("should not create a file");
|
|
118
|
+
FileLogger_1.fileLogger.info("informational");
|
|
119
|
+
FileLogger_1.fileLogger.warn("warning");
|
|
120
|
+
FileLogger_1.fileLogger.error("boom");
|
|
121
|
+
expect(countDebugLogs(tmpDir)).toBe(0);
|
|
122
|
+
expect(FileLogger_1.fileLogger.getLogFilePath()).toBe("");
|
|
123
|
+
});
|
|
124
|
+
it("still prints info to the console when disabled", () => {
|
|
125
|
+
FileLogger_1.fileLogger.info("visible to the user");
|
|
126
|
+
expect(logSpy).toHaveBeenCalled();
|
|
127
|
+
});
|
|
128
|
+
it("creates and writes a log file once enabled", async () => {
|
|
129
|
+
FileLogger_1.fileLogger.enable();
|
|
130
|
+
expect(FileLogger_1.fileLogger.isEnabled()).toBe(true);
|
|
131
|
+
FileLogger_1.fileLogger.debug("now this lands on disk");
|
|
132
|
+
// Path is assigned synchronously in initialize(); the stream opens async.
|
|
133
|
+
const logPath = FileLogger_1.fileLogger.getLogFilePath();
|
|
134
|
+
expect(logPath.startsWith(tmpDir)).toBe(true);
|
|
135
|
+
expect(/dovetail-debug-.*\.log$/.test(path.basename(logPath))).toBe(true);
|
|
136
|
+
await waitFor(() => fs.existsSync(logPath) &&
|
|
137
|
+
fs.readFileSync(logPath, "utf8").includes("now this lands on disk"));
|
|
138
|
+
expect(countDebugLogs(tmpDir)).toBe(1);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* TenonHQ/Dovetail#182: readUpdateSetConfig / writeUpdateSetRouting
|
|
4
|
+
*
|
|
5
|
+
* The push-routing file (.dove-update-sets.json) is the source of truth for
|
|
6
|
+
* `dove push`. These tests pin the read/merge/write contract that keeps it in
|
|
7
|
+
* sync with createUpdateSet / switchUpdateSet.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
var mockFsStore = {};
|
|
11
|
+
jest.mock("fs", function () {
|
|
12
|
+
return {
|
|
13
|
+
existsSync: jest.fn(function (p) {
|
|
14
|
+
return p in mockFsStore;
|
|
15
|
+
}),
|
|
16
|
+
readFileSync: jest.fn(function (p) {
|
|
17
|
+
if (p in mockFsStore)
|
|
18
|
+
return mockFsStore[p];
|
|
19
|
+
throw new Error("ENOENT: " + p);
|
|
20
|
+
}),
|
|
21
|
+
writeFileSync: jest.fn(function (p, data) {
|
|
22
|
+
mockFsStore[p] = data;
|
|
23
|
+
}),
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
var CONFIG_PATH = "/repo/.dove-update-sets.json";
|
|
27
|
+
jest.mock("../projectFiles", function () {
|
|
28
|
+
return {
|
|
29
|
+
getUpdateSetsConfigPath: jest.fn(function () {
|
|
30
|
+
return CONFIG_PATH;
|
|
31
|
+
}),
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
jest.mock("../Logger", function () {
|
|
35
|
+
return {
|
|
36
|
+
logger: {
|
|
37
|
+
info: jest.fn(),
|
|
38
|
+
error: jest.fn(),
|
|
39
|
+
warn: jest.fn(),
|
|
40
|
+
debug: jest.fn(),
|
|
41
|
+
success: jest.fn(),
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
const updateSetConfig_1 = require("../updateSetConfig");
|
|
46
|
+
const Logger_1 = require("../Logger");
|
|
47
|
+
describe("updateSetConfig", function () {
|
|
48
|
+
beforeEach(function () {
|
|
49
|
+
for (var k in mockFsStore)
|
|
50
|
+
delete mockFsStore[k];
|
|
51
|
+
jest.clearAllMocks();
|
|
52
|
+
});
|
|
53
|
+
describe("readUpdateSetConfig", function () {
|
|
54
|
+
test("returns {} when the file is absent", function () {
|
|
55
|
+
expect((0, updateSetConfig_1.readUpdateSetConfig)()).toEqual({});
|
|
56
|
+
});
|
|
57
|
+
test("parses an existing routing file", function () {
|
|
58
|
+
mockFsStore[CONFIG_PATH] = JSON.stringify({
|
|
59
|
+
x_cadso_core: { sys_id: "abc", name: "Set A" },
|
|
60
|
+
});
|
|
61
|
+
expect((0, updateSetConfig_1.readUpdateSetConfig)()).toEqual({
|
|
62
|
+
x_cadso_core: { sys_id: "abc", name: "Set A" },
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
test("returns {} and warns on unparseable JSON", function () {
|
|
66
|
+
mockFsStore[CONFIG_PATH] = "{ not json";
|
|
67
|
+
expect((0, updateSetConfig_1.readUpdateSetConfig)()).toEqual({});
|
|
68
|
+
expect(Logger_1.logger.warn).toHaveBeenCalled();
|
|
69
|
+
});
|
|
70
|
+
test("returns {} and warns when the file is a non-object (array)", function () {
|
|
71
|
+
mockFsStore[CONFIG_PATH] = JSON.stringify(["nope"]);
|
|
72
|
+
expect((0, updateSetConfig_1.readUpdateSetConfig)()).toEqual({});
|
|
73
|
+
expect(Logger_1.logger.warn).toHaveBeenCalled();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
describe("writeUpdateSetRouting", function () {
|
|
77
|
+
test("writes a fresh routing entry and returns the path", function () {
|
|
78
|
+
var written = (0, updateSetConfig_1.writeUpdateSetRouting)({
|
|
79
|
+
scope: "x_cadso_core",
|
|
80
|
+
sysId: "new123",
|
|
81
|
+
name: "New Set",
|
|
82
|
+
});
|
|
83
|
+
expect(written).toBe(CONFIG_PATH);
|
|
84
|
+
expect(JSON.parse(mockFsStore[CONFIG_PATH])).toEqual({
|
|
85
|
+
x_cadso_core: { sys_id: "new123", name: "New Set" },
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
test("merges without clobbering other scopes", function () {
|
|
89
|
+
mockFsStore[CONFIG_PATH] = JSON.stringify({
|
|
90
|
+
x_cadso_automate: { sys_id: "keep", name: "Keep Me" },
|
|
91
|
+
});
|
|
92
|
+
(0, updateSetConfig_1.writeUpdateSetRouting)({
|
|
93
|
+
scope: "x_cadso_core",
|
|
94
|
+
sysId: "new123",
|
|
95
|
+
name: "New Set",
|
|
96
|
+
});
|
|
97
|
+
expect(JSON.parse(mockFsStore[CONFIG_PATH])).toEqual({
|
|
98
|
+
x_cadso_automate: { sys_id: "keep", name: "Keep Me" },
|
|
99
|
+
x_cadso_core: { sys_id: "new123", name: "New Set" },
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
test("overwrites the same scope's stale entry", function () {
|
|
103
|
+
mockFsStore[CONFIG_PATH] = JSON.stringify({
|
|
104
|
+
x_cadso_core: { sys_id: "old", name: "Old Set" },
|
|
105
|
+
});
|
|
106
|
+
(0, updateSetConfig_1.writeUpdateSetRouting)({
|
|
107
|
+
scope: "x_cadso_core",
|
|
108
|
+
sysId: "fresh",
|
|
109
|
+
name: "Fresh Set",
|
|
110
|
+
});
|
|
111
|
+
expect(JSON.parse(mockFsStore[CONFIG_PATH])).toEqual({
|
|
112
|
+
x_cadso_core: { sys_id: "fresh", name: "Fresh Set" },
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
test("returns null and writes nothing when scope is missing", function () {
|
|
116
|
+
var written = (0, updateSetConfig_1.writeUpdateSetRouting)({ sysId: "x", name: "y" });
|
|
117
|
+
expect(written).toBeNull();
|
|
118
|
+
expect(mockFsStore[CONFIG_PATH]).toBeUndefined();
|
|
119
|
+
});
|
|
120
|
+
test("returns null when sys_id or name is blank", function () {
|
|
121
|
+
expect((0, updateSetConfig_1.writeUpdateSetRouting)({ scope: "x_cadso_core", sysId: " ", name: "y" })).toBeNull();
|
|
122
|
+
expect((0, updateSetConfig_1.writeUpdateSetRouting)({ scope: "x_cadso_core", sysId: "x", name: "" })).toBeNull();
|
|
123
|
+
expect(mockFsStore[CONFIG_PATH]).toBeUndefined();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -49,6 +49,7 @@ const commands_1 = require("./commands");
|
|
|
49
49
|
const chalk_1 = __importDefault(require("chalk"));
|
|
50
50
|
const dovetail_clickup_1 = require("@tenonhq/dovetail-clickup");
|
|
51
51
|
const clickupCommands_1 = require("./clickupCommands");
|
|
52
|
+
const updateSetConfig_1 = require("./updateSetConfig");
|
|
52
53
|
/**
|
|
53
54
|
* Shows the current scope
|
|
54
55
|
*/
|
|
@@ -233,6 +234,26 @@ async function createUpdateSetCommand(args) {
|
|
|
233
234
|
if (scope) {
|
|
234
235
|
Logger_1.logger.info(`Scope: ${scope}`);
|
|
235
236
|
}
|
|
237
|
+
// Keep .dove-update-sets.json in sync with the set we just activated.
|
|
238
|
+
// `dove push` routes captures by this file (per scope), not by the
|
|
239
|
+
// instance's active update set — so without this write the new set would
|
|
240
|
+
// stay empty and pushes would land in whatever stale set the file still
|
|
241
|
+
// points at (TenonHQ/Dovetail#182).
|
|
242
|
+
if (scope) {
|
|
243
|
+
const routed = (0, updateSetConfig_1.writeUpdateSetRouting)({
|
|
244
|
+
scope,
|
|
245
|
+
sysId: updateSetSysId,
|
|
246
|
+
name,
|
|
247
|
+
});
|
|
248
|
+
if (routed) {
|
|
249
|
+
Logger_1.logger.info(chalk_1.default.green(`✓ Push routing updated: ${scope} -> ${name} (${updateSetSysId})`));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
Logger_1.logger.warn("No scope provided, so .dove-update-sets.json was not updated. " +
|
|
254
|
+
"`dove push` routes captures by scope via that file — pass -s <scope> " +
|
|
255
|
+
"so pushes target this set, or edit .dove-update-sets.json directly.");
|
|
256
|
+
}
|
|
236
257
|
// If created from a ClickUp task, offer to post a comment back
|
|
237
258
|
if (args._clickupTaskId) {
|
|
238
259
|
try {
|
|
@@ -292,6 +313,22 @@ async function switchUpdateSetCommand(args) {
|
|
|
292
313
|
if (applicationName) {
|
|
293
314
|
Logger_1.logger.info(`Scope: ${applicationName}`);
|
|
294
315
|
}
|
|
316
|
+
// Keep .dove-update-sets.json in sync so `dove push` routes captures to the
|
|
317
|
+
// set we just switched to (it routes by this file per scope, not by the
|
|
318
|
+
// instance's active set — TenonHQ/Dovetail#182). `applicationScope` is the
|
|
319
|
+
// scope's sys_id, so resolve it back to the scope name the file is keyed on.
|
|
320
|
+
const scopeName = args.scope || (await resolveScopeName(applicationScope));
|
|
321
|
+
if (scopeName) {
|
|
322
|
+
const routed = (0, updateSetConfig_1.writeUpdateSetRouting)({ scope: scopeName, sysId, name });
|
|
323
|
+
if (routed) {
|
|
324
|
+
Logger_1.logger.info(chalk_1.default.green(`✓ Push routing updated: ${scopeName} -> ${name} (${sysId})`));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
Logger_1.logger.warn("Could not resolve the scope name, so .dove-update-sets.json was not " +
|
|
329
|
+
"updated. `dove push` routes captures by scope via that file — pass " +
|
|
330
|
+
"--scope <scope> so pushes target this set.");
|
|
331
|
+
}
|
|
295
332
|
}
|
|
296
333
|
catch (e) {
|
|
297
334
|
Logger_1.logger.error("Failed to switch update set");
|
|
@@ -489,6 +526,30 @@ async function getActiveUpdateSetName(client, scope) {
|
|
|
489
526
|
return null;
|
|
490
527
|
}
|
|
491
528
|
}
|
|
529
|
+
/**
|
|
530
|
+
* Resolves a scope's sys_id back to its scope name (e.g. "x_cadso_core"), used
|
|
531
|
+
* to key the .dove-update-sets.json push-routing entry. Returns undefined when
|
|
532
|
+
* the input is missing or the lookup fails so the caller can warn instead.
|
|
533
|
+
*/
|
|
534
|
+
async function resolveScopeName(applicationSysId) {
|
|
535
|
+
if (!applicationSysId || applicationSysId.trim() === "")
|
|
536
|
+
return undefined;
|
|
537
|
+
try {
|
|
538
|
+
const client = (0, snClient_1.defaultClient)();
|
|
539
|
+
const rows = await (0, snClient_1.unwrapSNResponse)(client.getScopeById(applicationSysId));
|
|
540
|
+
if (rows && rows.length > 0) {
|
|
541
|
+
const resolved = rows[0].scope;
|
|
542
|
+
if (resolved && typeof resolved === "string" && resolved.trim() !== "") {
|
|
543
|
+
return resolved;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
catch (e) {
|
|
548
|
+
Logger_1.logger.debug("Could not resolve scope name from application sys_id: " +
|
|
549
|
+
(e instanceof Error ? e.message : String(e)));
|
|
550
|
+
}
|
|
551
|
+
return undefined;
|
|
552
|
+
}
|
|
492
553
|
/**
|
|
493
554
|
* Helper function to switch to a scope
|
|
494
555
|
*/
|
|
@@ -0,0 +1,55 @@
|
|
|
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.readUpdateSetConfig = readUpdateSetConfig;
|
|
7
|
+
exports.writeUpdateSetRouting = writeUpdateSetRouting;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const projectFiles_1 = require("./projectFiles");
|
|
10
|
+
const Logger_1 = require("./Logger");
|
|
11
|
+
/**
|
|
12
|
+
* Read .dove-update-sets.json (or its legacy .sinc- sibling). Returns {} when
|
|
13
|
+
* the file is absent or unparseable, so callers never have to guard for null.
|
|
14
|
+
*/
|
|
15
|
+
function readUpdateSetConfig() {
|
|
16
|
+
const configPath = (0, projectFiles_1.getUpdateSetsConfigPath)();
|
|
17
|
+
try {
|
|
18
|
+
if (fs_1.default.existsSync(configPath)) {
|
|
19
|
+
const parsed = JSON.parse(fs_1.default.readFileSync(configPath, "utf8"));
|
|
20
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
21
|
+
return parsed;
|
|
22
|
+
}
|
|
23
|
+
Logger_1.logger.warn(`Update set config at ${configPath} is not an object; ignoring it.`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
Logger_1.logger.warn(`Failed to parse update set config at ${configPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
28
|
+
}
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Merge a single scope's update set into .dove-update-sets.json so that
|
|
33
|
+
* `dove push` routes that scope's records to the set the operator just created
|
|
34
|
+
* or switched to.
|
|
35
|
+
*
|
|
36
|
+
* Without this, `dove createUpdateSet` / `dove switchUpdateSet` only flip the
|
|
37
|
+
* instance's *active* update set, while `dove push` keeps routing via this file
|
|
38
|
+
* — so a freshly "activated" set can stay empty and the work lands in whatever
|
|
39
|
+
* stale set the file still points at (TenonHQ/Dovetail#182).
|
|
40
|
+
*
|
|
41
|
+
* Returns the path written, or null when any input is missing (no scope name,
|
|
42
|
+
* sys_id, or set name to route by) so the caller can warn instead.
|
|
43
|
+
*/
|
|
44
|
+
function writeUpdateSetRouting(opts) {
|
|
45
|
+
const scope = opts.scope ? opts.scope.trim() : "";
|
|
46
|
+
const sysId = opts.sysId ? opts.sysId.trim() : "";
|
|
47
|
+
const name = opts.name ? opts.name.trim() : "";
|
|
48
|
+
if (!scope || !sysId || !name)
|
|
49
|
+
return null;
|
|
50
|
+
const configPath = (0, projectFiles_1.getUpdateSetsConfigPath)();
|
|
51
|
+
const config = readUpdateSetConfig();
|
|
52
|
+
config[scope] = { sys_id: sysId, name };
|
|
53
|
+
fs_1.default.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
54
|
+
return configPath;
|
|
55
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tenonhq/dovetail-core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.108",
|
|
4
4
|
"description": "Next-gen file syncer",
|
|
5
5
|
"license": "GPL-3.0",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"node": ">=22"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@tenonhq/dovetail-types": "~0.0.
|
|
25
|
+
"@tenonhq/dovetail-types": "~0.0.20",
|
|
26
26
|
"@types/dotenv": "^6.1.1",
|
|
27
27
|
"@types/inquirer": "^8.2.10",
|
|
28
28
|
"@types/jest": "^29.5.5",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@tenonhq/dovetail-clickup": "~0.0.12",
|
|
42
|
-
"@tenonhq/dovetail-schema": "~0.0.
|
|
42
|
+
"@tenonhq/dovetail-schema": "~0.0.12",
|
|
43
43
|
"axios": "^1.5.1",
|
|
44
44
|
"axios-cookiejar-support": "^4.0.7",
|
|
45
45
|
"axios-rate-limit": "^1.3.0",
|