@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 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,11 @@ class MultiScopeWatcherManager {
54
54
  scopeWatchers = new Map();
55
55
  updateSetCheckInterval = null;
56
56
  scopeLock = Promise.resolve();
57
- async startWatchingAllScopes() {
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
- this.startUpdateSetMonitoring();
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
- // Create a debounced processor for this scope
109
- const processQueue = (0, lodash_1.debounce)(async () => {
110
- await this.processScopeQueue(scopeWatcher);
111
- }, DEBOUNCE_MS);
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
- processQueue();
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
- processQueue();
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
- // Ignore parse errors
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
- return JSON.parse(fs.readFileSync(taskPath, "utf8"));
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
- // Ignore parse errors
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
- Logger_1.logger.warn(`[${scopeName}] No update set configured and no active task to auto-create one. Pushing without update set routing.`);
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 = toProcess
255
- .map(FileUtils_1.getFileContextFromPath)
256
- .filter((ctx) => !!ctx);
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
- Logger_1.logger.success(`[${scopeWatcher.scope}] Successfully pushed ${updateResults.length} file(s)`);
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
- const { defaultClient, unwrapSNResponse } = await Promise.resolve().then(() => __importStar(require("./snClient")));
337
- const client = defaultClient();
338
- // Get the scope ID
339
- const scopeResponse = await unwrapSNResponse(client.getScopeId(scopeName));
340
- if (!scopeResponse || !Array.isArray(scopeResponse) || scopeResponse.length === 0 || !scopeResponse[0].sys_id) {
341
- throw new Error(`Scope ${scopeName} not found`);
342
- }
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");
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
- // Then check every 2 minutes
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
- }, 120000); // 2 minutes = 120000ms
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 (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
- }
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.warn(`⚠️ [${scopeName}] No update set selected or in DEFAULT`);
537
+ Logger_1.logger.info(`✅ [${scopeName}] Update Set: ${mapping.name}`);
416
538
  }
417
539
  }
418
- catch (error) {
419
- Logger_1.logger.error(`[${scopeName}] Error checking update set: ${error}`);
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.debug(`Could not get update set details: ${error}`);
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
- // 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
  }