@tenonhq/sincronia-core 0.0.78 → 0.0.80
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 +1 -1
- package/dist/FileUtils.js +19 -13
- package/dist/MultiScopeWatcher.js +212 -84
- package/dist/allScopesCommands.js +4 -2
- package/dist/appUtils.js +23 -12
- package/dist/commander.js +17 -0
- package/dist/commands.js +38 -1
- package/dist/config.js +4 -4
- package/dist/snClient.js +81 -1
- package/dist/tests/ensureUpdateSetWarnings.test.js +218 -0
- package/dist/tests/errorLogLevels.test.js +273 -0
- package/dist/tests/fileContextSkipReason.test.js +116 -0
- package/dist/tests/globalDebounce.test.js +307 -0
- package/dist/tests/multi-scope-watcher.test.js +109 -7
- package/dist/tests/pushFiles.test.js +162 -0
- package/dist/tests/rateLimitCoordination.test.js +271 -0
- package/dist/tests/retryOnHttpErr.test.js +154 -0
- package/dist/tests/scopeCaching.test.js +124 -0
- package/dist/tests/serializeUpdateSetConfig.test.js +325 -0
- package/dist/tests/taskClear.test.js +170 -0
- package/dist/tests/taskStaleness.test.js +220 -0
- package/dist/tests/validateTaskId.test.js +304 -0
- package/dist/tests/verifyUpdateSetSwitch.test.js +277 -0
- package/dist/updateSetCommands.js +59 -2
- package/package.json +1 -1
- package/skills/sinc-configure-pipeline.md +19 -19
- package/skills/sinc-debug-build.md +7 -7
- package/skills/sinc-setup-project.md +2 -2
- package/skills/sinc-troubleshoot-sync.md +5 -5
package/README.md
CHANGED
package/dist/FileUtils.js
CHANGED
|
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.writeFileForce = exports.writeSNFileForce = exports.writeSNFileIfNotExists = exports.writeBuildFile = exports.summarizeFile = exports.encodedPathsToFilePaths = exports.isValidPath = exports.splitEncodedPaths = exports.getPathsInPath = exports.isDirectory = exports.toAbsolutePath = exports.getFileContextFromPath = exports.getBuildExt = exports.isUnderPath = exports.appendToPath = exports.pathExists = exports.createDirRecursively = exports.writeSNFileCurry = exports.writeScopeManifest = exports.writeManifestFile = exports.SNFileExists = void 0;
|
|
39
|
+
exports.writeFileForce = exports.writeSNFileForce = exports.writeSNFileIfNotExists = exports.writeBuildFile = exports.summarizeFile = exports.encodedPathsToFilePaths = exports.isValidPath = exports.splitEncodedPaths = exports.getPathsInPath = exports.isDirectory = exports.toAbsolutePath = exports.getFileContextFromPath = exports.getFileContextWithSkipReason = exports.getBuildExt = exports.isUnderPath = exports.appendToPath = exports.pathExists = exports.createDirRecursively = exports.writeSNFileCurry = exports.writeScopeManifest = exports.writeManifestFile = exports.SNFileExists = void 0;
|
|
40
40
|
exports.writeEnvVar = writeEnvVar;
|
|
41
41
|
exports.writeEnvVars = writeEnvVars;
|
|
42
42
|
const constants_1 = require("./constants");
|
|
@@ -164,7 +164,7 @@ const getTargetFieldFromPath = (filePath, table, ext) => {
|
|
|
164
164
|
? "inputs.script"
|
|
165
165
|
: path_1.default.basename(filePath, ext);
|
|
166
166
|
};
|
|
167
|
-
const
|
|
167
|
+
const getFileContextWithSkipReason = (filePath) => {
|
|
168
168
|
const ext = getFileExtension(filePath);
|
|
169
169
|
const [tableName, recordName] = path_1.default
|
|
170
170
|
.dirname(filePath)
|
|
@@ -180,11 +180,11 @@ const getFileContextFromPath = (filePath) => {
|
|
|
180
180
|
if (ConfigManager.isMultiScopeManifest(manifest)) {
|
|
181
181
|
var detectedScope = ConfigManager.resolveScopeFromPath(filePath);
|
|
182
182
|
if (!detectedScope) {
|
|
183
|
-
return
|
|
183
|
+
return { skipReason: "scope not found" };
|
|
184
184
|
}
|
|
185
185
|
var scopeMan = ConfigManager.resolveManifestForScope(manifest, detectedScope);
|
|
186
186
|
if (!scopeMan) {
|
|
187
|
-
return
|
|
187
|
+
return { skipReason: "scope manifest not found" };
|
|
188
188
|
}
|
|
189
189
|
scope = scopeMan.scope || detectedScope;
|
|
190
190
|
tables = scopeMan.tables;
|
|
@@ -199,22 +199,28 @@ const getFileContextFromPath = (filePath) => {
|
|
|
199
199
|
const { files, sys_id } = record;
|
|
200
200
|
const field = files.find((file) => file.name === targetField);
|
|
201
201
|
if (!field) {
|
|
202
|
-
return
|
|
202
|
+
return { skipReason: "not in manifest" };
|
|
203
203
|
}
|
|
204
204
|
return {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
205
|
+
context: {
|
|
206
|
+
filePath,
|
|
207
|
+
ext,
|
|
208
|
+
sys_id,
|
|
209
|
+
name: recordName,
|
|
210
|
+
scope,
|
|
211
|
+
tableName,
|
|
212
|
+
targetField,
|
|
213
|
+
},
|
|
212
214
|
};
|
|
213
215
|
}
|
|
214
216
|
catch (e) {
|
|
215
|
-
return
|
|
217
|
+
return { skipReason: "not in manifest" };
|
|
216
218
|
}
|
|
217
219
|
};
|
|
220
|
+
exports.getFileContextWithSkipReason = getFileContextWithSkipReason;
|
|
221
|
+
const getFileContextFromPath = (filePath) => {
|
|
222
|
+
return (0, exports.getFileContextWithSkipReason)(filePath).context;
|
|
223
|
+
};
|
|
218
224
|
exports.getFileContextFromPath = getFileContextFromPath;
|
|
219
225
|
const toAbsolutePath = (p) => path_1.default.isAbsolute(p) ? p : path_1.default.join(process.cwd(), p);
|
|
220
226
|
exports.toAbsolutePath = toAbsolutePath;
|
|
@@ -54,7 +54,11 @@ class MultiScopeWatcherManager {
|
|
|
54
54
|
scopeWatchers = new Map();
|
|
55
55
|
updateSetCheckInterval = null;
|
|
56
56
|
scopeLock = Promise.resolve();
|
|
57
|
-
|
|
57
|
+
cachedScope = null;
|
|
58
|
+
pendingScopes = new Map(); // scope -> first change timestamp
|
|
59
|
+
globalProcessQueue = null;
|
|
60
|
+
async startWatchingAllScopes(options) {
|
|
61
|
+
var opts = options || { monitorIntervalMs: 120000 };
|
|
58
62
|
try {
|
|
59
63
|
// Load configuration
|
|
60
64
|
await ConfigManager.loadConfigs();
|
|
@@ -78,8 +82,13 @@ class MultiScopeWatcherManager {
|
|
|
78
82
|
}
|
|
79
83
|
this.startWatchingScope(scopeName, sourceDirectory);
|
|
80
84
|
}
|
|
81
|
-
// Start periodic update set checking
|
|
82
|
-
|
|
85
|
+
// Start periodic update set checking (unless disabled)
|
|
86
|
+
if (opts.monitorIntervalMs > 0) {
|
|
87
|
+
this.startUpdateSetMonitoring(opts.monitorIntervalMs);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
Logger_1.logger.info("Update set monitoring disabled (--noMonitoring)");
|
|
91
|
+
}
|
|
83
92
|
Logger_1.logger.success("✅ Multi-scope watch started successfully!");
|
|
84
93
|
Logger_1.logger.info("Watching for file changes across all scopes...");
|
|
85
94
|
Logger_1.logger.info("Press Ctrl+C to stop watching\n");
|
|
@@ -105,19 +114,27 @@ class MultiScopeWatcherManager {
|
|
|
105
114
|
pushQueue: [],
|
|
106
115
|
sourceDirectory: sourceDirectory
|
|
107
116
|
};
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
117
|
+
// Initialize global debounce once (shared across all scopes)
|
|
118
|
+
if (!this.globalProcessQueue) {
|
|
119
|
+
this.globalProcessQueue = (0, lodash_1.debounce)(async () => {
|
|
120
|
+
await this.processAllPendingScopes();
|
|
121
|
+
}, DEBOUNCE_MS);
|
|
122
|
+
}
|
|
112
123
|
watcher.on("change", (filePath) => {
|
|
113
124
|
Logger_1.logger.info(`[${scopeName}] File changed: ${path.relative(sourceDirectory, filePath)}`);
|
|
114
125
|
scopeWatcher.pushQueue.push(filePath);
|
|
115
|
-
|
|
126
|
+
if (!this.pendingScopes.has(scopeName)) {
|
|
127
|
+
this.pendingScopes.set(scopeName, Date.now());
|
|
128
|
+
}
|
|
129
|
+
this.globalProcessQueue();
|
|
116
130
|
});
|
|
117
131
|
watcher.on("add", (filePath) => {
|
|
118
132
|
Logger_1.logger.info(`[${scopeName}] File added: ${path.relative(sourceDirectory, filePath)}`);
|
|
119
133
|
scopeWatcher.pushQueue.push(filePath);
|
|
120
|
-
|
|
134
|
+
if (!this.pendingScopes.has(scopeName)) {
|
|
135
|
+
this.pendingScopes.set(scopeName, Date.now());
|
|
136
|
+
}
|
|
137
|
+
this.globalProcessQueue();
|
|
121
138
|
});
|
|
122
139
|
watcher.on("error", (error) => {
|
|
123
140
|
Logger_1.logger.error(`[${scopeName}] Watcher error: ${error.message}`);
|
|
@@ -145,7 +162,7 @@ class MultiScopeWatcherManager {
|
|
|
145
162
|
}
|
|
146
163
|
}
|
|
147
164
|
catch (e) {
|
|
148
|
-
|
|
165
|
+
Logger_1.logger.warn(`[MultiScope] Failed to parse update set config at ${configPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
149
166
|
}
|
|
150
167
|
return {};
|
|
151
168
|
}
|
|
@@ -153,11 +170,27 @@ class MultiScopeWatcherManager {
|
|
|
153
170
|
var taskPath = path.resolve(process.cwd(), ".sinc-active-task.json");
|
|
154
171
|
try {
|
|
155
172
|
if (fs.existsSync(taskPath)) {
|
|
156
|
-
|
|
173
|
+
var parsed = JSON.parse(fs.readFileSync(taskPath, "utf8"));
|
|
174
|
+
if (!parsed.taskId || typeof parsed.taskId !== "string" || parsed.taskId.trim() === "") {
|
|
175
|
+
Logger_1.logger.error("Active task file is missing a valid taskId. Ignoring active task.");
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
if (!parsed.updateSetName || typeof parsed.updateSetName !== "string" || parsed.updateSetName.trim() === "") {
|
|
179
|
+
Logger_1.logger.error("Active task file is missing a valid updateSetName. Ignoring active task.");
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
var stat = fs.statSync(taskPath);
|
|
183
|
+
var ageMs = Date.now() - stat.mtimeMs;
|
|
184
|
+
var ageDays = Math.floor(ageMs / (1000 * 60 * 60 * 24));
|
|
185
|
+
if (ageDays >= 7) {
|
|
186
|
+
var taskName = parsed.taskName || parsed.taskId;
|
|
187
|
+
Logger_1.logger.warn("Active task " + taskName + " was selected " + ageDays + " days ago. Run sinc task clear if you have moved on.");
|
|
188
|
+
}
|
|
189
|
+
return parsed;
|
|
157
190
|
}
|
|
158
191
|
}
|
|
159
192
|
catch (e) {
|
|
160
|
-
|
|
193
|
+
Logger_1.logger.warn(`Failed to parse active task file: ${e instanceof Error ? e.message : String(e)}`);
|
|
161
194
|
}
|
|
162
195
|
return null;
|
|
163
196
|
}
|
|
@@ -172,12 +205,50 @@ class MultiScopeWatcherManager {
|
|
|
172
205
|
}
|
|
173
206
|
var activeTask = this.readActiveTask();
|
|
174
207
|
if (!activeTask) {
|
|
175
|
-
|
|
208
|
+
// No active task — try to resolve the current update set from the ServiceNow
|
|
209
|
+
// session so pushWithUpdateSet can be used instead of the fallback updateRecord.
|
|
210
|
+
try {
|
|
211
|
+
var { defaultClient } = await Promise.resolve().then(() => __importStar(require("./snClient")));
|
|
212
|
+
var sessionClient = defaultClient();
|
|
213
|
+
var curResp = await sessionClient.getCurrentUpdateSet(scopeName);
|
|
214
|
+
var curData = curResp.data;
|
|
215
|
+
if (curData && curData.result) {
|
|
216
|
+
curData = curData.result;
|
|
217
|
+
}
|
|
218
|
+
var curSysId = curData && curData.sysId ? curData.sysId : null;
|
|
219
|
+
var curName = curData && curData.name ? curData.name : null;
|
|
220
|
+
if (curSysId && curName) {
|
|
221
|
+
var isDefault = curName === "Default" || curName.toLowerCase().indexOf("default") !== -1;
|
|
222
|
+
if (isDefault) {
|
|
223
|
+
Logger_1.logger.warn(`[${scopeName}] No update set configured and current update set is Default. ` +
|
|
224
|
+
`Use sinc createUpdateSet or activate a task in the dashboard.`);
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
// Use the session's current non-Default update set
|
|
228
|
+
config = this.getUpdateSetConfig();
|
|
229
|
+
config[scopeName] = { sys_id: curSysId, name: curName };
|
|
230
|
+
this.saveUpdateSetConfig(config);
|
|
231
|
+
Logger_1.logger.info(`[${scopeName}] Using session update set: ${curName}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
Logger_1.logger.warn(`[${scopeName}] No update set configured for scope ${scopeName}. ` +
|
|
236
|
+
`Use sinc createUpdateSet or activate a task in the dashboard.`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch (queryErr) {
|
|
240
|
+
Logger_1.logger.warn(`[${scopeName}] No update set configured and could not query current update set. ` +
|
|
241
|
+
`Changes will use direct Table API. Use sinc createUpdateSet or activate a task in the dashboard.`);
|
|
242
|
+
}
|
|
176
243
|
return;
|
|
177
244
|
}
|
|
178
245
|
var taskId = activeTask.taskId;
|
|
179
246
|
var updateSetName = activeTask.updateSetName;
|
|
180
247
|
var description = activeTask.description || "";
|
|
248
|
+
if (!taskId || taskId.trim() === "") {
|
|
249
|
+
Logger_1.logger.error(`[${scopeName}] Active task has an empty taskId. Skipping update set lookup.`);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
181
252
|
Logger_1.logger.info(`[${scopeName}] No update set found — auto-creating for task CU-${taskId}...`);
|
|
182
253
|
try {
|
|
183
254
|
var { defaultClient, unwrapSNResponse } = await Promise.resolve().then(() => __importStar(require("./snClient")));
|
|
@@ -185,7 +256,11 @@ class MultiScopeWatcherManager {
|
|
|
185
256
|
// Switch to the target scope and resolve its sys_id for update set creation
|
|
186
257
|
await client.changeScope(scopeName);
|
|
187
258
|
var scopeIdResult = await unwrapSNResponse(client.getScopeId(scopeName));
|
|
188
|
-
var scopeSysId = scopeIdResult.length > 0 ? scopeIdResult[0].sys_id : undefined;
|
|
259
|
+
var scopeSysId = scopeIdResult && scopeIdResult.length > 0 ? scopeIdResult[0].sys_id : undefined;
|
|
260
|
+
if (!scopeSysId) {
|
|
261
|
+
Logger_1.logger.error(`[${scopeName}] Scope "${scopeName}" not found on the instance. Cannot create update set for an invalid scope.`);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
189
264
|
// Search for an existing update set matching this task in this scope
|
|
190
265
|
var query = "application.scope=" + scopeName +
|
|
191
266
|
"^nameLIKECU-" + taskId +
|
|
@@ -210,17 +285,48 @@ class MultiScopeWatcherManager {
|
|
|
210
285
|
updateSet = { sys_id: createResp.sys_id, name: updateSetName };
|
|
211
286
|
Logger_1.logger.info(`[${scopeName}] Auto-created update set: ${updateSet.name}`);
|
|
212
287
|
}
|
|
213
|
-
// Switch the active update set on the instance
|
|
288
|
+
// Switch the active update set on the instance and verify
|
|
214
289
|
try {
|
|
215
290
|
await client.changeUpdateSet({ sysId: updateSet.sys_id });
|
|
291
|
+
// Verify the switch was successful
|
|
292
|
+
var verifyResp = await client.getCurrentUpdateSet(scopeName);
|
|
293
|
+
var verifyResult = verifyResp.data;
|
|
294
|
+
if (verifyResult && verifyResult.result) {
|
|
295
|
+
verifyResult = verifyResult.result;
|
|
296
|
+
}
|
|
297
|
+
var currentSysId = verifyResult && verifyResult.sysId ? verifyResult.sysId : null;
|
|
298
|
+
if (currentSysId !== updateSet.sys_id) {
|
|
299
|
+
// Retry once
|
|
300
|
+
Logger_1.logger.warn(`[${scopeName}] Update set verification failed, retrying switch...`);
|
|
301
|
+
await client.changeUpdateSet({ sysId: updateSet.sys_id });
|
|
302
|
+
var retryResp = await client.getCurrentUpdateSet(scopeName);
|
|
303
|
+
var retryResult = retryResp.data;
|
|
304
|
+
if (retryResult && retryResult.result) {
|
|
305
|
+
retryResult = retryResult.result;
|
|
306
|
+
}
|
|
307
|
+
var retrySysId = retryResult && retryResult.sysId ? retryResult.sysId : null;
|
|
308
|
+
if (retrySysId !== updateSet.sys_id) {
|
|
309
|
+
var actualName = retryResult && retryResult.name ? retryResult.name : "unknown";
|
|
310
|
+
Logger_1.logger.error(`[${scopeName}] Update set ${updateSet.name} was created but could not be activated. Current update set is ${actualName}.`);
|
|
311
|
+
throw new Error(`Update set ${updateSet.name} could not be activated for scope ${scopeName}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
Logger_1.logger.debug(`[${scopeName}] Update set switch verified: ${updateSet.name}`);
|
|
216
315
|
}
|
|
217
316
|
catch (changeErr) {
|
|
218
317
|
Logger_1.logger.warn(`[${scopeName}] Could not auto-switch update set on instance`);
|
|
219
318
|
}
|
|
220
|
-
// Persist the mapping
|
|
319
|
+
// Persist the mapping (serialized under scopeLock — safe from concurrent writes)
|
|
221
320
|
config = this.getUpdateSetConfig(); // Re-read in case another scope wrote
|
|
222
321
|
config[scopeName] = { sys_id: updateSet.sys_id, name: updateSet.name };
|
|
223
322
|
this.saveUpdateSetConfig(config);
|
|
323
|
+
// Verify the write persisted correctly
|
|
324
|
+
var verifiedConfig = this.getUpdateSetConfig();
|
|
325
|
+
if (!verifiedConfig[scopeName] || verifiedConfig[scopeName].sys_id !== updateSet.sys_id) {
|
|
326
|
+
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])}`);
|
|
327
|
+
throw new Error(`Update set config write verification failed for scope ${scopeName}`);
|
|
328
|
+
}
|
|
329
|
+
Logger_1.logger.debug(`[${scopeName}] Update set config verified: ${updateSet.name} (${updateSet.sys_id})`);
|
|
224
330
|
// Also update the active task file
|
|
225
331
|
if (activeTask) {
|
|
226
332
|
if (!activeTask.scopes) {
|
|
@@ -236,6 +342,20 @@ class MultiScopeWatcherManager {
|
|
|
236
342
|
Logger_1.logger.warn(`[${scopeName}] Pushing without update set routing.`);
|
|
237
343
|
}
|
|
238
344
|
}
|
|
345
|
+
async processAllPendingScopes() {
|
|
346
|
+
// Sort scopes by first file change timestamp (FIFO)
|
|
347
|
+
var sorted = Array.from(this.pendingScopes.entries()).sort(function (a, b) {
|
|
348
|
+
return a[1] - b[1];
|
|
349
|
+
});
|
|
350
|
+
this.pendingScopes.clear();
|
|
351
|
+
for (var i = 0; i < sorted.length; i++) {
|
|
352
|
+
var scopeName = sorted[i][0];
|
|
353
|
+
var scopeWatcher = this.scopeWatchers.get(scopeName);
|
|
354
|
+
if (scopeWatcher && scopeWatcher.pushQueue.length > 0) {
|
|
355
|
+
await this.processScopeQueue(scopeWatcher);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
239
359
|
async processScopeQueue(scopeWatcher) {
|
|
240
360
|
if (scopeWatcher.pushQueue.length === 0)
|
|
241
361
|
return;
|
|
@@ -250,12 +370,29 @@ class MultiScopeWatcherManager {
|
|
|
250
370
|
await this.ensureUpdateSetForScope(scopeWatcher.scope);
|
|
251
371
|
// Load the manifest for this specific scope
|
|
252
372
|
await this.loadScopeManifest(scopeWatcher.scope, scopeWatcher.sourceDirectory);
|
|
253
|
-
// Process the files
|
|
254
|
-
const fileContexts =
|
|
255
|
-
|
|
256
|
-
|
|
373
|
+
// Process the files — track skip reasons for summary
|
|
374
|
+
const fileContexts = [];
|
|
375
|
+
const skippedFiles = [];
|
|
376
|
+
toProcess.forEach(function (filePath) {
|
|
377
|
+
var result = (0, FileUtils_1.getFileContextWithSkipReason)(filePath);
|
|
378
|
+
if (result.context) {
|
|
379
|
+
// Scope validation: ensure the record's scope matches the watcher's scope
|
|
380
|
+
if (result.context.scope && result.context.scope !== scopeWatcher.scope) {
|
|
381
|
+
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.`);
|
|
382
|
+
skippedFiles.push({ filePath: filePath, reason: `scope mismatch (record: ${result.context.scope}, watcher: ${scopeWatcher.scope})` });
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
fileContexts.push(result.context);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
var reason = result.skipReason || "unknown";
|
|
390
|
+
Logger_1.logger.warn(`[${scopeWatcher.scope}] Skipped: ${filePath} (${reason})`);
|
|
391
|
+
skippedFiles.push({ filePath: filePath, reason: reason });
|
|
392
|
+
}
|
|
393
|
+
});
|
|
257
394
|
if (fileContexts.length === 0) {
|
|
258
|
-
Logger_1.logger.warn(`[${scopeWatcher.scope}] No valid file contexts found
|
|
395
|
+
Logger_1.logger.warn(`[${scopeWatcher.scope}] No valid file contexts found. ${skippedFiles.length} file(s) skipped.`);
|
|
259
396
|
return;
|
|
260
397
|
}
|
|
261
398
|
const buildables = (0, appUtils_1.groupAppFiles)(fileContexts);
|
|
@@ -268,7 +405,15 @@ class MultiScopeWatcherManager {
|
|
|
268
405
|
}
|
|
269
406
|
}
|
|
270
407
|
});
|
|
271
|
-
|
|
408
|
+
// Push summary
|
|
409
|
+
var pushedCount = updateResults.filter(function (r) { return r.success; }).length;
|
|
410
|
+
var totalCount = toProcess.length;
|
|
411
|
+
var summaryParts = [`Pushed ${pushedCount}/${totalCount} files to ${scopeWatcher.scope}.`];
|
|
412
|
+
if (skippedFiles.length > 0) {
|
|
413
|
+
var skippedList = skippedFiles.map(function (s) { return `${s.filePath} (${s.reason})`; }).join(", ");
|
|
414
|
+
summaryParts.push(`${skippedFiles.length} files skipped: [${skippedList}]`);
|
|
415
|
+
}
|
|
416
|
+
Logger_1.logger.info(`[${scopeWatcher.scope}] ${summaryParts.join(" ")}`);
|
|
272
417
|
});
|
|
273
418
|
}
|
|
274
419
|
catch (error) {
|
|
@@ -332,91 +477,68 @@ class MultiScopeWatcherManager {
|
|
|
332
477
|
}
|
|
333
478
|
}
|
|
334
479
|
async switchToScope(scopeName) {
|
|
480
|
+
// Skip if already on the correct scope
|
|
481
|
+
if (this.cachedScope === scopeName) {
|
|
482
|
+
Logger_1.logger.debug(`Already on scope ${scopeName}, skipping switch`);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
335
485
|
try {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
//
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
if (!userResponse || !Array.isArray(userResponse) || userResponse.length === 0 || !userResponse[0].sys_id) {
|
|
346
|
-
throw new Error("Could not get user sys_id");
|
|
347
|
-
}
|
|
348
|
-
// Get current app preference
|
|
349
|
-
const prefResponse = await unwrapSNResponse(client.getCurrentAppUserPrefSysId(userResponse[0].sys_id));
|
|
350
|
-
if (prefResponse && Array.isArray(prefResponse) && prefResponse.length > 0 && prefResponse[0].sys_id) {
|
|
351
|
-
// Update existing preference
|
|
352
|
-
await client.updateCurrentAppUserPref(scopeResponse[0].sys_id, prefResponse[0].sys_id);
|
|
353
|
-
}
|
|
354
|
-
else {
|
|
355
|
-
// Create new preference
|
|
356
|
-
await client.createCurrentAppUserPref(scopeResponse[0].sys_id, userResponse[0].sys_id);
|
|
357
|
-
}
|
|
486
|
+
var { defaultClient } = await Promise.resolve().then(() => __importStar(require("./snClient")));
|
|
487
|
+
var client = defaultClient();
|
|
488
|
+
// Use the Claude REST API changeScope endpoint to switch the session scope.
|
|
489
|
+
// This uses gs.setCurrentApplicationId() server-side, which correctly changes
|
|
490
|
+
// the REST API session scope. The previous approach used user preference updates
|
|
491
|
+
// (apps.current_app) which only writes a DB record and does NOT affect the
|
|
492
|
+
// active REST session — causing updateRecord() to operate in the wrong scope.
|
|
493
|
+
await client.changeScope(scopeName);
|
|
494
|
+
this.cachedScope = scopeName;
|
|
358
495
|
Logger_1.logger.debug(`Switched to scope: ${scopeName}`);
|
|
359
496
|
}
|
|
360
497
|
catch (error) {
|
|
498
|
+
// Invalidate cache on failure
|
|
499
|
+
this.cachedScope = null;
|
|
361
500
|
Logger_1.logger.error(`Failed to switch to scope ${scopeName}: ${error}`);
|
|
362
501
|
throw error;
|
|
363
502
|
}
|
|
364
503
|
}
|
|
365
|
-
async startUpdateSetMonitoring() {
|
|
504
|
+
async startUpdateSetMonitoring(intervalMs) {
|
|
366
505
|
// Check update sets immediately on start
|
|
367
506
|
await this.checkAllUpdateSets();
|
|
368
|
-
|
|
507
|
+
Logger_1.logger.info("Update set monitoring interval: " + Math.round(intervalMs / 1000) + "s");
|
|
369
508
|
this.updateSetCheckInterval = setInterval(async () => {
|
|
370
509
|
await this.checkAllUpdateSets();
|
|
371
|
-
},
|
|
510
|
+
}, intervalMs);
|
|
372
511
|
}
|
|
373
512
|
async checkAllUpdateSets() {
|
|
513
|
+
// Budget: monitoring should not consume more than 5% of the 20 RPS rate limit
|
|
514
|
+
// per cycle. That gives us ~1 request/second budget. Each scope check uses
|
|
515
|
+
// ~6 API calls (switchToScope ~3, getUserSysId ~1, pref ~1, details ~1).
|
|
516
|
+
// To stay within budget we use the local update set config file instead of
|
|
517
|
+
// querying ServiceNow for each scope — 0 API calls when config is present.
|
|
374
518
|
try {
|
|
375
|
-
const { defaultClient, unwrapSNResponse } = await Promise.resolve().then(() => __importStar(require("./snClient")));
|
|
376
|
-
const client = defaultClient();
|
|
377
519
|
const config = ConfigManager.getConfig();
|
|
378
520
|
if (!config.scopes)
|
|
379
521
|
return;
|
|
380
522
|
const scopes = Object.keys(config.scopes);
|
|
523
|
+
var updateSetConfig = this.getUpdateSetConfig();
|
|
381
524
|
Logger_1.logger.info("\n" + "=".repeat(60));
|
|
382
525
|
Logger_1.logger.info("Update Set Status Check");
|
|
383
526
|
Logger_1.logger.info("=".repeat(60));
|
|
384
|
-
for (
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
if (
|
|
391
|
-
Logger_1.logger.warn(
|
|
392
|
-
continue;
|
|
393
|
-
}
|
|
394
|
-
// Get current update set preference
|
|
395
|
-
const updateSetPref = await unwrapSNResponse(client.getCurrentUpdateSetUserPref(userResponse[0].sys_id));
|
|
396
|
-
if (updateSetPref && Array.isArray(updateSetPref) && updateSetPref.length > 0 && updateSetPref[0].value) {
|
|
397
|
-
// Get update set details
|
|
398
|
-
const updateSetId = updateSetPref[0].value;
|
|
399
|
-
const updateSetDetails = await this.getUpdateSetDetails(updateSetId);
|
|
400
|
-
if (updateSetDetails) {
|
|
401
|
-
const isDefault = updateSetDetails.name === "Default" ||
|
|
402
|
-
updateSetDetails.name.toLowerCase().includes("default");
|
|
403
|
-
if (isDefault) {
|
|
404
|
-
Logger_1.logger.warn(`⚠️ [${scopeName}] Currently in DEFAULT update set!`);
|
|
405
|
-
}
|
|
406
|
-
else {
|
|
407
|
-
Logger_1.logger.info(`✅ [${scopeName}] Update Set: ${updateSetDetails.name}`);
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
else {
|
|
411
|
-
Logger_1.logger.info(`[${scopeName}] Update Set ID: ${updateSetId}`);
|
|
412
|
-
}
|
|
527
|
+
for (var i = 0; i < scopes.length; i++) {
|
|
528
|
+
var scopeName = scopes[i];
|
|
529
|
+
var mapping = updateSetConfig[scopeName];
|
|
530
|
+
if (mapping && mapping.name) {
|
|
531
|
+
var isDefault = mapping.name === "Default" ||
|
|
532
|
+
mapping.name.toLowerCase().indexOf("default") !== -1;
|
|
533
|
+
if (isDefault) {
|
|
534
|
+
Logger_1.logger.warn(`⚠️ [${scopeName}] Currently in DEFAULT update set!`);
|
|
413
535
|
}
|
|
414
536
|
else {
|
|
415
|
-
Logger_1.logger.
|
|
537
|
+
Logger_1.logger.info(`✅ [${scopeName}] Update Set: ${mapping.name}`);
|
|
416
538
|
}
|
|
417
539
|
}
|
|
418
|
-
|
|
419
|
-
Logger_1.logger.
|
|
540
|
+
else {
|
|
541
|
+
Logger_1.logger.warn(`⚠️ [${scopeName}] No update set configured`);
|
|
420
542
|
}
|
|
421
543
|
}
|
|
422
544
|
Logger_1.logger.info("=".repeat(60) + "\n");
|
|
@@ -450,7 +572,7 @@ class MultiScopeWatcherManager {
|
|
|
450
572
|
return null;
|
|
451
573
|
}
|
|
452
574
|
catch (error) {
|
|
453
|
-
Logger_1.logger.
|
|
575
|
+
Logger_1.logger.warn(`Could not get update set details: ${error}`);
|
|
454
576
|
return null;
|
|
455
577
|
}
|
|
456
578
|
}
|
|
@@ -461,6 +583,12 @@ class MultiScopeWatcherManager {
|
|
|
461
583
|
scopeWatcher.watcher.close();
|
|
462
584
|
}
|
|
463
585
|
this.scopeWatchers.clear();
|
|
586
|
+
// Cancel global debounce
|
|
587
|
+
if (this.globalProcessQueue) {
|
|
588
|
+
this.globalProcessQueue.cancel();
|
|
589
|
+
this.globalProcessQueue = null;
|
|
590
|
+
}
|
|
591
|
+
this.pendingScopes.clear();
|
|
464
592
|
// Stop update set monitoring
|
|
465
593
|
if (this.updateSetCheckInterval) {
|
|
466
594
|
clearInterval(this.updateSetCheckInterval);
|
|
@@ -470,8 +598,8 @@ class MultiScopeWatcherManager {
|
|
|
470
598
|
}
|
|
471
599
|
}
|
|
472
600
|
exports.multiScopeWatcher = new MultiScopeWatcherManager();
|
|
473
|
-
function startMultiScopeWatching() {
|
|
474
|
-
return exports.multiScopeWatcher.startWatchingAllScopes();
|
|
601
|
+
function startMultiScopeWatching(options) {
|
|
602
|
+
return exports.multiScopeWatcher.startWatchingAllScopes(options);
|
|
475
603
|
}
|
|
476
604
|
function stopMultiScopeWatching() {
|
|
477
605
|
exports.multiScopeWatcher.stopWatching();
|
|
@@ -366,8 +366,10 @@ async function watchAllScopesCommand(args) {
|
|
|
366
366
|
}
|
|
367
367
|
// Import and start the multi-scope watcher
|
|
368
368
|
const { startMultiScopeWatching } = await Promise.resolve().then(() => __importStar(require("./MultiScopeWatcher")));
|
|
369
|
-
// Start watching all scopes
|
|
370
|
-
await startMultiScopeWatching(
|
|
369
|
+
// Start watching all scopes with monitoring options
|
|
370
|
+
await startMultiScopeWatching({
|
|
371
|
+
monitorIntervalMs: args.noMonitoring ? 0 : (args.monitorInterval || 120) * 1000,
|
|
372
|
+
});
|
|
371
373
|
// Keep the process running
|
|
372
374
|
process.on("SIGINT", async () => {
|
|
373
375
|
Logger_1.logger.info("\nStopping multi-scope watch...");
|
package/dist/appUtils.js
CHANGED
|
@@ -56,7 +56,7 @@ const getUpdateSetConfig = () => {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
catch (e) {
|
|
59
|
-
|
|
59
|
+
Logger_1.logger.warn(`Failed to parse update set config at ${configPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
60
60
|
}
|
|
61
61
|
return {};
|
|
62
62
|
};
|
|
@@ -318,9 +318,22 @@ const getAppFileList = async (paths) => {
|
|
|
318
318
|
const validPaths = typeof paths === "object"
|
|
319
319
|
? paths
|
|
320
320
|
: await fUtils.encodedPathsToFilePaths(paths);
|
|
321
|
-
const appFileCtxs =
|
|
322
|
-
|
|
323
|
-
.
|
|
321
|
+
const appFileCtxs = [];
|
|
322
|
+
validPaths.forEach(function (filePath) {
|
|
323
|
+
var result = fUtils.getFileContextWithSkipReason(filePath);
|
|
324
|
+
if (result.context) {
|
|
325
|
+
appFileCtxs.push(result.context);
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
var reason = result.skipReason || "unknown";
|
|
329
|
+
if (reason === "not in manifest") {
|
|
330
|
+
Logger_1.logger.info(`Skipped: ${filePath} (${reason})`);
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
Logger_1.logger.warn(`Skipped: ${filePath} (${reason})`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
});
|
|
324
337
|
return (0, exports.groupAppFiles)(appFileCtxs);
|
|
325
338
|
};
|
|
326
339
|
exports.getAppFileList = getAppFileList;
|
|
@@ -349,21 +362,19 @@ const buildRec = async (rec) => {
|
|
|
349
362
|
builtRec,
|
|
350
363
|
};
|
|
351
364
|
};
|
|
352
|
-
const pushRec = async (client, table, sysId, builtRec, summary, scope) => {
|
|
365
|
+
const pushRec = async (client, table, sysId, builtRec, summary, scope, updateSetConfig) => {
|
|
353
366
|
const recSummary = summary ?? `${table} > ${sysId}`;
|
|
354
367
|
try {
|
|
355
|
-
//
|
|
356
|
-
const
|
|
357
|
-
const updateSet = scope ?
|
|
368
|
+
// Use the batch-level config passed from pushFiles() to avoid re-reading per record
|
|
369
|
+
const config = updateSetConfig || {};
|
|
370
|
+
const updateSet = scope ? config[scope] : undefined;
|
|
358
371
|
const pushFn = updateSet
|
|
359
372
|
? () => {
|
|
360
373
|
Logger_1.logger.debug(`Pushing ${recSummary} via update set: ${updateSet.name}`);
|
|
361
374
|
return client.pushWithUpdateSet(updateSet.sys_id, table, sysId, builtRec);
|
|
362
375
|
}
|
|
363
376
|
: () => client.updateRecord(table, sysId, builtRec);
|
|
364
|
-
const pushRes = await (0, snClient_1.
|
|
365
|
-
Logger_1.logger.debug(`Failed to push ${recSummary}! Retrying with ${numTries} left...`);
|
|
366
|
-
});
|
|
377
|
+
const pushRes = await (0, snClient_1.retryOnHttpErr)(pushFn, recSummary);
|
|
367
378
|
return (0, snClient_1.processPushResponse)(pushRes, recSummary);
|
|
368
379
|
}
|
|
369
380
|
catch (e) {
|
|
@@ -398,7 +409,7 @@ const pushFiles = async (recs) => {
|
|
|
398
409
|
tick();
|
|
399
410
|
return { success: false, message: `${recSummary} : ${buildRes.message}` };
|
|
400
411
|
}
|
|
401
|
-
const pushRes = await pushRec(client, rec.table, rec.sysId, buildRes.builtRec, recSummary, scope);
|
|
412
|
+
const pushRes = await pushRec(client, rec.table, rec.sysId, buildRes.builtRec, recSummary, scope, updateSetConfig);
|
|
402
413
|
tick();
|
|
403
414
|
return pushRes;
|
|
404
415
|
});
|
package/dist/commander.js
CHANGED
|
@@ -35,6 +35,16 @@ async function initCommands() {
|
|
|
35
35
|
type: "number",
|
|
36
36
|
describe: "Dashboard port (default: DASHBOARD_PORT env or 3456)",
|
|
37
37
|
},
|
|
38
|
+
monitorInterval: {
|
|
39
|
+
type: "number",
|
|
40
|
+
default: 120,
|
|
41
|
+
describe: "Update set monitoring interval in seconds (default: 120)",
|
|
42
|
+
},
|
|
43
|
+
noMonitoring: {
|
|
44
|
+
type: "boolean",
|
|
45
|
+
default: false,
|
|
46
|
+
describe: "Disable background update set monitoring",
|
|
47
|
+
},
|
|
38
48
|
});
|
|
39
49
|
return cmdArgs;
|
|
40
50
|
}, async (args) => {
|
|
@@ -411,6 +421,13 @@ async function initCommands() {
|
|
|
411
421
|
.demandCommand(1, "Please specify a clickup subcommand");
|
|
412
422
|
}, function () {
|
|
413
423
|
/* subcommands handle execution */
|
|
424
|
+
})
|
|
425
|
+
.command("task", "Manage the active ClickUp task for update set routing", function (cmdArgs) {
|
|
426
|
+
return cmdArgs
|
|
427
|
+
.command("clear", "Clear the active task (removes .sinc-active-task.json)", sharedOptions, commands_1.taskClearCommand)
|
|
428
|
+
.demandCommand(1, "Please specify a task subcommand (e.g., clear)");
|
|
429
|
+
}, function () {
|
|
430
|
+
/* subcommands handle execution */
|
|
414
431
|
})
|
|
415
432
|
.help().argv;
|
|
416
433
|
}
|