@wdio/browserstack-service 8.47.1 → 8.48.3

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.
Files changed (68) hide show
  1. package/build/accessibility-handler.d.ts.map +1 -1
  2. package/build/accessibility-handler.js +13 -11
  3. package/build/ai-handler.d.ts.map +1 -1
  4. package/build/ai-handler.js +11 -0
  5. package/build/cli/cliLogger.d.ts +1 -0
  6. package/build/cli/cliLogger.d.ts.map +1 -1
  7. package/build/cli/cliLogger.js +23 -12
  8. package/build/cli/cliUtils.d.ts +18 -0
  9. package/build/cli/cliUtils.d.ts.map +1 -1
  10. package/build/cli/cliUtils.js +31 -1
  11. package/build/cli/frameworks/wdioMochaTestFramework.d.ts.map +1 -1
  12. package/build/cli/frameworks/wdioMochaTestFramework.js +4 -2
  13. package/build/cli/grpcClient.d.ts +19 -3
  14. package/build/cli/grpcClient.d.ts.map +1 -1
  15. package/build/cli/grpcClient.js +133 -21
  16. package/build/cli/index.d.ts +2 -0
  17. package/build/cli/index.d.ts.map +1 -1
  18. package/build/cli/index.js +52 -4
  19. package/build/cli/modules/accessibilityModule.d.ts +14 -1
  20. package/build/cli/modules/accessibilityModule.d.ts.map +1 -1
  21. package/build/cli/modules/accessibilityModule.js +113 -14
  22. package/build/constants.d.ts +28 -0
  23. package/build/constants.d.ts.map +1 -1
  24. package/build/constants.js +37 -1
  25. package/build/exitHandler.d.ts +1 -1
  26. package/build/exitHandler.d.ts.map +1 -1
  27. package/build/exitHandler.js +6 -5
  28. package/build/insights-handler.d.ts.map +1 -1
  29. package/build/insights-handler.js +33 -7
  30. package/build/instrumentation/funnelInstrumentation.d.ts +2 -2
  31. package/build/instrumentation/funnelInstrumentation.d.ts.map +1 -1
  32. package/build/instrumentation/funnelInstrumentation.js +30 -8
  33. package/build/instrumentation/performance/constants.d.ts +64 -0
  34. package/build/instrumentation/performance/constants.d.ts.map +1 -1
  35. package/build/instrumentation/performance/constants.js +73 -2
  36. package/build/instrumentation/performance/performance-tester.d.ts +11 -0
  37. package/build/instrumentation/performance/performance-tester.d.ts.map +1 -1
  38. package/build/instrumentation/performance/performance-tester.js +115 -12
  39. package/build/launcher.d.ts +1 -0
  40. package/build/launcher.d.ts.map +1 -1
  41. package/build/launcher.js +151 -6
  42. package/build/service.d.ts.map +1 -1
  43. package/build/service.js +77 -5
  44. package/build/testorchestration/apply-orchestration.d.ts +7 -0
  45. package/build/testorchestration/apply-orchestration.d.ts.map +1 -0
  46. package/build/testorchestration/apply-orchestration.js +42 -0
  47. package/build/testorchestration/helpers.d.ts +20 -0
  48. package/build/testorchestration/helpers.d.ts.map +1 -0
  49. package/build/testorchestration/helpers.js +412 -0
  50. package/build/testorchestration/request-utils.d.ts +23 -0
  51. package/build/testorchestration/request-utils.d.ts.map +1 -0
  52. package/build/testorchestration/request-utils.js +81 -0
  53. package/build/testorchestration/test-ordering-server.d.ts +33 -0
  54. package/build/testorchestration/test-ordering-server.d.ts.map +1 -0
  55. package/build/testorchestration/test-ordering-server.js +168 -0
  56. package/build/testorchestration/testorcherstrationhandler.d.ts +50 -0
  57. package/build/testorchestration/testorcherstrationhandler.d.ts.map +1 -0
  58. package/build/testorchestration/testorcherstrationhandler.js +133 -0
  59. package/build/testorchestration/testorcherstrationutils.d.ts +86 -0
  60. package/build/testorchestration/testorcherstrationutils.d.ts.map +1 -0
  61. package/build/testorchestration/testorcherstrationutils.js +379 -0
  62. package/build/types.d.ts +21 -0
  63. package/build/types.d.ts.map +1 -1
  64. package/build/util.d.ts +33 -4
  65. package/build/util.d.ts.map +1 -1
  66. package/build/util.js +112 -18
  67. package/package.json +3 -2
  68. package/tsconfig.prod.tsbuildinfo +1 -1
package/build/service.js CHANGED
@@ -8,7 +8,7 @@ import got from 'got';
8
8
  import { getBrowserDescription, getBrowserCapabilities, isBrowserstackCapability, getParentSuiteName, isBrowserstackSession, patchConsoleLogs, shouldAddServiceVersion, isTrue, normalizeTestReportingConfig, normalizeTestReportingEnvVariables } from './util.js';
