@eldrforge/git-tools 0.1.12 โ†’ 0.1.14

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 (2) hide show
  1. package/README.md +686 -116
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,15 +1,21 @@
1
1
  # @eldrforge/git-tools
2
2
 
3
- Git utilities for automation - secure process execution and comprehensive Git operations.
3
+ A comprehensive TypeScript library providing secure Git operations, process execution utilities, and NPM link management for automation workflows.
4
4
 
5
- ## Features
5
+ ## Overview
6
6
 
7
- - **Secure Process Execution** - Shell injection prevention with validated arguments
8
- - **Comprehensive Git Operations** - 20+ Git utilities for automation
9
- - **Semantic Version Support** - Intelligent tag finding and version comparison
10
- - **Branch Management** - Sync checking, safe syncing, and status queries
11
- - **NPM Link Management** - Link detection, compatibility checking, and problem diagnosis
12
- - **Flexible Logging** - Bring your own logger or use the built-in console logger
7
+ `@eldrforge/git-tools` is a production-ready library designed for building Git automation tools. It provides secure command execution primitives and high-level Git operations with a focus on safety, reliability, and ease of use.
8
+
9
+ **Key Features:**
10
+
11
+ - ๐Ÿ”’ **Secure Process Execution** - Shell injection prevention with validated arguments
12
+ - ๐ŸŒณ **Comprehensive Git Operations** - 20+ Git utilities for branch management, versioning, and status queries
13
+ - ๐Ÿท๏ธ **Semantic Version Support** - Intelligent tag finding and version comparison for release automation
14
+ - ๐Ÿ”„ **Branch Management** - Sync checking, safe syncing, and detailed status queries
15
+ - ๐Ÿ”— **NPM Link Management** - Link detection, compatibility checking, and problem diagnosis for monorepo workflows
16
+ - ๐Ÿ“ **Flexible Logging** - Bring your own logger (Winston, Pino, etc.) or use the built-in console logger
17
+ - โœ… **Runtime Validation** - Type-safe JSON parsing and validation utilities
18
+ - ๐Ÿงช **Well-Tested** - Comprehensive test coverage for reliability
13
19
 
14
20
  ## Installation
15
21
 
@@ -17,10 +23,28 @@ Git utilities for automation - secure process execution and comprehensive Git op
17
23
  npm install @eldrforge/git-tools
18
24
  ```
19
25
 
26
+ ### Requirements
27
+
28
+ - Node.js 14 or higher
29
+ - Git 2.0 or higher
30
+ - TypeScript 4.5+ (for TypeScript projects)
31
+
32
+ ### Optional Dependencies
33
+
34
+ ```bash
35
+ # If you want to use Winston for logging
36
+ npm install winston
37
+ ```
38
+
20
39
  ## Quick Start
21
40
 
22
41
  ```typescript
23
- import { getCurrentBranch, getGitStatusSummary, findPreviousReleaseTag } from '@eldrforge/git-tools';
42
+ import {
43
+ getCurrentBranch,
44
+ getGitStatusSummary,
45
+ findPreviousReleaseTag,
46
+ runSecure
47
+ } from '@eldrforge/git-tools';
24
48
 
25
49
  // Get current branch
26
50
  const branch = await getCurrentBranch();
@@ -29,15 +53,40 @@ console.log(`Current branch: ${branch}`);
29
53
  // Get comprehensive status
30
54
  const status = await getGitStatusSummary();
31
55
  console.log(`Status: ${status.status}`);
56
+ console.log(`Unstaged files: ${status.unstagedCount}`);
57
+ console.log(`Uncommitted changes: ${status.uncommittedCount}`);
58
+ console.log(`Unpushed commits: ${status.unpushedCount}`);
32
59
 
33
60
  // Find previous release tag
34
61
  const previousTag = await findPreviousReleaseTag('1.2.3', 'v*');
35
62
  console.log(`Previous release: ${previousTag}`);
63
+
64
+ // Execute Git commands securely
65
+ const { stdout } = await runSecure('git', ['log', '--oneline', '-n', '5']);
66
+ console.log('Recent commits:', stdout);
36
67
  ```
37
68
 
38
- ## Custom Logger
69
+ ## Core Concepts
70
+
71
+ ### 1. Secure Process Execution
39
72
 
40
- By default, git-tools uses a console-based logger. You can provide your own logger implementation (e.g., Winston):
73
+ All process execution functions prioritize security by preventing shell injection attacks:
74
+
75
+ ```typescript
76
+ import { runSecure, run } from '@eldrforge/git-tools';
77
+
78
+ // โœ… SECURE: Uses argument array, no shell interpretation
79
+ const { stdout } = await runSecure('git', ['log', '--format=%s', userInput]);
80
+
81
+ // โš ๏ธ LESS SECURE: Uses shell command string
82
+ const result = await run(`git log --format=%s ${userInput}`);
83
+ ```
84
+
85
+ **Best Practice**: Always use `runSecure` or `runSecureWithDryRunSupport` for user input.
86
+
87
+ ### 2. Custom Logger Integration
88
+
89
+ By default, git-tools uses a console-based logger. You can integrate your own logger:
41
90
 
42
91
  ```typescript
