@tenonhq/dovetail-core 0.0.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/FileLogger.js +207 -0
- package/dist/FileUtils.js +387 -0
- package/dist/Logger.js +88 -0
- package/dist/MultiScopeWatcher.js +615 -0
- package/dist/PluginManager.js +125 -0
- package/dist/_defaultOptionsOLD.js +44 -0
- package/dist/allScopesCommands.js +418 -0
- package/dist/appUtils.js +929 -0
- package/dist/benchmark.js +104 -0
- package/dist/bootstrap.js +62 -0
- package/dist/claudeCommand.js +60 -0
- package/dist/clickupCommands.js +472 -0
- package/dist/clickupPushHelper.js +33 -0
- package/dist/commander.js +466 -0
- package/dist/commands.js +378 -0
- package/dist/config.js +697 -0
- package/dist/constants.js +15 -0
- package/dist/createRecordCommand.js +314 -0
- package/dist/dashboardCommand.js +38 -0
- package/dist/defaultOptions.js +14 -0
- package/dist/deleteRecordCommand.js +282 -0
- package/dist/flowDesigner/values.js +55 -0
- package/dist/genericUtils.js +189 -0
- package/dist/gitUtils.js +99 -0
- package/dist/index.js +35 -0
- package/dist/initSystem/corePlugin.js +344 -0
- package/dist/initSystem/discovery.js +107 -0
- package/dist/initSystem/orchestrator.js +408 -0
- package/dist/logMessages.js +82 -0
- package/dist/loginCommand.js +20 -0
- package/dist/migrateCommand.js +123 -0
- package/dist/projectFiles.js +32 -0
- package/dist/recentEdits.js +66 -0
- package/dist/schemaCommand.js +85 -0
- package/dist/snClient.js +519 -0
- package/dist/tests/benchmarkRefresh.test.js +265 -0
- package/dist/tests/clickupCommands.test.js +384 -0
- package/dist/tests/clickupPushHelper.test.js +124 -0
- package/dist/tests/configFallback.test.js +68 -0
- package/dist/tests/cred.test.js +59 -0
- package/dist/tests/discovery.test.js +67 -0
- package/dist/tests/ensureUpdateSetWarnings.test.js +218 -0
- package/dist/tests/errorLogLevels.test.js +273 -0
- package/dist/tests/example.test.js +6 -0
- package/dist/tests/fileContextSkipReason.test.js +116 -0
- package/dist/tests/globalDebounce.test.js +308 -0
- package/dist/tests/migrateCommand.test.js +113 -0
- package/dist/tests/multi-scope-watcher.test.js +558 -0
- package/dist/tests/orchestrator.test.js +23 -0
- package/dist/tests/pushFiles.test.js +248 -0
- package/dist/tests/rateLimitCoordination.test.js +271 -0
- package/dist/tests/resolveConfigReadOnly.test.js +67 -0
- package/dist/tests/retryOnHttpErr.test.js +154 -0
- package/dist/tests/scopeCaching.test.js +115 -0
- package/dist/tests/serializeUpdateSetConfig.test.js +325 -0
- package/dist/tests/snClient.fallback.test.js +120 -0
- package/dist/tests/syncManifestRefresh.test.js +287 -0
- package/dist/tests/syncManifestWhitelist.test.js +192 -0
- package/dist/tests/taskClear.test.js +170 -0
- package/dist/tests/taskStaleness.test.js +220 -0
- package/dist/tests/unwrapSNResponseError.test.js +145 -0
- package/dist/tests/v2Values.test.js +79 -0
- package/dist/tests/validateTaskId.test.js +304 -0
- package/dist/tests/verifyUpdateSetSwitch.test.js +277 -0
- package/dist/tests/writeEnvVar.test.js +119 -0
- package/dist/updateSetCommands.js +702 -0
- package/dist/wizard.js +40 -0
- package/package.json +69 -0
- package/skills/dove-configure-pipeline.md +158 -0
- package/skills/dove-create-plugin.md +174 -0
- package/skills/dove-create-record.md +135 -0
- package/skills/dove-debug-build.md +117 -0
- package/skills/dove-delete-record.md +104 -0
- package/skills/dove-manage-tables.md +174 -0
- package/skills/dove-manage-update-sets.md +88 -0
- package/skills/dove-setup-project.md +154 -0
- package/skills/dove-troubleshoot-sync.md +113 -0
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.multiScopeWatcher = void 0;
|
|
40
|
+
exports.startMultiScopeWatching = startMultiScopeWatching;
|
|
41
|
+
exports.stopMultiScopeWatching = stopMultiScopeWatching;
|
|
42
|
+
const chokidar_1 = __importDefault(require("chokidar"));
|
|
43
|
+
const logMessages_1 = require("./logMessages");
|
|
44
|
+
const lodash_1 = require("lodash");
|
|
45
|
+
const FileUtils_1 = require("./FileUtils");
|
|
46
|
+
const appUtils_1 = require("./appUtils");
|
|
47
|
+
const recentEdits_1 = require("./recentEdits");
|
|
48
|
+
const Logger_1 = require("./Logger");
|
|
49
|
+
const path = __importStar(require("path"));
|
|
50
|
+
const fs = __importStar(require("fs"));
|
|
51
|
+
const ConfigManager = __importStar(require("./config"));
|
|
52
|
+
const projectFiles_1 = require("./projectFiles");
|
|
53
|
+
const DEBOUNCE_MS = 300;
|
|
54
|
+
class MultiScopeWatcherManager {
|
|
55
|
+
scopeWatchers = new Map();
|
|
56
|
+
updateSetCheckInterval = null;
|
|
57
|
+
scopeLock = Promise.resolve();
|
|
58
|
+
cachedScope = null;
|
|
59
|
+
pendingScopes = new Map(); // scope -> first change timestamp
|
|
60
|
+
globalProcessQueue = null;
|
|
61
|
+
async startWatchingAllScopes(options) {
|
|
62
|
+
var opts = options || { monitorIntervalMs: 120000 };
|
|
63
|
+
try {
|
|
64
|
+
// Load configuration
|
|
65
|
+
await ConfigManager.loadConfigs();
|
|
66
|
+
const config = ConfigManager.getConfig();
|
|
67
|
+
if (!config.scopes) {
|
|
68
|
+
Logger_1.logger.error("No scopes defined in dove.config.js");
|
|
69
|
+
throw new Error("No scopes defined in configuration");
|
|
70
|
+
}
|
|
71
|
+
const scopes = Object.keys(config.scopes);
|
|
72
|
+
Logger_1.logger.info(`Starting multi-scope watch for ${scopes.length} scopes: ${scopes.join(", ")}`);
|
|
73
|
+
// Start watching each scope
|
|
74
|
+
for (const scopeName of scopes) {
|
|
75
|
+
const scopeConfig = config.scopes[scopeName];
|
|
76
|
+
let sourceDirectory;
|
|
77
|
+
if (typeof scopeConfig === "object" && scopeConfig.sourceDirectory) {
|
|
78
|
+
sourceDirectory = path.resolve(ConfigManager.getRootDir(), scopeConfig.sourceDirectory);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
// Default to src/{scope} if no sourceDirectory specified
|
|
82
|
+
sourceDirectory = path.resolve(ConfigManager.getRootDir(), "src", scopeName);
|
|
83
|
+
}
|
|
84
|
+
this.startWatchingScope(scopeName, sourceDirectory);
|
|
85
|
+
}
|
|
86
|
+
// Start periodic update set checking (unless disabled)
|
|
87
|
+
if (opts.monitorIntervalMs > 0) {
|
|
88
|
+
this.startUpdateSetMonitoring(opts.monitorIntervalMs);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
Logger_1.logger.info("Update set monitoring disabled (--noMonitoring)");
|
|
92
|
+
}
|
|
93
|
+
Logger_1.logger.success("✅ Multi-scope watch started successfully!");
|
|
94
|
+
Logger_1.logger.info("Watching for file changes across all scopes...");
|
|
95
|
+
Logger_1.logger.info("Press Ctrl+C to stop watching\n");
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
Logger_1.logger.error("Failed to start multi-scope watch: " + error);
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
startWatchingScope(scopeName, sourceDirectory) {
|
|
103
|
+
Logger_1.logger.info(`Setting up watcher for scope ${scopeName} in ${sourceDirectory}`);
|
|
104
|
+
const watcher = chokidar_1.default.watch(sourceDirectory, {
|
|
105
|
+
persistent: true,
|
|
106
|
+
ignoreInitial: true,
|
|
107
|
+
awaitWriteFinish: {
|
|
108
|
+
stabilityThreshold: 300,
|
|
109
|
+
pollInterval: 100
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
const scopeWatcher = {
|
|
113
|
+
scope: scopeName,
|
|
114
|
+
watcher: watcher,
|
|
115
|
+
pushQueue: [],
|
|
116
|
+
sourceDirectory: sourceDirectory
|
|
117
|
+
};
|
|
118
|
+
// Initialize global debounce once (shared across all scopes)
|
|
119
|
+
if (!this.globalProcessQueue) {
|
|
120
|
+
this.globalProcessQueue = (0, lodash_1.debounce)(async () => {
|
|
121
|
+
await this.processAllPendingScopes();
|
|
122
|
+
}, DEBOUNCE_MS);
|
|
123
|
+
}
|
|
124
|
+
watcher.on("change", (filePath) => {
|
|
125
|
+
Logger_1.logger.info(`[${scopeName}] File changed: ${path.relative(sourceDirectory, filePath)}`);
|
|
126
|
+
scopeWatcher.pushQueue.push(filePath);
|
|
127
|
+
if (!this.pendingScopes.has(scopeName)) {
|
|
128
|
+
this.pendingScopes.set(scopeName, Date.now());
|
|
129
|
+
}
|
|
130
|
+
this.globalProcessQueue();
|
|
131
|
+
});
|
|
132
|
+
watcher.on("add", (filePath) => {
|
|
133
|
+
Logger_1.logger.info(`[${scopeName}] File added: ${path.relative(sourceDirectory, filePath)}`);
|
|
134
|
+
scopeWatcher.pushQueue.push(filePath);
|
|
135
|
+
if (!this.pendingScopes.has(scopeName)) {
|
|
136
|
+
this.pendingScopes.set(scopeName, Date.now());
|
|
137
|
+
}
|
|
138
|
+
this.globalProcessQueue();
|
|
139
|
+
});
|
|
140
|
+
watcher.on("error", (error) => {
|
|
141
|
+
Logger_1.logger.error(`[${scopeName}] Watcher error: ${error.message}`);
|
|
142
|
+
});
|
|
143
|
+
this.scopeWatchers.set(scopeName, scopeWatcher);
|
|
144
|
+
}
|
|
145
|
+
async withScopeLock(fn) {
|
|
146
|
+
var resolve;
|
|
147
|
+
var next = new Promise(function (r) { resolve = r; });
|
|
148
|
+
var prev = this.scopeLock;
|
|
149
|
+
this.scopeLock = next;
|
|
150
|
+
await prev;
|
|
151
|
+
try {
|
|
152
|
+
return await fn();
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
resolve();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
getUpdateSetConfig() {
|
|
159
|
+
var configPath = (0, projectFiles_1.getUpdateSetsConfigPath)();
|
|
160
|
+
try {
|
|
161
|
+
if (fs.existsSync(configPath)) {
|
|
162
|
+
return JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch (e) {
|
|
166
|
+
Logger_1.logger.warn(`[MultiScope] Failed to parse update set config at ${configPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
167
|
+
}
|
|
168
|
+
return {};
|
|
169
|
+
}
|
|
170
|
+
readActiveTask() {
|
|
171
|
+
var taskPath = (0, projectFiles_1.getActiveTaskPath)();
|
|
172
|
+
try {
|
|
173
|
+
if (fs.existsSync(taskPath)) {
|
|
174
|
+
var parsed = JSON.parse(fs.readFileSync(taskPath, "utf8"));
|
|
175
|
+
if (!parsed.taskId || typeof parsed.taskId !== "string" || parsed.taskId.trim() === "") {
|
|
176
|
+
Logger_1.logger.error("Active task file is missing a valid taskId. Ignoring active task.");
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
if (!parsed.updateSetName || typeof parsed.updateSetName !== "string" || parsed.updateSetName.trim() === "") {
|
|
180
|
+
Logger_1.logger.error("Active task file is missing a valid updateSetName. Ignoring active task.");
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
var stat = fs.statSync(taskPath);
|
|
184
|
+
var ageMs = Date.now() - stat.mtimeMs;
|
|
185
|
+
var ageDays = Math.floor(ageMs / (1000 * 60 * 60 * 24));
|
|
186
|
+
if (ageDays >= 7) {
|
|
187
|
+
var taskName = parsed.taskName || parsed.taskId;
|
|
188
|
+
Logger_1.logger.warn("Active task " + taskName + " was selected " + ageDays + " days ago. Run dove task clear if you have moved on.");
|
|
189
|
+
}
|
|
190
|
+
return parsed;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch (e) {
|
|
194
|
+
Logger_1.logger.warn(`Failed to parse active task file: ${e instanceof Error ? e.message : String(e)}`);
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
saveUpdateSetConfig(config) {
|
|
199
|
+
var configPath = (0, projectFiles_1.getUpdateSetsConfigPath)();
|
|
200
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
201
|
+
}
|
|
202
|
+
async ensureUpdateSetForScope(scopeName) {
|
|
203
|
+
var config = this.getUpdateSetConfig();
|
|
204
|
+
if (config[scopeName]) {
|
|
205
|
+
return; // Already has an update set mapped
|
|
206
|
+
}
|
|
207
|
+
var activeTask = this.readActiveTask();
|
|
208
|
+
if (!activeTask) {
|
|
209
|
+
// No active task — try to resolve the current update set from the ServiceNow
|
|
210
|
+
// session so pushWithUpdateSet can be used instead of the fallback updateRecord.
|
|
211
|
+
try {
|
|
212
|
+
var { defaultClient } = await Promise.resolve().then(() => __importStar(require("./snClient")));
|
|
213
|
+
var sessionClient = defaultClient();
|
|
214
|
+
var curResp = await sessionClient.getCurrentUpdateSet(scopeName);
|
|
215
|
+
var curData = curResp.data;
|
|
216
|
+
if (curData && curData.result) {
|
|
217
|
+
curData = curData.result;
|
|
218
|
+
}
|
|
219
|
+
var curSysId = curData && curData.sysId ? curData.sysId : null;
|
|
220
|
+
var curName = curData && curData.name ? curData.name : null;
|
|
221
|
+
if (curSysId && curName) {
|
|
222
|
+
var isDefault = curName === "Default" || curName.toLowerCase().indexOf("default") !== -1;
|
|
223
|
+
if (isDefault) {
|
|
224
|
+
Logger_1.logger.warn(`[${scopeName}] No update set configured for scope ${scopeName}. ` +
|
|
225
|
+
`Changes will go to Default. ` +
|
|
226
|
+
`Use dove createUpdateSet or activate a task in the dashboard.`);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
// Use the session's current non-Default update set
|
|
230
|
+
config = this.getUpdateSetConfig();
|
|
231
|
+
config[scopeName] = { sys_id: curSysId, name: curName };
|
|
232
|
+
this.saveUpdateSetConfig(config);
|
|
233
|
+
Logger_1.logger.info(`[${scopeName}] Using session update set: ${curName}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
Logger_1.logger.warn(`[${scopeName}] No update set configured for scope ${scopeName}. ` +
|
|
238
|
+
`Changes will go to Default. ` +
|
|
239
|
+
`Use dove createUpdateSet or activate a task in the dashboard.`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
catch (queryErr) {
|
|
243
|
+
Logger_1.logger.warn(`[${scopeName}] No update set configured for scope ${scopeName}. ` +
|
|
244
|
+
`Changes will go to Default (could not query current update set). ` +
|
|
245
|
+
`Use dove createUpdateSet or activate a task in the dashboard.`);
|
|
246
|
+
}
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
var taskId = activeTask.taskId;
|
|
250
|
+
var updateSetName = activeTask.updateSetName;
|
|
251
|
+
var description = activeTask.description || "";
|
|
252
|
+
if (!taskId || taskId.trim() === "") {
|
|
253
|
+
Logger_1.logger.error(`[${scopeName}] Active task has an empty taskId. Skipping update set lookup.`);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
Logger_1.logger.info(`[${scopeName}] No update set found — auto-creating for task CU-${taskId}...`);
|
|
257
|
+
try {
|
|
258
|
+
var { defaultClient, unwrapSNResponse } = await Promise.resolve().then(() => __importStar(require("./snClient")));
|
|
259
|
+
var client = defaultClient();
|
|
260
|
+
// Switch to the target scope and resolve its sys_id for update set creation
|
|
261
|
+
await client.changeScope(scopeName);
|
|
262
|
+
var scopeIdResult = await unwrapSNResponse(client.getScopeId(scopeName));
|
|
263
|
+
var scopeSysId = scopeIdResult && scopeIdResult.length > 0 ? scopeIdResult[0].sys_id : undefined;
|
|
264
|
+
if (!scopeSysId) {
|
|
265
|
+
Logger_1.logger.error(`[${scopeName}] Scope "${scopeName}" not found on the instance. Cannot create update set for an invalid scope.`);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
// Search for an existing update set matching this task in this scope
|
|
269
|
+
var query = "application.scope=" + scopeName +
|
|
270
|
+
"^nameLIKECU-" + taskId +
|
|
271
|
+
"^state=in progress" +
|
|
272
|
+
"^ORDERBYDESCsys_created_on";
|
|
273
|
+
var searchResp = await client.client.get("api/now/table/sys_update_set", {
|
|
274
|
+
params: {
|
|
275
|
+
sysparm_query: query,
|
|
276
|
+
sysparm_fields: "sys_id,name,state,application,sys_created_on",
|
|
277
|
+
sysparm_limit: "10",
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
var existing = (searchResp.data && searchResp.data.result) || [];
|
|
281
|
+
var updateSet = null;
|
|
282
|
+
if (existing.length > 0) {
|
|
283
|
+
updateSet = { sys_id: existing[0].sys_id, name: existing[0].name };
|
|
284
|
+
Logger_1.logger.info(`[${scopeName}] Found existing update set: ${updateSet.name}`);
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
// Create a new one with explicit scope sys_id
|
|
288
|
+
var createResp = await unwrapSNResponse(client.createUpdateSet(updateSetName, scopeSysId, description));
|
|
289
|
+
updateSet = { sys_id: createResp.sys_id, name: updateSetName };
|
|
290
|
+
Logger_1.logger.info(`[${scopeName}] Auto-created update set: ${updateSet.name}`);
|
|
291
|
+
}
|
|
292
|
+
// Switch the active update set on the instance and verify
|
|
293
|
+
try {
|
|
294
|
+
await client.changeUpdateSet({ sysId: updateSet.sys_id });
|
|
295
|
+
// Verify the switch was successful
|
|
296
|
+
var verifyResp = await client.getCurrentUpdateSet(scopeName);
|
|
297
|
+
var verifyResult = verifyResp.data;
|
|
298
|
+
if (verifyResult && verifyResult.result) {
|
|
299
|
+
verifyResult = verifyResult.result;
|
|
300
|
+
}
|
|
301
|
+
var currentSysId = verifyResult && verifyResult.sysId ? verifyResult.sysId : null;
|
|
302
|
+
if (currentSysId !== updateSet.sys_id) {
|
|
303
|
+
// Retry once
|
|
304
|
+
Logger_1.logger.warn(`[${scopeName}] Update set verification failed, retrying switch...`);
|
|
305
|
+
await client.changeUpdateSet({ sysId: updateSet.sys_id });
|
|
306
|
+
var retryResp = await client.getCurrentUpdateSet(scopeName);
|
|
307
|
+
var retryResult = retryResp.data;
|
|
308
|
+
if (retryResult && retryResult.result) {
|
|
309
|
+
retryResult = retryResult.result;
|
|
310
|
+
}
|
|
311
|
+
var retrySysId = retryResult && retryResult.sysId ? retryResult.sysId : null;
|
|
312
|
+
if (retrySysId !== updateSet.sys_id) {
|
|
313
|
+
var actualName = retryResult && retryResult.name ? retryResult.name : "unknown";
|
|
314
|
+
Logger_1.logger.error(`[${scopeName}] Update set ${updateSet.name} was created but could not be activated. Current update set is ${actualName}.`);
|
|
315
|
+
throw new Error(`Update set ${updateSet.name} could not be activated for scope ${scopeName}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
Logger_1.logger.debug(`[${scopeName}] Update set switch verified: ${updateSet.name}`);
|
|
319
|
+
}
|
|
320
|
+
catch (changeErr) {
|
|
321
|
+
Logger_1.logger.warn(`[${scopeName}] Could not auto-switch update set on instance`);
|
|
322
|
+
}
|
|
323
|
+
// Persist the mapping (serialized under scopeLock — safe from concurrent writes)
|
|
324
|
+
config = this.getUpdateSetConfig(); // Re-read in case another scope wrote
|
|
325
|
+
config[scopeName] = { sys_id: updateSet.sys_id, name: updateSet.name };
|
|
326
|
+
this.saveUpdateSetConfig(config);
|
|
327
|
+
// Verify the write persisted correctly
|
|
328
|
+
var verifiedConfig = this.getUpdateSetConfig();
|
|
329
|
+
if (!verifiedConfig[scopeName] || verifiedConfig[scopeName].sys_id !== updateSet.sys_id) {
|
|
330
|
+
Logger_1.logger.error(`[${scopeName}] Update set config write verification failed. Expected mapping for ${scopeName} with sys_id ${updateSet.sys_id} but got: ${JSON.stringify(verifiedConfig[scopeName])}`);
|
|
331
|
+
throw new Error(`Update set config write verification failed for scope ${scopeName}`);
|
|
332
|
+
}
|
|
333
|
+
Logger_1.logger.debug(`[${scopeName}] Update set config verified: ${updateSet.name} (${updateSet.sys_id})`);
|
|
334
|
+
// Also update the active task file
|
|
335
|
+
if (activeTask) {
|
|
336
|
+
if (!activeTask.scopes) {
|
|
337
|
+
activeTask.scopes = {};
|
|
338
|
+
}
|
|
339
|
+
activeTask.scopes[scopeName] = { sys_id: updateSet.sys_id, name: updateSet.name };
|
|
340
|
+
fs.writeFileSync((0, projectFiles_1.getActiveTaskPath)(), JSON.stringify(activeTask, null, 2));
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
Logger_1.logger.error(`[${scopeName}] Failed to auto-create update set: ${error}`);
|
|
345
|
+
Logger_1.logger.warn(`[${scopeName}] Pushing without update set routing.`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
async processAllPendingScopes() {
|
|
349
|
+
// Sort scopes by first file change timestamp (FIFO)
|
|
350
|
+
var sorted = Array.from(this.pendingScopes.entries()).sort(function (a, b) {
|
|
351
|
+
return a[1] - b[1];
|
|
352
|
+
});
|
|
353
|
+
this.pendingScopes.clear();
|
|
354
|
+
for (var i = 0; i < sorted.length; i++) {
|
|
355
|
+
var scopeName = sorted[i][0];
|
|
356
|
+
var scopeWatcher = this.scopeWatchers.get(scopeName);
|
|
357
|
+
if (scopeWatcher && scopeWatcher.pushQueue.length > 0) {
|
|
358
|
+
await this.processScopeQueue(scopeWatcher);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
async processScopeQueue(scopeWatcher) {
|
|
363
|
+
if (scopeWatcher.pushQueue.length === 0)
|
|
364
|
+
return;
|
|
365
|
+
const toProcess = Array.from(new Set([...scopeWatcher.pushQueue]));
|
|
366
|
+
scopeWatcher.pushQueue = [];
|
|
367
|
+
Logger_1.logger.info(`[${scopeWatcher.scope}] Processing ${toProcess.length} file(s)...`);
|
|
368
|
+
try {
|
|
369
|
+
await this.withScopeLock(async () => {
|
|
370
|
+
// First, switch to the correct scope
|
|
371
|
+
await this.switchToScope(scopeWatcher.scope);
|
|
372
|
+
// Ensure an update set exists for this scope
|
|
373
|
+
await this.ensureUpdateSetForScope(scopeWatcher.scope);
|
|
374
|
+
// Load the manifest for this specific scope
|
|
375
|
+
await this.loadScopeManifest(scopeWatcher.scope, scopeWatcher.sourceDirectory);
|
|
376
|
+
// Process the files — track skip reasons for summary
|
|
377
|
+
const fileContexts = [];
|
|
378
|
+
const skippedFiles = [];
|
|
379
|
+
toProcess.forEach(function (filePath) {
|
|
380
|
+
var result = (0, FileUtils_1.getFileContextWithSkipReason)(filePath);
|
|
381
|
+
if (result.context) {
|
|
382
|
+
// Scope validation: ensure the record's scope matches the watcher's scope
|
|
383
|
+
if (result.context.scope && result.context.scope !== scopeWatcher.scope) {
|
|
384
|
+
Logger_1.logger.error(`[${scopeWatcher.scope}] Scope mismatch: ${filePath} belongs to scope "${result.context.scope}" but was queued by watcher for "${scopeWatcher.scope}". Skipping to prevent cross-scope contamination.`);
|
|
385
|
+
skippedFiles.push({ filePath: filePath, reason: `scope mismatch (record: ${result.context.scope}, watcher: ${scopeWatcher.scope})` });
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
fileContexts.push(result.context);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
var reason = result.skipReason || "unknown";
|
|
393
|
+
Logger_1.logger.warn(`[${scopeWatcher.scope}] Skipped: ${filePath} (${reason})`);
|
|
394
|
+
skippedFiles.push({ filePath: filePath, reason: reason });
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
if (fileContexts.length === 0) {
|
|
398
|
+
Logger_1.logger.warn(`[${scopeWatcher.scope}] No valid file contexts found. ${skippedFiles.length} file(s) skipped.`);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const buildables = (0, appUtils_1.groupAppFiles)(fileContexts);
|
|
402
|
+
const updateResults = await (0, appUtils_1.pushFiles)(buildables);
|
|
403
|
+
updateResults.forEach((res, index) => {
|
|
404
|
+
if (index < fileContexts.length) {
|
|
405
|
+
(0, logMessages_1.logFilePush)(fileContexts[index], res);
|
|
406
|
+
if (res.success) {
|
|
407
|
+
(0, recentEdits_1.writeRecentEdit)(fileContexts[index]);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
// Push summary
|
|
412
|
+
var pushedCount = updateResults.filter(function (r) { return r.success; }).length;
|
|
413
|
+
var totalCount = toProcess.length;
|
|
414
|
+
var summaryParts = [`Pushed ${pushedCount}/${totalCount} files to ${scopeWatcher.scope}.`];
|
|
415
|
+
if (skippedFiles.length > 0) {
|
|
416
|
+
var skippedList = skippedFiles.map(function (s) { return `${s.filePath} (${s.reason})`; }).join(", ");
|
|
417
|
+
summaryParts.push(`${skippedFiles.length} files skipped: [${skippedList}]`);
|
|
418
|
+
}
|
|
419
|
+
Logger_1.logger.info(`[${scopeWatcher.scope}] ${summaryParts.join(" ")}`);
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
catch (error) {
|
|
423
|
+
Logger_1.logger.error(`[${scopeWatcher.scope}] Error processing queue: ${error}`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
async loadScopeManifest(scopeName, sourceDirectory) {
|
|
427
|
+
try {
|
|
428
|
+
// The sourceDirectory is like /path/to/ServiceNow/src/x_cadso_core
|
|
429
|
+
// We need to go up two levels to get to the ServiceNow directory where manifests are stored
|
|
430
|
+
const projectRoot = path.dirname(path.dirname(sourceDirectory)); // Go up from src/scope to project root
|
|
431
|
+
const fs = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
432
|
+
// First try to load scope-specific manifest file (prefer dove.*, fall back to sinc.*).
|
|
433
|
+
const doveScopeManifestPath = path.join(projectRoot, `dove.manifest.${scopeName}.json`);
|
|
434
|
+
const sincScopeManifestPath = path.join(projectRoot, `sinc.manifest.${scopeName}.json`);
|
|
435
|
+
const scopeManifestPath = fs.existsSync(doveScopeManifestPath)
|
|
436
|
+
? doveScopeManifestPath
|
|
437
|
+
: sincScopeManifestPath;
|
|
438
|
+
if (fs.existsSync(scopeManifestPath)) {
|
|
439
|
+
const manifestContent = await fs.promises.readFile(scopeManifestPath, "utf-8");
|
|
440
|
+
const scopeManifest = JSON.parse(manifestContent);
|
|
441
|
+
// Ensure scope field is set
|
|
442
|
+
if (!scopeManifest.scope) {
|
|
443
|
+
scopeManifest.scope = scopeName;
|
|
444
|
+
}
|
|
445
|
+
ConfigManager.updateManifest(scopeManifest);
|
|
446
|
+
Logger_1.logger.debug(`Loaded scope-specific manifest for: ${scopeName}`);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
// Fall back to checking single manifest file (prefer dove.manifest.json, then legacy sinc.manifest.json)
|
|
450
|
+
const doveManifestPath = path.join(projectRoot, "dove.manifest.json");
|
|
451
|
+
const sincManifestPath = path.join(projectRoot, "sinc.manifest.json");
|
|
452
|
+
const manifestPath = fs.existsSync(doveManifestPath) ? doveManifestPath : sincManifestPath;
|
|
453
|
+
if (fs.existsSync(manifestPath)) {
|
|
454
|
+
const manifestContent = await fs.promises.readFile(manifestPath, "utf-8");
|
|
455
|
+
const fullManifest = JSON.parse(manifestContent);
|
|
456
|
+
// Check if this is a multi-scope manifest (has scopes as top-level keys)
|
|
457
|
+
if (fullManifest[scopeName]) {
|
|
458
|
+
// Multi-scope manifest - extract the specific scope's data
|
|
459
|
+
const scopeManifest = fullManifest[scopeName];
|
|
460
|
+
// Add the scope field for compatibility
|
|
461
|
+
scopeManifest.scope = scopeName;
|
|
462
|
+
ConfigManager.updateManifest(scopeManifest);
|
|
463
|
+
Logger_1.logger.debug(`Loaded manifest for scope: ${scopeName} from legacy multi-scope manifest`);
|
|
464
|
+
}
|
|
465
|
+
else if (fullManifest.scope === scopeName) {
|
|
466
|
+
// Single-scope manifest for the correct scope
|
|
467
|
+
ConfigManager.updateManifest(fullManifest);
|
|
468
|
+
Logger_1.logger.debug(`Loaded single-scope manifest for scope: ${scopeName}`);
|
|
469
|
+
}
|
|
470
|
+
else if (fullManifest.tables) {
|
|
471
|
+
// Old-style single-scope manifest without scope field - assume it's for this scope
|
|
472
|
+
fullManifest.scope = scopeName;
|
|
473
|
+
ConfigManager.updateManifest(fullManifest);
|
|
474
|
+
Logger_1.logger.debug(`Loaded manifest for scope: ${scopeName} (legacy format)`);
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
Logger_1.logger.warn(`[${scopeName}] Scope not found in manifest`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
Logger_1.logger.warn(`[${scopeName}] No manifest found at ${scopeManifestPath} or ${manifestPath}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
catch (error) {
|
|
485
|
+
Logger_1.logger.error(`Failed to load manifest for scope ${scopeName}: ${error}`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
async switchToScope(scopeName) {
|
|
489
|
+
// Skip if already on the correct scope
|
|
490
|
+
if (this.cachedScope === scopeName) {
|
|
491
|
+
Logger_1.logger.debug(`Already on scope ${scopeName}, skipping switch`);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
try {
|
|
495
|
+
var { defaultClient } = await Promise.resolve().then(() => __importStar(require("./snClient")));
|
|
496
|
+
var client = defaultClient();
|
|
497
|
+
// Use the Claude REST API changeScope endpoint to switch the session scope.
|
|
498
|
+
// This uses gs.setCurrentApplicationId() server-side, which correctly changes
|
|
499
|
+
// the REST API session scope. The previous approach used user preference updates
|
|
500
|
+
// (apps.current_app) which only writes a DB record and does NOT affect the
|
|
501
|
+
// active REST session — causing updateRecord() to operate in the wrong scope.
|
|
502
|
+
await client.changeScope(scopeName);
|
|
503
|
+
this.cachedScope = scopeName;
|
|
504
|
+
Logger_1.logger.debug(`Switched to scope: ${scopeName}`);
|
|
505
|
+
}
|
|
506
|
+
catch (error) {
|
|
507
|
+
// Invalidate cache on failure
|
|
508
|
+
this.cachedScope = null;
|
|
509
|
+
Logger_1.logger.error(`Failed to switch to scope ${scopeName}: ${error}`);
|
|
510
|
+
throw error;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
async startUpdateSetMonitoring(intervalMs) {
|
|
514
|
+
// Check update sets immediately on start
|
|
515
|
+
await this.checkAllUpdateSets();
|
|
516
|
+
Logger_1.logger.info("Update set monitoring interval: " + Math.round(intervalMs / 1000) + "s");
|
|
517
|
+
this.updateSetCheckInterval = setInterval(async () => {
|
|
518
|
+
await this.checkAllUpdateSets();
|
|
519
|
+
}, intervalMs);
|
|
520
|
+
}
|
|
521
|
+
async checkAllUpdateSets() {
|
|
522
|
+
// Budget: monitoring should not consume more than 5% of the 20 RPS rate limit
|
|
523
|
+
// per cycle. That gives us ~1 request/second budget. Each scope check uses
|
|
524
|
+
// ~6 API calls (switchToScope ~3, getUserSysId ~1, pref ~1, details ~1).
|
|
525
|
+
// To stay within budget we use the local update set config file instead of
|
|
526
|
+
// querying ServiceNow for each scope — 0 API calls when config is present.
|
|
527
|
+
try {
|
|
528
|
+
const config = ConfigManager.getConfig();
|
|
529
|
+
if (!config.scopes)
|
|
530
|
+
return;
|
|
531
|
+
const scopes = Object.keys(config.scopes);
|
|
532
|
+
var updateSetConfig = this.getUpdateSetConfig();
|
|
533
|
+
Logger_1.logger.info("\n" + "=".repeat(60));
|
|
534
|
+
Logger_1.logger.info("Update Set Status Check");
|
|
535
|
+
Logger_1.logger.info("=".repeat(60));
|
|
536
|
+
for (var i = 0; i < scopes.length; i++) {
|
|
537
|
+
var scopeName = scopes[i];
|
|
538
|
+
var mapping = updateSetConfig[scopeName];
|
|
539
|
+
if (mapping && mapping.name) {
|
|
540
|
+
var isDefault = mapping.name === "Default" ||
|
|
541
|
+
mapping.name.toLowerCase().indexOf("default") !== -1;
|
|
542
|
+
if (isDefault) {
|
|
543
|
+
Logger_1.logger.warn(`⚠️ [${scopeName}] Currently in DEFAULT update set!`);
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
Logger_1.logger.info(`✅ [${scopeName}] Update Set: ${mapping.name}`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
Logger_1.logger.warn(`⚠️ [${scopeName}] No update set configured`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
Logger_1.logger.info("=".repeat(60) + "\n");
|
|
554
|
+
}
|
|
555
|
+
catch (error) {
|
|
556
|
+
Logger_1.logger.error(`Error during update set monitoring: ${error}`);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
async getUpdateSetDetails(updateSetId) {
|
|
560
|
+
try {
|
|
561
|
+
const { defaultClient } = await Promise.resolve().then(() => __importStar(require("./snClient")));
|
|
562
|
+
const client = defaultClient();
|
|
563
|
+
// Create axios client directly to get update set details
|
|
564
|
+
const axios = (await Promise.resolve().then(() => __importStar(require("axios")))).default;
|
|
565
|
+
const { SN_USER = "", SN_PASSWORD = "", SN_INSTANCE = "" } = process.env;
|
|
566
|
+
const axiosClient = axios.create({
|
|
567
|
+
auth: {
|
|
568
|
+
username: SN_USER,
|
|
569
|
+
password: SN_PASSWORD
|
|
570
|
+
},
|
|
571
|
+
baseURL: SN_INSTANCE
|
|
572
|
+
});
|
|
573
|
+
const response = await axiosClient.get(`/api/now/table/sys_update_set/${updateSetId}`, {
|
|
574
|
+
params: {
|
|
575
|
+
sysparm_fields: "name,state,sys_id"
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
if (response.data && response.data.result) {
|
|
579
|
+
return response.data.result;
|
|
580
|
+
}
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
catch (error) {
|
|
584
|
+
Logger_1.logger.warn(`Could not get update set details: ${error}`);
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
stopWatching() {
|
|
589
|
+
// Stop all scope watchers
|
|
590
|
+
for (const [scopeName, scopeWatcher] of this.scopeWatchers) {
|
|
591
|
+
Logger_1.logger.info(`Stopping watcher for scope: ${scopeName}`);
|
|
592
|
+
scopeWatcher.watcher.close();
|
|
593
|
+
}
|
|
594
|
+
this.scopeWatchers.clear();
|
|
595
|
+
// Cancel global debounce
|
|
596
|
+
if (this.globalProcessQueue) {
|
|
597
|
+
this.globalProcessQueue.cancel();
|
|
598
|
+
this.globalProcessQueue = null;
|
|
599
|
+
}
|
|
600
|
+
this.pendingScopes.clear();
|
|
601
|
+
// Stop update set monitoring
|
|
602
|
+
if (this.updateSetCheckInterval) {
|
|
603
|
+
clearInterval(this.updateSetCheckInterval);
|
|
604
|
+
this.updateSetCheckInterval = null;
|
|
605
|
+
}
|
|
606
|
+
Logger_1.logger.info("All watchers stopped");
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
exports.multiScopeWatcher = new MultiScopeWatcherManager();
|
|
610
|
+
function startMultiScopeWatching(options) {
|
|
611
|
+
return exports.multiScopeWatcher.startWatchingAllScopes(options);
|
|
612
|
+
}
|
|
613
|
+
function stopMultiScopeWatching() {
|
|
614
|
+
exports.multiScopeWatcher.stopWatching();
|
|
615
|
+
}
|