@tenonhq/sincronia-core 0.0.77 → 0.0.79
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 +180 -68
- 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,12 @@ class MultiScopeWatcherManager {
|
|
|
54
54
|
scopeWatchers = new Map();
|
|
55
55
|
updateSetCheckInterval = null;
|
|
56
56
|
scopeLock = Promise.resolve();
|
|
57
|
-
|
|
57
|
+
cachedScope = null;
|
|
58
|
+
cachedUserSysId = null;
|
|
59
|
+
pendingScopes = new Map(); // scope -> first change timestamp
|
|
60
|
+
globalProcessQueue = null;
|
|
61
|
+
async startWatchingAllScopes(options) {
|
|
62
|
+
var opts = options || { monitorIntervalMs: 120000 };
|
|
58
63
|
try {
|
|
59
64
|
// Load configuration
|
|
60
65
|
await ConfigManager.loadConfigs();
|
|
@@ -78,8 +83,13 @@ class MultiScopeWatcherManager {
|
|
|
78
83
|
}
|
|
79
84
|
this.startWatchingScope(scopeName, sourceDirectory);
|
|
80
85
|
}
|
|
81
|
-
// Start periodic update set checking
|
|
82
|
-
|
|
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
|
+
}
|
|
83
93
|
Logger_1.logger.success("✅ Multi-scope watch started successfully!");
|
|
84
94
|
Logger_1.logger.info("Watching for file changes across all scopes...");
|
|
85
95
|
Logger_1.logger.info("Press Ctrl+C to stop watching\n");
|
|
@@ -105,19 +115,27 @@ class MultiScopeWatcherManager {
|
|
|
105
115
|
pushQueue: [],
|
|
106
116
|
sourceDirectory: sourceDirectory
|
|
107
117
|
};
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
+
}
|
|
112
124
|
watcher.on("change", (filePath) => {
|
|
113
125
|
Logger_1.logger.info(`[${scopeName}] File changed: ${path.relative(sourceDirectory, filePath)}`);
|
|
114
126
|
scopeWatcher.pushQueue.push(filePath);
|
|
115
|
-
|
|
127
|
+
if (!this.pendingScopes.has(scopeName)) {
|
|
128
|
+
this.pendingScopes.set(scopeName, Date.now());
|
|
129
|
+
}
|
|
130
|
+
this.globalProcessQueue();
|
|
116
131
|
});
|
|
117
132
|
watcher.on("add", (filePath) => {
|
|
118
133
|
Logger_1.logger.info(`[${scopeName}] File added: ${path.relative(sourceDirectory, filePath)}`);
|
|
119
134
|
scopeWatcher.pushQueue.push(filePath);
|
|
120
|
-
|
|
135
|
+
if (!this.pendingScopes.has(scopeName)) {
|
|
136
|
+
this.pendingScopes.set(scopeName, Date.now());
|
|
137
|
+
}
|
|
138
|
+
this.globalProcessQueue();
|
|
121
139
|
});
|
|
122
140
|
watcher.on("error", (error) => {
|
|
123
141
|
Logger_1.logger.error(`[${scopeName}] Watcher error: ${error.message}`);
|
|
@@ -145,7 +163,7 @@ class MultiScopeWatcherManager {
|
|
|
145
163
|
}
|
|
146
164
|
}
|
|
147
165
|
catch (e) {
|
|
148
|
-
|
|
166
|
+
Logger_1.logger.warn(`[MultiScope] Failed to parse update set config at ${configPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
149
167
|
}
|
|
150
168
|
return {};
|
|
151
169
|
}
|
|
@@ -153,11 +171,27 @@ class MultiScopeWatcherManager {
|
|
|
153
171
|
var taskPath = path.resolve(process.cwd(), ".sinc-active-task.json");
|
|
154
172
|
try {
|
|
155
173
|
if (fs.existsSync(taskPath)) {
|
|
156
|
-
|
|
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 sinc task clear if you have moved on.");
|
|
189
|
+
}
|
|
190
|
+
return parsed;
|
|
157
191
|
}
|
|
158
192
|
}
|
|
159
193
|
catch (e) {
|
|
160
|
-
|
|
194
|
+
Logger_1.logger.warn(`Failed to parse active task file: ${e instanceof Error ? e.message : String(e)}`);
|
|
161
195
|
}
|
|
162
196
|
return null;
|
|
163
197
|
}
|
|
@@ -172,12 +206,16 @@ class MultiScopeWatcherManager {
|
|
|
172
206
|
}
|
|
173
207
|
var activeTask = this.readActiveTask();
|
|
174
208
|
if (!activeTask) {
|
|
175
|
-
Logger_1.logger.warn(`[${scopeName}] No update set configured
|
|
209
|
+
Logger_1.logger.warn(`[${scopeName}] No update set configured for scope ${scopeName}. Changes will go to Default. Use sinc createUpdateSet or activate a task in the dashboard.`);
|
|
176
210
|
return;
|
|
177
211
|
}
|
|
178
212
|
var taskId = activeTask.taskId;
|
|
179
213
|
var updateSetName = activeTask.updateSetName;
|
|
180
214
|
var description = activeTask.description || "";
|
|
215
|
+
if (!taskId || taskId.trim() === "") {
|
|
216
|
+
Logger_1.logger.error(`[${scopeName}] Active task has an empty taskId. Skipping update set lookup.`);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
181
219
|
Logger_1.logger.info(`[${scopeName}] No update set found — auto-creating for task CU-${taskId}...`);
|
|
182
220
|
try {
|
|
183
221
|
var { defaultClient, unwrapSNResponse } = await Promise.resolve().then(() => __importStar(require("./snClient")));
|
|
@@ -185,7 +223,11 @@ class MultiScopeWatcherManager {
|
|
|
185
223
|
// Switch to the target scope and resolve its sys_id for update set creation
|
|
186
224
|
await client.changeScope(scopeName);
|
|
187
225
|
var scopeIdResult = await unwrapSNResponse(client.getScopeId(scopeName));
|
|
188
|
-
var scopeSysId = scopeIdResult.length > 0 ? scopeIdResult[0].sys_id : undefined;
|
|
226
|
+
var scopeSysId = scopeIdResult && scopeIdResult.length > 0 ? scopeIdResult[0].sys_id : undefined;
|
|
227
|
+
if (!scopeSysId) {
|
|
228
|
+
Logger_1.logger.error(`[${scopeName}] Scope "${scopeName}" not found on the instance. Cannot create update set for an invalid scope.`);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
189
231
|
// Search for an existing update set matching this task in this scope
|
|
190
232
|
var query = "application.scope=" + scopeName +
|
|
191
233
|
"^nameLIKECU-" + taskId +
|
|
@@ -210,17 +252,48 @@ class MultiScopeWatcherManager {
|
|
|
210
252
|
updateSet = { sys_id: createResp.sys_id, name: updateSetName };
|
|
211
253
|
Logger_1.logger.info(`[${scopeName}] Auto-created update set: ${updateSet.name}`);
|
|
212
254
|
}
|
|
213
|
-
// Switch the active update set on the instance
|
|
255
|
+
// Switch the active update set on the instance and verify
|
|
214
256
|
try {
|
|
215
257
|
await client.changeUpdateSet({ sysId: updateSet.sys_id });
|
|
258
|
+
// Verify the switch was successful
|
|
259
|
+
var verifyResp = await client.getCurrentUpdateSet(scopeName);
|
|
260
|
+
var verifyResult = verifyResp.data;
|
|
261
|
+
if (verifyResult && verifyResult.result) {
|
|
262
|
+
verifyResult = verifyResult.result;
|
|
263
|
+
}
|
|
264
|
+
var currentSysId = verifyResult && verifyResult.sysId ? verifyResult.sysId : null;
|
|
265
|
+
if (currentSysId !== updateSet.sys_id) {
|
|
266
|
+
// Retry once
|
|
267
|
+
Logger_1.logger.warn(`[${scopeName}] Update set verification failed, retrying switch...`);
|
|
268
|
+
await client.changeUpdateSet({ sysId: updateSet.sys_id });
|
|
269
|
+
var retryResp = await client.getCurrentUpdateSet(scopeName);
|
|
270
|
+
var retryResult = retryResp.data;
|
|
271
|
+
if (retryResult && retryResult.result) {
|
|
272
|
+
retryResult = retryResult.result;
|
|
273
|
+
}
|
|
274
|
+
var retrySysId = retryResult && retryResult.sysId ? retryResult.sysId : null;
|
|
275
|
+
if (retrySysId !== updateSet.sys_id) {
|
|
276
|
+
var actualName = retryResult && retryResult.name ? retryResult.name : "unknown";
|
|
277
|
+
Logger_1.logger.error(`[${scopeName}] Update set ${updateSet.name} was created but could not be activated. Current update set is ${actualName}.`);
|
|
278
|
+
throw new Error(`Update set ${updateSet.name} could not be activated for scope ${scopeName}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
Logger_1.logger.debug(`[${scopeName}] Update set switch verified: ${updateSet.name}`);
|
|
216
282
|
}
|
|
217
283
|
catch (changeErr) {
|
|
218
284
|
Logger_1.logger.warn(`[${scopeName}] Could not auto-switch update set on instance`);
|
|
219
285
|
}
|
|
220
|
-
// Persist the mapping
|
|
286
|
+
// Persist the mapping (serialized under scopeLock — safe from concurrent writes)
|
|
221
287
|
config = this.getUpdateSetConfig(); // Re-read in case another scope wrote
|
|
222
288
|
config[scopeName] = { sys_id: updateSet.sys_id, name: updateSet.name };
|
|
223
289
|
this.saveUpdateSetConfig(config);
|
|
290
|
+
// Verify the write persisted correctly
|
|
291
|
+
var verifiedConfig = this.getUpdateSetConfig();
|
|
292
|
+
if (!verifiedConfig[scopeName] || verifiedConfig[scopeName].sys_id !== updateSet.sys_id) {
|
|
293
|
+
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])}`);
|
|
294
|
+
throw new Error(`Update set config write verification failed for scope ${scopeName}`);
|
|
295
|
+
}
|
|
296
|
+
Logger_1.logger.debug(`[${scopeName}] Update set config verified: ${updateSet.name} (${updateSet.sys_id})`);
|
|
224
297
|
// Also update the active task file
|
|
225
298
|
if (activeTask) {
|
|
226
299
|
if (!activeTask.scopes) {
|
|
@@ -236,6 +309,20 @@ class MultiScopeWatcherManager {
|
|
|
236
309
|
Logger_1.logger.warn(`[${scopeName}] Pushing without update set routing.`);
|
|
237
310
|
}
|
|
238
311
|
}
|
|
312
|
+
async processAllPendingScopes() {
|
|
313
|
+
// Sort scopes by first file change timestamp (FIFO)
|
|
314
|
+
var sorted = Array.from(this.pendingScopes.entries()).sort(function (a, b) {
|
|
315
|
+
return a[1] - b[1];
|
|
316
|
+
});
|
|
317
|
+
this.pendingScopes.clear();
|
|
318
|
+
for (var i = 0; i < sorted.length; i++) {
|
|
319
|
+
var scopeName = sorted[i][0];
|
|
320
|
+
var scopeWatcher = this.scopeWatchers.get(scopeName);
|
|
321
|
+
if (scopeWatcher && scopeWatcher.pushQueue.length > 0) {
|
|
322
|
+
await this.processScopeQueue(scopeWatcher);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
239
326
|
async processScopeQueue(scopeWatcher) {
|
|
240
327
|
if (scopeWatcher.pushQueue.length === 0)
|
|
241
328
|
return;
|
|
@@ -250,12 +337,29 @@ class MultiScopeWatcherManager {
|
|
|
250
337
|
await this.ensureUpdateSetForScope(scopeWatcher.scope);
|
|
251
338
|
// Load the manifest for this specific scope
|
|
252
339
|
await this.loadScopeManifest(scopeWatcher.scope, scopeWatcher.sourceDirectory);
|
|
253
|
-
// Process the files
|
|
254
|
-
const fileContexts =
|
|
255
|
-
|
|
256
|
-
|
|
340
|
+
// Process the files — track skip reasons for summary
|
|
341
|
+
const fileContexts = [];
|
|
342
|
+
const skippedFiles = [];
|
|
343
|
+
toProcess.forEach(function (filePath) {
|
|
344
|
+
var result = (0, FileUtils_1.getFileContextWithSkipReason)(filePath);
|
|
345
|
+
if (result.context) {
|
|
346
|
+
// Scope validation: ensure the record's scope matches the watcher's scope
|
|
347
|
+
if (result.context.scope && result.context.scope !== scopeWatcher.scope) {
|
|
348
|
+
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.`);
|
|
349
|
+
skippedFiles.push({ filePath: filePath, reason: `scope mismatch (record: ${result.context.scope}, watcher: ${scopeWatcher.scope})` });
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
fileContexts.push(result.context);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
var reason = result.skipReason || "unknown";
|
|
357
|
+
Logger_1.logger.warn(`[${scopeWatcher.scope}] Skipped: ${filePath} (${reason})`);
|
|
358
|
+
skippedFiles.push({ filePath: filePath, reason: reason });
|
|
359
|
+
}
|
|
360
|
+
});
|
|
257
361
|
if (fileContexts.length === 0) {
|
|
258
|
-
Logger_1.logger.warn(`[${scopeWatcher.scope}] No valid file contexts found
|
|
362
|
+
Logger_1.logger.warn(`[${scopeWatcher.scope}] No valid file contexts found. ${skippedFiles.length} file(s) skipped.`);
|
|
259
363
|
return;
|
|
260
364
|
}
|
|
261
365
|
const buildables = (0, appUtils_1.groupAppFiles)(fileContexts);
|
|
@@ -268,7 +372,15 @@ class MultiScopeWatcherManager {
|
|
|
268
372
|
}
|
|
269
373
|
}
|
|
270
374
|
});
|
|
271
|
-
|
|
375
|
+
// Push summary
|
|
376
|
+
var pushedCount = updateResults.filter(function (r) { return r.success; }).length;
|
|
377
|
+
var totalCount = toProcess.length;
|
|
378
|
+
var summaryParts = [`Pushed ${pushedCount}/${totalCount} files to ${scopeWatcher.scope}.`];
|
|
379
|
+
if (skippedFiles.length > 0) {
|
|
380
|
+
var skippedList = skippedFiles.map(function (s) { return `${s.filePath} (${s.reason})`; }).join(", ");
|
|
381
|
+
summaryParts.push(`${skippedFiles.length} files skipped: [${skippedList}]`);
|
|
382
|
+
}
|
|
383
|
+
Logger_1.logger.info(`[${scopeWatcher.scope}] ${summaryParts.join(" ")}`);
|
|
272
384
|
});
|
|
273
385
|
}
|
|
274
386
|
catch (error) {
|
|
@@ -332,6 +444,11 @@ class MultiScopeWatcherManager {
|
|
|
332
444
|
}
|
|
333
445
|
}
|
|
334
446
|
async switchToScope(scopeName) {
|
|
447
|
+
// Skip if already on the correct scope
|
|
448
|
+
if (this.cachedScope === scopeName) {
|
|
449
|
+
Logger_1.logger.debug(`Already on scope ${scopeName}, skipping switch`);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
335
452
|
try {
|
|
336
453
|
const { defaultClient, unwrapSNResponse } = await Promise.resolve().then(() => __importStar(require("./snClient")));
|
|
337
454
|
const client = defaultClient();
|
|
@@ -340,83 +457,72 @@ class MultiScopeWatcherManager {
|
|
|
340
457
|
if (!scopeResponse || !Array.isArray(scopeResponse) || scopeResponse.length === 0 || !scopeResponse[0].sys_id) {
|
|
341
458
|
throw new Error(`Scope ${scopeName} not found`);
|
|
342
459
|
}
|
|
343
|
-
// Get user sys_id
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
460
|
+
// Get user sys_id (cached for the session — never changes)
|
|
461
|
+
if (!this.cachedUserSysId) {
|
|
462
|
+
const userResponse = await unwrapSNResponse(client.getUserSysId());
|
|
463
|
+
if (!userResponse || !Array.isArray(userResponse) || userResponse.length === 0 || !userResponse[0].sys_id) {
|
|
464
|
+
throw new Error("Could not get user sys_id");
|
|
465
|
+
}
|
|
466
|
+
this.cachedUserSysId = userResponse[0].sys_id;
|
|
347
467
|
}
|
|
348
468
|
// Get current app preference
|
|
349
|
-
const prefResponse = await unwrapSNResponse(client.getCurrentAppUserPrefSysId(
|
|
469
|
+
const prefResponse = await unwrapSNResponse(client.getCurrentAppUserPrefSysId(this.cachedUserSysId));
|
|
350
470
|
if (prefResponse && Array.isArray(prefResponse) && prefResponse.length > 0 && prefResponse[0].sys_id) {
|
|
351
471
|
// Update existing preference
|
|
352
472
|
await client.updateCurrentAppUserPref(scopeResponse[0].sys_id, prefResponse[0].sys_id);
|
|
353
473
|
}
|
|
354
474
|
else {
|
|
355
475
|
// Create new preference
|
|
356
|
-
await client.createCurrentAppUserPref(scopeResponse[0].sys_id,
|
|
476
|
+
await client.createCurrentAppUserPref(scopeResponse[0].sys_id, this.cachedUserSysId);
|
|
357
477
|
}
|
|
478
|
+
this.cachedScope = scopeName;
|
|
358
479
|
Logger_1.logger.debug(`Switched to scope: ${scopeName}`);
|
|
359
480
|
}
|
|
360
481
|
catch (error) {
|
|
482
|
+
// Invalidate cache on failure
|
|
483
|
+
this.cachedScope = null;
|
|
361
484
|
Logger_1.logger.error(`Failed to switch to scope ${scopeName}: ${error}`);
|
|
362
485
|
throw error;
|
|
363
486
|
}
|
|
364
487
|
}
|
|
365
|
-
async startUpdateSetMonitoring() {
|
|
488
|
+
async startUpdateSetMonitoring(intervalMs) {
|
|
366
489
|
// Check update sets immediately on start
|
|
367
490
|
await this.checkAllUpdateSets();
|
|
368
|
-
|
|
491
|
+
Logger_1.logger.info("Update set monitoring interval: " + Math.round(intervalMs / 1000) + "s");
|
|
369
492
|
this.updateSetCheckInterval = setInterval(async () => {
|
|
370
493
|
await this.checkAllUpdateSets();
|
|
371
|
-
},
|
|
494
|
+
}, intervalMs);
|
|
372
495
|
}
|
|
373
496
|
async checkAllUpdateSets() {
|
|
497
|
+
// Budget: monitoring should not consume more than 5% of the 20 RPS rate limit
|
|
498
|
+
// per cycle. That gives us ~1 request/second budget. Each scope check uses
|
|
499
|
+
// ~6 API calls (switchToScope ~3, getUserSysId ~1, pref ~1, details ~1).
|
|
500
|
+
// To stay within budget we use the local update set config file instead of
|
|
501
|
+
// querying ServiceNow for each scope — 0 API calls when config is present.
|
|
374
502
|
try {
|
|
375
|
-
const { defaultClient, unwrapSNResponse } = await Promise.resolve().then(() => __importStar(require("./snClient")));
|
|
376
|
-
const client = defaultClient();
|
|
377
503
|
const config = ConfigManager.getConfig();
|
|
378
504
|
if (!config.scopes)
|
|
379
505
|
return;
|
|
380
506
|
const scopes = Object.keys(config.scopes);
|
|
507
|
+
var updateSetConfig = this.getUpdateSetConfig();
|
|
381
508
|
Logger_1.logger.info("\n" + "=".repeat(60));
|
|
382
509
|
Logger_1.logger.info("Update Set Status Check");
|
|
383
510
|
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
|
-
}
|
|
511
|
+
for (var i = 0; i < scopes.length; i++) {
|
|
512
|
+
var scopeName = scopes[i];
|
|
513
|
+
var mapping = updateSetConfig[scopeName];
|
|
514
|
+
if (mapping && mapping.name) {
|
|
515
|
+
var isDefault = mapping.name === "Default" ||
|
|
516
|
+
mapping.name.toLowerCase().indexOf("default") !== -1;
|
|
517
|
+
if (isDefault) {
|
|
518
|
+
Logger_1.logger.warn(`⚠️ [${scopeName}] Currently in DEFAULT update set!`);
|
|
413
519
|
}
|
|
414
520
|
else {
|
|
415
|
-
Logger_1.logger.
|
|
521
|
+
Logger_1.logger.info(`✅ [${scopeName}] Update Set: ${mapping.name}`);
|
|
416
522
|
}
|
|
417
523
|
}
|
|
418
|
-
|
|
419
|
-
Logger_1.logger.
|
|
524
|
+
else {
|
|
525
|
+
Logger_1.logger.warn(`⚠️ [${scopeName}] No update set configured`);
|
|
420
526
|
}
|
|
421
527
|
}
|
|
422
528
|
Logger_1.logger.info("=".repeat(60) + "\n");
|
|
@@ -450,7 +556,7 @@ class MultiScopeWatcherManager {
|
|
|
450
556
|
return null;
|
|
451
557
|
}
|
|
452
558
|
catch (error) {
|
|
453
|
-
Logger_1.logger.
|
|
559
|
+
Logger_1.logger.warn(`Could not get update set details: ${error}`);
|
|
454
560
|
return null;
|
|
455
561
|
}
|
|
456
562
|
}
|
|
@@ -461,6 +567,12 @@ class MultiScopeWatcherManager {
|
|
|
461
567
|
scopeWatcher.watcher.close();
|
|
462
568
|
}
|
|
463
569
|
this.scopeWatchers.clear();
|
|
570
|
+
// Cancel global debounce
|
|
571
|
+
if (this.globalProcessQueue) {
|
|
572
|
+
this.globalProcessQueue.cancel();
|
|
573
|
+
this.globalProcessQueue = null;
|
|
574
|
+
}
|
|
575
|
+
this.pendingScopes.clear();
|
|
464
576
|
// Stop update set monitoring
|
|
465
577
|
if (this.updateSetCheckInterval) {
|
|
466
578
|
clearInterval(this.updateSetCheckInterval);
|
|
@@ -470,8 +582,8 @@ class MultiScopeWatcherManager {
|
|
|
470
582
|
}
|
|
471
583
|
}
|
|
472
584
|
exports.multiScopeWatcher = new MultiScopeWatcherManager();
|
|
473
|
-
function startMultiScopeWatching() {
|
|
474
|
-
return exports.multiScopeWatcher.startWatchingAllScopes();
|
|
585
|
+
function startMultiScopeWatching(options) {
|
|
586
|
+
return exports.multiScopeWatcher.startWatchingAllScopes(options);
|
|
475
587
|
}
|
|
476
588
|
function stopMultiScopeWatching() {
|
|
477
589
|
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
|
}
|