43
92
  import { setLogger } from '@eldrforge/git-tools';
@@ -45,169 +94,647 @@ import winston from 'winston';
45
94
 
46
95
  // Create Winston logger
47
96
  const logger = winston.createLogger({
48
- level: 'info',
49
- format: winston.format.json(),
50
- transports: [new winston.transports.Console()]
97
+ level: 'info',
98
+ format: winston.format.combine(
99
+ winston.format.timestamp(),
100
+ winston.format.json()
101
+ ),
102
+ transports: [
103
+ new winston.transports.Console(),
104
+ new winston.transports.File({ filename: 'git-tools.log' })
105
+ ]
51
106
  });
52
107
 
53
- // Use Winston with git-tools
108
+ // Set global logger for git-tools
54
109
  setLogger(logger);
55
- ```
56
110
 
57
- ## Core Modules
111
+ // Now all git-tools operations will use your logger
112
+ const branch = await getCurrentBranch(); // Logs via Winston
113
+ ```
58
114
 
59
- ### Process Execution (child.ts)
115
+ ### 3. Dry-Run Support
60
116
 
61
- Secure command execution with shell injection prevention:
117
+ Many automation workflows need dry-run capability:
62
118
 
63
119
  ```typescript
64
- import { runSecure, runSecureWithDryRunSupport } from '@eldrforge/git-tools';
120
+ import { runSecureWithDryRunSupport } from '@eldrforge/git-tools';
65
121
 
66
- // Secure execution with argument array
67
- const { stdout } = await runSecure('git', ['status', '--porcelain']);
122
+ const isDryRun = process.env.DRY_RUN === 'true';
68
123
 
69
- // With dry-run support
70
- const result = await runSecureWithDryRunSupport('git', ['commit', '-m', 'message'], isDryRun);
124
+ // This will only log what would happen if isDryRun is true
125
+ const result = await runSecureWithDryRunSupport(
126
+ 'git',
127
+ ['push', 'origin', 'main'],
128
+ isDryRun
129
+ );
71
130
  ```
72
131
 
73
- ### Git Operations (git.ts)
132
+ ## Usage Guide
133
+
134
+ ### Branch Operations
74
135
 
75
- Comprehensive Git utilities:
136
+ #### Check Branch Status
76
137
 
77
- **Branch Operations:**
78
138
  ```typescript
79
139
  import {
80
- getCurrentBranch,
81
- localBranchExists,
82
- remoteBranchExists,
83
- isBranchInSyncWithRemote,
84
- safeSyncBranchWithRemote
140
+ getCurrentBranch,
141
+ localBranchExists,
142
+ remoteBranchExists,
143
+ isBranchInSyncWithRemote
85
144
  } from '@eldrforge/git-tools';
86
145
 
87
- const branch = await getCurrentBranch();
88
- const exists = await localBranchExists('main');
146
+ // Get current branch
147
+ const currentBranch = await getCurrentBranch();
148
+ console.log(`On branch: ${currentBranch}`);
149
+
150
+ // Check if branches exist
151
+ const hasMain = await localBranchExists('main');
152
+ const hasRemoteMain = await remoteBranchExists('main', 'origin');
153
+
154
+ console.log(`Local main exists: ${hasMain}`);
155
+ console.log(`Remote main exists: ${hasRemoteMain}`);
156
+
157
+ // Check if local and remote are in sync
89
158
  const syncStatus = await isBranchInSyncWithRemote('main');
159
+ console.log(`In sync: ${syncStatus.inSync}`);
160
+ console.log(`Local SHA: ${syncStatus.localSha}`);
161
+ console.log(`Remote SHA: ${syncStatus.remoteSha}`);
90
162
  ```
91
163
 
92
- **Tag & Version Operations:**
93
- ```typescript
94
- import {
95
- findPreviousReleaseTag,
96
- getCurrentVersion,
97
- getDefaultFromRef
98
- } from '@eldrforge/git-tools';
164
+ #### Safe Branch Synchronization
99
165
 
100
- const currentVersion = await getCurrentVersion();
101
- const previousTag = await findPreviousReleaseTag('1.2.3', 'v*');
102
- const defaultRef = await getDefaultFromRef(false, 'working');
166
+ ```typescript
167
+ import { safeSyncBranchWithRemote } from '@eldrforge/git-tools';
168
+
169
+ // Safely sync branch with remote (handles conflicts gracefully)
170
+ const result = await safeSyncBranchWithRemote('main', 'origin');
171
+
172
+ if (result.success) {
173
+ console.log('Branch successfully synced with remote');
174
+ } else if (result.conflictResolutionRequired) {
175
+ console.error('Conflict resolution required:', result.error);
176
+ // Handle conflicts manually
177
+ } else {
178
+ console.error('Sync failed:', result.error);
179
+ }
103
180
  ```
104
181
 
105
- **Status Operations:**
182
+ ### Repository Status
183
+
184
+ #### Get Comprehensive Status
185
+
106
186
  ```typescript
