github-update-submodule 1.2.1 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,5 @@
1
1
  # github-update-submodule
2
2
 
3
- [![npm version](https://badge.fury.io/js/github-update-submodule.svg)](https://badge.fury.io/js/github-update-submodule)
4
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
- [![Node.js Version](https://img.shields.io/badge/node-%3E%3D14.0.0-brightgreen.svg)](https://nodejs.org/)
6
- [![Downloads](https://img.shields.io/npm/dm/github-update-submodule.svg)](https://www.npmjs.com/package/github-update-submodule)
7
-
8
3
  > Recursively pull all Git submodules to their latest remote commit and push the updated refs up every parent repo — so **GitHub always points to the latest commit** in every submodule, no matter how deeply nested.
9
4
 
10
5
  ---
@@ -18,25 +13,9 @@ GitHub (parent repo) ──pins──▶ old commit ❌
18
13
  Your local submodule latest commit ✅
19
14
  ```
20
15
 
21
- **Common scenarios where this becomes painful:**
22
- - **Microservices architectures** with shared libraries as submodules
23
- - **Documentation sites** that embed multiple component repositories
24
- - **Monorepo workflows** using submodules for versioned dependencies
25
- - **CI/CD pipelines** that need to ensure all submodules are up-to-date
26
- - **Multi-repo projects** with complex dependency trees
27
-
28
- Without automation, updating submodules requires:
29
- 1. Manually traversing each submodule directory
30
- 2. Pulling the latest changes
31
- 3. Committing and pushing the updated pointer
32
- 4. Repeating for nested submodules in the correct order
33
- 5. Handling merge conflicts and branch resolution
34
-
35
- This process is error-prone, time-consuming, and doesn't scale.
36
-
37
16
  ## The Solution
38
17
 
39
- **One command from any repo with submodules:**
18
+ One command from any repo with submodules:
40
19
 
41
20
  ```bash
42
21
  github-update-submodule
@@ -44,26 +23,6 @@ github-update-submodule
44
23
 
45
24
  Everything is handled automatically — pull, commit, push — all the way down the tree and back up again.
46
25
 
47
- ### Key Benefits
48
-
49
- - **🚀 Zero Configuration** - Works out of the box with any Git repository using submodules
50
- - **🔄 Recursive Updates** - Handles deeply nested submodules in the correct dependency order
51
- - **⚡ Parallel Processing** - Optionally fetch all submodules concurrently for massive speedup
52
- - **🔒 Safe Operations** - Interactive mode, dry-run previews, and comprehensive error handling
53
- - **📊 Rich Feedback** - Progress bars, GitHub compare links, and detailed statistics
54
- - **⚙️ Highly Configurable** - Config files, CLI flags, and ignore patterns for complex workflows
55
- - **🎯 Production Ready** - Battle-tested in enterprise environments with comprehensive edge case handling
56
-
57
- ### What It Does
58
-
59
- 1. **Discovers** all submodules recursively (respects `.gitmodules` configuration)
60
- 2. **Fetches** latest changes from remote repositories (in parallel when requested)
61
- 3. **Resolves** correct branches (`.gitmodules` → remote HEAD → fallback)
62
- 4. **Updates** submodule pointers to latest commits
63
- 5. **Commits** changes with descriptive messages
64
- 6. **Pushes** updates in dependency order (innermost first)
65
- 7. **Reports** GitHub compare URLs for easy review
66
-
67
26
  ---
68
27
 
69
28
  ## Installation
@@ -74,41 +33,6 @@ npm install -g github-update-submodule
74
33
 
75
34
  ---
76
35
 
77
- ## Table of Contents
78
-
79
- - [Quick Start](#quick-start)
80
- - [Usage Examples](#usage-examples)
81
- - [Command Line Options](#command-line-options)
82
- - [Configuration File](#configuration-file)
83
- - [How It Works](#how-it-works)
84
- - [Prerequisites](#prerequisites)
85
- - [Troubleshooting](#troubleshooting)
86
- - [Advanced Usage](#advanced-usage)
87
- - [Performance Considerations](#performance-considerations)
88
- - [Security](#security)
89
- - [Contributing](#contributing)
90
- - [License](#license)
91
-
92
- ---
93
-
94
- ## Quick Start
95
-
96
- ```bash
97
- # Install globally
98
- npm install -g github-update-submodule
99
-
100
- # Navigate to your repository with submodules
101
- cd your-project
102
-
103
- # Update all submodules to latest commits
104
- github-update-submodule
105
-
106
- # Preview what would change without making any modifications
107
- github-update-submodule --dry-run
108
- ```
109
-
110
- ---
111
-
112
36
  ## Usage
113
37
 
114
38
  ```bash
@@ -303,616 +227,11 @@ Type `y` to push or anything else to skip that repo.
303
227
 
304
228
  ---
305
229
 
306
- ## Advanced Usage
307
-
308
- ### Real-World Scenarios
309
-
310
- #### 1. CI/CD Pipeline Integration
311
-
312
- **GitHub Actions Example:**
313
- ```yaml
314
- name: Update Submodules
315
- on:
316
- schedule:
317
- - cron: '0 2 * * *' # Daily at 2 AM
318
- workflow_dispatch:
319
-
320
- jobs:
321
- update:
322
- runs-on: ubuntu-latest
323
- steps:
324
- - uses: actions/checkout@v3
325
- with:
326
- token: ${{ secrets.PAT }} # Personal Access Token
327
- fetch-depth: 0
328
-
329
- - name: Setup Node.js
330
- uses: actions/setup-node@v3
331
- with:
332
- node-version: '18'
333
-
334
- - name: Install github-update-submodule
335
- run: npm install -g github-update-submodule
336
-
337
- - name: Update all submodules
338
- run: |
339
- github-update-submodule \
340
- --parallel \
341
- --message "ci: automated submodule update" \
342
- --verbose
343
- ```
344
-
345
- #### 2. Monorepo with Shared Libraries
346
-
347
- **Scenario:** Frontend application with shared component libraries
348
-
349
- ```bash
350
- # Update only production dependencies
351
- github-update-submodule \
352
- --ignore docs \
353
- --ignore examples \
354
- --ignore staging-components \
355
- --message "chore: update production submodule refs"
356
-
357
- # Interactive review for staging environment
358
- github-update-submodule \
359
- --interactive \
360
- --branch staging \
361
- --dry-run
362
- ```
363
-
364
- #### 3. Documentation Site with Multiple Sources
365
-
366
- **Scenario:** Docs site embedding content from multiple repositories
367
-
368
- ```bash
369
- # Update documentation submodules only
370
- github-update-submodule \
371
- --ignore frontend \
372
- --ignore backend \
373
- --ignore api \
374
- --message "docs: update documentation sources"
375
-
376
- # Parallel update for faster builds
377
- github-update-submodule \
378
- --parallel \
379
- --depth 2 \
380
- --verbose
381
- ```
382
-
383
- #### 4. Microservices Architecture
384
-
385
- **Scenario:** Main repo with multiple service submodules
386
-
387
- ```bash
388
- # Configuration file for team consistency
389
- cat > submodule.config.json << EOF
390
- {
391
- "defaultBranch": "main",
392
- "parallel": true,
393
- "ignore": ["legacy-service", "experimental-feature"],
394
- "commitMessage": "chore: update service submodule refs",
395
- "interactive": false,
396
- "verbose": true,
397
- "color": true,
398
- "progress": true
399
- }
400
- EOF
401
-
402
- # Update with team defaults
403
- github-update-submodule
404
- ```
405
-
406
- ### Best Practices
407
-
408
- #### 1. Branch Strategy
409
- ```bash
410
- # Development environment
411
- github-update-submodule --branch develop --message "chore: update dev refs"
412
-
413
- # Production updates with care
414
- github-update-submodule --interactive --branch main --dry-run
415
- ```
416
-
417
- #### 2. Team Workflows
418
- ```bash
419
- # Generate team config file
420
- github-update-submodule --make-config
421
-
422
- # Commit the config for consistency
423
- git add submodule.config.json
424
- git commit -m "Add submodule update configuration"
425
- ```
426
-
427
- #### 3. Safety First
428
- ```bash
429
- # Always preview first
430
- github-update-submodule --dry-run --verbose
431
-
432
- # Then run the actual update
433
- github-update-submodule
434
-
435
- # Or use interactive mode for critical repos
436
- github-update-submodule --interactive
437
- ```
438
-
439
- #### 4. Performance Optimization
440
- ```bash
441
- # Large repositories - use parallel and depth limiting
442
- github-update-submodule --parallel --depth 3 --verbose
443
-
444
- # Network-constrained environments
445
- github-update-submodule --ignore heavy-assets --no-progress
446
- ```
447
-
448
- ### Integration Examples
449
-
450
- #### Git Hooks
451
- ```bash
452
- # .git/hooks/pre-push
453
- #!/bin/bash
454
- echo "Checking submodule status..."
455
- github-update-submodule --dry-run --no-color
456
-
457
- if [ $? -ne 0 ]; then
458
- echo "⚠️ Submodules need updating. Run 'github-update-submodule' first."
459
- exit 1
460
- fi
461
- ```
462
-
463
- #### Makefile Integration
464
- ```makefile
465
- # Makefile
466
- .PHONY: update-submodules
467
- update-submodules:
468
- @echo "Updating all submodules..."
469
- github-update-submodule --parallel --verbose
470
-
471
- .PHONY: check-submodules
472
- check-submodules:
473
- @echo "Checking submodule status..."
474
- github-update-submodule --dry-run
475
- ```
476
-
477
- #### Docker Integration
478
- ```dockerfile
479
- # Dockerfile
480
- FROM node:18-alpine
481
-
482
- RUN npm install -g github-update-submodule
483
-
484
- WORKDIR /app
485
- COPY . .
486
-
487
- # Update submodules during build
488
- RUN github-update-submodule --no-push --verbose
489
- ```
490
-
491
- ---
492
-
493
- ## Prerequisites
494
-
495
- ### System Requirements
496
-
497
- - **Node.js** >= 14.0.0
498
- - **Git** installed and available in your PATH
499
- - **Remote authentication** set up (SSH keys or credential manager) so pushes don't require password prompts
500
-
501
- ### Git Configuration
502
-
503
- Ensure your Git is properly configured:
504
-
505
- ```bash
506
- # Set your identity (required for commits)
507
- git config --global user.name "Your Name"
508
- git config --global user.email "your.email@example.com"
509
-
510
- # Verify remote access
511
- git ls-remote origin
512
- ```
513
-
514
- ### Authentication Setup
515
-
516
- #### SSH Keys (Recommended)
517
- ```bash
518
- # Generate SSH key if you don't have one
519
- ssh-keygen -t ed25519 -C "your.email@example.com"
520
-
521
- # Add to GitHub
522
- cat ~/.ssh/id_ed25519.pub
523
- # Copy the output and add it to GitHub > Settings > SSH and GPG keys
524
- ```
525
-
526
- #### Personal Access Token
527
- ```bash
528
- # Configure Git to use token
529
- git config --global credential.helper store
530
- # Git will prompt for username and token on first push
531
- ```
532
-
533
- ### Repository Requirements
534
-
535
- The target repository must:
536
- - Be a valid Git repository
537
- - Have at least one submodule configured in `.gitmodules`
538
- - Have `origin` remote configured with push access
539
- - Have submodules accessible from the current network
540
-
541
- ### Optional Enhancements
542
-
543
- For the best experience, consider:
544
- - **Git LFS** if your submodules contain large files
545
- - **Parallel processing** enabled for large submodule trees (`--parallel`)
546
- - **Configuration file** for consistent settings across teams (`--make-config`)
547
-
548
- ---
549
-
550
- ## Troubleshooting
551
-
552
- ### Common Issues and Solutions
553
-
554
- #### Authentication Errors
555
-
556
- **Problem:** `Permission denied (publickey)` or authentication prompts
557
- ```bash
558
- ✘ Push failed in 'my-repo': Permission denied (publickey)
559
- ```
560
-
561
- **Solutions:**
562
- 1. **SSH Key Issues:**
563
- ```bash
564
- # Test SSH connection
565
- ssh -T git@github.com
566
-
567
- # Add SSH key to ssh-agent
568
- ssh-add ~/.ssh/id_ed25519
569
- ```
570
-
571
- 2. **Token Issues:**
572
- ```bash
573
- # Clear cached credentials
574
- git config --global --unset credential.helper
575
-
576
- # Or update stored credentials
577
- git config --global credential.helper store
578
- ```
579
-
580
- #### Submodule Not Found
581
-
582
- **Problem:** `fatal: not a git repository` in submodule directory
583
- ```bash
584
- ✘ Init failed: fatal: not a git repository
585
- ```
586
-
587
- **Solutions:**
588
- 1. **Initialize manually:**
589
- ```bash
590
- git submodule update --init --recursive
591
- ```
592
-
593
- 2. **Check .gitmodules configuration:**
594
- ```bash
595
- cat .gitmodules
596
- # Verify URLs are accessible
597
- git ls-remote <submodule-url>
598
- ```
599
-
600
- #### Branch Resolution Issues
601
-
602
- **Problem:** Cannot determine correct branch for submodule
603
- ```bash
604
- ⚠ Cannot resolve origin/main — skipping
605
- ```
606
-
607
- **Solutions:**
608
- 1. **Specify branch in .gitmodules:**
609
- ```ini
610
- [submodule "my-submodule"]
611
- path = my-submodule
612
- url = git@github.com:user/my-submodule.git
613
- branch = main # Add this line
614
- ```
615
-
616
- 2. **Use CLI flag:**
617
- ```bash
618
- github-update-submodule --branch develop
619
- ```
620
-
621
- #### Push Conflicts
622
-
623
- **Problem:** Push fails due to remote changes
624
- ```bash
625
- ✘ Push failed in 'my-repo': ! [rejected] (non-fast-forward)
626
- ```
627
-
628
- **Solutions:**
629
- 1. **Pull latest changes first:**
630
- ```bash
631
- git pull origin main
632
- github-update-submodule
633
- ```
634
-
635
- 2. **Use interactive mode to review:**
636
- ```bash
637
- github-update-submodule --interactive
638
- ```
639
-
640
- #### Network Issues
641
-
642
- **Problem:** Timeouts or connection failures
643
- ```bash
644
- ✘ Fetch warning: unable to access '...': Connection timed out
645
- ```
646
-
647
- **Solutions:**
648
- 1. **Increase Git timeout:**
649
- ```bash
650
- git config --global http.lowSpeedLimit 0
651
- git config --global http.lowSpeedTime 999999
652
- ```
653
-
654
- 2. **Use sequential mode:**
655
- ```bash
656
- github-update-submodule # without --parallel
657
- ```
658
-
659
- #### Large Repository Performance
660
-
661
- **Problem:** Very slow execution on large submodule trees
662
-
663
- **Solutions:**
664
- 1. **Enable parallel fetching:**
665
- ```bash
666
- github-update-submodule --parallel
667
- ```
668
-
669
- 2. **Limit recursion depth:**
670
- ```bash
671
- github-update-submodule --depth 2
672
- ```
673
-
674
- 3. **Ignore specific submodules:**
675
- ```bash
676
- github-update-submodule --ignore heavy-lib --ignore docs
677
- ```
678
-
679
- ### Debug Mode
680
-
681
- For troubleshooting, enable verbose output:
682
- ```bash
683
- github-update-submodule --verbose --dry-run
684
- ```
685
-
686
- This will show:
687
- - Detailed Git command output
688
- - Branch resolution process
689
- - Remote URL detection
690
- - Authentication attempts
691
-
692
- ### Getting Help
693
-
694
- If you encounter issues not covered here:
695
-
696
- 1. **Check the GitHub Issues:** [SENODROOM/github-update-submodule/issues](https://github.com/SENODROOM/github-update-submodule/issues)
697
- 2. **Create a new issue** with:
698
- - Full command output with `--verbose`
699
- - Your `.gitmodules` file content
700
- - Operating system and versions
701
- - Steps to reproduce
702
-
703
- 3. **Community support:** Tag your issue with `question` or `bug` for appropriate attention.
704
-
705
- ---
706
-
707
- ## Contributing
708
-
709
- We welcome contributions from the community! Here's how you can help:
710
-
711
- ### Development Setup
712
-
713
- ```bash
714
- # Clone the repository
715
- git clone https://github.com/SENODROOM/github-update-submodule.git
716
- cd github-update-submodule
717
-
718
- # Install dependencies
719
- npm install
720
-
721
- # Link for local testing
722
- npm link
723
- ```
724
-
725
- ### Running Tests
726
-
727
- ```bash
728
- # Run the test suite
729
- npm test
730
-
731
- # Run with coverage
732
- npm run test:coverage
733
- ```
734
-
735
- ### Making Changes
736
-
737
- 1. **Fork** the repository
738
- 2. **Create** a feature branch: `git checkout -b feature/amazing-feature`
739
- 3. **Make** your changes
740
- 4. **Test** thoroughly:
741
- ```bash
742
- # Test with various scenarios
743
- github-update-submodule --dry-run
744
- github-update-submodule --verbose
745
- ```
746
- 5. **Commit** your changes with clear messages
747
- 6. **Push** to your fork
748
- 7. **Create** a Pull Request
749
-
750
- ### Code Style
751
-
752
- - Use **ES6+** features appropriately
753
- - Follow the existing code style and patterns
754
- - Add **JSDoc** comments for new functions
755
- - Ensure **error handling** is comprehensive
756
- - Test with **different Git versions** and platforms
757
-
758
- ### Bug Reports
759
-
760
- When reporting bugs, please include:
761
- - **Node.js** and **Git** versions
762
- - **Operating system** details
763
- - **Full error output** with `--verbose` flag
764
- - **Minimal reproduction** steps
765
- - **Expected vs actual** behavior
766
-
767
- ### Feature Requests
768
-
769
- For new features:
770
- 1. **Check existing issues** first
771
- 2. **Describe the use case** clearly
772
- 3. **Consider the impact** on existing users
773
- 4. **Suggest an API** if applicable
774
-
775
- ### Release Process
776
-
777
- Releases follow semantic versioning:
778
- - **Patch** (x.x.1): Bug fixes
779
- - **Minor** (x.1.x): New features
780
- - **Major** (1.x.x): Breaking changes
781
-
782
- Maintainers will handle version bumps and npm publishing.
783
-
784
- ---
785
-
786
- ## Performance Considerations
787
-
788
- ### Benchmarks
789
-
790
- Performance varies based on repository structure and network conditions:
791
-
792
- | Repository Size | Submodules | Sequential | Parallel | Improvement |
793
- |---|---|---|---|---|
794
- | Small | 5-10 | 2-5s | 1-3s | 40-60% |
795
- | Medium | 20-50 | 15-30s | 5-12s | 60-70% |
796
- | Large | 100+ | 2-5min | 30-60s | 70-80% |
797
-
798
- *Benchmarks measured on typical corporate network with GitHub Enterprise.*
799
-
800
- ### Optimization Strategies
801
-
802
- #### 1. Enable Parallel Fetching
803
- ```bash
804
- # Best for large repositories
805
- github-update-submodule --parallel
806
- ```
807
-
808
- #### 2. Limit Recursion Depth
809
- ```bash
810
- # Only update top-level submodules
811
- github-update-submodule --depth 1
812
- ```
813
-
814
- #### 3. Selective Updates
815
- ```bash
816
- # Skip heavy documentation submodules
817
- github-update-submodule --ignore docs --ignore examples
818
- ```
819
-
820
- #### 4. Network Optimization
821
- ```bash
822
- # Configure Git for better performance
823
- git config --global http.lowSpeedLimit 1000
824
- git config --global http.lowSpeedTime 30
825
- git config --global http.maxRequestBuffer 100M
826
- ```
827
-
828
- ### Memory Usage
829
-
830
- - **Small repos** (< 50 submodules): ~10-20MB RAM
831
- - **Medium repos** (50-200 submodules): ~20-50MB RAM
832
- - **Large repos** (200+ submodules): ~50-100MB RAM
833
-
834
- Memory scales linearly with submodule count due to parallel processing.
835
-
836
- ### Comparison with Alternatives
837
-
838
- | Feature | github-update-submodule | git submodule update | Manual scripts |
839
- |---|---|---|---|
840
- | **Recursive updates** | ✅ Automatic | ❌ Manual per level | ❌ Custom implementation |
841
- | **Parallel fetching** | ✅ Built-in | ❌ Sequential | ⚠️ Complex to implement |
842
- | **GitHub integration** | ✅ Compare links | ❌ None | ⚠️ Manual |
843
- | **Interactive mode** | ✅ Built-in | ❌ None | ⚠️ Custom |
844
- | **Progress tracking** | ✅ Rich output | ⚠️ Basic | ⚠️ Custom |
845
- | **Error handling** | ✅ Comprehensive | ⚠️ Limited | ⚠️ Variable |
846
- | **Configuration** | ✅ Files + CLI | ⚠️ CLI only | ⚠️ Custom |
847
-
848
- ### Performance Tips
849
-
850
- 1. **Use SSH** over HTTPS when possible (faster authentication)
851
- 2. **Enable git gc** in submodules regularly
852
- 3. **Consider shallow clones** for large submodules
853
- 4. **Use .gitignore** to exclude unnecessary files
854
- 5. **Schedule updates** during off-peak hours for CI/CD
855
-
856
- ---
857
-
858
- ## Security
859
-
860
- ### Security Considerations
861
-
862
- `github-update-submodule` is designed with security in mind, but there are important considerations:
863
-
864
- #### Trust Boundaries
865
-
866
- - **Submodule URLs** are fetched from `.gitmodules` - ensure this file is trusted
867
- - **Remote repositories** are accessed with your Git credentials
868
- - **Branch resolution** follows Git's remote HEAD detection
869
-
870
- #### Recommended Practices
871
-
872
- 1. **Review `.gitmodules`** before running:
873
- ```bash
874
- cat .gitmodules
875
- # Verify all URLs are legitimate
876
- ```
877
-
878
- 2. **Use dry-run** for untrusted repositories:
879
- ```bash
880
- github-update-submodule --dry-run --verbose
881
- ```
882
-
883
- 3. **Limit scope** with ignore patterns:
884
- ```bash
885
- github-update-submodule --ignore suspicious-submodule
886
- ```
887
-
888
- 4. **Audit submodules** regularly:
889
- ```bash
890
- # List all submodule URLs
891
- git submodule status
892
- ```
893
-
894
- #### Credential Security
895
-
896
- - **SSH keys** are preferred over HTTPS tokens
897
- - **Personal Access Tokens** should have minimal required scopes
898
- - **Credential helpers** should be configured securely
899
-
900
- #### Network Security
901
-
902
- - **HTTPS URLs** are automatically detected for GitHub compare links
903
- - **SSH URLs** are used for Git operations when configured
904
- - **Proxy settings** are respected from Git configuration
905
-
906
- ### Reporting Security Issues
907
-
908
- If you discover a security vulnerability:
909
-
910
- 1. **Do not** open a public issue
911
- 2. **Email** security@senodroom.com with details
912
- 3. **Include** steps to reproduce and potential impact
913
- 4. **Wait** for confirmation before disclosing
230
+ ## Requirements
914
231
 
915
- We'll respond within 48 hours and provide a timeline for fixes.
232
+ - **Node.js** >= 14
233
+ - **Git** installed and in your PATH
234
+ - Remote authentication set up (SSH keys or credential manager) so pushes don't require a password prompt
916
235
 
917
236
  ---
918
237
 
@@ -25,11 +25,13 @@
25
25
  * --no-color Disable colored output
26
26
  * --no-progress Disable the progress bar
27
27
  * --make-config Generate a submodule.config.json in the current repo and exit
28
+ * --version / -v Print version and exit
29
+ * --help / -h Print this help and exit
28
30
  */
29
31
 
30
32
  const { spawnSync, spawn } = require("child_process");
31
- const path = require("path");
32
- const fs = require("fs");
33
+ const path = require("path");
34
+ const fs = require("fs");
33
35
  const readline = require("readline");
34
36
 
35
37
  // ─── Config file loader ───────────────────────────────────────────────────────
@@ -61,18 +63,20 @@ const cliArgs = process.argv.slice(2);
61
63
 
62
64
  // Defaults (lowest priority)
63
65
  const options = {
64
- repoPath: process.cwd(),
65
- push: true,
66
- interactive: false,
67
- ignore: [], // array of submodule names to skip
68
- parallel: false,
66
+ repoPath: process.cwd(),
67
+ push: true,
68
+ interactive: false,
69
+ ignore: [], // array of submodule names to skip
70
+ parallel: false,
69
71
  commitMessage: "chore: update submodule refs",
70
- dryRun: false,
72
+ dryRun: false,
71
73
  defaultBranch: "main",
72
- maxDepth: Infinity,
73
- verbose: false,
74
- color: true,
75
- progress: true,
74
+ maxDepth: Infinity,
75
+ verbose: false,
76
+ color: true,
77
+ progress: true,
78
+ showVersion: false,
79
+ showHelp: false,
76
80
  };
77
81
 
78
82
  // Collect positional repo path first so config is loaded from correct dir
@@ -82,71 +86,71 @@ for (let i = 0; i < cliArgs.length; i++) {
82
86
 
83
87
  // Merge config file (overrides defaults, CLI will override config)
84
88
  const cfg = loadConfig(options.repoPath);
85
- if (cfg.push !== undefined) options.push = cfg.push;
86
- if (cfg.interactive !== undefined) options.interactive = cfg.interactive;
87
- if (cfg.ignore !== undefined) options.ignore = [].concat(cfg.ignore);
88
- if (cfg.parallel !== undefined) options.parallel = cfg.parallel;
89
+ if (cfg.push !== undefined) options.push = cfg.push;
90
+ if (cfg.interactive !== undefined) options.interactive = cfg.interactive;
91
+ if (cfg.ignore !== undefined) options.ignore = [].concat(cfg.ignore);
92
+ if (cfg.parallel !== undefined) options.parallel = cfg.parallel;
89
93
  if (cfg.commitMessage !== undefined) options.commitMessage = cfg.commitMessage;
90
94
  if (cfg.defaultBranch !== undefined) options.defaultBranch = cfg.defaultBranch;
91
- if (cfg.maxDepth !== undefined) options.maxDepth = cfg.maxDepth;
92
- if (cfg.verbose !== undefined) options.verbose = cfg.verbose;
93
- if (cfg.color !== undefined) options.color = cfg.color;
94
- if (cfg.progress !== undefined) options.progress = cfg.progress;
95
+ if (cfg.maxDepth !== undefined) options.maxDepth = cfg.maxDepth;
96
+ if (cfg.verbose !== undefined) options.verbose = cfg.verbose;
97
+ if (cfg.color !== undefined) options.color = cfg.color;
98
+ if (cfg.progress !== undefined) options.progress = cfg.progress;
95
99
 
96
100
  // CLI flags (highest priority)
97
101
  for (let i = 0; i < cliArgs.length; i++) {
98
102
  const a = cliArgs[i];
99
- if (a === "--no-push") options.push = false;
100
- else if (a === "--interactive") options.interactive = true;
101
- else if (a === "--parallel") options.parallel = true;
102
- else if (a === "--dry-run") options.dryRun = true;
103
- else if (a === "--verbose") options.verbose = true;
104
- else if (a === "--no-color") options.color = false;
105
- else if (a === "--no-progress") options.progress = false;
106
- else if (a === "--make-config") options.makeConfig = true;
107
- else if (a === "--branch") options.defaultBranch = cliArgs[++i];
108
- else if (a === "--message") options.commitMessage = cliArgs[++i];
109
- else if (a === "--depth") options.maxDepth = parseInt(cliArgs[++i], 10);
110
- else if (a === "--ignore") options.ignore.push(cliArgs[++i]);
103
+ if (a === "--no-push") options.push = false;
104
+ else if (a === "--interactive") options.interactive = true;
105
+ else if (a === "--parallel") options.parallel = true;
106
+ else if (a === "--dry-run") options.dryRun = true;
107
+ else if (a === "--verbose") options.verbose = true;
108
+ else if (a === "--no-color") options.color = false;
109
+ else if (a === "--no-progress") options.progress = false;
110
+ else if (a === "--make-config") options.makeConfig = true;
111
+ else if (a === "--branch") options.defaultBranch = cliArgs[++i];
112
+ else if (a === "--message") options.commitMessage = cliArgs[++i];
113
+ else if (a === "--depth") options.maxDepth = parseInt(cliArgs[++i], 10);
114
+ else if (a === "--ignore") options.ignore.push(cliArgs[++i]);
115
+ else if (a === "--version" || a === "-v") options.showVersion = true;
116
+ else if (a === "--help" || a === "-h") options.showHelp = true;
111
117
  }
112
118
 
113
119
  // ─── Colour helpers ──────────────────────────────────────────────────────────
114
120
 
115
121
  const C = options.color
116
- ? {
117
- reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m", green: "\x1b[32m",
118
- yellow: "\x1b[33m", cyan: "\x1b[36m", red: "\x1b[31m", magenta: "\x1b[35m",
119
- blue: "\x1b[34m", white: "\x1b[37m"
120
- }
122
+ ? { reset:"\x1b[0m", bold:"\x1b[1m", dim:"\x1b[2m", green:"\x1b[32m",
123
+ yellow:"\x1b[33m", cyan:"\x1b[36m", red:"\x1b[31m", magenta:"\x1b[35m",
124
+ blue:"\x1b[34m", white:"\x1b[37m" }
121
125
  : Object.fromEntries(
122
- ["reset", "bold", "dim", "green", "yellow", "cyan", "red", "magenta", "blue", "white"].map(k => [k, ""])
123
- );
126
+ ["reset","bold","dim","green","yellow","cyan","red","magenta","blue","white"].map(k=>[k,""])
127
+ );
124
128
 
125
129
  // ─── Logging ─────────────────────────────────────────────────────────────────
126
130
 
127
- const indent = (d) => " ".repeat(d);
128
- const log = (d, sym, col, msg) => console.log(`${indent(d)}${col}${sym} ${msg}${C.reset}`);
129
- const info = (d, m) => log(d, "›", C.cyan, m);
130
- const success = (d, m) => log(d, "✔", C.green, m);
131
- const warn = (d, m) => log(d, "⚠", C.yellow, m);
132
- const error = (d, m) => log(d, "✘", C.red, m);
133
- const header = (d, m) => log(d, "▸", C.bold + C.magenta, m);
134
- const pushLog = (d, m) => log(d, "↑", C.bold + C.green, m);
135
- const linkLog = (d, m) => log(d, "⎘", C.bold + C.blue, m);
131
+ const indent = (d) => " ".repeat(d);
132
+ const log = (d, sym, col, msg) => console.log(`${indent(d)}${col}${sym} ${msg}${C.reset}`);
133
+ const info = (d, m) => log(d, "›", C.cyan, m);
134
+ const success = (d, m) => log(d, "✔", C.green, m);
135
+ const warn = (d, m) => log(d, "⚠", C.yellow, m);
136
+ const error = (d, m) => log(d, "✘", C.red, m);
137
+ const header = (d, m) => log(d, "▸", C.bold + C.magenta, m);
138
+ const pushLog = (d, m) => log(d, "↑", C.bold + C.green, m);
139
+ const linkLog = (d, m) => log(d, "⎘", C.bold + C.blue, m);
136
140
  const verbose = (d, m) => { if (options.verbose) log(d, " ", C.dim, m); };
137
141
 
138
142
  // ─── Progress bar ─────────────────────────────────────────────────────────────
139
143
 
140
144
  const progress = {
141
- total: 0,
145
+ total: 0,
142
146
  current: 0,
143
- active: false,
147
+ active: false,
144
148
 
145
149
  init(total) {
146
150
  if (!options.progress || !process.stdout.isTTY) return;
147
- this.total = total;
151
+ this.total = total;
148
152
  this.current = 0;
149
- this.active = true;
153
+ this.active = true;
150
154
  this._render();
151
155
  },
152
156
 
@@ -164,13 +168,13 @@ const progress = {
164
168
  },
165
169
 
166
170
  _render(label = "") {
167
- const W = 28;
168
- const filled = Math.round((this.current / this.total) * W);
169
- const empty = W - filled;
170
- const bar = C.green + "█".repeat(filled) + C.dim + "░".repeat(empty) + C.reset;
171
- const pct = String(Math.round((this.current / this.total) * 100)).padStart(3);
171
+ const W = 28;
172
+ const filled = Math.round((this.current / this.total) * W);
173
+ const empty = W - filled;
174
+ const bar = C.green + "█".repeat(filled) + C.dim + "░".repeat(empty) + C.reset;
175
+ const pct = String(Math.round((this.current / this.total) * 100)).padStart(3);
172
176
  const counter = `${this.current}/${this.total}`;
173
- const lbl = label ? ` ${C.dim}${label.slice(0, 24)}${C.reset}` : "";
177
+ const lbl = label ? ` ${C.dim}${label.slice(0, 24)}${C.reset}` : "";
174
178
  process.stdout.write(`\r${C.bold}[${bar}${C.bold}] ${pct}% (${counter})${lbl}\x1b[K`);
175
179
  },
176
180
  };
@@ -211,7 +215,7 @@ function git(cwd, ...gitArgs) {
211
215
  return {
212
216
  stdout: (r.stdout || "").trim(),
213
217
  stderr: (r.stderr || "").trim(),
214
- ok: r.status === 0,
218
+ ok: r.status === 0,
215
219
  };
216
220
  }
217
221
 
@@ -258,8 +262,8 @@ function parseGitmodules(repoDir) {
258
262
 
259
263
  const kv = line.match(/^(\w+)\s*=\s*(.+)$/);
260
264
  if (kv) {
261
- if (kv[1] === "path") cur.path = kv[2];
262
- if (kv[1] === "url") cur.url = kv[2];
265
+ if (kv[1] === "path") cur.path = kv[2];
266
+ if (kv[1] === "url") cur.url = kv[2];
263
267
  if (kv[1] === "branch") cur.branch = kv[2];
264
268
  }
265
269
  }
@@ -399,7 +403,7 @@ function pullSubmodules(repoDir, depth = 0) {
399
403
  }
400
404
 
401
405
  // ── Resolve branch + remote tip ───────────────────────────────────────
402
- const branch = resolveBranch(subDir, sub.branch);
406
+ const branch = resolveBranch(subDir, sub.branch);
403
407
  const remoteRef = `origin/${branch}`;
404
408
  const remoteTip = git(subDir, "rev-parse", remoteRef).stdout;
405
409
 
@@ -412,7 +416,7 @@ function pullSubmodules(repoDir, depth = 0) {
412
416
  }
413
417
 
414
418
  const beforeHash = git(subDir, "rev-parse", "HEAD").stdout;
415
- const remoteUrl = getRemoteUrl(subDir);
419
+ const remoteUrl = getRemoteUrl(subDir);
416
420
 
417
421
  // ── Dry-run ───────────────────────────────────────────────────────────
418
422
  if (options.dryRun) {
@@ -557,14 +561,14 @@ async function runMakeConfig() {
557
561
  const exists = fs.existsSync(dest);
558
562
 
559
563
  const template = {
560
- defaultBranch: "main",
561
- parallel: false,
562
- ignore: [],
563
- commitMessage: "chore: update submodule refs",
564
- interactive: false,
565
- verbose: false,
566
- color: true,
567
- progress: true
564
+ defaultBranch: "main",
565
+ parallel: false,
566
+ ignore: [],
567
+ commitMessage: "chore: update submodule refs",
568
+ interactive: false,
569
+ verbose: false,
570
+ color: true,
571
+ progress: true
568
572
  };
569
573
 
570
574
  console.log();
@@ -606,9 +610,83 @@ async function runMakeConfig() {
606
610
  process.exit(0);
607
611
  }
608
612
 
613
+ // ─── Version & help ──────────────────────────────────────────────────────────
614
+
615
+ const VERSION = "2.0.0";
616
+
617
+ function printVersion() {
618
+ console.log(`github-update-submodule ${VERSION}`);
619
+ process.exit(0);
620
+ }
621
+
622
+ function printHelp() {
623
+ const b = C.bold;
624
+ const r = C.reset;
625
+ const cy = C.cyan;
626
+ const ye = C.yellow;
627
+ const di = C.dim;
628
+ const gr = C.green;
629
+
630
+ console.log();
631
+ console.log(`${b}${C.blue}╔══════════════════════════════════════════╗${r}`);
632
+ console.log(`${b}${C.blue}║ github-update-submodule v${VERSION} ║${r}`);
633
+ console.log(`${b}${C.blue}╚══════════════════════════════════════════╝${r}`);
634
+ console.log();
635
+ console.log(`${b}USAGE${r}`);
636
+ console.log(` github-update-submodule ${cy}[repo-path]${r} ${ye}[options]${r}`);
637
+ console.log();
638
+ console.log(`${b}DESCRIPTION${r}`);
639
+ console.log(` Recursively pulls every Git submodule to the latest remote commit,`);
640
+ console.log(` then commits and pushes the updated refs up every parent repo so`);
641
+ console.log(` GitHub always points to the latest commit at every nesting level.`);
642
+ console.log();
643
+ console.log(`${b}OPTIONS${r}`);
644
+ const flags = [
645
+ ["--no-push", "", "Pull locally only — skip commit and push"],
646
+ ["--interactive", "", "Prompt yes/no before pushing each parent repo"],
647
+ ["--ignore", "<name>", "Skip a submodule by name (repeatable)"],
648
+ ["--parallel", "", "Fetch all submodules concurrently (faster)"],
649
+ ["--dry-run", "", "Preview changes without modifying anything"],
650
+ ["--message", "<msg>", `Commit message ${di}(default: "chore: update submodule refs")${r}`],
651
+ ["--branch", "<b>", `Default branch if not in .gitmodules ${di}(default: main)${r}`],
652
+ ["--depth", "<n>", "Limit recursion depth"],
653
+ ["--verbose", "", "Show full git output"],
654
+ ["--no-color", "", "Disable colored output"],
655
+ ["--no-progress", "", "Disable the progress bar"],
656
+ ["--make-config", "", "Generate submodule.config.json with all defaults"],
657
+ ["--version, -v", "", "Print version and exit"],
658
+ ["--help, -h", "", "Print this help and exit"],
659
+ ];
660
+ for (const [flag, arg, desc] of flags) {
661
+ const left = ` ${cy}${flag}${r}${arg ? " " + ye + arg + r : ""}`;
662
+ const pad = " ".repeat(Math.max(1, 36 - flag.length - arg.length));
663
+ console.log(`${left}${pad}${desc}`);
664
+ }
665
+ console.log();
666
+ console.log(`${b}EXAMPLES${r}`);
667
+ console.log(` ${gr}github-update-submodule${r} ${di}# pull + commit + push everything${r}`);
668
+ console.log(` ${gr}github-update-submodule${r} ${ye}--dry-run${r} ${di}# preview only${r}`);
669
+ console.log(` ${gr}github-update-submodule${r} ${ye}--parallel${r} ${di}# concurrent fetch${r}`);
670
+ console.log(` ${gr}github-update-submodule${r} ${ye}--interactive${r} ${di}# confirm before each push${r}`);
671
+ console.log(` ${gr}github-update-submodule${r} ${ye}--ignore frontend${r} ${di}# skip a submodule${r}`);
672
+ console.log(` ${gr}github-update-submodule${r} ${ye}--no-push${r} ${di}# local update only${r}`);
673
+ console.log(` ${gr}github-update-submodule${r} ${ye}--make-config${r} ${di}# create config file${r}`);
674
+ console.log(` ${gr}github-update-submodule${r} ${ye}--version${r} ${di}# print version${r}`);
675
+ console.log();
676
+ console.log(`${b}CONFIG FILE${r}`);
677
+ console.log(` Run ${cy}--make-config${r} to generate ${cy}submodule.config.json${r} in your repo root.`);
678
+ console.log(` CLI flags always override config file values.`);
679
+ console.log();
680
+ process.exit(0);
681
+ }
682
+
609
683
  // ─── Entry point ─────────────────────────────────────────────────────────────
610
684
 
611
685
  async function main() {
686
+ // early-exit flags
687
+ if (options.showVersion) { printVersion(); return; }
688
+ if (options.showHelp) { printHelp(); return; }
689
+
612
690
  // --make-config: generate a config file and exit immediately
613
691
  if (options.makeConfig) {
614
692
  await runMakeConfig();
@@ -629,9 +707,9 @@ async function main() {
629
707
  // Print active config
630
708
  info(0, `Repository : ${C.bold}${options.repoPath}${C.reset}`);
631
709
  info(0, `Default branch : ${C.bold}${options.defaultBranch}${C.reset}`);
632
- info(0, `Push mode : ${options.push ? C.bold + C.green + "ON" : C.dim + "OFF"}${C.reset}`);
633
- info(0, `Interactive : ${options.interactive ? C.bold + C.yellow + "ON" : C.dim + "OFF"}${C.reset}`);
634
- info(0, `Parallel fetch : ${options.parallel ? C.bold + C.cyan + "ON" : C.dim + "OFF"}${C.reset}`);
710
+ info(0, `Push mode : ${options.push ? C.bold+C.green+"ON" : C.dim+"OFF"}${C.reset}`);
711
+ info(0, `Interactive : ${options.interactive ? C.bold+C.yellow+"ON" : C.dim+"OFF"}${C.reset}`);
712
+ info(0, `Parallel fetch : ${options.parallel ? C.bold+C.cyan+"ON" : C.dim+"OFF"}${C.reset}`);
635
713
  if (options.ignore.length)
636
714
  info(0, `Ignoring : ${C.bold}${C.yellow}${options.ignore.join(", ")}${C.reset}`);
637
715
  if (options.dryRun)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-update-submodule",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "Recursively pull all Git submodules to their latest remote commit and push updated refs up every parent repo — so GitHub always points to the latest.",
5
5
  "main": "bin/github-update-submodule.js",
6
6
  "bin": {