@eldrforge/kodrdriv 1.2.19 → 1.2.20

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 (46) hide show
  1. package/INTEGRATION-SUMMARY.md +232 -0
  2. package/TEST-STATUS.md +168 -0
  3. package/dist/application.js +3 -0
  4. package/dist/application.js.map +1 -1
  5. package/dist/arguments.js +1 -1
  6. package/dist/arguments.js.map +1 -1
  7. package/dist/commands/commit.js +2 -2
  8. package/dist/commands/commit.js.map +1 -1
  9. package/dist/commands/development.js +1 -2
  10. package/dist/commands/development.js.map +1 -1
  11. package/dist/commands/link.js +1 -2
  12. package/dist/commands/link.js.map +1 -1
  13. package/dist/commands/publish.js +1 -3
  14. package/dist/commands/publish.js.map +1 -1
  15. package/dist/commands/release.js +2 -2
  16. package/dist/commands/release.js.map +1 -1
  17. package/dist/commands/tree.js +7 -8
  18. package/dist/commands/tree.js.map +1 -1
  19. package/dist/commands/unlink.js +1 -2
  20. package/dist/commands/unlink.js.map +1 -1
  21. package/dist/commands/updates.js +1 -1
  22. package/dist/commands/updates.js.map +1 -1
  23. package/dist/commands/versions.js +1 -1
  24. package/dist/commands/versions.js.map +1 -1
  25. package/dist/constants.js +1 -1
  26. package/dist/content/diff.js +1 -1
  27. package/dist/content/diff.js.map +1 -1
  28. package/dist/content/log.js +1 -1
  29. package/dist/content/log.js.map +1 -1
  30. package/dist/util/general.js +2 -3
  31. package/dist/util/general.js.map +1 -1
  32. package/dist/util/github.js +1 -1
  33. package/dist/util/github.js.map +1 -1
  34. package/dist/util/openai.js +1 -1
  35. package/dist/util/openai.js.map +1 -1
  36. package/dist/util/performance.js +1 -1
  37. package/dist/util/performance.js.map +1 -1
  38. package/dist/util/safety.js +1 -1
  39. package/dist/util/safety.js.map +1 -1
  40. package/dist/util/validation.js +4 -39
  41. package/dist/util/validation.js.map +1 -1
  42. package/package.json +2 -1
  43. package/dist/util/child.js +0 -174
  44. package/dist/util/child.js.map +0 -1
  45. package/dist/util/git.js +0 -836
  46. package/dist/util/git.js.map +0 -1