107
187
  import { getGitStatusSummary } from '@eldrforge/git-tools';
108
188
 
109
189
  const status = await getGitStatusSummary();
110
- // {
111
- // branch: 'main',
112
- // hasUnstagedFiles: false,
113
- // hasUncommittedChanges: false,
114
- // hasUnpushedCommits: true,
115
- // unstagedCount: 0,
116
- // uncommittedCount: 0,
117
- // unpushedCount: 2,
118
- // status: '2 unpushed'
119
- // }
190
+
191
+ console.log(`Branch: ${status.branch}`);
192
+ console.log(`Status: ${status.status}`); // e.g., "2 unstaged, 1 uncommitted, 3 unpushed"
193
+
194
+ // Individual status flags
195
+ if (status.hasUnstagedFiles) {
196
+ console.log(`โš ๏ธ ${status.unstagedCount} unstaged files`);
197
+ }
198
+
199
+ if (status.hasUncommittedChanges) {
200
+ console.log(`๐Ÿ“ ${status.uncommittedCount} uncommitted changes`);
201
+ }
202
+
203
+ if (status.hasUnpushedCommits) {
204
+ console.log(`โฌ†๏ธ ${status.unpushedCount} unpushed commits`);
205
+ }
206
+
207
+ if (status.status === 'clean') {
208
+ console.log('โœ… Working directory clean');
209
+ }
210
+ ```
211
+
212
+ #### Check if Directory is a Git Repository
213
+
214
+ ```typescript
215
+ import { isGitRepository } from '@eldrforge/git-tools';
216
+
217
+ const isRepo = await isGitRepository('/path/to/directory');
218
+ if (isRepo) {
219
+ console.log('This is a Git repository');
220
+ } else {
221
+ console.log('Not a Git repository');
222
+ }
223
+ ```
224
+
225
+ ### Version and Tag Operations
226
+
227
+ #### Find Previous Release Tag
228
+
229
+ Useful for generating release notes or comparing versions:
230
+
231
+ ```typescript
232
+ import { findPreviousReleaseTag, getCurrentVersion } from '@eldrforge/git-tools';
233
+
234
+ // Get current version from package.json
235
+ const currentVersion = await getCurrentVersion();
236
+ console.log(`Current version: ${currentVersion}`);
237
+
238
+ // Find previous release tag
239
+ // Looks for tags matching "v*" pattern that are < current version
240
+ const previousTag = await findPreviousReleaseTag(currentVersion, 'v*');
241
+
242
+ if (previousTag) {
243
+ console.log(`Previous release: ${previousTag}`);
244
+ // Now you can generate release notes from previousTag..HEAD
245
+ } else {
246
+ console.log('No previous release found (possibly first release)');
247
+ }
120
248
  ```
121
249
 
122
- **NPM Link Operations:**
250
+ #### Working with Tag Patterns
251
+
252
+ ```typescript
253
+ import { findPreviousReleaseTag } from '@eldrforge/git-tools';
254
+
255
+ // Standard version tags (v1.0.0, v1.2.3)
256
+ const prevRelease = await findPreviousReleaseTag('1.2.3', 'v*');
257
+
258
+ // Working branch tags (working/v1.0.0)
259
+ const prevWorking = await findPreviousReleaseTag('1.2.3', 'working/v*');
260
+
261
+ // Custom prefix tags (release/v1.0.0)
262
+ const prevCustom = await findPreviousReleaseTag('1.2.3', 'release/v*');
263
+ ```
264
+
265
+ #### Get Default Reference for Comparisons
266
+
267
+ ```typescript
268
+ import { getDefaultFromRef } from '@eldrforge/git-tools';
269
+
270
+ // Intelligently determines the best reference for release comparisons
271
+ // Tries: previous tag -> main -> master -> origin/main -> origin/master
272
+ const fromRef = await getDefaultFromRef(false, 'working');
273
+ console.log(`Compare from: ${fromRef}`);
274
+
275
+ // Force main branch (skip tag detection)
276
+ const mainRef = await getDefaultFromRef(true);
277
+ console.log(`Compare from main: ${mainRef}`);
278
+ ```
279
+
280
+ ### NPM Link Management
281
+
282
+ Perfect for monorepo development and local package testing:
283
+
284
+ #### Check Link Status
285
+
123
286
  ```typescript
124
287
  import {
125
- getGloballyLinkedPackages,
126
- getLinkedDependencies,
127
- getLinkCompatibilityProblems,
128
- isNpmLinked
288
+ isNpmLinked,
289
+ getGloballyLinkedPackages,
290
+ getLinkedDependencies
129
291
  } from '@eldrforge/git-tools';
130
292
 
