@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 CHANGED
@@ -1,4 +1,4 @@
1
- # @sincronia/core
1
+ # @tenonhq/sincronia-core
2
2
 
3
3
  This module contains the core of Sincronia. It is required to use Sincronia at all.
4
4
  It can interact with other plugins after you configure them.
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 getFileContextFromPath = (filePath) => {
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 undefined;
183
+ return { skipReason: "scope not found" };
184
184
  }
185
185
  var scopeMan = ConfigManager.resolveManifestForScope(manifest, detectedScope);
186
186
  if (!scopeMan) {
187
- return undefined;
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 undefined;
202
+ return { skipReason: "not in manifest" };
203
203
  }
204
204
  return {
205
- filePath,
206
- ext,
207
- sys_id,
208
- name: recordName,
209
- scope,
210
- tableName,
211
- targetField,
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 undefined;
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
- async startWatchingAllScopes() {
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
- this.startUpdateSetMonitoring();
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
- // Create a debounced processor for this scope
109
- const processQueue = (0, lodash_1.debounce)(async () => {
110
- await this.processScopeQueue(scopeWatcher);
111
- }, DEBOUNCE_MS);
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
- processQueue();
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
- processQueue();
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
- // Ignore parse errors
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
- return JSON.parse(fs.readFileSync(taskPath, "utf8"));
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
- // Ignore parse errors
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 and no active task to auto-create one. Pushing without update set routing.`);
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 = toProcess
255
- .map(FileUtils_1.getFileContextFromPath)
256
- .filter((ctx) => !!ctx);
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
- Logger_1.logger.success(`[${scopeWatcher.scope}] Successfully pushed ${updateResults.length} file(s)`);
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
- const userResponse = await unwrapSNResponse(client.getUserSysId());
345
- if (!userResponse || !Array.isArray(userResponse) || userResponse.length === 0 || !userResponse[0].sys_id) {
346
- throw new Error("Could not get user sys_id");
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(userResponse[0].sys_id));
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, userResponse[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
- // Then check every 2 minutes
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
- }, 120000); // 2 minutes = 120000ms
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 (const scopeName of scopes) {
385
- try {
386
- // Switch to scope to check its update set
387
- await this.switchToScope(scopeName);
388
- // Get user sys_id
389
- const userResponse = await unwrapSNResponse(client.getUserSysId());
390
- if (!userResponse || !Array.isArray(userResponse) || userResponse.length === 0 || !userResponse[0].sys_id) {
391
- Logger_1.logger.warn(`[${scopeName}] Could not get user information`);
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.warn(`⚠️ [${scopeName}] No update set selected or in DEFAULT`);
521
+ Logger_1.logger.info(`✅ [${scopeName}] Update Set: ${mapping.name}`);
416
522
  }
417
523
  }
418
- catch (error) {
419
- Logger_1.logger.error(`[${scopeName}] Error checking update set: ${error}`);
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.debug(`Could not get update set details: ${error}`);
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
- // Ignore parse errors
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 = validPaths
322
- .map(fUtils.getFileContextFromPath)
323
- .filter((maybeCtx) => !!maybeCtx);
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
- // Check if an update set is configured for this scope
356
- const updateSetConfig = getUpdateSetConfig();
357
- const updateSet = scope ? updateSetConfig[scope] : undefined;
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.retryOnErr)(pushFn, constants_1.PUSH_RETRY_LIMIT, constants_1.PUSH_RETRY_WAIT, (numTries) => {
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
  }