package/dist/util/git.js DELETED
@@ -1,836 +0,0 @@
1
- import { getLogger } from '../logging.js';
2
- import { runSecure, validateGitRef } from './child.js';
3
- import fs__default from 'fs/promises';
4
- import path__default from 'path';
5
- import { exec } from 'child_process';
6
- import util from 'util';
7
- import * as semver from 'semver';
8
- import { safeJsonParse, validatePackageJson } from './validation.js';
9
-
10
- /**
11
- * Tests if a git reference exists and is valid (silent version that doesn't log errors)
12
- */ const isValidGitRefSilent = async (ref)=>{
13
- try {
14
- // Validate the ref first to prevent injection
15
- if (!validateGitRef(ref)) {
16
- return false;
17
- }
18
- await runSecure('git', [
19
- 'rev-parse',
20
- '--verify',
21
- ref
22
- ], {
23
- stdio: 'ignore'
24
- });
25
- return true;
26
- } catch {
27
- return false;
28
- }
29
- };
30
- /**
31
- * Tests if a git reference exists and is valid
32
- */ const isValidGitRef = async (ref)=>{
33
- const logger = getLogger();
34
- try {
35
- // Validate the ref first to prevent injection
36
- if (!validateGitRef(ref)) {
37
- logger.debug(`Git reference '${ref}' contains invalid characters`);
38
- return false;
39
- }
40
- await runSecure('git', [
41
- 'rev-parse',
42
- '--verify',
43
- ref
44
- ], {
45
- stdio: 'ignore'
46
- });
47
- logger.debug(`Git reference '${ref}' is valid`);
48
- return true;
49
- } catch (error) {
50
- logger.debug(`Git reference '${ref}' is not valid: ${error}`);
51
- return false;
52
- }
53
- };
54
- /**
55
- * Finds the previous release tag based on the current version using semantic versioning.
56
- * Returns the highest version tag that is less than the current version.
57
- *
58
- * @param currentVersion The current version (e.g., "1.2.3", "2.0.0")
59
- * @param tagPattern The pattern to match tags (e.g., "v*", "working/v*")
60
- * @returns The previous release tag or null if none found
61
- */ const findPreviousReleaseTag = async (currentVersion, tagPattern = 'v*')=>{
62
- const logger = getLogger();
63
- try {
64
- // Parse current version first to validate it
65
- const currentSemver = semver.parse(currentVersion);
66
- if (!currentSemver) {
67
- logger.warn(`❌ Invalid version format: ${currentVersion}`);
68
- return null;
69
- }
70
- logger.info(`🔍 findPreviousReleaseTag: Looking for tags matching "${tagPattern}" < ${currentVersion}`);
71
- // Get all tags - try sorted first, fallback to unsorted
72
- let tags;
73
- try {
74
- logger.info(` Running: git tag -l "${tagPattern}" --sort=-version:refname`);
75
- const { stdout } = await runSecure('git', [
76
- 'tag',
77
- '-l',
78
- tagPattern,
79
- '--sort=-version:refname'
80
- ]);
81
- tags = stdout.trim().split('\n').filter((tag)=>tag.length > 0);
82
- logger.info(` ✅ Found ${tags.length} tags matching pattern "${tagPattern}"`);
83
- if (tags.length > 0) {
84
- logger.info(` 📋 Tags (newest first): ${tags.slice(0, 15).join(', ')}${tags.length > 15 ? ` ... (${tags.length - 15} more)` : ''}`);
85
- }
86
- } catch (sortError) {
87
- // Fallback for older git versions that don't support --sort
88
- logger.info(` ⚠️ Git tag --sort failed: ${sortError.message}`);
89
- logger.info(` Falling back to manual sorting...`);
90
- const { stdout } = await runSecure('git', [
91
- 'tag',
92
- '-l',
93
- tagPattern
94
- ]);
95
- tags = stdout.trim().split('\n').filter((tag)=>tag.length > 0);
96
- logger.info(` Found ${tags.length} tags (unsorted) matching pattern "${tagPattern}"`);
97
- // Manual semantic version sorting
98
- tags.sort((a, b)=>{
99
- const aMatch = a.match(/v?(\d+\.\d+\.\d+.*?)$/);
100
- const bMatch = b.match(/v?(\d+\.\d+\.\d+.*?)$/);
101
- if (!aMatch || !bMatch) return 0;
102
- const aSemver = semver.parse(aMatch[1]);
103
- const bSemver = semver.parse(bMatch[1]);
104
- if (!aSemver || !bSemver) return 0;
105
- return semver.rcompare(aSemver, bSemver);
106
- });
107
- logger.info(` ✅ Sorted ${tags.length} tags manually`);
108
- }
109
- if (tags.length === 0) {
110
- logger.warn('');
111
- logger.warn(`❌ NO TAGS FOUND matching pattern "${tagPattern}"`);
112
- logger.warn(` To verify, run: git tag -l '${tagPattern}'`);
113
- logger.warn('');
114
- return null;
115
- }
116
- logger.info(` 🔬 Processing ${tags.length} tags to find the highest version < ${currentVersion}...`);
117
- // Find the highest version that is less than the current version
118
- let previousTag = null;
119
- let previousVersion = null;
120
- let validTags = 0;
121
- let skippedTags = 0;
122
- for (const tag of tags){
123
- // Extract version from tag - handle "v1.2.13", "1.2.13", and "working/v1.2.13"
124
- const versionMatch = tag.match(/v?(\d+\.\d+\.\d+.*?)$/);
125
- if (!versionMatch) {
126
- logger.debug(` ⏭️ Skipping tag "${tag}" (doesn't match version pattern)`);
127
- continue;
128
- }
129
- const versionString = versionMatch[1];
130
- const tagSemver = semver.parse(versionString);
131
- if (tagSemver) {
132
- validTags++;
133
- // Check if this tag version is less than current version
134
- if (semver.lt(tagSemver, currentSemver)) {
135
- // If we don't have a previous version yet, or this one is higher than our current previous
136
- if (!previousVersion || semver.gt(tagSemver, previousVersion)) {
137
- previousVersion = tagSemver;
138
- previousTag = tag; // Keep the original tag format
139
- logger.info(` ✅ New best candidate: ${tag} (${versionString} < ${currentVersion})`);
140
- } else {
141
- logger.debug(` ⏭️ ${tag} (${versionString}) is < current but not better than ${previousTag}`);
142
- }
143
- } else {
144
- skippedTags++;
145
- logger.debug(` ⏭️ ${tag} (${versionString}) >= current (${currentVersion}), skipping`);
146
- }
147
- }
148
- }
149
- logger.info('');
150
- logger.info(` 📊 Tag analysis results:`);
151
- logger.info(` - Total tags examined: ${tags.length}`);
152
- logger.info(` - Valid semver tags: ${validTags}`);
153
- logger.info(` - Tags >= current version (skipped): ${skippedTags}`);
154
- logger.info(` - Best match: ${previousTag || 'none'}`);
155
- logger.info('');
156
- if (previousTag) {
157
- logger.info(`✅ SUCCESS: Found previous tag: ${previousTag}`);
158
- logger.info(` Version comparison: ${previousVersion === null || previousVersion === void 0 ? void 0 : previousVersion.version} < ${currentVersion}`);
159
- logger.info('');
160
- return previousTag;
161
- }
162
- logger.warn(`❌ FAILED: No previous tag found for version ${currentVersion}`);
163
- logger.warn(` Pattern searched: "${tagPattern}"`);
164
- logger.warn(` Reason: All ${validTags} valid tags were >= ${currentVersion}`);
165
- logger.warn('');
166
- return null;
167
- } catch (error) {
168
- logger.debug(`Error finding previous release tag: ${error.message}`);
169
- return null;
170
- }
171
- };
172
- /**
173
- * Gets the current version from package.json
174
- *
175
- * @returns The current version string or null if not found
176
- */ const getCurrentVersion = async ()=>{
177
- const logger = getLogger();
178
- try {
179
- // First try to get from committed version in HEAD
180
- const { stdout } = await runSecure('git', [
181
- 'show',
182
- 'HEAD:package.json'
183
- ]);
184
- const packageJson = safeJsonParse(stdout, 'package.json');
185
- const validated = validatePackageJson(packageJson, 'package.json');
186
- if (validated.version) {
187
- logger.debug(`Current version from HEAD:package.json: ${validated.version}`);
188
- return validated.version;
189
- }
190
- return null;
191
- } catch (error) {
192
- logger.debug(`Could not read version from HEAD:package.json: ${error.message}`);
193
- // Fallback to reading from working directory
194
- try {
195
- const packageJsonPath = path__default.join(process.cwd(), 'package.json');
196
- const content = await fs__default.readFile(packageJsonPath, 'utf-8');
197
- const packageJson = safeJsonParse(content, 'package.json');
198
- const validated = validatePackageJson(packageJson, 'package.json');
199
- if (validated.version) {
200
- logger.debug(`Current version from working directory package.json: ${validated.version}`);
201
- return validated.version;
202
- }
203
- return null;
204
- } catch (fallbackError) {
205
- logger.debug(`Error reading current version from filesystem: ${fallbackError.message}`);
206
- return null;
207
- }
208
- }
209
- };
210
- /**
211
- * Gets a reliable default for the --from parameter by trying multiple fallbacks
212
- *
213
- * Tries in order:
214
- * 1. Previous working branch tag (if on working branch)
215
- * 2. Previous release tag (if current version can be determined)
216
- * 3. main (local main branch - typical release comparison base)
217
- * 4. master (local master branch - legacy default)
218
- * 5. origin/main (remote main branch fallback)
219
- * 6. origin/master (remote master branch fallback)
220
- *
221
- * @param forceMainBranch If true, skip tag detection and use main branch
222
- * @param currentBranch Current branch name for branch-aware tag detection
223
- * @returns A valid git reference to use as the default from parameter
224
- * @throws Error if no valid reference can be found
225
- */ const getDefaultFromRef = async (forceMainBranch = false, currentBranch)=>{
226
- const logger = getLogger();
227
- logger.info('');
228
- logger.info('═══════════════════════════════════════════════════════════');
229
- logger.info('🔍 DETECTING DEFAULT --from REFERENCE FOR RELEASE NOTES');
230
- logger.info('═══════════════════════════════════════════════════════════');
231
- logger.info(`📋 Input parameters:`);
232
- logger.info(` - forceMainBranch: ${forceMainBranch}`);
233
- logger.info(` - currentBranch: "${currentBranch}"`);
234
- logger.info('');
235
- // If forced to use main branch, skip tag detection
236
- if (forceMainBranch) {
237
- logger.info('⚡ Forced to use main branch, skipping tag detection');
238
- } else {
239
- // If on working branch, look for working branch tags first
240
- logger.info(`🎯 Branch check: currentBranch="${currentBranch}", isWorking=${currentBranch === 'working'}`);
241
- if (currentBranch && currentBranch === 'working') {
242
- logger.info('');
243
- logger.info('📍 DETECTED WORKING BRANCH - Searching for working branch tags...');
244
- logger.info('───────────────────────────────────────────────────────────');
245
- try {
246
- const currentVersion = await getCurrentVersion();
247
- logger.info(`📦 Current version from package.json: ${currentVersion}`);
248
- if (currentVersion) {
249
- logger.info(`🔍 Searching for tags matching pattern: "working/v*"`);
250
- logger.info(` (Looking for tags < ${currentVersion})`);
251
- logger.info('');
252
- const previousTag = await findPreviousReleaseTag(currentVersion, 'working/v*');
253
- logger.info(`🎯 findPreviousReleaseTag result: ${previousTag || 'null (no tag found)'}`);
254
- if (previousTag) {
255
- logger.info(`🔬 Validating tag reference: "${previousTag}"`);
256
- const isValid = await isValidGitRef(previousTag);
257
- logger.info(` Tag is valid git ref: ${isValid}`);
258
- if (isValid) {
259
- logger.info('');
260
- logger.info(`✅ SUCCESS: Using previous working branch tag '${previousTag}'`);
261
- logger.info(` This shows commits added since the last release`);
262
- logger.info('═══════════════════════════════════════════════════════════');
263
- logger.info('');
264
- return previousTag;
265
- } else {
266
- logger.warn('');
267
- logger.warn(`⚠️ VALIDATION FAILED: Tag "${previousTag}" exists but is not a valid git reference`);
268
- logger.warn(` This should not happen - the tag might be corrupted`);
269
- }
270
- } else {
271
- logger.warn('');
272
- logger.warn('❌ NO WORKING BRANCH TAG FOUND matching pattern "working/v*"');
273
- logger.warn(` Current version: ${currentVersion}`);
274
- logger.warn(' 💡 To create working branch tags for past releases, run:');
275
- logger.warn(' kodrdriv development --create-retroactive-tags');
276
- logger.warn('');
277
- logger.warn(' Falling back to regular tag search...');
278
- }
279
- } else {
280
- logger.warn('');
281
- logger.warn('❌ CANNOT READ VERSION from package.json');
282
- logger.warn(' Cannot search for working branch tags without current version');
283
- }
284
- } catch (error) {
285
- logger.warn('');
286
- logger.warn(`❌ ERROR while searching for working branch tag: ${error.message}`);
287
- logger.debug(`Full error stack: ${error.stack}`);
288
- logger.warn(' Falling back to regular tag search...');
289
- }
290
- logger.info('───────────────────────────────────────────────────────────');
291
- logger.info('');
292
- } else {
293
- logger.info(`ℹ️ Not on "working" branch - skipping working tag search`);
294
- logger.info(` (Only search for working/v* tags when on working branch)`);
295
- logger.info('');
296
- }
297
- // First, try to find the previous release tag
298
- try {
299
- const currentVersion = await getCurrentVersion();
300
- if (currentVersion) {
301
- const previousTag = await findPreviousReleaseTag(currentVersion);
302
- if (previousTag && await isValidGitRef(previousTag)) {
303
- logger.info(`Using previous release tag '${previousTag}' as default --from reference`);
304
- return previousTag;
305
- }
306
- }
307
- } catch (error) {
308
- logger.debug(`Could not determine previous release tag: ${error.message}`);
309
- }
310
- }
311
- // Fallback to branch-based references
312
- const candidates = [
313
- 'main',
314
- 'master',
315
- 'origin/main',
316
- 'origin/master'
317
- ];
318
- for (const candidate of candidates){
319
- logger.debug(`Testing git reference candidate: ${candidate}`);
320
- if (await isValidGitRef(candidate)) {
321
- if (forceMainBranch) {
322
- logger.info(`Using '${candidate}' as forced main branch reference`);
323
- } else {
324
- logger.info(`Using '${candidate}' as fallback --from reference (no previous release tag found)`);
325
- }
326
- return candidate;
327
- }
328
- }
329
- // If we get here, something is seriously wrong with the git repository
330
- throw new Error('Could not find a valid default git reference for --from parameter. ' + 'Please specify --from explicitly or check your git repository configuration. ' + `Tried: ${forceMainBranch ? 'main branch only' : 'previous release tag'}, ${candidates.join(', ')}`);
331
- };
332
- /**
333
- * Checks if a local branch exists
334
- */ const localBranchExists = async (branchName)=>{
335
- const logger = getLogger();
336
- const result = await isValidGitRefSilent(`refs/heads/${branchName}`);
337
- if (result) {
338
- logger.debug(`Local branch '${branchName}' exists`);
339
- } else {
340
- logger.debug(`Local branch '${branchName}' does not exist`);
341
- }
342
- return result;
343
- };
344
- /**
345
- * Checks if a remote branch exists
346
- */ const remoteBranchExists = async (branchName, remote = 'origin')=>{
347
- const logger = getLogger();
348
- const result = await isValidGitRefSilent(`refs/remotes/${remote}/${branchName}`);
349
- if (result) {
350
- logger.debug(`Remote branch '${remote}/${branchName}' exists`);
351
- } else {
352
- logger.debug(`Remote branch '${remote}/${branchName}' does not exist`);
353
- }
354
- return result;
355
- };
356
- /**
357
- * Gets the commit SHA for a given branch (local or remote)
358
- */ const getBranchCommitSha = async (branchRef)=>{
359
- // Validate the ref first to prevent injection
360
- if (!validateGitRef(branchRef)) {
361
- throw new Error(`Invalid git reference: ${branchRef}`);
362
- }
363
- const { stdout } = await runSecure('git', [
364
- 'rev-parse',
365
- branchRef
366
- ]);
367
- return stdout.trim();
368
- };
369
- /**
370
- * Checks if a local branch is in sync with its remote counterpart
371
- */ const isBranchInSyncWithRemote = async (branchName, remote = 'origin')=>{
372
- const logger = getLogger();
373
- try {
374
- // Validate inputs first to prevent injection
375
- if (!validateGitRef(branchName)) {
376
- throw new Error(`Invalid branch name: ${branchName}`);
377
- }
378
- if (!validateGitRef(remote)) {
379
- throw new Error(`Invalid remote name: ${remote}`);
380
- }
381
- // First, fetch latest remote refs without affecting working directory
382
- await runSecure('git', [
383
- 'fetch',
384
- remote,
385
- '--quiet'
386
- ]);
387
- const localExists = await localBranchExists(branchName);
388
- const remoteExists = await remoteBranchExists(branchName, remote);
389
- if (!localExists) {
390
- return {
391
- inSync: false,
392
- localExists: false,
393
- remoteExists,
394
- error: `Local branch '${branchName}' does not exist`
395
- };
396
- }
397
- if (!remoteExists) {
398
- return {
399
- inSync: false,
400
- localExists: true,
401
- remoteExists: false,
402
- error: `Remote branch '${remote}/${branchName}' does not exist`
403
- };
404
- }
405
- // Both branches exist, compare their SHAs
406
- const localSha = await getBranchCommitSha(`refs/heads/${branchName}`);
407
- const remoteSha = await getBranchCommitSha(`refs/remotes/${remote}/${branchName}`);
408
- const inSync = localSha === remoteSha;
409
- logger.debug(`Branch sync check for '${branchName}': local=${localSha.substring(0, 8)}, remote=${remoteSha.substring(0, 8)}, inSync=${inSync}`);
410
- return {
411
- inSync,
412
- localSha,
413
- remoteSha,
414
- localExists: true,
415
- remoteExists: true
416
- };
417
- } catch (error) {
418
- logger.debug(`Failed to check branch sync for '${branchName}': ${error.message}`);
419
- return {
420
- inSync: false,
421
- localExists: false,
422
- remoteExists: false,
423
- error: `Failed to check branch sync: ${error.message}`
424
- };
425
- }
426
- };
427
- /**
428
- * Attempts to safely sync a local branch with its remote counterpart
429
- * Returns true if successful, false if conflicts exist that require manual resolution
430
- */ const safeSyncBranchWithRemote = async (branchName, remote = 'origin')=>{
431
- const logger = getLogger();
432
- try {
433
- // Validate inputs first to prevent injection
434
- if (!validateGitRef(branchName)) {
435
- throw new Error(`Invalid branch name: ${branchName}`);
436
- }
437
- if (!validateGitRef(remote)) {
438
- throw new Error(`Invalid remote name: ${remote}`);
439
- }
440
- // Check current branch to restore later if needed
441
- const { stdout: currentBranch } = await runSecure('git', [
442
- 'branch',
443
- '--show-current'
444
- ]);
445
- const originalBranch = currentBranch.trim();
446
- // Fetch latest remote refs
447
- await runSecure('git', [
448
- 'fetch',
449
- remote,
450
- '--quiet'
451
- ]);
452
- // Check if local branch exists
453
- const localExists = await localBranchExists(branchName);
454
- const remoteExists = await remoteBranchExists(branchName, remote);
455
- if (!remoteExists) {
456
- return {
457
- success: false,
458
- error: `Remote branch '${remote}/${branchName}' does not exist`
459
- };
460
- }
461
- if (!localExists) {
462
- // Create local branch tracking the remote
463
- await runSecure('git', [
464
- 'branch',
465
- branchName,
466
- `${remote}/${branchName}`
467
- ]);
468
- logger.debug(`Created local branch '${branchName}' tracking '${remote}/${branchName}'`);
469
- return {
470
- success: true
471
- };
472
- }
473
- // Check if we need to switch to the target branch
474
- const needToSwitch = originalBranch !== branchName;
475
- if (needToSwitch) {
476
- // Check for uncommitted changes before switching
477
- const { stdout: statusOutput } = await runSecure('git', [
478
- 'status',
479
- '--porcelain'
480
- ]);
481
- if (statusOutput.trim()) {
482
- return {
483
- success: false,
484
- error: `Cannot switch to branch '${branchName}' because you have uncommitted changes. Please commit or stash your changes first.`
485
- };
486
- }
487
- // Switch to target branch
488
- await runSecure('git', [
489
- 'checkout',
490
- branchName
491
- ]);
492
- }
493
- try {
494
- // Try to pull with fast-forward only
495
- await runSecure('git', [
496
- 'pull',
497
- remote,
498
- branchName,
499
- '--ff-only'
500
- ]);
501
- logger.debug(`Successfully synced '${branchName}' with '${remote}/${branchName}'`);
502
- // Switch back to original branch if we switched
503
- if (needToSwitch && originalBranch) {
504
- await runSecure('git', [
505
- 'checkout',
506
- originalBranch
507
- ]);
508
- }
509
- return {
510
- success: true
511
- };
512
- } catch (pullError) {
513
- // Switch back to original branch if we switched
514
- if (needToSwitch && originalBranch) {
515
- try {
516
- await runSecure('git', [
517
- 'checkout',
518
- originalBranch
519
- ]);
520
- } catch (checkoutError) {
521
- logger.warn(`Failed to switch back to original branch '${originalBranch}': ${checkoutError}`);
522
- }
523
- }
524
- // Check if this is a merge conflict or diverged branches
525
- if (pullError.message.includes('diverged') || pullError.message.includes('non-fast-forward') || pullError.message.includes('conflict') || pullError.message.includes('CONFLICT')) {
526
- return {
527
- success: false,
528
- conflictResolutionRequired: true,
529
- error: `Branch '${branchName}' has diverged from '${remote}/${branchName}' and requires manual conflict resolution`
530
- };
531
- }
532
- return {
533
- success: false,
534
- error: `Failed to sync branch '${branchName}': ${pullError.message}`
535
- };
536
- }
537
- } catch (error) {
538
- return {
539
- success: false,
540
- error: `Failed to sync branch '${branchName}': ${error.message}`
541
- };
542
- }
543
- };
544
- /**
545
- * Gets the current branch name
546
- */ const getCurrentBranch = async ()=>{
547
- const { stdout } = await runSecure('git', [
548
- 'branch',
549
- '--show-current'
550
- ]);
551
- return stdout.trim();
552
- };
553
- /**
554
- * Gets git status summary including unstaged files, uncommitted changes, and unpushed commits
555
- */ const getGitStatusSummary = async (workingDir)=>{
556
- const logger = getLogger();
557
- try {
558
- const originalCwd = process.cwd();
559
- if (workingDir) {
560
- process.chdir(workingDir);
561
- }
562
- try {
563
- // Get current branch
564
- const branch = await getCurrentBranch();
565
- // Get git status for unstaged and uncommitted changes
566
- const { stdout: statusOutput } = await runSecure('git', [
567
- 'status',
568
- '--porcelain'
569
- ]);
570
- const statusLines = statusOutput.trim().split('\n').filter((line)=>line.trim());
571
- // Count different types of changes
572
- let unstagedCount = 0;
573
- let uncommittedCount = 0;
574
- for (const line of statusLines){
575
- const statusCode = line.substring(0, 2);
576
- // For untracked files (??) count as unstaged only once
577
- if (statusCode === '??') {
578
- unstagedCount++;
579
- continue;
580
- }
581
- // Check for unstaged changes (working directory changes)
582
- // Second character represents working tree status
583
- if (statusCode[1] !== ' ' && statusCode[1] !== '') {
584
- unstagedCount++;
585
- }
586
- // Check for uncommitted changes (staged changes)
587
- // First character represents index status
588
- if (statusCode[0] !== ' ' && statusCode[0] !== '') {
589
- uncommittedCount++;
590
- }
591
- }
592
- // Check for unpushed commits by comparing with remote
593
- let unpushedCount = 0;
594
- let hasUnpushedCommits = false;
595
- try {
596
- // First fetch to get latest remote refs
597
- await runSecure('git', [
598
- 'fetch',
599
- 'origin',
600
- '--quiet'
601
- ]);
602
- // Check if remote branch exists
603
- const remoteExists = await remoteBranchExists(branch);
604
- if (remoteExists) {
605
- // Get count of commits ahead of remote (branch already validated in calling function)
606
- const { stdout: aheadOutput } = await runSecure('git', [
607
- 'rev-list',
608
- '--count',
609
- `origin/${branch}..HEAD`
610
- ]);
611
- unpushedCount = parseInt(aheadOutput.trim()) || 0;
612
- hasUnpushedCommits = unpushedCount > 0;
613
- }
614
- } catch (error) {
615
- logger.debug(`Could not check for unpushed commits: ${error}`);
616
- // Remote might not exist or other issues - not critical for status
617
- }
618
- const hasUnstagedFiles = unstagedCount > 0;
619
- const hasUncommittedChanges = uncommittedCount > 0;
620
- // Build status summary
621
- const statusParts = [];
622
- if (hasUnstagedFiles) {
623
- statusParts.push(`${unstagedCount} unstaged`);
624
- }
625
- if (hasUncommittedChanges) {
626
- statusParts.push(`${uncommittedCount} uncommitted`);
627
- }
628
- if (hasUnpushedCommits) {
629
- statusParts.push(`${unpushedCount} unpushed`);
630
- }
631
- const status = statusParts.length > 0 ? statusParts.join(', ') : 'clean';
632
- return {
633
- branch,
634
- hasUnstagedFiles,
635
- hasUncommittedChanges,
636
- hasUnpushedCommits,
637
- unstagedCount,
638
- uncommittedCount,
639
- unpushedCount,
640
- status
641
- };
642
- } finally{
643
- if (workingDir) {
644
- process.chdir(originalCwd);
645
- }
646
- }
647
- } catch (error) {
648
- logger.debug(`Failed to get git status summary: ${error.message}`);
649
- return {
650
- branch: 'unknown',
651
- hasUnstagedFiles: false,
652
- hasUncommittedChanges: false,
653
- hasUnpushedCommits: false,
654
- unstagedCount: 0,
655
- uncommittedCount: 0,
656
- unpushedCount: 0,
657
- status: 'error'
658
- };
659
- }
660
- };
661
- /**
662
- * Gets the list of globally linked packages (packages available to be linked to)
663
- */ const getGloballyLinkedPackages = async ()=>{
664
- const execPromise = util.promisify(exec);
665
- try {
666
- const { stdout } = await execPromise('npm ls --link -g --json');
667
- const result = safeJsonParse(stdout, 'npm ls global output');
668
- if (result.dependencies && typeof result.dependencies === 'object') {
669
- return new Set(Object.keys(result.dependencies));
670
- }
671
- return new Set();
672
- } catch (error) {
673
- // Try to parse from error stdout if available
674
- if (error.stdout) {
675
- try {
676
- const result = safeJsonParse(error.stdout, 'npm ls global error output');
677
- if (result.dependencies && typeof result.dependencies === 'object') {
678
- return new Set(Object.keys(result.dependencies));
679
- }
680
- } catch {
681
- // If JSON parsing fails, return empty set
682
- }
683
- }
684
- return new Set();
685
- }
686
- };
687
- /**
688
- * Gets the list of packages that this package is actively linking to (consuming linked packages)
689
- */ const getLinkedDependencies = async (packageDir)=>{
690
- const execPromise = util.promisify(exec);
691
- try {
692
- const { stdout } = await execPromise('npm ls --link --json', {
693
- cwd: packageDir
694
- });
695
- const result = safeJsonParse(stdout, 'npm ls local output');
696
- if (result.dependencies && typeof result.dependencies === 'object') {
697
- return new Set(Object.keys(result.dependencies));
698
- }
699
- return new Set();
700
- } catch (error) {
701
- // npm ls --link often exits with non-zero code but still provides valid JSON in stdout
702
- if (error.stdout) {
703
- try {
704
- const result = safeJsonParse(error.stdout, 'npm ls local error output');
705
- if (result.dependencies && typeof result.dependencies === 'object') {
706
- return new Set(Object.keys(result.dependencies));
707
- }
708
- } catch {
709
- // If JSON parsing fails, return empty set
710
- }
711
- }
712
- return new Set();
713
- }
714
- };
715
- /**
716
- * Checks for actual semantic version compatibility issues between linked packages and their consumers
717
- * Returns a set of dependency names that have real compatibility problems
718
- *
719
- * This function ignores npm's strict prerelease handling and focuses on actual compatibility:
720
- * - "^4.4" is compatible with "4.4.53-dev.0" (prerelease of compatible minor version)
721
- * - "^4.4" is incompatible with "4.5.3" (different minor version)
722
- */ const getLinkCompatibilityProblems = async (packageDir, allPackagesInfo)=>{
723
- try {
724
- // Read the consumer package.json
725
- const packageJsonPath = path__default.join(packageDir, 'package.json');
726
- const packageJsonContent = await fs__default.readFile(packageJsonPath, 'utf-8');
727
- const parsed = safeJsonParse(packageJsonContent, packageJsonPath);
728
- const packageJson = validatePackageJson(parsed, packageJsonPath);
729
- const problemDependencies = new Set();
730
- // Get linked dependencies
731
- const linkedDeps = await getLinkedDependencies(packageDir);
732
- // Check each dependency type
733
- const dependencyTypes = [
734
- 'dependencies',
735
- 'devDependencies',
736
- 'peerDependencies',
737
- 'optionalDependencies'
738
- ];
739
- for (const depType of dependencyTypes){
740
- const deps = packageJson[depType];
741
- if (!deps || typeof deps !== 'object') continue;
742
- for (const [depName, versionRange] of Object.entries(deps)){
743
- // Only check dependencies that are currently linked
744
- if (!linkedDeps.has(depName)) continue;
745
- // Skip if version range is not a string or is invalid
746
- if (typeof versionRange !== 'string') continue;
747
- try {
748
- let linkedVersion;
749
- // If we have package info provided, use it
750
- if (allPackagesInfo) {
751
- const packageInfo = allPackagesInfo.get(depName);
752
- if (packageInfo) {
753
- linkedVersion = packageInfo.version;
754
- }
755
- }
756
- // If we don't have version from package info, try to read it from the linked package
757
- if (!linkedVersion) {
758
- try {
759
- // Get the linked package path and read its version
760
- const nodeModulesPath = path__default.join(packageDir, 'node_modules', depName, 'package.json');
761
- const linkedPackageJson = await fs__default.readFile(nodeModulesPath, 'utf-8');
762
- const linkedParsed = safeJsonParse(linkedPackageJson, nodeModulesPath);
763
- const linkedValidated = validatePackageJson(linkedParsed, nodeModulesPath);
764
- linkedVersion = linkedValidated.version;
765
- } catch {
766
- continue;
767
- }
768
- }
769
- if (!linkedVersion) continue;
770
- // Check compatibility with custom logic for prerelease versions
771
- if (!isVersionCompatibleWithRange(linkedVersion, versionRange)) {
772
- problemDependencies.add(depName);
773
- }
774
- } catch {
775
- continue;
776
- }
777
- }
778
- }
779
- return problemDependencies;
780
- } catch {
781
- // If we can't read the package.json or process it, return empty set
782
- return new Set();
783
- }
784
- };
785
- /**
786
- * Custom semver compatibility check that handles prerelease versions more intelligently
787
- * than npm's strict checking, with stricter caret range handling
788
- *
789
- * Examples:
790
- * - isVersionCompatibleWithRange("4.4.53-dev.0", "^4.4") => true
791
- * - isVersionCompatibleWithRange("4.5.3", "^4.4") => false
792
- * - isVersionCompatibleWithRange("4.4.1", "^4.4") => true
793
- */ const isVersionCompatibleWithRange = (version, range)=>{
794
- try {
795
- const parsedVersion = semver.parse(version);
796
- if (!parsedVersion) return false;
797
- // Parse the range to understand what we're comparing against
798
- const rangeObj = semver.validRange(range);
799
- if (!rangeObj) return false;
800
- // For caret ranges like "^4.4", we want more strict checking than semver's default
801
- if (range.startsWith('^')) {
802
- const rangeVersion = range.substring(1); // Remove the ^
803
- // Try to parse as a complete version first
804
- let parsedRange = semver.parse(rangeVersion);
805
- // If that fails, try to coerce it (handles cases like "4.4" -> "4.4.0")
806
- if (!parsedRange) {
807
- const coercedRange = semver.coerce(rangeVersion);
808
- if (coercedRange) {
809
- parsedRange = coercedRange;
810
- } else {
811
- return false;
812
- }
813
- }
814
- // For prerelease versions, check if the base version (without prerelease)
815
- // matches the major.minor from the range
816
- if (parsedVersion.prerelease.length > 0) {
817
- return parsedVersion.major === parsedRange.major && parsedVersion.minor === parsedRange.minor;
818
- }
819
- // For regular versions with caret ranges, be strict about minor version
820
- // ^4.4 should only accept 4.4.x, not 4.5.x
821
- return parsedVersion.major === parsedRange.major && parsedVersion.minor === parsedRange.minor;
822
- }
823
- // For other range types (exact, tilde, etc.), use standard semver checking
824
- if (parsedVersion.prerelease.length > 0) {
825
- const baseVersion = `${parsedVersion.major}.${parsedVersion.minor}.${parsedVersion.patch}`;
826
- return semver.satisfies(baseVersion, range);
827
- }
828
- return semver.satisfies(version, range);
829
- } catch {
830
- // If semver parsing fails, assume incompatible
831
- return false;
832
- }
833
- };
834
-
835
- export { findPreviousReleaseTag, getBranchCommitSha, getCurrentBranch, getCurrentVersion, getDefaultFromRef, getGitStatusSummary, getGloballyLinkedPackages, getLinkCompatibilityProblems, getLinkedDependencies, isBranchInSyncWithRemote, isValidGitRef, localBranchExists, remoteBranchExists, safeSyncBranchWithRemote };
836
- //# sourceMappingURL=git.js.map