293
+ // Check if a package is globally linked
294
+ const isLinked = await isNpmLinked('/path/to/my-package');
295
+ console.log(`Package is linked: ${isLinked}`);
296
+
297
+ // Get all globally linked packages
131
298
  const globalPackages = await getGloballyLinkedPackages();
132
- const linkedDeps = await getLinkedDependencies('/path/to/package');
299
+ console.log('Globally linked packages:', Array.from(globalPackages));
300
+
301
+ // Get packages that this project is linked to (consuming)
302
+ const linkedDeps = await getLinkedDependencies('/path/to/consumer');
303
+ console.log('Consuming linked packages:', Array.from(linkedDeps));
304
+ ```
305
+
306
+ #### Detect Link Compatibility Problems
307
+
308
+ ```typescript
309
+ import { getLinkCompatibilityProblems } from '@eldrforge/git-tools';
310
+
311
+ // Check for version compatibility issues with linked dependencies
133
312
  const problems = await getLinkCompatibilityProblems('/path/to/package');
134
- const isLinked = await isNpmLinked('/path/to/package');
313
+
314
+ if (problems.size > 0) {
315
+ console.error('โš ๏ธ Link compatibility problems detected:');
316
+ for (const packageName of problems) {
317
+ console.error(` - ${packageName}`);
318
+ }
319
+ } else {
320
+ console.log('โœ… All linked dependencies are compatible');
321
+ }
135
322
  ```
136
323
 
137
- ### Validation (validation.ts)
324
+ **Note**: `getLinkCompatibilityProblems` intelligently handles prerelease versions (e.g., `4.4.53-dev.0` is compatible with `^4.4`).
325
+
326
+ ### Process Execution
138
327
 
139
- Runtime type validation utilities:
328
+ #### Secure Command Execution
329
+
330
+ ```typescript
331
+ import { runSecure, runSecureWithInheritedStdio } from '@eldrforge/git-tools';
332
+
333
+ // Execute and capture output
334
+ const { stdout, stderr } = await runSecure('git', ['status', '--porcelain']);
335
+ console.log(stdout);
336
+
337
+ // Execute with inherited stdio (output goes directly to terminal)
338
+ await runSecureWithInheritedStdio('git', ['push', 'origin', 'main']);
339
+ ```
340
+
341
+ #### Suppress Error Logging
342
+
343
+ Some commands are expected to fail in certain scenarios:
344
+
345
+ ```typescript
346
+ import { runSecure } from '@eldrforge/git-tools';
347
+
348
+ try {
349
+ // Check if a branch exists without logging errors
350
+ await runSecure('git', ['rev-parse', '--verify', 'feature-branch'], {
351
+ suppressErrorLogging: true
352
+ });
353
+ console.log('Branch exists');
354
+ } catch (error) {
355
+ console.log('Branch does not exist');
356
+ }
357
+ ```
358
+
359
+ #### Input Validation
360
+
361
+ ```typescript
362
+ import { validateGitRef, validateFilePath } from '@eldrforge/git-tools';
363
+
364
+ const userBranch = getUserInput();
365
+
366
+ // Validate before using in commands
367
+ if (validateGitRef(userBranch)) {
368
+ await runSecure('git', ['checkout', userBranch]);
369
+ } else {
370
+ console.error('Invalid branch name');
371
+ }
372
+
373
+ const userFile = getUserInput();
374
+ if (validateFilePath(userFile)) {
375
+ await runSecure('git', ['add', userFile]);
376
+ } else {
377
+ console.error('Invalid file path');
378
+ }
379
+ ```
380
+
381
+ ### Validation Utilities
382
+
383
+ #### Safe JSON Parsing
384
+
385
+ ```typescript
386
+ import { safeJsonParse, validatePackageJson } from '@eldrforge/git-tools';
387
+
388
+ // Parse JSON with automatic error handling
389
+ try {
390
+ const data = safeJsonParse(jsonString, 'config.json');
391
+ console.log(data);
392
+ } catch (error) {
393
+ console.error('Failed to parse JSON:', error.message);
394
+ }
395
+
396
+ // Validate package.json structure
397
+ try {
398
+ const packageJson = safeJsonParse(fileContents, 'package.json');
399
+ const validated = validatePackageJson(packageJson, 'package.json');
400
+
401
+ console.log(`Package: ${validated.name}`);
402
+ console.log(`Version: ${validated.version}`);
403
+ } catch (error) {
404
+ console.error('Invalid package.json:', error.message);
405
+ }
406
+ ```
407
+
408
+ #### String Validation
409
+
410
+ ```typescript
411
+ import { validateString, validateHasProperty } from '@eldrforge/git-tools';
412
+
413
+ // Validate non-empty string
414
+ try {
415
+ const username = validateString(userInput, 'username');
416
+ console.log(`Valid username: ${username}`);
417
+ } catch (error) {
418
+ console.error('Invalid username:', error.message);
419
+ }
420
+
421
+ // Validate object has required property
422
+ try {
423
+ validateHasProperty(config, 'apiKey', 'config.json');
424
+ console.log('Config has required apiKey');
425
+ } catch (error) {
426
+ console.error('Missing required property:', error.message);
427
+ }
428
+ ```
429
+
430
+ ## Practical Examples
431
+
432
+ ### Example 1: Release Note Generator
433
+
434
+ ```typescript
435
+ import {
436
+ getCurrentVersion,
437
+ findPreviousReleaseTag,
438
+ runSecure
439
+ } from '@eldrforge/git-tools';
440
+
441
+ async function generateReleaseNotes() {
442
+ // Get version range
443
+ const currentVersion = await getCurrentVersion();
444
+ const previousTag = await findPreviousReleaseTag(currentVersion, 'v*');
445
+
446
+ if (!previousTag) {
447
+ console.log('No previous release found');
448
+ return;
449
+ }
450
+
451
+ // Get commits between tags
452
+ const { stdout } = await runSecure('git', [
453
+ 'log',
454
+ `${previousTag}..HEAD`,
455
+ '--pretty=format:%s',
456
+ '--no-merges'
457
+ ]);
458
+
459
+ const commits = stdout.trim().split('\n');
460
+
461
+ console.log(`Release Notes for ${currentVersion}`);
462
+ console.log(`Changes since ${previousTag}:`);
463
+ console.log('');
464
+ commits.forEach(commit => console.log(`- ${commit}`));
465
+ }
466
+
467
+ generateReleaseNotes().catch(console.error);
468
+ ```
469
+
470
+ ### Example 2: Pre-Push Validation
471
+
472
+ ```typescript
473
+ import {
474
+ getGitStatusSummary,
475
+ isBranchInSyncWithRemote
476
+ } from '@eldrforge/git-tools';
477
+
478
+ async function validateBeforePush() {
479
+ const status = await getGitStatusSummary();
480
+
481
+ // Check for uncommitted changes
482
+ if (status.hasUnstagedFiles || status.hasUncommittedChanges) {
483
+ console.error('โŒ Cannot push with uncommitted changes');
484
+ return false;
485
+ }
486
+
487
+ // Check if in sync with remote
488
+ const syncStatus = await isBranchInSyncWithRemote(status.branch);
489
+
490
+ if (!syncStatus.inSync) {
491
+ console.error('โŒ Branch not in sync with remote');
492
+ console.error(`Local: ${syncStatus.localSha}`);
493
+ console.error(`Remote: ${syncStatus.remoteSha}`);
494
+ return false;
495
+ }
496
+
497
+ console.log('โœ… Ready to push');
498
+ return true;
499
+ }
500
+
501
+ validateBeforePush().catch(console.error);
502
+ ```
503
+
504
+ ### Example 3: Monorepo Link Checker
140
505
 
141
506
  ```typescript