9
9
  import InsightsHandler from './insights-handler.js';
10
10
  import TestReporter from './reporter.js';
11
- import { DEFAULT_OPTIONS, PERF_MEASUREMENT_ENV } from './constants.js';
11
+ import { DEFAULT_OPTIONS, NOT_ALLOWED_KEYS_IN_CAPS, PERF_MEASUREMENT_ENV } from './constants.js';
12
12
  import CrashReporter from './crash-reporter.js';
13
13
  import AccessibilityHandler from './accessibility-handler.js';
14
14
  import { BStackLogger } from './bstackLogger.js';
@@ -20,6 +20,7 @@ import { shouldProcessEventForTesthub } from './testHub/utils.js';
20
20
  import AiHandler from './ai-handler.js';
21
21
  import PerformanceTester from './instrumentation/performance/performance-tester.js';
22
22
  import * as PERFORMANCE_SDK_EVENTS from './instrumentation/performance/constants.js';
23
+ import { EVENTS } from './instrumentation/performance/constants.js';
23
24
  import { BrowserstackCLI } from './cli/index.js';
24
25
  import { TestFrameworkState } from './cli/states/testFrameworkState.js';
25
26
  import { HookState } from './cli/states/hookState.js';
@@ -94,6 +95,7 @@ export default class BrowserstackService {
94
95
  return fn(this._caps);
95
96
  }
