@tenonhq/sincronia-core 0.0.73 → 0.0.76

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/dist/FileUtils.js CHANGED
@@ -138,12 +138,20 @@ const getFileExtension = (filePath) => {
138
138
  return "";
139
139
  }
140
140
  };
141
- const getBuildExt = (table, recordName, field) => {
142
- const manifest = ConfigManager.getManifest();
141
+ const getBuildExt = (table, recordName, field, scope) => {
142
+ var manifest = ConfigManager.getManifest();
143
143
  if (!manifest) {
144
144
  throw new Error("Failed to retrieve manifest");
145
145
  }
146
- const files = manifest.tables[table].records[recordName].files;
146
+ var resolvedManifest = manifest;
147
+ if (scope && ConfigManager.isMultiScopeManifest(manifest)) {
148
+ var scopeMan = ConfigManager.resolveManifestForScope(manifest, scope);
149
+ if (!scopeMan) {
150
+ throw new Error("Failed to find scope " + scope + " in manifest");
151
+ }
152
+ resolvedManifest = scopeMan;
153
+ }
154
+ const files = resolvedManifest.tables[table].records[recordName].files;
147
155
  const file = files.find((f) => f.name === field);
148
156
  if (!file) {
149
157
  throw new Error("Unable to find file");
@@ -163,11 +171,28 @@ const getFileContextFromPath = (filePath) => {
163
171
  .split(path_1.default.sep)
164
172
  .slice(-2);
165
173
  const targetField = getTargetFieldFromPath(filePath, tableName, ext);
166
- const manifest = ConfigManager.getManifest();
174
+ var manifest = ConfigManager.getManifest();
167
175
  if (!manifest) {
168
176
  throw new Error("No manifest has been loaded!");
169
177
  }
170
- const { tables, scope } = manifest;
178
+ var scope;
179
+ var tables;
180
+ if (ConfigManager.isMultiScopeManifest(manifest)) {
181
+ var detectedScope = ConfigManager.resolveScopeFromPath(filePath);
182
+ if (!detectedScope) {
183
+ return undefined;
184
+ }
185
+ var scopeMan = ConfigManager.resolveManifestForScope(manifest, detectedScope);
186
+ if (!scopeMan) {
187
+ return undefined;
188
+ }
189
+ scope = scopeMan.scope || detectedScope;
190
+ tables = scopeMan.tables;
191
+ }
192
+ else {
193
+ scope = manifest.scope;
194
+ tables = manifest.tables;
195
+ }
171
196
  try {
172
197
  const { records } = tables[tableName];
173
198
  const record = records[recordName];
@@ -41,6 +41,7 @@ exports.watchAllScopesCommand = watchAllScopesCommand;
41
41
  const Logger_1 = require("./Logger");
42
42
  const FileLogger_1 = require("./FileLogger");
43
43
  const ConfigManager = __importStar(require("./config"));
44
+ const AppUtils = __importStar(require("./appUtils"));
44
45
  const wizard_1 = require("./wizard");
45
46
  const snClient_1 = require("./snClient");
46
47
  const commands_1 = require("./commands");
@@ -193,6 +194,8 @@ async function processScope(scopeName, scopeConfig, apiDelay = 0) {
193
194
  else {
194
195
  Logger_1.logger.warn("No _tables whitelist defined — writing ALL tables for " + scopeName);
195
196
  }
197
+ // Normalize record keys from sys_id to display name
198
+ AppUtils.normalizeManifestKeys(manifest);
196
199
  // Build the missing files structure from the filtered manifest
197
200
  var manifestTables = manifest.tables || {};
198
201
  var allMissingFiles = {};
package/dist/appUtils.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.checkScope = exports.createAndAssignUpdateSet = exports.swapScope = exports.buildFiles = exports.summarizeRecord = exports.pushFiles = exports.getAppFileList = exports.groupAppFiles = exports.processMissingFiles = exports.findMissingFiles = exports.syncManifest = exports.processManifest = void 0;
39
+ exports.createAndAssignUpdateSet = exports.swapScope = exports.buildFiles = exports.summarizeRecord = exports.pushFiles = exports.getAppFileList = exports.groupAppFiles = exports.processMissingFiles = exports.findMissingFiles = exports.syncManifest = exports.processManifest = exports.normalizeManifestKeys = void 0;
40
40
  const path_1 = __importDefault(require("path"));
41
41
  const fs_1 = __importDefault(require("fs"));
42
42
  const progress_1 = __importDefault(require("progress"));
@@ -113,6 +113,34 @@ const processTablesInManifest = async (tables, forceWrite, sourcePath, onRecordP
113
113
  return processRecsInManTable(path_1.default.join(basePath, tableName), tables[tableName], forceWrite, onRecordProcessed);
114
114
  });
115
115
  };
116
+ /**
117
+ * Re-keys manifest records from sys_id to record.name (display value).
118
+ * Some ServiceNow tables return records keyed by sys_id instead of display name.
119
+ * This ensures consistent naming for directories and manifest lookups.
120
+ */
121
+ const normalizeManifestKeys = (manifest) => {
122
+ var tables = manifest.tables || {};
123
+ var tableNames = Object.keys(tables);
124
+ for (var i = 0; i < tableNames.length; i++) {
125
+ var tableName = tableNames[i];
126
+ var records = tables[tableName].records || {};
127
+ var recordKeys = Object.keys(records);
128
+ var normalized = {};
129
+ for (var j = 0; j < recordKeys.length; j++) {
130
+ var key = recordKeys[j];
131
+ var record = records[key];
132
+ var displayKey = record.name || key;
133
+ // Handle duplicate display names by appending sys_id suffix
134
+ if (normalized[displayKey]) {
135
+ displayKey = displayKey + " (" + record.sys_id.substring(0, 8) + ")";
136
+ }
137
+ normalized[displayKey] = record;
138
+ }
139
+ tables[tableName].records = normalized;
140
+ }
141
+ return manifest;
142
+ };
143
+ exports.normalizeManifestKeys = normalizeManifestKeys;
116
144
  const processManifest = async (manifest, forceWrite = false, sourcePath) => {
117
145
  const tableCount = Object.keys(manifest.tables).length;
118
146
  FileLogger_1.fileLogger.debug("Processing manifest: " + (manifest.scope || "legacy") + " (" + tableCount + " tables)");
@@ -142,20 +170,20 @@ const syncManifest = async (scope) => {
142
170
  const config = ConfigManager.getConfig();
143
171
  // Resolve scope-specific source directory
144
172
  var scopeSourcePath = ConfigManager.getSourcePathForScope(scope);
145
- const newManifest = await (0, snClient_1.unwrapSNResponse)(client.getManifest(scope, config));
173
+ const newManifest = (0, exports.normalizeManifestKeys)(await (0, snClient_1.unwrapSNResponse)(client.getManifest(scope, config)));
146
174
  const refreshTableCount = Object.keys(newManifest.tables).length;
147
175
  FileLogger_1.fileLogger.debug("Refreshed manifest for " + scope + ": " + refreshTableCount + " tables");
148
176
  await fUtils.writeScopeManifest(scope, newManifest);
149
177
  await (0, exports.processMissingFiles)(newManifest, scopeSourcePath);
150
178
  // Update the in-memory manifest for this scope
151
- if (typeof curManifest === "object" && !curManifest.tables) {
179
+ if (ConfigManager.isMultiScopeManifest(curManifest)) {
152
180
  curManifest[scope] = newManifest;
153
181
  ConfigManager.updateManifest(curManifest);
154
182
  }
155
183
  }
156
184
  else {
157
185
  // Sync all scopes if manifest has multiple scopes
158
- if (typeof curManifest === "object" && !curManifest.tables) {
186
+ if (ConfigManager.isMultiScopeManifest(curManifest)) {
159
187
  // Multiple scopes detected
160
188
  for (const scopeName of Object.keys(curManifest)) {
161
189
  await (0, exports.syncManifest)(scopeName);
@@ -422,6 +450,18 @@ const writeBuildFile = async (preBuild, buildRes, summary) => {
422
450
  const sourcePath = ConfigManager.getSourcePath();
423
451
  const buildPath = ConfigManager.getBuildPath();
424
452
  const fieldNames = Object.keys(fields);
453
+ const writePromises = fieldNames.map(async (field) => {
454
+ const fieldCtx = fields[field];
455
+ const srcFilePath = fieldCtx.filePath;
456
+ const relativePath = path_1.default.relative(sourcePath, srcFilePath);
457
+ const relPathNoExt = relativePath.split(".").slice(0, -1).join();
458
+ const buildExt = fUtils.getBuildExt(fieldCtx.tableName, fieldCtx.name, fieldCtx.targetField, fieldCtx.scope);
459
+ const relPathNewExt = `${relPathNoExt}.${buildExt}`;
460
+ const buildFilePath = path_1.default.join(buildPath, relPathNewExt);
461
+ await fUtils.createDirRecursively(path_1.default.dirname(buildFilePath));
462
+ const writeResult = await fUtils.writeFileForce(buildFilePath, buildRes.builtRec[fieldCtx.targetField]);
463
+ return writeResult;
464
+ });
425
465
  try {
426
466
  await (0, genericUtils_1.processBatched)(fieldNames, constants_1.CONCURRENCY_FILES, async function (field) {
427
467
  const fieldCtx = fields[field];
@@ -532,92 +572,3 @@ const createAndAssignUpdateSet = async (updateSetName = "", scope) => {
532
572
  };
533
573
  };
534
574
  exports.createAndAssignUpdateSet = createAndAssignUpdateSet;
535
- const checkScope = async (swap) => {
536
- try {
537
- const man = ConfigManager.getManifest();
538
- if (man) {
539
- // Detect multi-scope manifest (keys are scope names, no top-level .scope)
540
- var isMultiScope = typeof man === "object" && !man.scope && !man.tables;
541
- if (isMultiScope) {
542
- // Multi-scope: session scope just needs to match any configured scope
543
- var configuredScopes = Object.keys(man);
544
- var client = (0, snClient_1.defaultClient)();
545
- var scopeObj;
546
- try {
547
- scopeObj = await (0, snClient_1.unwrapSNResponse)(client.getCurrentScope());
548
- }
549
- catch (scopeErr) {
550
- Logger_1.logger.info("Scope check endpoint unavailable, assuming match for multi-scope project");
551
- return { match: true, sessionScope: configuredScopes[0], manifestScope: configuredScopes[0] };
552
- }
553
- var sessionScope = scopeObj.scope;
554
- var scopeMatch = configuredScopes.indexOf(sessionScope) !== -1;
555
- Logger_1.logger.info("Current scope: " + sessionScope + ", Configured scopes: " + configuredScopes.join(", "));
556
- if (scopeMatch) {
557
- return { match: true, sessionScope: sessionScope, manifestScope: sessionScope };
558
- }
559
- else if (swap && configuredScopes.length > 0) {
560
- Logger_1.logger.info("Current scope (" + sessionScope + ") not in configured scopes. Swapping to " + configuredScopes[0] + "...\n");
561
- var swappedScopeObj = await (0, exports.swapScope)(configuredScopes[0]);
562
- return {
563
- match: configuredScopes.indexOf(swappedScopeObj.scope) !== -1,
564
- sessionScope: swappedScopeObj.scope,
565
- manifestScope: configuredScopes[0],
566
- };
567
- }
568
- else {
569
- return { match: false, sessionScope: sessionScope, manifestScope: configuredScopes.join(", ") };
570
- }
571
- }
572
- // Single-scope manifest
573
- var singleClient = (0, snClient_1.defaultClient)();
574
- var singleScopeObj;
575
- try {
576
- singleScopeObj = await (0, snClient_1.unwrapSNResponse)(singleClient.getCurrentScope());
577
- }
578
- catch (scopeErr) {
579
- // getCurrentScope endpoint may not exist on this instance — skip scope check
580
- Logger_1.logger.info(`Scope check endpoint unavailable, assuming scope match for: ${man.scope}`);
581
- return {
582
- match: true,
583
- sessionScope: man.scope,
584
- manifestScope: man.scope,
585
- };
586
- }
587
- Logger_1.logger.info(`Current scope: ${singleScopeObj.scope}, Manifest scope: ${man.scope}`);
588
- if (singleScopeObj.scope === man.scope) {
589
- return {
590
- match: true,
591
- sessionScope: singleScopeObj.scope,
592
- manifestScope: man.scope,
593
- };
594
- }
595
- else if (swap) {
596
- Logger_1.logger.info(`Current scope (${singleScopeObj.scope}) does not match manifest scope (${man.scope}). Swapping...\n`);
597
- var singleSwapped = await (0, exports.swapScope)(man.scope);
598
- return {
599
- match: singleSwapped.scope === man.scope,
600
- sessionScope: singleSwapped.scope,
601
- manifestScope: man.scope,
602
- };
603
- }
604
- else {
605
- return {
606
- match: false,
607
- sessionScope: singleScopeObj.scope,
608
- manifestScope: man.scope,
609
- };
610
- }
611
- }
612
- //first time case
613
- return {
614
- match: true,
615
- sessionScope: "",
616
- manifestScope: "",
617
- };
618
- }
619
- catch (e) {
620
- throw e;
621
- }
622
- };
623
- exports.checkScope = checkScope;
package/dist/commander.js CHANGED
@@ -50,12 +50,6 @@ async function initCommands() {
50
50
  default: "",
51
51
  describe: "Specify branch to do git diff against",
52
52
  },
53
- scopeSwap: {
54
- alias: "ss",
55
- type: "boolean",
56
- default: false,
57
- describe: "Will auto-swap to the correct scope for the files being pushed",
58
- },
59
53
  updateSet: {
60
54
  alias: "us",
61
55
  type: "string",
package/dist/commands.js CHANGED
@@ -37,7 +37,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.setLogLevel = setLogLevel;
40
- exports.devCommand = devCommand;
41
40
  exports.refreshCommand = refreshCommand;
42
41
  exports.pushCommand = pushCommand;
43
42
  exports.downloadCommand = downloadCommand;
@@ -46,7 +45,6 @@ exports.buildCommand = buildCommand;
46
45
  exports.deployCommand = deployCommand;
47
46
  exports.statusCommand = statusCommand;
48
47
  const ConfigManager = __importStar(require("./config"));
49
- const Watcher_1 = require("./Watcher");
50
48
  const AppUtils = __importStar(require("./appUtils"));
51
49
  const orchestrator_1 = require("./initSystem/orchestrator");
52
50
  const Logger_1 = require("./Logger");
@@ -56,135 +54,108 @@ const snClient_1 = require("./snClient");
56
54
  const inquirer_1 = __importDefault(require("inquirer"));
57
55
  const gitUtils_1 = require("./gitUtils");
58
56
  const FileUtils_1 = require("./FileUtils");
59
- async function scopeCheck(successFunc, swapScopes = false) {
57
+ function setLogLevel(args) {
58
+ Logger_1.logger.setLogLevel(args.logLevel);
59
+ }
60
+ async function refreshCommand(args, log = true) {
61
+ setLogLevel(args);
60
62
  try {
61
- const scopeCheck = await AppUtils.checkScope(swapScopes);
62
- if (!scopeCheck.match) {
63
- (0, logMessages_1.scopeCheckMessage)(scopeCheck);
64
- // Throw exception to register this as an error
65
- throw new Error();
66
- }
67
- else {
68
- await successFunc();
69
- }
63
+ if (!log)
64
+ setLogLevel({ logLevel: "warn" });
65
+ FileLogger_1.fileLogger.debug("Syncing manifest from instance");
66
+ await AppUtils.syncManifest();
67
+ Logger_1.logger.success("Refresh complete!");
68
+ setLogLevel(args);
70
69
  }
71
70
  catch (e) {
72
- Logger_1.logger.error("Scope check failed. Verify your project configuration or run `npx sinc init`");
73
- // Throw exception to register this as an error
74
- process.exit(1);
71
+ throw e;
75
72
  }
76
73
  }
77
- function setLogLevel(args) {
78
- Logger_1.logger.setLogLevel(args.logLevel);
79
- }
80
- async function devCommand(args) {
74
+ async function pushCommand(args) {
81
75
  setLogLevel(args);
82
- await scopeCheck(async () => {
83
- (0, Watcher_1.startWatching)(ConfigManager.getSourcePath());
84
- (0, logMessages_1.devModeLog)();
85
- let refresher = () => {
86
- refreshCommand(args, false);
87
- };
88
- let interval = ConfigManager.getRefresh();
89
- if (interval && interval > 0) {
90
- Logger_1.logger.info(`Checking for new manifest files every ${interval} seconds`);
91
- setInterval(refresher, interval * 1000);
76
+ try {
77
+ const { updateSet, ci: skipPrompt, target, diff } = args;
78
+ let encodedPaths;
79
+ if (target !== undefined && target !== "")
80
+ encodedPaths = target;
81
+ else
82
+ encodedPaths = await (0, gitUtils_1.gitDiffToEncodedPaths)(diff);
83
+ const fileList = await AppUtils.getAppFileList(encodedPaths);
84
+ const instance = process.env.SN_INSTANCE || "unknown";
85
+ Logger_1.logger.info(fileList.length + " files to push to " + instance);
86
+ if (!skipPrompt) {
87
+ const targetServer = process.env.SN_INSTANCE;
88
+ if (!targetServer) {
89
+ Logger_1.logger.error("No SN_INSTANCE configured. Set it in your .env file.");
90
+ return;
91
+ }
92
+ const answers = await inquirer_1.default.prompt([
93
+ {
94
+ type: "confirm",
95
+ name: "confirmed",
96
+ message: "Pushing will overwrite code in your instance. Are you sure?",
97
+ default: false,
98
+ },
99
+ ]);
100
+ if (!answers["confirmed"])
101
+ return;
92
102
  }
93
- });
94
- }
95
- async function refreshCommand(args, log = true) {
96
- setLogLevel(args);
97
- await scopeCheck(async () => {
98
- try {
99
- if (!log)
100
- setLogLevel({ logLevel: "warn" });
101
- FileLogger_1.fileLogger.debug("Syncing manifest from instance");
102
- await AppUtils.syncManifest();
103
- Logger_1.logger.success("Refresh complete!");
104
- setLogLevel(args);
103
+ // Handle --clickup flag: resolve ClickUp task into an update set name
104
+ let resolvedUpdateSet = updateSet;
105
+ if (!resolvedUpdateSet && args.clickup) {
106
+ try {
107
+ const { resolveClickUpForPush } = await Promise.resolve().then(() => __importStar(require("./clickupPushHelper")));
108
+ resolvedUpdateSet = await resolveClickUpForPush(args.clickup);
109
+ }
110
+ catch (cuErr) {
111
+ if (cuErr instanceof Error)
112
+ Logger_1.logger.error(cuErr.message);
113
+ process.exit(1);
114
+ }
105
115
  }
106
- catch (e) {
107
- throw e;
116
+ // Extract scope from file list and auto-swap to it
117
+ var pushScope;
118
+ if (fileList.length > 0) {
119
+ var fieldKeys = Object.keys(fileList[0].fields);
120
+ if (fieldKeys.length > 0) {
121
+ pushScope = fileList[0].fields[fieldKeys[0]].scope;
122
+ }
108
123
  }
109
- });
110
- }
111
- async function pushCommand(args) {
112
- setLogLevel(args);
113
- await scopeCheck(async () => {
114
- try {
115
- const { updateSet, ci: skipPrompt, target, diff } = args;
116
- let encodedPaths;
117
- if (target !== undefined && target !== "")
118
- encodedPaths = target;
119
- else
120
- encodedPaths = await (0, gitUtils_1.gitDiffToEncodedPaths)(diff);
121
- const fileList = await AppUtils.getAppFileList(encodedPaths);
122
- const instance = process.env.SN_INSTANCE || "unknown";
123
- Logger_1.logger.info(fileList.length + " files to push to " + instance);
124
+ if (pushScope) {
125
+ try {
126
+ var client = (0, snClient_1.defaultClient)();
127
+ await client.changeScope(pushScope);
128
+ Logger_1.logger.info("Switched to scope: " + pushScope);
129
+ }
130
+ catch (scopeErr) {
131
+ Logger_1.logger.warn("Could not auto-switch scope: " + scopeErr);
132
+ }
133
+ }
134
+ // Does not create update set if updateSetName is blank
135
+ if (resolvedUpdateSet) {
124
136
  if (!skipPrompt) {
125
- const targetServer = process.env.SN_INSTANCE;
126
- if (!targetServer) {
127
- Logger_1.logger.error("No SN_INSTANCE configured. Set it in your .env file.");
128
- return;
129
- }
130
- const answers = await inquirer_1.default.prompt([
137
+ let answers = await inquirer_1.default.prompt([
131
138
  {
132
139
  type: "confirm",
133
140
  name: "confirmed",
134
- message: "Pushing will overwrite code in your instance. Are you sure?",
141
+ message: `A new Update Set "${resolvedUpdateSet}" will be created for these pushed changes. Do you want to proceed?`,
135
142
  default: false,
136
143
  },
137
144
  ]);
138
- if (!answers["confirmed"])
139
- return;
140
- }
141
- // Handle --clickup flag: resolve ClickUp task into an update set name
142
- let resolvedUpdateSet = updateSet;
143
- if (!resolvedUpdateSet && args.clickup) {
144
- try {
145
- const { resolveClickUpForPush } = await Promise.resolve().then(() => __importStar(require("./clickupPushHelper")));
146
- resolvedUpdateSet = await resolveClickUpForPush(args.clickup);
147
- }
148
- catch (cuErr) {
149
- if (cuErr instanceof Error)
150
- Logger_1.logger.error(cuErr.message);
151
- process.exit(1);
145
+ if (!answers["confirmed"]) {
146
+ process.exit(0);
152
147
  }
153
148
  }
154
- // Extract scope from file list for update set creation
155
- var pushScope;
156
- if (fileList.length > 0) {
157
- var fieldKeys = Object.keys(fileList[0].fields);
158
- if (fieldKeys.length > 0) {
159
- pushScope = fileList[0].fields[fieldKeys[0]].scope;
160
- }
161
- }
162
- // Does not create update set if updateSetName is blank
163
- if (resolvedUpdateSet) {
164
- if (!skipPrompt) {
165
- let answers = await inquirer_1.default.prompt([
166
- {
167
- type: "confirm",
168
- name: "confirmed",
169
- message: `A new Update Set "${resolvedUpdateSet}" will be created for these pushed changes. Do you want to proceed?`,
170
- default: false,
171
- },
172
- ]);
173
- if (!answers["confirmed"]) {
174
- process.exit(0);
175
- }
176
- }
177
- const newUpdateSet = await AppUtils.createAndAssignUpdateSet(resolvedUpdateSet, pushScope);
178
- Logger_1.logger.debug(`New Update Set Created(${newUpdateSet.name}) sys_id:${newUpdateSet.id}`);
179
- }
180
- const pushResults = await AppUtils.pushFiles(fileList);
181
- (0, logMessages_1.logPushResults)(pushResults);
182
- }
183
- catch (e) {
184
- Logger_1.logger.getInternalLogger().error(e);
185
- process.exit(1);
149
+ const newUpdateSet = await AppUtils.createAndAssignUpdateSet(resolvedUpdateSet, pushScope);
150
+ Logger_1.logger.debug(`New Update Set Created(${newUpdateSet.name}) sys_id:${newUpdateSet.id}`);
186
151
  }
187
- }, args.scopeSwap);
152
+ const pushResults = await AppUtils.pushFiles(fileList);
153
+ (0, logMessages_1.logPushResults)(pushResults);
154
+ }
155
+ catch (e) {
156
+ Logger_1.logger.getInternalLogger().error(e);
157
+ process.exit(1);
158
+ }
188
159
  }
189
160
  async function downloadCommand(args) {
190
161
  setLogLevel(args);
@@ -284,43 +255,72 @@ async function getDeployPaths() {
284
255
  }
285
256
  async function deployCommand(args) {
286
257
  setLogLevel(args);
287
- await scopeCheck(async () => {
288
- try {
289
- const targetServer = process.env.SN_INSTANCE || "";
290
- if (!targetServer) {
291
- Logger_1.logger.error("No SN_INSTANCE configured. Set it in your .env file.");
292
- return;
293
- }
294
- const { confirmed } = await inquirer_1.default.prompt([
295
- {
296
- type: "confirm",
297
- name: "confirmed",
298
- message: "Deploying will overwrite code in your instance. Are you sure?",
299
- default: false,
300
- },
301
- ]);
302
- if (!confirmed) {
303
- return;
304
- }
305
- const paths = await getDeployPaths();
306
- Logger_1.logger.silly(`${paths.length} paths found...`);
307
- Logger_1.logger.silly(JSON.stringify(paths, null, 2));
308
- const appFileList = await AppUtils.getAppFileList(paths);
309
- const pushResults = await AppUtils.pushFiles(appFileList);
310
- (0, logMessages_1.logPushResults)(pushResults);
258
+ try {
259
+ const targetServer = process.env.SN_INSTANCE || "";
260
+ if (!targetServer) {
261
+ Logger_1.logger.error("No SN_INSTANCE configured. Set it in your .env file.");
262
+ return;
311
263
  }
312
- catch (e) {
313
- throw e;
264
+ const { confirmed } = await inquirer_1.default.prompt([
265
+ {
266
+ type: "confirm",
267
+ name: "confirmed",
268
+ message: "Deploying will overwrite code in your instance. Are you sure?",
269
+ default: false,
270
+ },
271
+ ]);
272
+ if (!confirmed) {
273
+ return;
314
274
  }
315
- });
275
+ const paths = await getDeployPaths();
276
+ Logger_1.logger.silly(`${paths.length} paths found...`);
277
+ Logger_1.logger.silly(JSON.stringify(paths, null, 2));
278
+ const appFileList = await AppUtils.getAppFileList(paths);
279
+ // Auto-swap to scope detected from files
280
+ if (appFileList.length > 0) {
281
+ var fieldKeys = Object.keys(appFileList[0].fields);
282
+ if (fieldKeys.length > 0) {
283
+ var deployScope = appFileList[0].fields[fieldKeys[0]].scope;
284
+ if (deployScope) {
285
+ try {
286
+ var client = (0, snClient_1.defaultClient)();
287
+ await client.changeScope(deployScope);
288
+ Logger_1.logger.info("Switched to scope: " + deployScope);
289
+ }
290
+ catch (scopeErr) {
291
+ Logger_1.logger.warn("Could not auto-switch scope: " + scopeErr);
292
+ }
293
+ }
294
+ }
295
+ }
296
+ const pushResults = await AppUtils.pushFiles(appFileList);
297
+ (0, logMessages_1.logPushResults)(pushResults);
298
+ }
299
+ catch (e) {
300
+ throw e;
301
+ }
316
302
  }
317
303
  async function statusCommand() {
318
304
  try {
319
305
  const client = (0, snClient_1.defaultClient)();
306
+ var config = ConfigManager.getConfig();
320
307
  let scopeObj = await (0, snClient_1.unwrapSNResponse)(client.getCurrentScope());
321
- Logger_1.logger.info("Instance: " + (process.env.SN_INSTANCE || "not set"));
322
- Logger_1.logger.info("Scope: " + scopeObj.scope);
323
- Logger_1.logger.info("User: " + (process.env.SN_USER || "not set"));
308
+ Logger_1.logger.info("Instance: " + (process.env.SN_INSTANCE || "not set"));
309
+ Logger_1.logger.info("User: " + (process.env.SN_USER || "not set"));
310
+ Logger_1.logger.info("Active scope: " + scopeObj.scope);
311
+ if (config.scopes) {
312
+ var scopeNames = Object.keys(config.scopes);
313
+ Logger_1.logger.info("\nConfigured scopes (" + scopeNames.length + "):");
314
+ for (var i = 0; i < scopeNames.length; i++) {
315
+ var scopeName = scopeNames[i];
316
+ var scopeConf = config.scopes[scopeName];
317
+ var srcDir = (typeof scopeConf === "object" && scopeConf.sourceDirectory)
318
+ ? scopeConf.sourceDirectory
319
+ : "src/" + scopeName;
320
+ var marker = scopeName === scopeObj.scope ? " (active)" : "";
321
+ Logger_1.logger.info(" " + scopeName + marker + " — " + srcDir);
322
+ }
323
+ }
324
324
  }
325
325
  catch (e) {
326
326
  throw e;
package/dist/config.js CHANGED
@@ -54,6 +54,9 @@ exports.getRefresh = getRefresh;
54
54
  exports.getDefaultConfigFile = getDefaultConfigFile;
55
55
  exports.loadScopeManifest = loadScopeManifest;
56
56
  exports.updateManifest = updateManifest;
57
+ exports.isMultiScopeManifest = isMultiScopeManifest;
58
+ exports.resolveManifestForScope = resolveManifestForScope;
59
+ exports.resolveScopeFromPath = resolveScopeFromPath;
57
60
  exports.isDirectiveKey = isDirectiveKey;
58
61
  exports.stripDirectiveKeys = stripDirectiveKeys;
59
62
  exports.resolveConfigForScope = resolveConfigForScope;
@@ -290,6 +293,35 @@ async function loadScopeManifest(scope) {
290
293
  function updateManifest(man) {
291
294
  manifest = man;
292
295
  }
296
+ function isMultiScopeManifest(man) {
297
+ return typeof man === "object" && man !== null && !man.scope && !man.tables;
298
+ }
299
+ function resolveManifestForScope(man, scope) {
300
+ if (!isMultiScopeManifest(man)) {
301
+ if (!scope || man.scope === scope) {
302
+ return man;
303
+ }
304
+ return undefined;
305
+ }
306
+ var scopeMan = man[scope];
307
+ if (scopeMan && scopeMan.tables) {
308
+ return scopeMan;
309
+ }
310
+ return undefined;
311
+ }
312
+ function resolveScopeFromPath(filePath) {
313
+ var cfg = getConfig();
314
+ if (!cfg.scopes)
315
+ return undefined;
316
+ var scopeNames = Object.keys(cfg.scopes);
317
+ for (var i = 0; i < scopeNames.length; i++) {
318
+ var scopeSourcePath = getSourcePathForScope(scopeNames[i]);
319
+ if (filePath.indexOf(scopeSourcePath) === 0) {
320
+ return scopeNames[i];
321
+ }
322
+ }
323
+ return undefined;
324
+ }
293
325
  async function loadConfigPath(pth) {
294
326
  if (!pth) {
295
327
  pth = process.cwd();
@@ -102,7 +102,7 @@ async function resolveScope(args) {
102
102
  // Try to get scope from current manifest
103
103
  try {
104
104
  var manifest = ConfigManager.getManifest();
105
- if (manifest && manifest.scope) {
105
+ if (manifest && !ConfigManager.isMultiScopeManifest(manifest) && manifest.scope) {
106
106
  return manifest.scope;
107
107
  }
108
108
  }
@@ -57,7 +57,7 @@ async function resolveScope(args) {
57
57
  // Try to get scope from current manifest
58
58
  try {
59
59
  var manifest = ConfigManager.getManifest();
60
- if (manifest && manifest.scope) {
60
+ if (manifest && !ConfigManager.isMultiScopeManifest(manifest) && manifest.scope) {
61
61
  return manifest.scope;
62
62
  }
63
63
  }
package/dist/index.js CHANGED
File without changes
@@ -113,6 +113,31 @@ async function runLoginPhase(plugin, context) {
113
113
  return;
114
114
  // Core plugin: retry loop with specific error messages
115
115
  if (plugin.name === "core") {
116
+ // If .env already has all required credentials, offer to skip login
117
+ const hasAllCreds = context.env.SN_INSTANCE && context.env.SN_USER && context.env.SN_PASSWORD;
118
+ if (hasAllCreds) {
119
+ Logger_1.logger.info("Found existing credentials:");
120
+ Logger_1.logger.info(" Instance: " + context.env.SN_INSTANCE);
121
+ Logger_1.logger.info(" User: " + context.env.SN_USER);
122
+ Logger_1.logger.info("");
123
+ const useExisting = await inquirer_1.default.prompt([{
124
+ type: "confirm",
125
+ name: "confirmed",
126
+ message: "Use these credentials?",
127
+ default: true,
128
+ }]);
129
+ if (useExisting.confirmed) {
130
+ Logger_1.logger.info("Validating credentials...");
131
+ const result = await (0, corePlugin_1.validateCoreLogin)(context);
132
+ if (result === true) {
133
+ Logger_1.logger.success(chalk_1.default.green("✓ Connected to " + context.env.SN_INSTANCE));
134
+ return;
135
+ }
136
+ Logger_1.logger.error(chalk_1.default.red("✗ " + result));
137
+ Logger_1.logger.info("Please re-enter your credentials.");
138
+ context.env.SN_PASSWORD = "";
139
+ }
140
+ }
116
141
  while (true) {
117
142
  await collectLoginHooks(hooks, context);
118
143
  // Run per-hook validation
@@ -173,6 +198,34 @@ async function runLoginPhase(plugin, context) {
173
198
  }
174
199
  }
175
200
  }
201
+ async function runConfigPhase(context) {
202
+ if (context.hasConfig) {
203
+ var answer = await inquirer_1.default.prompt([{
204
+ type: "list",
205
+ name: "configAction",
206
+ message: "Existing config found. Would you like to update it or use the current one?",
207
+ choices: [
208
+ { name: "Use current config", value: "keep" },
209
+ { name: "Update config", value: "update" },
210
+ ],
211
+ }]);
212
+ if (answer.configAction === "keep") {
213
+ Logger_1.logger.info(chalk_1.default.green(" ✓ Using existing sinc.config.js"));
214
+ return;
215
+ }
216
+ }
217
+ // TODO: Future config wizard steps:
218
+ // 1. Scopes — multi-select from available scopes
219
+ // 2. Tables — multi-select with search (inquirer-autocomplete)
220
+ // 3. Fields for selected tables
221
+ // 4. Special scope tables
222
+ // 5. Special scope fields for tables
223
+ Logger_1.logger.info("");
224
+ Logger_1.logger.info(chalk_1.default.magenta(" 🎬 Coming soon to a terminal near you!"));
225
+ Logger_1.logger.info(chalk_1.default.dim(" The config wizard is still in development — stay tuned."));
226
+ Logger_1.logger.info(chalk_1.default.dim(" For now, " + (context.hasConfig ? "we'll keep your current config." : "we'll set you up with the defaults.")));
227
+ Logger_1.logger.info("");
228
+ }
176
229
  async function runConfigurePhase(plugin, context) {
177
230
  const hooks = plugin.configure;
178
231
  if (!hooks || hooks.length === 0)
@@ -239,6 +292,11 @@ async function runInit(options) {
239
292
  }
240
293
  // 5. Save env vars after login
241
294
  saveEnvVars(context, selectedPlugins);
295
+ // 5.5 Config phase
296
+ Logger_1.logger.info("");
297
+ Logger_1.logger.info(chalk_1.default.bold(" ── Config " + "─".repeat(29)));
298
+ Logger_1.logger.info("");
299
+ await runConfigPhase(context);
242
300
  // 6. Configure phase
243
301
  Logger_1.logger.info("");
244
302
  Logger_1.logger.info(chalk_1.default.bold(" ── Configure " + "─".repeat(26)));
@@ -4,21 +4,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.logBuildResults = exports.logPushResults = exports.log = void 0;
7
- exports.scopeCheckMessage = scopeCheckMessage;
8
- exports.devModeLog = devModeLog;
9
7
  exports.logFilePush = logFilePush;
10
8
  exports.logDeploy = logDeploy;
11
9
  const Logger_1 = require("./Logger");
12
10
  const chalk_1 = __importDefault(require("chalk"));
13
11
  exports.log = console.log;
14
- function scopeCheckMessage(scopeCheck) {
15
- let sScope = chalk_1.default.blue(scopeCheck.sessionScope);
16
- let mScope = chalk_1.default.blue(scopeCheck.manifestScope);
17
- Logger_1.logger.error("Scope mismatch: your session is " + sScope + " but this project targets " + mScope + ". Switch scopes in ServiceNow to continue.");
18
- }
19
- function devModeLog() {
20
- Logger_1.logger.info(`Dev mode started! Watching for changes...[${chalk_1.default.red("Press CTRL-C to Stop")}]\n`);
21
- }
22
12
  function parseError(err) {
23
13
  return `${err.name}:
24
14
  ${err.message}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenonhq/sincronia-core",
3
- "version": "0.0.73",
3
+ "version": "0.0.76",
4
4
  "description": "Next-gen file syncer",
5
5
  "license": "GPL-3.0",
6
6
  "main": "./dist/index.js",
package/dist/Watcher.js DELETED
@@ -1,51 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.startWatching = startWatching;
7
- exports.stopWatching = stopWatching;
8
- const chokidar_1 = __importDefault(require("chokidar"));
9
- const logMessages_1 = require("./logMessages");
10
- const Logger_1 = require("./Logger");
11
- const lodash_1 = require("lodash");
12
- const FileUtils_1 = require("./FileUtils");
13
- const appUtils_1 = require("./appUtils");
14
- const recentEdits_1 = require("./recentEdits");
15
- const DEBOUNCE_MS = 300;
16
- let pushQueue = [];
17
- let watcher = undefined;
18
- const processQueue = (0, lodash_1.debounce)(async () => {
19
- if (pushQueue.length > 0) {
20
- //dedupe pushes
21
- const toProcess = Array.from(new Set([...pushQueue]));
22
- pushQueue = [];
23
- const fileContexts = toProcess
24
- .map(FileUtils_1.getFileContextFromPath)
25
- .filter((ctx) => !!ctx);
26
- const buildables = (0, appUtils_1.groupAppFiles)(fileContexts);
27
- const updateResults = await (0, appUtils_1.pushFiles)(buildables);
28
- updateResults.forEach((res, index) => {
29
- (0, logMessages_1.logFilePush)(fileContexts[index], res);
30
- if (res.success) {
31
- (0, recentEdits_1.writeRecentEdit)(fileContexts[index]);
32
- }
33
- });
34
- }
35
- }, DEBOUNCE_MS);
36
- function startWatching(directory) {
37
- watcher = chokidar_1.default.watch(directory);
38
- watcher.on("change", fileChanged);
39
- watcher.on("error", (error) => {
40
- Logger_1.logger.error(`Watcher error: ${error.message}`);
41
- });
42
- }
43
- async function fileChanged(path) {
44
- pushQueue.push(path);
45
- processQueue();
46
- }
47
- function stopWatching() {
48
- if (watcher) {
49
- watcher.close();
50
- }
51
- }
@@ -1,228 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- function createMockWatcher() {
7
- const handlers = {};
8
- const mock = {
9
- _handlers: handlers,
10
- on: jest.fn((event, handler) => {
11
- if (!handlers[event])
12
- handlers[event] = [];
13
- handlers[event].push(handler);
14
- return mock;
15
- }),
16
- close: jest.fn(),
17
- _emit: (event, ...args) => {
18
- (handlers[event] || []).forEach((h) => h(...args));
19
- },
20
- };
21
- return mock;
22
- }
23
- let latestMockWatcher;
24
- jest.mock("chokidar", () => ({
25
- watch: jest.fn(() => {
26
- latestMockWatcher = createMockWatcher();
27
- return latestMockWatcher;
28
- }),
29
- }));
30
- // Controllable debounce — captures fn, tests trigger manually
31
- let capturedProcessQueue;
32
- jest.mock("lodash", () => {
33
- const actual = jest.requireActual("lodash");
34
- return {
35
- ...actual,
36
- debounce: jest.fn((fn) => {
37
- capturedProcessQueue = fn;
38
- const wrapper = jest.fn((...args) => {
39
- // Don't call automatically — tests call capturedProcessQueue()
40
- });
41
- wrapper.cancel = jest.fn();
42
- wrapper.flush = jest.fn(() => fn());
43
- return wrapper;
44
- }),
45
- };
46
- });
47
- jest.mock("../FileUtils", () => ({
48
- getFileContextFromPath: jest.fn(),
49
- }));
50
- jest.mock("../appUtils", () => ({
51
- groupAppFiles: jest.fn(),
52
- pushFiles: jest.fn(),
53
- }));
54
- jest.mock("../logMessages", () => ({
55
- logFilePush: jest.fn(),
56
- }));
57
- jest.mock("../Logger", () => ({
58
- logger: {
59
- info: jest.fn(),
60
- error: jest.fn(),
61
- warn: jest.fn(),
62
- debug: jest.fn(),
63
- success: jest.fn(),
64
- },
65
- }));
66
- // --- Imports (after mocks) ---
67
- const chokidar_1 = __importDefault(require("chokidar"));
68
- const Watcher_1 = require("../Watcher");
69
- const FileUtils_1 = require("../FileUtils");
70
- const appUtils_1 = require("../appUtils");
71
- const logMessages_1 = require("../logMessages");
72
- const Logger_1 = require("../Logger");
73
- // --- Test Fixtures ---
74
- const MOCK_FILE_PATH = "/project/src/sys_script_include/TestScript/script.js";
75
- const MOCK_FILE_PATH_2 = "/project/src/sys_script_include/OtherScript/script.js";
76
- const makeFileContext = (overrides = {}) => ({
77
- filePath: MOCK_FILE_PATH,
78
- ext: ".js",
79
- sys_id: "abc123",
80
- name: "TestScript",
81
- scope: "x_test",
82
- tableName: "sys_script_include",
83
- targetField: "script",
84
- ...overrides,
85
- });
86
- const MOCK_BUILDABLE = {
87
- table: "sys_script_include",
88
- sysId: "abc123",
89
- fields: {},
90
- };
91
- const MOCK_PUSH_SUCCESS = {
92
- success: true,
93
- message: "Pushed successfully",
94
- };
95
- // --- Tests ---
96
- describe("Watcher", () => {
97
- beforeEach(() => {
98
- jest.clearAllMocks();
99
- });
100
- afterEach(() => {
101
- (0, Watcher_1.stopWatching)();
102
- });
103
- describe("startWatching", () => {
104
- it("calls chokidar.watch with the given directory", () => {
105
- (0, Watcher_1.startWatching)("/project/src");
106
- expect(chokidar_1.default.watch).toHaveBeenCalledWith("/project/src");
107
- });
108
- it("registers change event handler", () => {
109
- (0, Watcher_1.startWatching)("/project/src");
110
- expect(latestMockWatcher.on).toHaveBeenCalledWith("change", expect.any(Function));
111
- });
112
- it("registers error event handler", () => {
113
- (0, Watcher_1.startWatching)("/project/src");
114
- expect(latestMockWatcher.on).toHaveBeenCalledWith("error", expect.any(Function));
115
- });
116
- });
117
- describe("file change processing", () => {
118
- it("processes a changed file through the full pipeline", async () => {
119
- const ctx = makeFileContext();
120
- FileUtils_1.getFileContextFromPath.mockReturnValue(ctx);
121
- appUtils_1.groupAppFiles.mockReturnValue([MOCK_BUILDABLE]);
122
- appUtils_1.pushFiles.mockResolvedValue([MOCK_PUSH_SUCCESS]);
123
- (0, Watcher_1.startWatching)("/project/src");
124
- latestMockWatcher._emit("change", MOCK_FILE_PATH);
125
- // Manually trigger the debounced processQueue
126
- await capturedProcessQueue();
127
- expect(FileUtils_1.getFileContextFromPath).toHaveBeenCalledWith(MOCK_FILE_PATH, 0, [MOCK_FILE_PATH]);
128
- expect(appUtils_1.groupAppFiles).toHaveBeenCalledWith([ctx]);
129
- expect(appUtils_1.pushFiles).toHaveBeenCalledWith([MOCK_BUILDABLE]);
130
- expect(logMessages_1.logFilePush).toHaveBeenCalledWith(ctx, MOCK_PUSH_SUCCESS);
131
- });
132
- it("deduplicates multiple changes to the same file", async () => {
133
- const ctx = makeFileContext();
134
- FileUtils_1.getFileContextFromPath.mockReturnValue(ctx);
135
- appUtils_1.groupAppFiles.mockReturnValue([MOCK_BUILDABLE]);
136
- appUtils_1.pushFiles.mockResolvedValue([MOCK_PUSH_SUCCESS]);
137
- (0, Watcher_1.startWatching)("/project/src");
138
- // Emit the same file path three times before processing
139
- latestMockWatcher._emit("change", MOCK_FILE_PATH);
140
- latestMockWatcher._emit("change", MOCK_FILE_PATH);
141
- latestMockWatcher._emit("change", MOCK_FILE_PATH);
142
- await capturedProcessQueue();
143
- // Should only process the file once due to Set dedup
144
- expect(FileUtils_1.getFileContextFromPath).toHaveBeenCalledTimes(1);
145
- });
146
- it("batches changes to multiple files", async () => {
147
- const ctx1 = makeFileContext();
148
- const ctx2 = makeFileContext({
149
- filePath: MOCK_FILE_PATH_2,
150
- sys_id: "def456",
151
- name: "OtherScript",
152
- });
153
- FileUtils_1.getFileContextFromPath
154
- .mockReturnValueOnce(ctx1)
155
- .mockReturnValueOnce(ctx2);
156
- appUtils_1.groupAppFiles.mockReturnValue([MOCK_BUILDABLE]);
157
- appUtils_1.pushFiles.mockResolvedValue([MOCK_PUSH_SUCCESS]);
158
- (0, Watcher_1.startWatching)("/project/src");
159
- latestMockWatcher._emit("change", MOCK_FILE_PATH);
160
- latestMockWatcher._emit("change", MOCK_FILE_PATH_2);
161
- await capturedProcessQueue();
162
- expect(FileUtils_1.getFileContextFromPath).toHaveBeenCalledTimes(2);
163
- expect(appUtils_1.groupAppFiles).toHaveBeenCalledWith([ctx1, ctx2]);
164
- });
165
- it("filters out files where getFileContextFromPath returns undefined", async () => {
166
- const ctx = makeFileContext();
167
- FileUtils_1.getFileContextFromPath
168
- .mockReturnValueOnce(undefined)
169
- .mockReturnValueOnce(ctx);
170
- appUtils_1.groupAppFiles.mockReturnValue([MOCK_BUILDABLE]);
171
- appUtils_1.pushFiles.mockResolvedValue([MOCK_PUSH_SUCCESS]);
172
- (0, Watcher_1.startWatching)("/project/src");
173
- latestMockWatcher._emit("change", "/project/src/unknown/file.txt");
174
- latestMockWatcher._emit("change", MOCK_FILE_PATH);
175
- await capturedProcessQueue();
176
- // groupAppFiles should only receive the valid context
177
- expect(appUtils_1.groupAppFiles).toHaveBeenCalledWith([ctx]);
178
- });
179
- it("does not process when queue is empty", async () => {
180
- (0, Watcher_1.startWatching)("/project/src");
181
- // Trigger processQueue without emitting any events
182
- await capturedProcessQueue();
183
- expect(FileUtils_1.getFileContextFromPath).not.toHaveBeenCalled();
184
- expect(appUtils_1.groupAppFiles).not.toHaveBeenCalled();
185
- expect(appUtils_1.pushFiles).not.toHaveBeenCalled();
186
- });
187
- it("calls logFilePush for each result paired with its file context", async () => {
188
- const ctx1 = makeFileContext();
189
- const ctx2 = makeFileContext({
190
- filePath: MOCK_FILE_PATH_2,
191
- sys_id: "def456",
192
- name: "OtherScript",
193
- });
194
- const result1 = { success: true, message: "ok" };
195
- const result2 = { success: false, message: "failed" };
196
- FileUtils_1.getFileContextFromPath
197
- .mockReturnValueOnce(ctx1)
198
- .mockReturnValueOnce(ctx2);
199
- appUtils_1.groupAppFiles.mockReturnValue([MOCK_BUILDABLE, MOCK_BUILDABLE]);
200
- appUtils_1.pushFiles.mockResolvedValue([result1, result2]);
201
- (0, Watcher_1.startWatching)("/project/src");
202
- latestMockWatcher._emit("change", MOCK_FILE_PATH);
203
- latestMockWatcher._emit("change", MOCK_FILE_PATH_2);
204
- await capturedProcessQueue();
205
- expect(logMessages_1.logFilePush).toHaveBeenCalledTimes(2);
206
- expect(logMessages_1.logFilePush).toHaveBeenCalledWith(ctx1, result1);
207
- expect(logMessages_1.logFilePush).toHaveBeenCalledWith(ctx2, result2);
208
- });
209
- });
210
- describe("error handling", () => {
211
- it("logs error when chokidar emits error event", () => {
212
- (0, Watcher_1.startWatching)("/project/src");
213
- const error = new Error("Watch failed");
214
- latestMockWatcher._emit("error", error);
215
- expect(Logger_1.logger.error).toHaveBeenCalledWith("Watcher error: Watch failed");
216
- });
217
- });
218
- describe("stopWatching", () => {
219
- it("calls watcher.close() when watcher exists", () => {
220
- (0, Watcher_1.startWatching)("/project/src");
221
- (0, Watcher_1.stopWatching)();
222
- expect(latestMockWatcher.close).toHaveBeenCalled();
223
- });
224
- it("does not throw when called before startWatching", () => {
225
- expect(() => (0, Watcher_1.stopWatching)()).not.toThrow();
226
- });
227
- });
228
- });