142
507
  import {
143
- safeJsonParse,
144
- validateString,
145
- validatePackageJson
508
+ getLinkedDependencies,
509
+ getLinkCompatibilityProblems
146
510
  } from '@eldrforge/git-tools';
147
511
 
148
- const data = safeJsonParse(jsonString, 'context');
149
- const validated = validateString(value, 'fieldName');
150
- const packageJson = validatePackageJson(data, 'package.json');
512
+ async function checkMonorepoLinks(packageDirs: string[]) {
513
+ for (const packageDir of packageDirs) {
514
+ console.log(`\nChecking: ${packageDir}`);
515
+
516
+ const linked = await getLinkedDependencies(packageDir);
517
+ console.log(`Linked dependencies: ${Array.from(linked).join(', ') || 'none'}`);
518
+
519
+ const problems = await getLinkCompatibilityProblems(packageDir);
520
+
521
+ if (problems.size > 0) {
522
+ console.error('โš ๏ธ Compatibility issues:');
523
+ for (const pkg of problems) {
524
+ console.error(` - ${pkg}`);
525
+ }
526
+ } else {
527
+ console.log('โœ… All links compatible');
528
+ }
529
+ }
530
+ }
531
+
532
+ checkMonorepoLinks([
533
+ './packages/core',
534
+ './packages/cli',
535
+ './packages/utils'
536
+ ]).catch(console.error);
151
537
  ```
152
538
 
153
- ## API Documentation
539
+ ### Example 4: Branch Sync Script
540
+
541
+ ```typescript
542
+ import {
543
+ getCurrentBranch,
544
+ localBranchExists,
545
+ safeSyncBranchWithRemote
546
+ } from '@eldrforge/git-tools';
547
+
548
+ async function syncMainBranch() {
549
+ const currentBranch = await getCurrentBranch();
550
+ const hasMain = await localBranchExists('main');
551
+
552
+ if (!hasMain) {
553
+ console.error('โŒ Main branch does not exist locally');
554
+ return;
555
+ }
556
+
557
+ console.log(`Current branch: ${currentBranch}`);
558
+ console.log('Syncing main branch with remote...');
559
+
560
+ const result = await safeSyncBranchWithRemote('main');
561
+
562
+ if (result.success) {
563
+ console.log('โœ… Main branch synced successfully');
564
+ } else if (result.conflictResolutionRequired) {
565
+ console.error('โŒ Conflict resolution required');
566
+ console.error(result.error);
567
+ } else {
568
+ console.error('โŒ Sync failed:', result.error);
569
+ }
570
+ }
571
+
572
+ syncMainBranch().catch(console.error);
573
+ ```
574
+
575
+ ## API Reference
154
576
 
155
577
  ### Git Functions
156
578
 
157
- | Function | Description |
158
- |----------|-------------|
159
- | `isValidGitRef(ref)` | Tests if a git reference exists and is valid |
160
- | `findPreviousReleaseTag(version, pattern?)` | Finds the highest tag less than current version |
161
- | `getCurrentVersion()` | Gets current version from package.json |
162
- | `getDefaultFromRef(forceMain?, branch?)` | Gets reliable default for release comparison |
163
- | `getRemoteDefaultBranch()` | Gets default branch name from remote |
164
- | `localBranchExists(branch)` | Checks if local branch exists |
165
- | `remoteBranchExists(branch, remote?)` | Checks if remote branch exists |
166
- | `getBranchCommitSha(ref)` | Gets commit SHA for a branch |
167
- | `isBranchInSyncWithRemote(branch, remote?)` | Checks if local/remote branches match |
168
- | `safeSyncBranchWithRemote(branch, remote?)` | Safely syncs branch with remote |
169
- | `getCurrentBranch()` | Gets current branch name |
170
- | `getGitStatusSummary(workingDir?)` | Gets comprehensive git status |
171
- | `getGloballyLinkedPackages()` | Gets globally linked npm packages |
172
- | `getLinkedDependencies(packageDir)` | Gets linked dependencies for package |
173
- | `getLinkCompatibilityProblems(packageDir)` | Finds version compatibility issues |
174
- | `isNpmLinked(packageDir)` | Checks if package is globally linked |
175
- | `getBranchNameForVersion(version)` | Gets branch name for a version |
579
+ | Function | Parameters | Returns | Description |
580
+ |----------|------------|---------|-------------|
581
+ | `isValidGitRef(ref)` | `ref: string` | `Promise<boolean>` | Tests if a git reference exists and is valid |
582
+ | `isGitRepository(cwd?)` | `cwd?: string` | `Promise<boolean>` | Checks if directory is a git repository |
583
+ | `findPreviousReleaseTag(version, pattern?)` | `version: string, pattern?: string` | `Promise<string \| null>` | Finds highest tag less than current version |
584
+ | `getCurrentVersion()` | - | `Promise<string \| null>` | Gets current version from package.json |
585
+ | `getCurrentBranch()` | - | `Promise<string>` | Gets current branch name |
586
+ | `getDefaultFromRef(forceMain?, branch?)` | `forceMain?: boolean, branch?: string` | `Promise<string>` | Gets reliable default for release comparison |
587
+ | `getRemoteDefaultBranch(cwd?)` | `cwd?: string` | `Promise<string \| null>` | Gets default branch name from remote |
588
+ | `localBranchExists(branch)` | `branch: string` | `Promise<boolean>` | Checks if local branch exists |
589
+ | `remoteBranchExists(branch, remote?)` | `branch: string, remote?: string` | `Promise<boolean>` | Checks if remote branch exists |
590
+ | `getBranchCommitSha(ref)` | `ref: string` | `Promise<string>` | Gets commit SHA for a branch |
591
+ | `isBranchInSyncWithRemote(branch, remote?)` | `branch: string, remote?: string` | `Promise<SyncStatus>` | Checks if local/remote branches match |
592
+ | `safeSyncBranchWithRemote(branch, remote?)` | `branch: string, remote?: string` | `Promise<SyncResult>` | Safely syncs branch with remote |
593
+ | `getGitStatusSummary(workingDir?)` | `workingDir?: string` | `Promise<GitStatus>` | Gets comprehensive git status |
594
+ | `getGloballyLinkedPackages()` | - | `Promise<Set<string>>` | Gets globally linked npm packages |
595
+ | `getLinkedDependencies(packageDir)` | `packageDir: string` | `Promise<Set<string>>` | Gets linked dependencies for package |
596
+ | `getLinkCompatibilityProblems(packageDir)` | `packageDir: string` | `Promise<Set<string>>` | Finds version compatibility issues |
597
+ | `isNpmLinked(packageDir)` | `packageDir: string` | `Promise<boolean>` | Checks if package is globally linked |
176
598
 
177
599
  ### Process Execution Functions
178
600
 
179
- | Function | Description |
180
- |----------|-------------|
181
- | `runSecure(cmd, args, opts?)` | Securely executes command with argument array |
182
- | `runSecureWithInheritedStdio(cmd, args, opts?)` | Secure execution with inherited stdio |
183
- | `run(command, opts?)` | Executes command string (less secure) |
184
- | `runWithDryRunSupport(cmd, dryRun, opts?)` | Run with dry-run support |
185
- | `runSecureWithDryRunSupport(cmd, args, dryRun, opts?)` | Secure run with dry-run support |
186
- | `validateGitRef(ref)` | Validates git reference for injection |
187
- | `validateFilePath(path)` | Validates file path for injection |
188
- | `escapeShellArg(arg)` | Escapes shell arguments |
601
+ | Function | Parameters | Returns | Description |
602
+ |----------|------------|---------|-------------|
603
+ | `runSecure(cmd, args, opts?)` | `cmd: string, args: string[], opts?: RunSecureOptions` | `Promise<{stdout, stderr}>` | Securely executes command with argument array |
604
+ | `runSecureWithInheritedStdio(cmd, args, opts?)` | `cmd: string, args: string[], opts?: SpawnOptions` | `Promise<void>` | Secure execution with inherited stdio |
605
+ | `run(command, opts?)` | `command: string, opts?: RunOptions` | `Promise<{stdout, stderr}>` | Executes command string (less secure) |
606
+ | `runWithDryRunSupport(cmd, dryRun, opts?)` | `cmd: string, dryRun: boolean, opts?: ExecOptions` | `Promise<{stdout, stderr}>` | Run with dry-run support |
607
+ | `runSecureWithDryRunSupport(cmd, args, dryRun, opts?)` | `cmd: string, args: string[], dryRun: boolean, opts?: SpawnOptions` | `Promise<{stdout, stderr}>` | Secure run with dry-run support |
608
+ | `validateGitRef(ref)` | `ref: string` | `boolean` | Validates git reference for injection |
609
+ | `validateFilePath(path)` | `path: string` | `boolean` | Validates file path for injection |
610
+ | `escapeShellArg(arg)` | `arg: string` | `string` | Escapes shell arguments |
611
+
612
+ ### Logger Functions
613
+
614
+ | Function | Parameters | Returns | Description |
615
+ |----------|------------|---------|-------------|
616
+ | `setLogger(logger)` | `logger: Logger` | `void` | Sets the global logger instance |
617
+ | `getLogger()` | - | `Logger` | Gets the global logger instance |
189
618
 
190
619
  ### Validation Functions
191
620
 
192
- | Function | Description |
193
- |----------|-------------|
194
- | `safeJsonParse<T>(json, context?)` | Safely parses JSON with error handling |
195
- | `validateString(value, fieldName)` | Validates non-empty string |
196
- | `validateHasProperty(obj, property, context?)` | Validates object has property |
197
- | `validatePackageJson(data, context?, requireName?)` | Validates package.json structure |
621
+ | Function | Parameters | Returns | Description |
622
+ |----------|------------|---------|-------------|
623
+ | `safeJsonParse<T>(json, context?)` | `json: string, context?: string` | `T` | Safely parses JSON with error handling |
624
+ | `validateString(value, fieldName)` | `value: any, fieldName: string` | `string` | Validates non-empty string |
625
+ | `validateHasProperty(obj, property, context?)` | `obj: any, property: string, context?: string` | `void` | Validates object has property |
626
+ | `validatePackageJson(data, context?, requireName?)` | `data: any, context?: string, requireName?: boolean` | `any` | Validates package.json structure |
198
627
 
199
- ## Security
628
+ ### Type Definitions
629
+
630
+ ```typescript
631
+ interface GitStatus {
632
+ branch: string;
633
+ hasUnstagedFiles: boolean;
634
+ hasUncommittedChanges: boolean;
635
+ hasUnpushedCommits: boolean;
636
+ unstagedCount: number;
637
+ uncommittedCount: number;
638
+ unpushedCount: number;
639
+ status: string;
640
+ }
641
+
642
+ interface SyncStatus {
643
+ inSync: boolean;
644
+ localSha?: string;
645
+ remoteSha?: string;
646
+ localExists: boolean;
647
+ remoteExists: boolean;
648
+ error?: string;
649
+ }
650
+
651
+ interface SyncResult {
652
+ success: boolean;
653
+ error?: string;
654
+ conflictResolutionRequired?: boolean;
655
+ }
656
+
657
+ interface Logger {
658
+ error(message: string, ...meta: any[]): void;
659
+ warn(message: string, ...meta: any[]): void;
660
+ info(message: string, ...meta: any[]): void;
661
+ verbose(message: string, ...meta: any[]): void;
662
+ debug(message: string, ...meta: any[]): void;
663
+ }
664
+
665
+ interface RunSecureOptions extends SpawnOptions {
666
+ suppressErrorLogging?: boolean;
667
+ }
668
+
669
+ interface RunOptions extends ExecOptions {
670
+ suppressErrorLogging?: boolean;
671
+ }
672
+ ```
673
+
674
+ ## Security Considerations
200
675
 
201
676
  This library prioritizes security in command execution:
202
677
 
203
- - **Shell Injection Prevention**: All `runSecure*` functions use argument arrays without shell execution
204
- - **Input Validation**: Git references and file paths are validated before use
205
- - **No Shell Metacharacters**: Commands are executed directly without shell interpretation
206
- - **Escaped Arguments**: Shell argument escaping utilities provided
678
+ ### Shell Injection Prevention
679
+
680
+ All `runSecure*` functions use argument arrays without shell execution:
681
+
682
+ ```typescript
683
+ // โœ… SAFE: No shell interpretation
684
+ await runSecure('git', ['log', userInput]);
685
+
686
+ // โš ๏ธ UNSAFE: Shell interprets special characters
687
+ await run(`git log ${userInput}`);
688
+ ```
689
+
690
+ ### Input Validation
691
+
692
+ Git references and file paths are validated before use:
693
+
694
+ ```typescript
695
+ // Validates against: .., leading -, shell metacharacters
696
+ if (!validateGitRef(userRef)) {
697
+ throw new Error('Invalid git reference');
698
+ }
699
+
700
+ // Validates against: shell metacharacters
701
+ if (!validateFilePath(userPath)) {
702
+ throw new Error('Invalid file path');
703
+ }
704
+ ```
705
+
706
+ ### Best Practices
707
+
708
+ 1. **Always use `runSecure` for user input**
709
+ 2. **Validate all git references with `validateGitRef`**
710
+ 3. **Validate all file paths with `validateFilePath`**
711
+ 4. **Use `suppressErrorLogging` to avoid leaking sensitive info**
712
+ 5. **Set custom logger for production environments**
713
+
714
+ ## Testing
715
+
716
+ The library includes comprehensive test coverage:
717
+
718
+ ```bash
719
+ # Run tests
720
+ npm test
721
+
722
+ # Run tests with coverage
723
+ npm run test
724
+
725
+ # Watch mode
726
+ npm run watch
727
+ ```
207
728
 
208
729
  ## Development
209
730
 
731
+ ### Building from Source
732
+
210
733
  ```bash