96
97
  async beforeSession(config, capabilities) {
98
+ PerformanceTester.start(PERFORMANCE_SDK_EVENTS.DRIVER_EVENT.INIT);
97
99
  // if no user and key is specified even though a browserstack service was
98
100
  // provided set user and key with values so that the session request
99
101
  // will fail
@@ -107,13 +109,19 @@ export default class BrowserstackService {
107
109
  this._config.user = config.user;
108
110
  this._config.key = config.key;
109
111
  try {
110
- if (CLIUtils.checkCLISupportedFrameworks(this._config.framework)) {
112
+ // Detect if multi-remote and disable CLI for those sessions
113
+ if (CLIUtils.checkCLISupportedFrameworks(this._config.framework) && process.env.BROWSERSTACK_IS_MULTIREMOTE !== 'true') {
111
114
  // Connect to Browserstack CLI from worker
112
115
  await BrowserstackCLI.getInstance().bootstrap(this._options, this._config);
113
116
  // Get the nearest hub and update it in config
117
+ PerformanceTester.start(PERFORMANCE_SDK_EVENTS.EVENTS.SDK_FIND_NEAREST_HUB);
114
118
  const hubUrl = BrowserstackCLI.getInstance().getConfig().hubUrl;
115
119
  if (hubUrl) {
116
120
  this._config.hostname = new URL(hubUrl).hostname;
121
+ PerformanceTester.end(PERFORMANCE_SDK_EVENTS.EVENTS.SDK_FIND_NEAREST_HUB);
122
+ }
123
+ else {
124
+ PerformanceTester.end(PERFORMANCE_SDK_EVENTS.EVENTS.SDK_FIND_NEAREST_HUB, false, 'Hub URL not found');
117
125
  }
118
126
  }
119
127
  if (BrowserstackCLI.getInstance().isRunning()) {
@@ -121,13 +129,30 @@ export default class BrowserstackService {
121
129
  const instance = AutomationFramework.getTrackedInstance();
122
130
  const caps = AutomationFramework.getState(instance, AutomationFrameworkConstants.KEY_CAPABILITIES);
123
131
  Object.assign(capabilities, caps);
132
+ // Strip CLI-only options that BrowserStack hub doesn't accept
133
+ const bstackOptions = capabilities['bstack:options'];
134
+ if (bstackOptions && typeof bstackOptions === 'object') {
135
+ NOT_ALLOWED_KEYS_IN_CAPS.forEach(key => delete bstackOptions[key]);
136
+ }
137
+ NOT_ALLOWED_KEYS_IN_CAPS.forEach(key => delete capabilities[`browserstack.${key}`]);
124
138
  }
125
139
  }
126
140
  catch (err) {
127
141
  BStackLogger.error(`Error while connecting to Browserstack CLI: ${err}`);
128
- }
142
+ PerformanceTester.end(PERFORMANCE_SDK_EVENTS.DRIVER_EVENT.INIT, false, util.format(err));
143
+ throw err;
144
+ }
145
+ PerformanceTester.end(PERFORMANCE_SDK_EVENTS.DRIVER_EVENT.INIT);
146
+ // Start tracking device allocation - this measures the gap between beforeSession end and before start
147
+ // This captures the time WebDriverIO takes to create the remote session with BrowserStack
148
+ PerformanceTester.start(PERFORMANCE_SDK_EVENTS.EVENTS.SDK_DEVICE_ALLOCATION);
149
+ BStackLogger.debug('Device allocation tracking started - waiting for WebDriverIO to create remote session');
129
150
  }
130
151
  async before(caps, specs, browser) {
152
+ // End device allocation tracking - remote session is now created and browser object is available
153
+ PerformanceTester.end(PERFORMANCE_SDK_EVENTS.EVENTS.SDK_DEVICE_ALLOCATION, true, 'Device allocated and session created');
154
+ BStackLogger.debug('Device allocation tracking ended - remote session created successfully');
155
+ PerformanceTester.start(PERFORMANCE_SDK_EVENTS.DRIVER_EVENT.PRE_INITIALIZE);
131
156
  // added to maintain backward compatibility with webdriverIO v5
132
157
  this._browser = browser ? browser : globalThis.browser;
133
158
  PerformanceTester.browser = this._browser;
@@ -174,6 +199,28 @@ export default class BrowserstackService {
174
199
  if (BrowserstackCLI.getInstance().isRunning()) {
175
200
  await BrowserstackCLI.getInstance().getAutomationFramework().trackEvent(AutomationFrameworkState.CREATE, HookState.POST, { browser: this._browser, hubUrl: this._config.hostname });
176
201
  this._insightsHandler.setGitConfigPath();
202
+ /**
203
+ * SDK-6277: register the command/result listeners in the CLI/binary flow too,
204
+ * so the user's saveScreenshot()/takeScreenshot() command result is captured and
205
+ * forwarded for screenshot-on-failure. Without this the screenshot is dropped in v8.
206
+ * (browserCommand routes the screenshot over gRPC in CLI mode — see insights-handler.)
207
+ */
208
+ // 'client:beforeCommand' only records the command synchronously — no await needed.
209
+ this._browser.on('command', (command) => {
210
+ if (shouldProcessEventForTesthub('')) {
211
+ this._insightsHandler?.browserCommand('client:beforeCommand', Object.assign(command, { sessionId }), this._currentTest);
212
+ }
213
+ });
214
+ this._browser.on('result', (result) => {
215
+ if (shouldProcessEventForTesthub('')) {
216
+ // 'client:afterCommand' is async in CLI mode (forwards the screenshot to the
217
+ // binary over gRPC). Handle the returned promise so a failed upload is logged
218
+ // rather than dropped. (o11yClassErrorHandler also routes method errors to
219
+ // processError; this .catch makes the call-site handling explicit.)
220
+ this._insightsHandler?.browserCommand('client:afterCommand', Object.assign(result, { sessionId }), this._currentTest)?.catch((err) => BStackLogger.debug(`Error forwarding afterCommand in CLI mode: ${err}`));
221
+ }
222
+ });
223
+ PerformanceTester.end(PERFORMANCE_SDK_EVENTS.DRIVER_EVENT.PRE_INITIALIZE);
177
224
  return;
178
225
  }
179
226
  await this._insightsHandler.before();
@@ -212,7 +259,11 @@ export default class BrowserstackService {
212
259
  }
213
260
  }
214
261
  }
215
- return await this._printSessionURL();
262
+ PerformanceTester.end(PERFORMANCE_SDK_EVENTS.DRIVER_EVENT.PRE_INITIALIZE);
263
+ PerformanceTester.start(PERFORMANCE_SDK_EVENTS.DRIVER_EVENT.POST_INITIALIZE);
264
+ const result = await this._printSessionURL();
265
+ PerformanceTester.end(PERFORMANCE_SDK_EVENTS.DRIVER_EVENT.POST_INITIALIZE);
266
+ return result;
216
267
  }
217
268
  /**
218
269
  * Set the default job name at the suite level to make sure we account
@@ -295,6 +346,7 @@ export default class BrowserstackService {
295
346
  }
296
347
  async after(result) {
297
348
  PerformanceTester.start(PERFORMANCE_SDK_EVENTS.HOOK_EVENTS.AFTER);
349
+ PerformanceTester.start(PERFORMANCE_SDK_EVENTS.DRIVER_EVENT.QUIT);
298
350
  try {
299
351
  if (BrowserstackCLI.getInstance().isRunning()) {
300
352
  await BrowserstackCLI.getInstance().getAutomationFramework().trackEvent(AutomationFrameworkState.EXECUTE, HookState.POST, {});
@@ -362,12 +414,31 @@ export default class BrowserstackService {
362
414
  });
363
415
  }
364
416
  })();
417
+ // Track Listener cleanup
418
+ PerformanceTester.start(EVENTS.SDK_LISTENER_WORKER_END);
365
419
  await Listener.getInstance().onWorkerEnd();
420
+ PerformanceTester.end(EVENTS.SDK_LISTENER_WORKER_END, true);
421
+ // Track Percy teardown (only if CLI is not running)
366
422
  if (!BrowserstackCLI.getInstance().isRunning()) {
367
- await this._percyHandler?.teardown();
423
+ PerformanceTester.start(EVENTS.SDK_PERCY_TEARDOWN);
424
+ try {
425
+ await this._percyHandler?.teardown();
426
+ PerformanceTester.end(EVENTS.SDK_PERCY_TEARDOWN, true);
427
+ }
428
+ catch (error) {
429
+ PerformanceTester.end(EVENTS.SDK_PERCY_TEARDOWN, false, util.format(error));
430
+ }
368
431
  }
432
+ // Track worker data save
433
+ PerformanceTester.start(EVENTS.SDK_WORKER_SAVE_DATA);
369
434
  this.saveWorkerData();
435
+ PerformanceTester.end(EVENTS.SDK_WORKER_SAVE_DATA, true);
436
+ PerformanceTester.end(PERFORMANCE_SDK_EVENTS.DRIVER_EVENT.QUIT);
370
437
  PerformanceTester.end(PERFORMANCE_SDK_EVENTS.HOOK_EVENTS.AFTER);
438
+ // Give the performance observer time to process the last few events
439
+ // before disconnecting in stopAndGenerate()
440
+ // await new Promise(resolve => setTimeout(resolve, 100))
441
+ // Track performance report generation (this is the big operation!)
371
442
  await PerformanceTester.stopAndGenerate('performance-service.html');
372
443
  if (process.env[PERF_MEASUREMENT_ENV]) {
373
444
  PerformanceTester.calculateTimes([
@@ -387,6 +458,7 @@ export default class BrowserstackService {
387
458
  }
388
459
  catch (error) {
389
460
  BStackLogger.error(`Error in after hook: ${error}`);
461
+ PerformanceTester.end(PERFORMANCE_SDK_EVENTS.DRIVER_EVENT.QUIT, false, util.format(error));
390
462
  PerformanceTester.end(PERFORMANCE_SDK_EVENTS.HOOK_EVENTS.AFTER, false, util.format(error));
391
463
  await PerformanceTester.stopAndGenerate('performance-service.html');
392
464
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Applies test orchestration to the WebdriverIO test run
3
+ * This function is the main entry point for the orchestration integration
4
+ */
5
+ export declare function applyOrchestrationIfEnabled(specs: string[], config: Record<string, any>): Promise<string[]>;
6
+ export default applyOrchestrationIfEnabled;
7
+ //# sourceMappingURL=apply-orchestration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply-orchestration.d.ts","sourceRoot":"","sources":["../../src/testorchestration/apply-orchestration.ts"],"names":[],"mappings":"AAIA;;;GAGG;AACH,wBAAsB,2BAA2B,CAC7C,KAAK,EAAE,MAAM,EAAE,EACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC5B,OAAO,CAAC,MAAM,EAAE,CAAC,CA4CnB;AAED,eAAe,2BAA2B,CAAA"}
@@ -0,0 +1,42 @@
1
+ import { performance } from 'node:perf_hooks';
2
+ import { TestOrchestrationHandler } from './testorcherstrationhandler.js';
3
+ import { BStackLogger } from '../bstackLogger.js';
4
+ import { isValidEnabledValue } from '../util.js';
5
+ /**
6
+ * Applies test orchestration to the WebdriverIO test run
7
+ * This function is the main entry point for the orchestration integration
8
+ */
9
+ export async function applyOrchestrationIfEnabled(specs, config) {
10
+ // Initialize orchestration handler
11
+ const orchestrationHandler = TestOrchestrationHandler.getInstance(config);
12
+ if (!orchestrationHandler) {
13
+ BStackLogger.debug('Orchestration handler is not initialized. Skipping orchestration.');
14
+ return specs;
15
+ }
16
+ // Check if runSmartSelection is enabled in config
17
+ const runSmartSelectionEnabled = isValidEnabledValue(config?.testOrchestrationOptions?.runSmartSelection?.enabled);
18
+ if (!runSmartSelectionEnabled) {
19
+ BStackLogger.info('runSmartSelection is not enabled in config. Skipping orchestration.');
20
+ return specs;
21
+ }
22
+ const startTime = performance.now();
23
+ // if (orchestrationHandler.testOrderingEnabled()) {
24
+ BStackLogger.info('Test orchestration is enabled. Attempting to reorder test files.');
25
+ // Get the test files from the specs - pass them as received
26
+ const testFiles = specs;
27
+ BStackLogger.info(`Test files to be reordered: ${testFiles.join(', ')}`);
28
+ // Reorder the test files
29
+ const orderedFiles = await orchestrationHandler.reorderTestFiles(testFiles);
30
+ if (orderedFiles && orderedFiles.length > 0) {
31
+ BStackLogger.info(`Tests reordered using orchestration: ${orderedFiles.join(', ')}`);
32
+ // Return the ordered files as the new specs
33
+ orchestrationHandler.addToOrderingInstrumentationData('timeTakenToApply', Math.floor(performance.now() - startTime) // Time in milliseconds
34
+ );
35
+ return orderedFiles;
36
+ }
37
+ BStackLogger.info('No test files were reordered by orchestration.');
38
+ orchestrationHandler.addToOrderingInstrumentationData('timeTakenToApply', Math.floor(performance.now() - startTime) // Time in milliseconds
39
+ );
40
+ return specs;
41
+ }
42
+ export default applyOrchestrationIfEnabled;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Get host information for the test orchestration
3
+ */
4
+ export declare function getHostInfo(): {
5
+ hostname: string;
6
+ platform: NodeJS.Platform;
7
+ architecture: NodeJS.Architecture;
8
+ release: string;
9
+ username: string;
10
+ };
11
+ /**
12
+ * Get git metadata
13
+ */
14
+ export declare function getGitMetadata(): Record<string, any>;
15
+ /**
16
+ * Get Git metadata for AI selection
17
+ * @param multiRepoSource Array of repository paths for multi-repo setup
18
+ */
19
+ export declare function getGitMetadataForAISelection(folders?: string[]): any[];
20
+ //# sourceMappingURL=helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/testorchestration/helpers.ts"],"names":[],"mappings":"AAUA;;GAEG;AACH,wBAAgB,WAAW;;;;;;EAQ1B;AA0ED;;GAEG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAyFpD;AA8FD;;;GAGG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,GAAE,MAAM,EAAO,GAAG,GAAG,EAAE,CA4K1E"}
@@ -0,0 +1,412 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import { execSync } from 'node:child_process';
4
+ import logger from '@wdio/logger';
5
+ const log = logger('wdio-browserstack-service:helpers');
6
+ // Constants
7
+ const MAX_GIT_META_DATA_SIZE_IN_BYTES = 512 * 1024; // 512 KB
8
+ /**
9
+ * Get host information for the test orchestration
10
+ */
11
+ export function getHostInfo() {
12
+ return {
13
+ hostname: os.hostname(),
14
+ platform: process.platform,
15
+ architecture: process.arch,
16
+ release: os.release(),
17
+ username: os.userInfo().username
18
+ };
19
+ }
20
+ /**
21
+ * Format git author information
22
+ */
23
+ function gitAuthor(name, email) {
24
+ if (!name && !email) {
25
+ return '';
26
+ }
27
+ return `${name} (${email})`;
28
+ }
29
+ /**
30
+ * Get the size of a JSON object in bytes
31
+ */
32
+ function getSizeOfJsonObjectInBytes(obj) {
33
+ try {
34
+ const jsonString = JSON.stringify(obj);
35
+ return Buffer.byteLength(jsonString, 'utf8');
36
+ }
37
+ catch (e) {
38
+ log.error(`Error calculating object size: ${e}`);
39
+ return 0;
40
+ }
41
+ }
42
+ /**
43
+ * Truncate a string to reduce its size by the specified number of bytes
44
+ */
45
+ function truncateString(str, bytesToTruncate) {
46
+ if (!str || bytesToTruncate <= 0) {
47
+ return str;
48
+ }
49
+ const originalBytes = Buffer.byteLength(str, 'utf8');
50
+ const targetBytes = Math.max(0, originalBytes - bytesToTruncate);
51
+ if (targetBytes >= originalBytes) {
52
+ return str;
53
+ }
54
+ // Perform binary search to find the right truncation point
55
+ let left = 0;
56
+ let right = str.length;
57
+ while (left < right) {
58
+ const mid = Math.floor((left + right) / 2);
59
+ const truncated = str.substring(0, mid);
60
+ const bytes = Buffer.byteLength(truncated, 'utf8');
61
+ if (bytes <= targetBytes) {
62
+ left = mid + 1;
63
+ }
64
+ else {
65
+ right = mid;
66
+ }
67
+ }
68
+ return str.substring(0, left - 1) + '...';
69
+ }
70
+ /**
71
+ * Check and truncate VCS info if needed
72
+ */
73
+ function checkAndTruncateVcsInfo(gitMetaData) {
74
+ const gitMetaDataSizeInBytes = getSizeOfJsonObjectInBytes(gitMetaData);
75
+ if (gitMetaDataSizeInBytes && gitMetaDataSizeInBytes > MAX_GIT_META_DATA_SIZE_IN_BYTES) {
76
+ const truncateSize = gitMetaDataSizeInBytes - MAX_GIT_META_DATA_SIZE_IN_BYTES;
77
+ const truncatedCommitMessage = truncateString(gitMetaData.commit_message, truncateSize);
78
+ gitMetaData.commit_message = truncatedCommitMessage;
79
+ log.info(`The commit has been truncated. Size of commit after truncation is ${getSizeOfJsonObjectInBytes(gitMetaData) / 1024} KB`);
80
+ }
81
+ return gitMetaData;
82
+ }
83
+ /**
84
+ * Get git metadata
85
+ */
86
+ export function getGitMetadata() {
87
+ try {
88
+ // Find git repo root
89
+ const rootOutput = execSync('git rev-parse --show-toplevel').toString().trim();
90
+ const commonDirOutput = execSync('git rev-parse --git-common-dir').toString().trim();
91
+ // Get basic git info
92
+ const sha = execSync('git rev-parse HEAD').toString().trim();
93
+ const shortSha = execSync('git rev-parse --short HEAD').toString().trim();
94
+ const branch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
95
+ let tag = '';
96
+ try {
97
+ tag = execSync('git describe --all --tags --exact-match').toString().trim();
98
+ }
99
+ catch (e) {
100
+ // Tag might not exist
101
+ }
102
+ // Get commit info
103
+ const commitAuthorName = execSync('git log -1 --pretty=%an').toString().trim();
104
+ const commitAuthorEmail = execSync('git log -1 --pretty=%ae').toString().trim();
105
+ const commitAuthorDate = execSync('git log -1 --pretty=%aI').toString().trim();
106
+ const committerName = execSync('git log -1 --pretty=%cn').toString().trim();
107
+ const committerEmail = execSync('git log -1 --pretty=%ce').toString().trim();
108
+ const committerDate = execSync('git log -1 --pretty=%cI').toString().trim();
109
+ const commitMessage = execSync('git log -1 --pretty=%B').toString().trim();
110
+ // Get last tag and commits since
111
+ let lastTag = '';
112
+ let commitsSinceLastTag = '';
113
+ try {
114
+ lastTag = execSync('git describe --tags --abbrev=0 --always').toString().trim();
115
+ commitsSinceLastTag = execSync(`git rev-list ${sha}..${lastTag} --count`).toString().trim();
116
+ }
117
+ catch (e) {
118
+ // Last tag might not exist
119
+ }
120
+ // Get remotes
121
+ const remotesOutput = execSync('git remote -v').toString().trim();
122
+ const remoteLines = remotesOutput.split('\n');
123
+ const remotes = [];
124
+ const processedRemotes = new Set();
125
+ for (const line of remoteLines) {
126
+ const parts = line.split(/\s+/);
127
+ if (parts.length >= 2) {
128
+ const remoteName = parts[0];
129
+ const remoteUrl = parts[1];
130
+ // Only process each remote once (git remote -v shows fetch and push)
131
+ const key = `${remoteName}:${remoteUrl}`;
132
+ if (!processedRemotes.has(key)) {
133
+ processedRemotes.add(key);
134
+ remotes.push({
135
+ name: remoteName,
136
+ url: remoteUrl
137
+ });
138
+ }
139
+ }
140
+ }
141
+ const info = {
142
+ sha,
143
+ short_sha: shortSha,
144
+ branch,
145
+ tag,
146
+ committer: gitAuthor(committerName, committerEmail),
147
+ committer_date: committerDate,
148
+ author: gitAuthor(commitAuthorName, commitAuthorEmail),
149
+ author_date: commitAuthorDate,
150
+ commit_message: commitMessage,
151
+ root: rootOutput,
152
+ common_git_dir: commonDirOutput,
153
+ worktree_git_dir: commonDirOutput,
154
+ last_tag: lastTag,
155
+ commits_since_last_tag: commitsSinceLastTag
156
+ };
157
+ const gitMetaData = {
158
+ name: 'git',
159
+ ...info,
160
+ remotes
161
+ };
162
+ return checkAndTruncateVcsInfo(gitMetaData);
163
+ }
164
+ catch (e) {
165
+ log.error(`Error getting git metadata: ${e}`);
166
+ return {};
167
+ }
168
+ }
169
+ /**
170
+ * Check if a git metadata result is valid
171
+ */
172
+ function isValidGitResult(result) {
173
+ return (Array.isArray(result.filesChanged) &&
174
+ result.filesChanged.length > 0 &&
175
+ Array.isArray(result.authors) &&
176
+ result.authors.length > 0);
177
+ }
178
+ /**
179
+ * Get base branch from repository
180
+ */
181
+ function getBaseBranch() {
182
+ try {
183
+ // Try to get the default branch from origin/HEAD symbolic ref (works for most providers)
184
+ try {
185
+ const originHeadOutput = execSync('git symbolic-ref refs/remotes/origin/HEAD').toString().trim();
186
+ if (originHeadOutput.startsWith('refs/remotes/origin/')) {
187
+ return originHeadOutput.replace('refs/remotes/', '');
188
+ }
189
+ }
190
+ catch (e) {
191
+ // Symbolic ref might not exist
192
+ }
193
+ // Fallback: use the first branch in local heads
194
+ try {
195
+ const branchesOutput = execSync('git branch').toString().trim();
196
+ const branches = branchesOutput.split('\n').filter(Boolean);
197
+ if (branches.length > 0) {
198
+ // Remove the '* ' from current branch if present and return first branch
199
+ const firstBranch = branches[0].replace(/^\*\s+/, '').trim();
200
+ return firstBranch;
201
+ }
202
+ }
203
+ catch (e) {
204
+ // Branches might not exist
205
+ }
206
+ // Fallback: use the first remote branch if available
207
+ try {
208
+ const remoteBranchesOutput = execSync('git branch -r').toString().trim();
209
+ const remoteBranches = remoteBranchesOutput.split('\n').filter(Boolean);
210
+ for (const branch of remoteBranches) {
211
+ const cleanBranch = branch.trim();
212
+ if (cleanBranch.startsWith('origin/') && !cleanBranch.includes('HEAD')) {
213
+ return cleanBranch;
214
+ }
215
+ }
216
+ }
217
+ catch (e) {
218
+ // Remote branches might not exist
219
+ }
220
+ }
221
+ catch (e) {
222
+ log.debug(`Error finding base branch: ${e}`);
223
+ }
224
+ return null;
225
+ }
226
+ /**
227
+ * Get changed files from commits
228
+ */
229
+ function getChangedFilesFromCommits(commitHashes) {
230
+ const changedFiles = new Set();
231
+ try {
232
+ for (const commit of commitHashes) {
233
+ try {
234
+ // Check if commit has parents
235
+ const parentsOutput = execSync(`git log -1 --pretty=%P ${commit}`).toString().trim();
236
+ const parents = parentsOutput.split(' ').filter(Boolean);
237
+ for (const parent of parents) {
238
+ const diffOutput = execSync(`git diff --name-only ${parent} ${commit}`).toString().trim();
239
+ const files = diffOutput.split('\n').filter(Boolean);
240
+ for (const file of files) {
241
+ changedFiles.add(file);
242
+ }
243
+ }
244
+ }
245
+ catch (e) {
246
+ log.debug(`Error processing commit ${commit}: ${e}`);
247
+ }
248
+ }
249
+ }
250
+ catch (e) {
251
+ log.debug(`Error getting changed files from commits: ${e}`);
252
+ }
253
+ return Array.from(changedFiles);
254
+ }
255
+ /**
256
+ * Get Git metadata for AI selection
257
+ * @param multiRepoSource Array of repository paths for multi-repo setup
258
+ */
259
+ export function getGitMetadataForAISelection(folders = []) {
260
+ if (folders && folders.length === 0) {
261
+ return [];
262
+ }
263
+ if (folders === null) {
264
+ folders = [process.cwd()];
265
+ }
266
+ // Deduplicate folders to avoid calculating PR diff multiple times for the same source
267
+ // Normalize paths using path.resolve to handle relative paths, trailing slashes, etc.
268
+ const uniqueFolders = [...new Set(folders.map(f => path.resolve(f)))];
269
+ log.debug(`Processing ${uniqueFolders.length} unique folders out of ${folders.length} total`);
270
+ const results = [];
271
+ for (const folder of uniqueFolders) {
272
+ const originalDir = process.cwd();
273
+ try {
274
+ // Initialize the result structure
275
+ const result = {
276
+ prId: '',
277
+ filesChanged: [],
278
+ authors: [],
279
+ prDate: '',
280
+ commitMessages: [],
281
+ prTitle: '',
282
+ prDescription: '',
283
+ prRawDiff: ''
284
+ };
285
+ // Change directory to the folder
286
+ process.chdir(folder);
287
+ // Get current branch and latest commit
288
+ const currentBranch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
289
+ const latestCommit = execSync('git rev-parse HEAD').toString().trim();
290
+ result.prId = latestCommit;
291
+ // Find base branch
292
+ const baseBranch = getBaseBranch();
293
+ log.debug(`Base branch for comparison: ${baseBranch}`);
294
+ let commits = [];
295
+ if (baseBranch) {
296
+ try {
297
+ // Get changed files between base branch and current branch
298
+ const changedFilesOutput = execSync(`git diff --name-only ${baseBranch}..${currentBranch}`).toString().trim();
299
+ log.debug(`Changed files between ${baseBranch} and ${currentBranch}: ${changedFilesOutput}`);
300
+ result.filesChanged = changedFilesOutput.split('\n').filter(f => f.trim());
301
+ // Get commits between base branch and current branch
302
+ const commitsOutput = execSync(`git log ${baseBranch}..${currentBranch} --pretty=%H`).toString().trim();
303
+ commits = commitsOutput.split('\n').filter(Boolean);
304
+ }
305
+ catch (e) {
306
+ log.debug('Failed to get changed files from branch comparison. Falling back to recent commits.');
307
+ // Fallback to recent commits
308
+ const recentCommitsOutput = execSync('git log -10 --pretty=%H').toString().trim();
309
+ commits = recentCommitsOutput.split('\n').filter(Boolean);
310
+ if (commits.length > 0) {
311
+ result.filesChanged = getChangedFilesFromCommits(commits.slice(0, 5));
312
+ }
313
+ }
314
+ }
315
+ else {
316
+ // Fallback to recent commits
317
+ const recentCommitsOutput = execSync('git log -10 --pretty=%H').toString().trim();
318
+ commits = recentCommitsOutput.split('\n').filter(Boolean);
319
+ if (commits.length > 0) {
320
+ result.filesChanged = getChangedFilesFromCommits(commits.slice(0, 5));
321
+ }
322
+ }
323
+ // Process commit authors and messages
324
+ const authorsSet = new Set();
325
+ const commitMessages = [];
326
+ // Only process commits if we have them
327
+ if (commits.length > 0) {
328
+ for (const commit of commits) {
329
+ try {
330
+ const commitMessage = execSync(`git log -1 --pretty=%B ${commit}`).toString().trim();
331
+ log.debug(`Processing commit: ${commitMessage}`);
332
+ const authorName = execSync(`git log -1 --pretty=%an ${commit}`).toString().trim();
333
+ authorsSet.add(authorName || 'Unknown');
334
+ commitMessages.push({
335
+ message: commitMessage.trim(),
336
+ user: authorName || 'Unknown'
337
+ });
338
+ }
339
+ catch (e) {
340
+ log.debug(`Error processing commit ${commit}: ${e}`);
341
+ }
342
+ }
343
+ }
344
+ // If we have no commits but have changed files, add a fallback author
345
+ if (commits.length === 0 && result.filesChanged.length > 0) {
346
+ try {
347
+ // Try to get current git user as fallback
348
+ const fallbackAuthor = execSync('git config user.name').toString().trim() || 'Unknown';
349
+ authorsSet.add(fallbackAuthor);
350
+ log.debug(`Added fallback author: ${fallbackAuthor}`);
351
+ }
352
+ catch (e) {
353
+ authorsSet.add('Unknown');
354
+ log.debug('Added Unknown as fallback author');
355
+ }
356
+ }
357
+ result.authors = Array.from(authorsSet);
358
+ result.commitMessages = commitMessages;
359
+ // Get commit date
360
+ if (latestCommit) {
361
+ const commitDate = execSync(`git log -1 --pretty=%cd --date=format:'%Y-%m-%d' ${latestCommit}`).toString().trim();
362
+ result.prDate = commitDate.replace(/'/g, '');
363
+ }
364
+ // Set PR title and description from latest commit if not already set
365
+ if ((!result.prTitle || result.prTitle.trim() === '') && latestCommit) {
366
+ try {
367
+ const latestCommitMessage = execSync(`git log -1 --pretty=%B ${latestCommit}`).toString().trim();
368
+ const messageLines = latestCommitMessage.trim().split('\n');
369
+ result.prTitle = messageLines[0] || '';
370
+ if (messageLines.length > 2) {
371
+ result.prDescription = messageLines.slice(2).join('\n').trim();
372
+ }
373
+ }
374
+ catch (e) {
375
+ log.debug(`Error extracting commit message for PR title: ${e}`);
376
+ }
377
+ }
378
+ // Reset directory
379
+ process.chdir(originalDir);
380
+ results.push(result);
381
+ }
382
+ catch (e) {
383
+ log.error(`Exception in populating Git metadata for AI selection (folder: ${folder}): ${e}`);
384
+ // Reset directory if needed
385
+ try {
386
+ process.chdir(originalDir);
387
+ }
388
+ catch (dirError) {
389
+ log.error(`Error resetting directory: ${dirError}`);
390
+ }
391
+ }
392
+ }
393
+ // Filter out results with empty filesChanged
394
+ const filteredResults = results.filter(isValidGitResult);
395
+ // Map to required output format
396
+ const formattedResults = filteredResults.map((result) => ({
397
+ prId: result.prId || '',
398
+ filesChanged: Array.isArray(result.filesChanged) ? result.filesChanged : [],
399
+ authors: Array.isArray(result.authors) ? result.authors : [],
400
+ prDate: result.prDate || '',
401
+ commitMessages: Array.isArray(result.commitMessages)
402
+ ? result.commitMessages.map((cm) => ({
403
+ message: cm.message || '',
404
+ user: cm.user || ''
405
+ }))
406
+ : [],
407
+ prTitle: result.prTitle || '',
408
+ prDescription: result.prDescription || '',
409
+ prRawDiff: result.prRawDiff || ''
410
+ }));
411
+ return formattedResults;
412
+ }