734
+ # Clone the repository
735
+ git clone https://github.com/calenvarek/git-tools.git
736
+ cd git-tools
737
+
211
738
  # Install dependencies
212
739
  npm install
213
740
 
@@ -219,20 +746,63 @@ npm run test
219
746
 
220
747
  # Lint
221
748
  npm run lint
222
-
223
- # Watch mode
224
- npm run watch
225
749
  ```
226
750
 
751
+ ### Contributing
752
+
753
+ Contributions are welcome! Please ensure:
754
+
755
+ 1. All tests pass: `npm test`
756
+ 2. Code is linted: `npm run lint`
757
+ 3. Add tests for new features
758
+ 4. Update documentation for API changes
759
+
760
+ ## Troubleshooting
761
+
762
+ ### Common Issues
763
+
764
+ **"Command failed with exit code 128"**
765
+ - Check if the directory is a git repository
766
+ - Verify git is installed and accessible
767
+ - Check git configuration
768
+
769
+ **"Invalid git reference"**
770
+ - Ensure branch/tag names don't contain special characters
771
+ - Verify the reference exists: `git rev-parse --verify <ref>`
772
+
773
+ **"Branch not in sync"**
774
+ - Run `git fetch` to update remote refs
775
+ - Use `safeSyncBranchWithRemote` to sync automatically
776
+
777
+ **NPM link detection not working**
778
+ - Verify package is globally linked: `npm ls -g <package-name>`
779
+ - Check symlinks in global node_modules: `npm prefix -g`
780
+
227
781
  ## License
228
782
 
229
783
  Apache-2.0 - see [LICENSE](LICENSE) file for details.
230
784
 
231
785
  ## Author
232
786
 
233
- Calen Varek <calenvarek@gmail.com>
787
+ **Calen Varek**
788
+ Email: calenvarek@gmail.com
789
+ GitHub: [@calenvarek](https://github.com/calenvarek)
234
790
 
235
791
  ## Related Projects
236
792
 
237
- This library was extracted from [kodrdriv](https://github.com/calenvarek/kodrdriv), an AI-powered Git workflow automation tool.
793
+ This library was extracted from [kodrdriv](https://github.com/calenvarek/kodrdriv), an AI-powered Git workflow automation tool that uses these utilities for:
794
+
795
+ - Automated commit message generation
796
+ - Release note creation
797
+ - Branch management
798
+ - Monorepo publishing workflows
799
+
800
+ ## Changelog
801
+
802
+ See [RELEASE_NOTES.md](RELEASE_NOTES.md) for version history and changes.
803
+
804
+ ## Support
238
805
 
806
+ - ๐Ÿ› **Bug Reports**: [GitHub Issues](https://github.com/calenvarek/git-tools/issues)
807
+ - ๐Ÿ’ฌ **Questions**: [GitHub Discussions](https://github.com/calenvarek/git-tools/discussions)
808
+ - ๐Ÿ“ง **Email**: calenvarek@gmail.com
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eldrforge/git-tools",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "Git utilities for automation - secure process execution and Git operations",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",