codeowners-git 2.0.1 → 2.1.0

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 (3) hide show
  1. package/README.md +315 -0
  2. package/dist/cli.js +507 -38
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -12,6 +12,8 @@ Managing large-scale migrations in big monorepos with multiple codeowners can be
12
12
  - Creating compact, team-specific branches with only their affected files.
13
13
  - Streamlining the review process with smaller, targeted PRs.
14
14
  - **Graceful error handling** with automatic recovery from failures.
15
+ - **Dry-run previews** to see exactly what will happen before committing.
16
+ - **JSON output** for piping to other tools, scripts, and agent workflows.
15
17
 
16
18
  > ❗❗ ❗ **Note:** Starting from v2.0.0, this tool works with **staged files**. Stage your changes with `git add` before running commands.
17
19
 
@@ -149,6 +151,7 @@ Options:
149
151
  - `--group, -g` Group files by code owner
150
152
  - `--exclusive, -e` Only include files with a single owner (no co-owned files)
151
153
  - `--co-owned, -c` Only include files with multiple owners (co-owned files)
154
+ - `--json` Output results as JSON (suppresses all other output)
152
155
 
153
156
  Examples:
154
157
 
@@ -180,6 +183,12 @@ cg list --co-owned
180
183
 
181
184
  # List co-owned files where @myteam is one of the owners
182
185
  cg list --include "@myteam" --co-owned
186
+
187
+ # Output as JSON for piping to other tools
188
+ cg list --json
189
+
190
+ # JSON output with filters (pipe to jq)
191
+ cg list -i "@myteam" --group --json | jq '.grouped'
183
192
  ```
184
193
 
185
194
  ### `branch`
@@ -214,6 +223,8 @@ Options:
214
223
  - `--draft-pr` Create a draft pull request after pushing (requires `--push` and GitHub CLI)
215
224
  - `--exclusive, -e` Only include files where the owner is the sole owner (no co-owned files)
216
225
  - `--co-owned, -c` Only include files with multiple owners (co-owned files)
226
+ - `--dry-run` Preview the operation without making any changes
227
+ - `--json` Output results as JSON (suppresses all other output)
217
228
 
218
229
  Example:
219
230
 
@@ -253,6 +264,15 @@ cg branch -i @myteam -b "feature/exclusive" -m "Team exclusive changes" -p --exc
253
264
 
254
265
  # Only include co-owned files where @myteam is one of the owners
255
266
  cg branch -i @myteam -b "feature/co-owned" -m "Co-owned changes" -p --co-owned
267
+
268
+ # Preview what would happen without making any changes
269
+ cg branch -i @myteam -b "feature/new" -m "Add feature" --dry-run
270
+
271
+ # Dry-run with JSON output (for agents/scripts)
272
+ cg branch -i @myteam -b "feature/new" -m "Add feature" --dry-run --json
273
+
274
+ # Normal execution with JSON output
275
+ cg branch -i @myteam -b "feature/new" -m "Add feature" -p --json
256
276
  ```
257
277
 
258
278
  ### `multi-branch`
@@ -289,6 +309,8 @@ Options:
289
309
  - `--draft-pr` Create draft pull requests after pushing (requires `--push` and GitHub CLI)
290
310
  - `--exclusive, -e` Only include files where each owner is the sole owner (no co-owned files)
291
311
  - `--co-owned, -c` Only include files with multiple owners (co-owned files)
312
+ - `--dry-run` Preview the operation without making any changes
313
+ - `--json` Output results as JSON (suppresses all other output)
292
314
 
293
315
  > **Note:** You cannot use both `--ignore` and `--include` options at the same time. You also cannot use both `--exclusive` and `--co-owned` options at the same time.
294
316
 
@@ -336,6 +358,18 @@ cg multi-branch -b "feature/exclusive" -m "Exclusive changes" -p --exclusive
336
358
 
337
359
  # Only include co-owned files
338
360
  cg multi-branch -b "feature/co-owned" -m "Co-owned changes" -p --co-owned
361
+
362
+ # Preview all branches that would be created
363
+ cg multi-branch -b "feature/migration" -m "Migrate" --dry-run
364
+
365
+ # Dry-run with JSON output
366
+ cg multi-branch -b "feature/migration" -m "Migrate" --dry-run --json
367
+
368
+ # Pipe dry-run JSON to see owners with matching files
369
+ cg multi-branch -b "mig" -m "Fix" --dry-run --json | jq '.owners[] | select(.files | length > 0)'
370
+
371
+ # Normal execution with JSON output
372
+ cg multi-branch -b "feature/migration" -m "Migrate" -p --json
339
373
  ```
340
374
 
341
375
  This will:
@@ -371,6 +405,8 @@ Options:
371
405
  - `--compare-main` Compare source against main branch instead of detecting merge-base
372
406
  - `--exclusive, -e` Only include files where the owner is the sole owner (no co-owned files)
373
407
  - `--co-owned, -c` Only include files with multiple owners (co-owned files)
408
+ - `--dry-run` Preview the operation without making any changes
409
+ - `--json` Output results as JSON (suppresses all other output)
374
410
 
375
411
  Examples:
376
412
 
@@ -403,6 +439,21 @@ cg extract -s feature/other-team --co-owned
403
439
 
404
440
  # Extract co-owned files where @my-team is one of the owners
405
441
  cg extract -s feature/other-team -i "@my-team" --co-owned
442
+
443
+ # Preview what would be extracted without making any changes
444
+ cg extract -s feature/other-team --dry-run
445
+
446
+ # Dry-run with owner filter
447
+ cg extract -s feature/other-team -i "@my-team" --dry-run
448
+
449
+ # Dry-run with JSON output (for agents/scripts)
450
+ cg extract -s feature/other-team -i "@my-team" --dry-run --json
451
+
452
+ # Normal execution with JSON output
453
+ cg extract -s feature/other-team -i "@my-team" --json
454
+
455
+ # Pipe JSON to jq to get just the file list
456
+ cg extract -s feature/other-team --json | jq '.files'
406
457
  ```
407
458
 
408
459
  > **Note:** Files are extracted to your working directory (unstaged), allowing you to review and modify them. Stage the files with `git add`, then use the `branch` command to create a branch, commit, push, and create PRs.
@@ -477,6 +528,270 @@ The tool automatically handles:
477
528
 
478
529
  > **Note:** State files are stored in `~/.codeowners-git/state/` outside your project directory, so no `.gitignore` entries are needed.
479
530
 
531
+ ## Dry Run & JSON Output
532
+
533
+ ### Dry Run (`--dry-run`)
534
+
535
+ Available on: `branch`, `multi-branch`, `extract`
536
+
537
+ The `--dry-run` flag shows a complete preview of what would happen without performing any git operations. No branches are created, no files are committed, and nothing is pushed.
538
+
539
+ ```bash
540
+ # Preview branch creation
541
+ cg branch -i @myteam -b "feature/new" -m "Add feature" --dry-run
542
+
543
+ # Preview all branches in a multi-branch run
544
+ cg multi-branch -b "feature/migration" -m "Migrate" --dry-run
545
+
546
+ # Preview file extraction
547
+ cg extract -s feature/other-team -i "@myteam" --dry-run
548
+ ```
549
+
550
+ The dry-run output includes:
551
+
552
+ - **branch**: Owner, branch name, branch existence, commit message, matched files, excluded files, push/PR/flag settings
553
+ - **multi-branch**: Per-owner breakdown (branch, message, files), uncovered files, unowned files, summary totals
554
+ - **extract**: Source, compare target, files to extract, excluded files, filter settings
555
+
556
+ ### JSON Output (`--json`)
557
+
558
+ Available on: `list`, `branch`, `multi-branch`, `extract`
559
+
560
+ The `--json` flag outputs machine-readable JSON to stdout and suppresses all human-readable log messages. This is useful for piping to other tools, scripts, and agent workflows.
561
+
562
+ ```bash
563
+ # JSON output for any command
564
+ cg list --json
565
+ cg branch -i @myteam -b "feature/new" -m "Add feature" --json
566
+ cg multi-branch -b "feature/migration" -m "Migrate" --json
567
+ cg extract -s feature/other-team --json
568
+ ```
569
+
570
+ ### Combining `--dry-run` and `--json`
571
+
572
+ The two flags work together — `--dry-run --json` outputs the dry-run preview as structured JSON:
573
+
574
+ ```bash
575
+ cg branch -i @myteam -b "feature/new" -m "Add feature" --dry-run --json
576
+ cg multi-branch -b "feature/migration" -m "Migrate" --dry-run --json
577
+ cg extract -s feature/other-team --dry-run --json
578
+ ```
579
+
580
+ ### JSON Schema Examples
581
+
582
+ Every JSON response includes a `command` field identifying the source command.
583
+
584
+ **`list --json`**
585
+
586
+ ```json
587
+ {
588
+ "command": "list",
589
+ "files": [
590
+ { "file": "src/index.ts", "owners": ["@org/team-a"] },
591
+ { "file": "src/shared.ts", "owners": ["@org/team-a", "@org/team-b"] }
592
+ ],
593
+ "filters": {
594
+ "include": null,
595
+ "pathPattern": null,
596
+ "exclusive": false,
597
+ "coOwned": false
598
+ }
599
+ }
600
+ ```
601
+
602
+ **`list --group --json`**
603
+
604
+ ```json
605
+ {
606
+ "command": "list",
607
+ "grouped": {
608
+ "@org/team-a": ["src/index.ts", "src/shared.ts"],
609
+ "@org/team-b": ["src/shared.ts"]
610
+ },
611
+ "filters": {
612
+ "include": null,
613
+ "pathPattern": null,
614
+ "exclusive": false,
615
+ "coOwned": false
616
+ }
617
+ }
618
+ ```
619
+
620
+ **`branch --dry-run --json`**
621
+
622
+ ```json
623
+ {
624
+ "command": "branch",
625
+ "dryRun": true,
626
+ "owner": "@org/team-a",
627
+ "branch": "feature/new/team-a",
628
+ "branchExists": false,
629
+ "message": "Add feature - @org/team-a",
630
+ "files": ["src/index.ts"],
631
+ "excludedFiles": ["src/other.ts"],
632
+ "options": {
633
+ "push": true,
634
+ "remote": "origin",
635
+ "force": false,
636
+ "pr": false,
637
+ "draftPr": false,
638
+ "noVerify": false,
639
+ "append": false,
640
+ "exclusive": false,
641
+ "coOwned": false,
642
+ "pathPattern": null
643
+ }
644
+ }
645
+ ```
646
+
647
+ **`branch --json`** (normal execution)
648
+
649
+ ```json
650
+ {
651
+ "command": "branch",
652
+ "dryRun": false,
653
+ "success": true,
654
+ "branchName": "feature/new/team-a",
655
+ "owner": "@org/team-a",
656
+ "files": ["src/index.ts"],
657
+ "pushed": true,
658
+ "prUrl": "https://github.com/org/repo/pull/42",
659
+ "prNumber": 42,
660
+ "error": null
661
+ }
662
+ ```
663
+
664
+ **`multi-branch --dry-run --json`**
665
+
666
+ ```json
667
+ {
668
+ "command": "multi-branch",
669
+ "dryRun": true,
670
+ "owners": [
671
+ {
672
+ "owner": "@org/team-a",
673
+ "branch": "feature/migration/team-a",
674
+ "message": "Migrate - @org/team-a",
675
+ "files": ["src/index.ts"]
676
+ },
677
+ {
678
+ "owner": "@org/team-b",
679
+ "branch": "feature/migration/team-b",
680
+ "message": "Migrate - @org/team-b",
681
+ "files": ["src/shared.ts"]
682
+ }
683
+ ],
684
+ "uncoveredFiles": [],
685
+ "filesWithoutOwners": [],
686
+ "totalFiles": 2,
687
+ "coveredFiles": 2,
688
+ "options": {
689
+ "baseBranch": "feature/migration",
690
+ "baseMessage": "Migrate",
691
+ "push": false,
692
+ "remote": "origin",
693
+ "force": false,
694
+ "pr": false,
695
+ "draftPr": false,
696
+ "noVerify": false,
697
+ "append": false,
698
+ "exclusive": false,
699
+ "coOwned": false,
700
+ "pathPattern": null,
701
+ "defaultOwner": null
702
+ }
703
+ }
704
+ ```
705
+
706
+ **`multi-branch --json`** (normal execution)
707
+
708
+ ```json
709
+ {
710
+ "command": "multi-branch",
711
+ "dryRun": false,
712
+ "success": true,
713
+ "totalOwners": 2,
714
+ "successCount": 2,
715
+ "failureCount": 0,
716
+ "results": [
717
+ {
718
+ "owner": "@org/team-a",
719
+ "branch": "feature/migration/team-a",
720
+ "success": true,
721
+ "files": ["src/index.ts"],
722
+ "pushed": true,
723
+ "prUrl": null,
724
+ "prNumber": null,
725
+ "error": null
726
+ }
727
+ ]
728
+ }
729
+ ```
730
+
731
+ **`extract --dry-run --json`**
732
+
733
+ ```json
734
+ {
735
+ "command": "extract",
736
+ "dryRun": true,
737
+ "source": "feature/other-team",
738
+ "compareTarget": "main",
739
+ "files": ["src/component.tsx"],
740
+ "excludedFiles": ["src/unrelated.ts"],
741
+ "totalChanged": 2,
742
+ "options": {
743
+ "include": "@org/team-a",
744
+ "pathPattern": null,
745
+ "exclusive": false,
746
+ "coOwned": false,
747
+ "compareMain": false
748
+ }
749
+ }
750
+ ```
751
+
752
+ **`extract --json`** (normal execution)
753
+
754
+ ```json
755
+ {
756
+ "command": "extract",
757
+ "dryRun": false,
758
+ "source": "feature/other-team",
759
+ "compareTarget": "main",
760
+ "files": ["src/component.tsx"],
761
+ "totalChanged": 2
762
+ }
763
+ ```
764
+
765
+ **Error responses** (any command)
766
+
767
+ ```json
768
+ {
769
+ "command": "branch",
770
+ "error": "Error: No staged files found"
771
+ }
772
+ ```
773
+
774
+ ### Piping Examples
775
+
776
+ ```bash
777
+ # Count files per owner
778
+ cg list --group --json | jq '.grouped | to_entries[] | {owner: .key, count: (.value | length)}'
779
+
780
+ # Get list of branches that would be created
781
+ cg multi-branch -b "mig" -m "Fix" --dry-run --json | jq '.owners[].branch'
782
+
783
+ # Find owners with more than 5 files
784
+ cg multi-branch -b "mig" -m "Fix" --dry-run --json | jq '.owners[] | select(.files | length > 5) | .owner'
785
+
786
+ # Check if a branch operation succeeded
787
+ cg branch -i @myteam -b "feat" -m "Update" -p --json | jq '.success'
788
+
789
+ # List only extracted file paths
790
+ cg extract -s feature/other --json | jq -r '.files[]'
791
+ ```
792
+
793
+ > **Note:** The `recover` command does not support `--dry-run` or `--json` because it is an interactive command with user prompts.
794
+
480
795
  ## Contributing
481
796
 
482
797
  1. Clone the repository
package/dist/cli.js CHANGED
@@ -5,15 +5,29 @@ var __getProtoOf = Object.getPrototypeOf;
5
5
  var __defProp = Object.defineProperty;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ function __accessProp(key) {
9
+ return this[key];
10
+ }
11
+ var __toESMCache_node;
12
+ var __toESMCache_esm;
8
13
  var __toESM = (mod, isNodeMode, target) => {
14
+ var canCache = mod != null && typeof mod === "object";
15
+ if (canCache) {
16
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
17
+ var cached = cache.get(mod);
18
+ if (cached)
19
+ return cached;
20
+ }
9
21
  target = mod != null ? __create(__getProtoOf(mod)) : {};
10
22
  const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
11
23
  for (let key of __getOwnPropNames(mod))
12
24
  if (!__hasOwnProp.call(to, key))
13
25
  __defProp(to, key, {
14
- get: () => mod[key],
26
+ get: __accessProp.bind(mod, key),
15
27
  enumerable: true
16
28
  });
29
+ if (canCache)
30
+ cache.set(mod, to);
17
31
  return to;
18
32
  };
19
33
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
@@ -2287,7 +2301,7 @@ var require_ignore = __commonJS((exports, module) => {
2287
2301
  }
2288
2302
  }, {
2289
2303
  key: "filter",
2290
- value: function filter(paths) {
2304
+ value: function filter2(paths) {
2291
2305
  var _this = this;
2292
2306
  return make_array(paths).filter(function(path) {
2293
2307
  return _this._filter(path);
@@ -2438,7 +2452,7 @@ var require_ignore = __commonJS((exports, module) => {
2438
2452
  }
2439
2453
  if (typeof process !== "undefined" && (process.env && process.env.IGNORE_TEST_WIN32 || process.platform === "win32")) {
2440
2454
  filter = IgnoreBase.prototype._filter;
2441
- make_posix = function make_posix(str) {
2455
+ make_posix = function make_posix2(str) {
2442
2456
  return /^\\\\\?\\/.test(str) || /[^\x00-\x80]+/.test(str) ? str : str.replace(/\\/g, "/");
2443
2457
  };
2444
2458
  IgnoreBase.prototype._filter = function(path, slices) {
@@ -3039,25 +3053,25 @@ var require_minimatch = __commonJS((exports, module) => {
3039
3053
  return minimatch;
3040
3054
  }
3041
3055
  var orig = minimatch;
3042
- var m = function minimatch(p, pattern, options) {
3056
+ var m = function minimatch2(p, pattern, options) {
3043
3057
  return orig(p, pattern, ext(def, options));
3044
3058
  };
3045
- m.Minimatch = function Minimatch(pattern, options) {
3059
+ m.Minimatch = function Minimatch2(pattern, options) {
3046
3060
  return new orig.Minimatch(pattern, ext(def, options));
3047
3061
  };
3048
3062
  m.Minimatch.defaults = function defaults(options) {
3049
3063
  return orig.defaults(ext(def, options)).Minimatch;
3050
3064
  };
3051
- m.filter = function filter(pattern, options) {
3065
+ m.filter = function filter2(pattern, options) {
3052
3066
  return orig.filter(pattern, ext(def, options));
3053
3067
  };
3054
3068
  m.defaults = function defaults(options) {
3055
3069
  return orig.defaults(ext(def, options));
3056
3070
  };
3057
- m.makeRe = function makeRe(pattern, options) {
3071
+ m.makeRe = function makeRe2(pattern, options) {
3058
3072
  return orig.makeRe(pattern, ext(def, options));
3059
3073
  };
3060
- m.braceExpand = function braceExpand(pattern, options) {
3074
+ m.braceExpand = function braceExpand2(pattern, options) {
3061
3075
  return orig.braceExpand(pattern, ext(def, options));
3062
3076
  };
3063
3077
  m.match = function(list, pattern, options) {
@@ -6584,7 +6598,7 @@ var require_colors = __commonJS((exports, module) => {
6584
6598
  colors.stripColors = colors.strip = function(str) {
6585
6599
  return ("" + str).replace(/\x1B\[\d+m/g, "");
6586
6600
  };
6587
- var stylize = colors.stylize = function stylize(str, style) {
6601
+ var stylize = colors.stylize = function stylize2(str, style) {
6588
6602
  if (!colors.enabled) {
6589
6603
  return str + "";
6590
6604
  }
@@ -6602,8 +6616,8 @@ var require_colors = __commonJS((exports, module) => {
6602
6616
  return str.replace(matchOperatorsRe, "\\$&");
6603
6617
  };
6604
6618
  function build(_styles) {
6605
- var builder = function builder() {
6606
- return applyStyle2.apply(builder, arguments);
6619
+ var builder = function builder2() {
6620
+ return applyStyle2.apply(builder2, arguments);
6607
6621
  };
6608
6622
  builder._styles = _styles;
6609
6623
  builder.__proto__ = proto2;
@@ -6622,7 +6636,7 @@ var require_colors = __commonJS((exports, module) => {
6622
6636
  });
6623
6637
  return ret;
6624
6638
  }();
6625
- var proto2 = defineProps(function colors() {}, styles3);
6639
+ var proto2 = defineProps(function colors2() {}, styles3);
6626
6640
  function applyStyle2() {
6627
6641
  var args = Array.prototype.slice.call(arguments);
6628
6642
  var str = args.map(function(arg) {
@@ -6681,7 +6695,7 @@ var require_colors = __commonJS((exports, module) => {
6681
6695
  });
6682
6696
  return ret;
6683
6697
  }
6684
- var sequencer = function sequencer(map2, str) {
6698
+ var sequencer = function sequencer2(map2, str) {
6685
6699
  var exploded = str.split("");
6686
6700
  exploded = exploded.map(map2);
6687
6701
  return exploded.join("");
@@ -11612,7 +11626,7 @@ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
11612
11626
  var __esm = (fn, res) => function __init() {
11613
11627
  return fn && (res = (0, fn[__getOwnPropNames2(fn)[0]])(fn = 0)), res;
11614
11628
  };
11615
- var __commonJS2 = (cb, mod) => function __require() {
11629
+ var __commonJS2 = (cb, mod) => function __require2() {
11616
11630
  return mod || (0, cb[__getOwnPropNames2(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
11617
11631
  };
11618
11632
  var __export = (target, all) => {
@@ -16080,6 +16094,26 @@ var import_cli_table3 = __toESM(require_table(), 1);
16080
16094
  var DEFAULT_COLUMN_WIDTH = 50;
16081
16095
  var MAX_LINE_LENGTH = 80;
16082
16096
  var MAX_PATH_LENGTH = 60;
16097
+ var _silent = false;
16098
+ var _origConsoleLog = console.log;
16099
+ var _origConsoleWarn = console.warn;
16100
+ var _origConsoleError = console.error;
16101
+ var noop = () => {};
16102
+ var setSilent = (silent) => {
16103
+ _silent = silent;
16104
+ if (silent) {
16105
+ console.log = noop;
16106
+ console.warn = noop;
16107
+ console.error = noop;
16108
+ } else {
16109
+ console.log = _origConsoleLog;
16110
+ console.warn = _origConsoleWarn;
16111
+ console.error = _origConsoleError;
16112
+ }
16113
+ };
16114
+ var outputJson = (data) => {
16115
+ _origConsoleLog(JSON.stringify(data, null, 2));
16116
+ };
16083
16117
  var log = {
16084
16118
  success: (message) => console.log(source_default.green(`✓ ${message}`)),
16085
16119
  error: (message) => console.error(source_default.red(`✗ ${message}`)),
@@ -16154,6 +16188,14 @@ var hasUnstagedChanges = async () => {
16154
16188
  const unstagedFiles = await getUnstagedFiles();
16155
16189
  return unstagedFiles.length > 0;
16156
16190
  };
16191
+ var getStagedFiles = async () => {
16192
+ const status = await git.status();
16193
+ return status.files.filter((file) => file.index !== " " && file.index !== "?").map((file) => file.path);
16194
+ };
16195
+ var hasStagedChanges = async () => {
16196
+ const stagedFiles = await getStagedFiles();
16197
+ return stagedFiles.length > 0;
16198
+ };
16157
16199
  var branchExists = async (branchName) => {
16158
16200
  try {
16159
16201
  const branches = await git.branch();
@@ -16219,10 +16261,8 @@ var checkout = async (name) => {
16219
16261
  };
16220
16262
  var commitChanges = async (files, { message, noVerify = false }) => {
16221
16263
  try {
16222
- log.info("Adding files to commit...");
16223
- await git.add(files);
16224
16264
  log.info(`Running commit with message: "${message}"`);
16225
- await git.commit(message, [], {
16265
+ await git.commit(message, files, {
16226
16266
  ...noVerify ? { "--no-verify": null } : {}
16227
16267
  });
16228
16268
  log.info("Commit finished successfully.");
@@ -16242,7 +16282,8 @@ var pushBranch = async (branchName, {
16242
16282
  remote = "origin",
16243
16283
  upstream,
16244
16284
  force = false,
16245
- noVerify = false
16285
+ noVerify = false,
16286
+ silent = false
16246
16287
  } = {}) => {
16247
16288
  const targetUpstream = upstream || branchName;
16248
16289
  log.info(`Pushing branch "${branchName}" to ${remote}/${targetUpstream}...`);
@@ -16255,8 +16296,9 @@ var pushBranch = async (branchName, {
16255
16296
  if (noVerify) {
16256
16297
  pushArgs.push("--no-verify");
16257
16298
  }
16299
+ const stdioOption = silent ? ["pipe", "pipe", "pipe"] : ["inherit", "inherit", "inherit"];
16258
16300
  const gitProcess = spawn2("git", ["push", ...pushArgs], {
16259
- stdio: ["inherit", "inherit", "inherit"]
16301
+ stdio: stdioOption
16260
16302
  });
16261
16303
  return new Promise((resolve, reject) => {
16262
16304
  gitProcess.on("close", (code) => {
@@ -16432,6 +16474,9 @@ var getOwnerFiles = async (ownerPattern, includeUnowned = false, pathPattern, ex
16432
16474
  // src/commands/list.ts
16433
16475
  var listCodeowners = async (options) => {
16434
16476
  try {
16477
+ if (options.json) {
16478
+ setSilent(true);
16479
+ }
16435
16480
  if (await hasUnstagedChanges()) {
16436
16481
  const unstagedFiles = await getUnstagedFiles();
16437
16482
  log.warn("Warning: Unstaged changes detected (these will be ignored):");
@@ -16464,6 +16509,34 @@ Only staged files will be processed.`);
16464
16509
  return matchFn(owners, patterns);
16465
16510
  });
16466
16511
  }
16512
+ if (options.json) {
16513
+ let grouped;
16514
+ if (options.group) {
16515
+ grouped = {};
16516
+ for (const { file, owners } of filteredFiles) {
16517
+ if (owners.length === 0) {
16518
+ grouped["(unowned)"] = grouped["(unowned)"] || [];
16519
+ grouped["(unowned)"].push(file);
16520
+ } else {
16521
+ for (const owner of owners) {
16522
+ grouped[owner] = grouped[owner] || [];
16523
+ grouped[owner].push(file);
16524
+ }
16525
+ }
16526
+ }
16527
+ }
16528
+ outputJson({
16529
+ command: "list",
16530
+ ...grouped ? { grouped } : { files: filteredFiles.map(({ file, owners }) => ({ file, owners })) },
16531
+ filters: {
16532
+ include: options.include || null,
16533
+ pathPattern: options.pathPattern || null,
16534
+ exclusive: options.exclusive || false,
16535
+ coOwned: options.coOwned || false
16536
+ }
16537
+ });
16538
+ return;
16539
+ }
16467
16540
  if (options.group) {
16468
16541
  const ownerGroups = new Map;
16469
16542
  for (const { file, owners } of filteredFiles) {
@@ -16530,6 +16603,10 @@ Only staged files will be processed.`);
16530
16603
  ]);
16531
16604
  }
16532
16605
  } catch (err) {
16606
+ if (options.json) {
16607
+ outputJson({ command: "list", error: String(err) });
16608
+ process.exit(1);
16609
+ }
16533
16610
  log.error(err);
16534
16611
  process.exit(1);
16535
16612
  }
@@ -16782,6 +16859,9 @@ var branch = async (options) => {
16782
16859
  let operationState = options.operationState || null;
16783
16860
  const isSubOperation = !!options.operationState;
16784
16861
  let autoRecoverySucceeded = false;
16862
+ if (options.json && !isSubOperation) {
16863
+ setSilent(true);
16864
+ }
16785
16865
  try {
16786
16866
  if (!options.branch || !options.message || !options.include) {
16787
16867
  throw new Error("Missing required options for branch creation");
@@ -16831,6 +16911,93 @@ Only staged files will be processed.`);
16831
16911
  log.file(`Files to be committed:
16832
16912
  ${filesToCommit.join(`
16833
16913
  `)}`);
16914
+ if (options.dryRun) {
16915
+ const allStagedFiles = await getChangedFiles();
16916
+ const excludedFiles = allStagedFiles.filter((f) => !filesToCommit.includes(f));
16917
+ const branchAlreadyExistsDry = await branchExists(options.branch);
16918
+ if (options.json && !isSubOperation) {
16919
+ outputJson({
16920
+ command: "branch",
16921
+ dryRun: true,
16922
+ owner: options.include,
16923
+ branch: options.branch,
16924
+ branchExists: branchAlreadyExistsDry,
16925
+ message: options.message,
16926
+ files: filesToCommit,
16927
+ excludedFiles,
16928
+ options: {
16929
+ push: options.push || false,
16930
+ remote: options.remote || "origin",
16931
+ force: options.force || false,
16932
+ pr: options.pr || false,
16933
+ draftPr: options.draftPr || false,
16934
+ noVerify: !options.verify,
16935
+ append: options.append || false,
16936
+ exclusive: options.exclusive || false,
16937
+ coOwned: options.coOwned || false,
16938
+ pathPattern: options.pathPattern || null
16939
+ }
16940
+ });
16941
+ return {
16942
+ success: true,
16943
+ branchName: options.branch,
16944
+ owner: options.include,
16945
+ files: filesToCommit,
16946
+ pushed: false
16947
+ };
16948
+ }
16949
+ if (!isSubOperation) {
16950
+ log.header("Dry Run Preview — branch");
16951
+ console.log("");
16952
+ }
16953
+ const detailsTable = new import_cli_table32.default({
16954
+ style: { head: ["cyan"] },
16955
+ wordWrap: true
16956
+ });
16957
+ detailsTable.push({ [source_default.bold("Owner pattern")]: options.include }, { [source_default.bold("Branch name")]: options.branch }, {
16958
+ [source_default.bold("Branch exists")]: branchAlreadyExistsDry ? options.append ? "Yes (--append: will add commit)" : "Yes (will fail without --append)" : "No (will be created)"
16959
+ }, { [source_default.bold("Commit message")]: options.message }, {
16960
+ [source_default.bold("Files matched")]: `${filesToCommit.length} file${filesToCommit.length !== 1 ? "s" : ""}`
16961
+ }, {
16962
+ [source_default.bold("Files excluded")]: `${excludedFiles.length} staged file${excludedFiles.length !== 1 ? "s" : ""} not matching`
16963
+ }, { [source_default.bold("No-verify")]: !options.verify ? "Yes" : "No" }, {
16964
+ [source_default.bold("Push")]: options.push ? `Yes → ${options.remote || "origin"}${options.force ? " (force)" : ""}` : "No"
16965
+ }, {
16966
+ [source_default.bold("Pull request")]: options.pr ? "Yes" : options.draftPr ? "Yes (draft)" : "No"
16967
+ });
16968
+ if (options.pathPattern) {
16969
+ detailsTable.push({
16970
+ [source_default.bold("Path filter")]: options.pathPattern
16971
+ });
16972
+ }
16973
+ if (options.exclusive) {
16974
+ detailsTable.push({
16975
+ [source_default.bold("Exclusive mode")]: "Yes (only files solely owned by this owner)"
16976
+ });
16977
+ }
16978
+ if (options.coOwned) {
16979
+ detailsTable.push({
16980
+ [source_default.bold("Co-owned mode")]: "Yes (only files with multiple owners)"
16981
+ });
16982
+ }
16983
+ console.log(detailsTable.toString());
16984
+ console.log(source_default.bold.green(`
16985
+ Files to be committed (${filesToCommit.length}):`));
16986
+ filesToCommit.forEach((file) => console.log(` ${source_default.green("+")} ${file}`));
16987
+ if (excludedFiles.length > 0) {
16988
+ console.log(source_default.bold.dim(`
16989
+ Excluded staged files (${excludedFiles.length}):`));
16990
+ excludedFiles.forEach((file) => console.log(` ${source_default.dim("-")} ${source_default.dim(file)}`));
16991
+ }
16992
+ console.log("");
16993
+ return {
16994
+ success: true,
16995
+ branchName: options.branch,
16996
+ owner: options.include,
16997
+ files: filesToCommit,
16998
+ pushed: false
16999
+ };
17000
+ }
16834
17001
  const branchAlreadyExists = await branchExists(options.branch);
16835
17002
  if (branchAlreadyExists && !options.append) {
16836
17003
  throw new Error(`Branch "${options.branch}" already exists. Use --append to add commits to it, or use a different name.`);
@@ -16885,7 +17052,8 @@ Only staged files will be processed.`);
16885
17052
  remote: options.remote,
16886
17053
  upstream: options.upstream,
16887
17054
  force: options.force,
16888
- noVerify: !options.verify
17055
+ noVerify: !options.verify,
17056
+ silent: !!options.json
16889
17057
  });
16890
17058
  pushed = true;
16891
17059
  if (operationState) {
@@ -16951,7 +17119,7 @@ Only staged files will be processed.`);
16951
17119
  Files committed:`);
16952
17120
  filesToCommit.forEach((file) => console.log(` - ${file}`));
16953
17121
  }
16954
- return {
17122
+ const result = {
16955
17123
  success: true,
16956
17124
  branchName: options.branch,
16957
17125
  owner: options.include,
@@ -16960,6 +17128,15 @@ Files committed:`);
16960
17128
  prUrl,
16961
17129
  prNumber
16962
17130
  };
17131
+ if (options.json && !isSubOperation) {
17132
+ outputJson({
17133
+ command: "branch",
17134
+ dryRun: false,
17135
+ ...result,
17136
+ error: null
17137
+ });
17138
+ }
17139
+ return result;
16963
17140
  } catch (operationError) {
16964
17141
  log.error(`Operation failed: ${operationError}`);
16965
17142
  if (operationState && !isSubOperation) {
@@ -17029,6 +17206,21 @@ Files committed:`);
17029
17206
  error: String(err)
17030
17207
  };
17031
17208
  }
17209
+ if (options.json && !isSubOperation) {
17210
+ outputJson({
17211
+ command: "branch",
17212
+ dryRun: false,
17213
+ success: false,
17214
+ branchName: options.branch ?? "",
17215
+ owner: options.include ?? "",
17216
+ files: filesToCommit,
17217
+ pushed,
17218
+ prUrl: prUrl || null,
17219
+ prNumber: prNumber || null,
17220
+ error: String(err)
17221
+ });
17222
+ process.exit(1);
17223
+ }
17032
17224
  if (operationState && !autoRecoverySucceeded) {
17033
17225
  log.info(`
17034
17226
  Auto-recovery failed. Manual recovery options:`);
@@ -17051,7 +17243,6 @@ Auto-recovery failed. Manual recovery options:`);
17051
17243
  }
17052
17244
  }
17053
17245
  };
17054
-
17055
17246
  // node_modules/@inquirer/core/dist/esm/lib/key.js
17056
17247
  var isUpKey = (key) => key.name === "up" || key.name === "k" || key.ctrl && key.name === "p";
17057
17248
  var isDownKey = (key) => key.name === "down" || key.name === "j" || key.ctrl && key.name === "n";
@@ -18339,9 +18530,21 @@ Operation ID: ${op.id}`);
18339
18530
  log.info(formatBranchState(op));
18340
18531
  }
18341
18532
  };
18342
- var performRecovery = async (state, keepBranches) => {
18533
+ var performRecovery = async (state, keepBranches, options) => {
18343
18534
  log.header(`Recovering from operation ${state.id}`);
18344
18535
  let hadWarnings = false;
18536
+ if (!options?.skipDirtyCheck) {
18537
+ const hasUnstaged = await hasUnstagedChanges();
18538
+ const hasStaged = await hasStagedChanges();
18539
+ if (hasUnstaged || hasStaged) {
18540
+ log.warn("Working directory has uncommitted changes.");
18541
+ log.warn("Recovery may overwrite files in your working directory.");
18542
+ log.info("Consider committing or stashing your changes first:");
18543
+ log.info(" git stash push -m 'before recovery'");
18544
+ throw new Error("Working directory is not clean. Commit or stash changes before recovering.");
18545
+ }
18546
+ }
18547
+ const branchesWithRestoreFailure = new Set;
18345
18548
  const currentBranch = await getCurrentBranch();
18346
18549
  if (currentBranch !== state.originalBranch) {
18347
18550
  try {
@@ -18372,7 +18575,8 @@ Restoring files from branches...`);
18372
18575
  }
18373
18576
  } catch (error) {
18374
18577
  log.error(`Failed to restore files from ${branch2.name}: ${error}`);
18375
- log.info(`Files may still be accessible from the branch if it exists`);
18578
+ log.warn(`Branch ${branch2.name} will be kept to prevent data loss`);
18579
+ branchesWithRestoreFailure.add(branch2.name);
18376
18580
  hadWarnings = true;
18377
18581
  }
18378
18582
  }
@@ -18383,6 +18587,12 @@ Restoring files from branches...`);
18383
18587
  Cleaning up created branches...`);
18384
18588
  for (const branch2 of state.branches) {
18385
18589
  if (branch2.created) {
18590
+ if (branchesWithRestoreFailure.has(branch2.name)) {
18591
+ log.warn(`Skipping deletion of ${branch2.name} — file restoration failed, branch preserved to prevent data loss`);
18592
+ log.info(` To recover files manually: git checkout ${branch2.name} -- <file>`);
18593
+ log.info(` To delete manually when done: git branch -D ${branch2.name}`);
18594
+ continue;
18595
+ }
18386
18596
  try {
18387
18597
  const exists2 = await branchExists(branch2.name);
18388
18598
  if (exists2) {
@@ -18480,6 +18690,9 @@ Operation details:`);
18480
18690
  var import_cli_table33 = __toESM(require_table(), 1);
18481
18691
  var multiBranch = async (options) => {
18482
18692
  let operationState = null;
18693
+ if (options.json) {
18694
+ setSilent(true);
18695
+ }
18483
18696
  try {
18484
18697
  if (!options.branch || !options.message) {
18485
18698
  throw new Error("Missing required options for multi-branch creation");
@@ -18558,6 +18771,137 @@ Only staged files will be processed.`);
18558
18771
  }
18559
18772
  log.info(`Processing ${codeowners2.length} codeowners after filtering: ${codeowners2.join(", ")}`);
18560
18773
  }
18774
+ if (options.dryRun) {
18775
+ const previews = [];
18776
+ const allCoveredFiles = new Set;
18777
+ for (const owner of codeowners2) {
18778
+ const sanitizedOwner = owner.replace(/[^a-zA-Z0-9-_@]/g, "-").replace(/^@/, "");
18779
+ const branchName = `${options.branch}/${sanitizedOwner}`;
18780
+ const commitMessage = `${options.message} - ${owner}`;
18781
+ const ownerFiles = await getOwnerFiles(owner, owner === options.defaultOwner, options.pathPattern, options.exclusive || false, options.coOwned || false);
18782
+ for (const f of ownerFiles)
18783
+ allCoveredFiles.add(f);
18784
+ previews.push({
18785
+ owner,
18786
+ branchName,
18787
+ commitMessage,
18788
+ files: ownerFiles
18789
+ });
18790
+ }
18791
+ const uncoveredFiles = changedFiles.filter((f) => !allCoveredFiles.has(f));
18792
+ if (options.json) {
18793
+ outputJson({
18794
+ command: "multi-branch",
18795
+ dryRun: true,
18796
+ owners: previews.map((p) => ({
18797
+ owner: p.owner,
18798
+ branch: p.branchName,
18799
+ message: p.commitMessage,
18800
+ files: p.files
18801
+ })),
18802
+ uncoveredFiles,
18803
+ filesWithoutOwners: options.defaultOwner ? [] : filesWithoutOwners,
18804
+ totalFiles: changedFiles.length,
18805
+ coveredFiles: allCoveredFiles.size,
18806
+ options: {
18807
+ baseBranch: options.branch,
18808
+ baseMessage: options.message,
18809
+ push: options.push || false,
18810
+ remote: options.remote || "origin",
18811
+ force: options.force || false,
18812
+ pr: options.pr || false,
18813
+ draftPr: options.draftPr || false,
18814
+ noVerify: !options.verify,
18815
+ append: options.append || false,
18816
+ exclusive: options.exclusive || false,
18817
+ coOwned: options.coOwned || false,
18818
+ pathPattern: options.pathPattern || null,
18819
+ defaultOwner: options.defaultOwner || null
18820
+ }
18821
+ });
18822
+ return;
18823
+ }
18824
+ log.header("Dry Run Preview — multi-branch");
18825
+ console.log("");
18826
+ const settingsTable = new import_cli_table33.default({
18827
+ style: { head: ["cyan"] },
18828
+ wordWrap: true
18829
+ });
18830
+ settingsTable.push({ [source_default.bold("Base branch name")]: options.branch }, { [source_default.bold("Base commit message")]: options.message }, { [source_default.bold("Total codeowners")]: `${codeowners2.length}` }, { [source_default.bold("No-verify")]: !options.verify ? "Yes" : "No" }, {
18831
+ [source_default.bold("Push")]: options.push ? `Yes → ${options.remote || "origin"}${options.force ? " (force)" : ""}` : "No"
18832
+ }, {
18833
+ [source_default.bold("Pull request")]: options.pr ? "Yes" : options.draftPr ? "Yes (draft)" : "No"
18834
+ }, { [source_default.bold("Append mode")]: options.append ? "Yes" : "No" });
18835
+ if (options.pathPattern) {
18836
+ settingsTable.push({
18837
+ [source_default.bold("Path filter")]: options.pathPattern
18838
+ });
18839
+ }
18840
+ if (options.exclusive) {
18841
+ settingsTable.push({
18842
+ [source_default.bold("Exclusive mode")]: "Yes (only files solely owned by each owner)"
18843
+ });
18844
+ }
18845
+ if (options.coOwned) {
18846
+ settingsTable.push({
18847
+ [source_default.bold("Co-owned mode")]: "Yes (only files with multiple owners)"
18848
+ });
18849
+ }
18850
+ if (options.defaultOwner) {
18851
+ settingsTable.push({
18852
+ [source_default.bold("Default owner")]: options.defaultOwner
18853
+ });
18854
+ }
18855
+ console.log(settingsTable.toString());
18856
+ console.log("");
18857
+ const summaryTable = new import_cli_table33.default({
18858
+ head: ["Owner", "Branch", "Files", "Commit Message"],
18859
+ colWidths: [22, 35, 8, 45],
18860
+ wordWrap: true,
18861
+ style: { head: ["cyan"] }
18862
+ });
18863
+ for (const p of previews) {
18864
+ summaryTable.push([
18865
+ p.owner,
18866
+ p.branchName,
18867
+ `${p.files.length}`,
18868
+ p.commitMessage
18869
+ ]);
18870
+ }
18871
+ console.log(summaryTable.toString());
18872
+ console.log(source_default.bold.cyan(`
18873
+ Files by branch:`));
18874
+ for (const p of previews) {
18875
+ if (p.files.length > 0) {
18876
+ console.log(`
18877
+ ${source_default.bold(p.branchName)} ${source_default.dim(`(${p.owner})`)} — ${p.files.length} file${p.files.length !== 1 ? "s" : ""}:`);
18878
+ p.files.forEach((file) => console.log(` ${source_default.green("+")} ${file}`));
18879
+ } else {
18880
+ console.log(`
18881
+ ${source_default.bold(p.branchName)} ${source_default.dim(`(${p.owner})`)} — ${source_default.yellow("0 files (branch will be skipped)")}`);
18882
+ }
18883
+ }
18884
+ if (uncoveredFiles.length > 0) {
18885
+ console.log(source_default.bold.yellow(`
18886
+ Uncovered staged files (${uncoveredFiles.length}) — not included in any branch:`));
18887
+ uncoveredFiles.forEach((file) => console.log(` ${source_default.yellow("!")} ${file}`));
18888
+ }
18889
+ if (filesWithoutOwners.length > 0 && !options.defaultOwner) {
18890
+ console.log(source_default.bold.yellow(`
18891
+ Files without CODEOWNERS (${filesWithoutOwners.length}):`));
18892
+ filesWithoutOwners.forEach((file) => console.log(` ${source_default.yellow("?")} ${file}`));
18893
+ console.log(source_default.dim(" Tip: Use --default-owner <owner> to assign these files"));
18894
+ }
18895
+ console.log(source_default.bold.cyan(`
18896
+ Summary:`));
18897
+ console.log(` Branches to create: ${source_default.bold(`${previews.length}`)}`);
18898
+ console.log(` Total files covered: ${source_default.bold(`${allCoveredFiles.size}`)} of ${changedFiles.length} staged`);
18899
+ if (uncoveredFiles.length > 0) {
18900
+ console.log(` Uncovered files: ${source_default.yellow(`${uncoveredFiles.length}`)}`);
18901
+ }
18902
+ console.log("");
18903
+ return;
18904
+ }
18561
18905
  const results = [];
18562
18906
  for (const owner of codeowners2) {
18563
18907
  const sanitizedOwner = owner.replace(/[^a-zA-Z0-9-_@]/g, "-").replace(/^@/, "");
@@ -18581,7 +18925,8 @@ Only staged files will be processed.`);
18581
18925
  operationState: operationState || undefined,
18582
18926
  pathPattern: options.pathPattern,
18583
18927
  exclusive: options.exclusive,
18584
- coOwned: options.coOwned
18928
+ coOwned: options.coOwned,
18929
+ json: options.json
18585
18930
  });
18586
18931
  results.push(result);
18587
18932
  }
@@ -18644,7 +18989,36 @@ Note: ${failureCount} branch(es) failed. Files were auto-restored to working dir
18644
18989
  log.info(`State preserved for reference. Run 'codeowners-git recover --id ${operationState.id}' if needed.`);
18645
18990
  }
18646
18991
  }
18992
+ if (options.json) {
18993
+ outputJson({
18994
+ command: "multi-branch",
18995
+ dryRun: false,
18996
+ success: failureCount === 0,
18997
+ totalOwners: codeowners2.length,
18998
+ successCount,
18999
+ failureCount,
19000
+ results: results.map((r) => ({
19001
+ owner: r.owner,
19002
+ branch: r.branchName,
19003
+ success: r.success,
19004
+ files: r.files,
19005
+ pushed: r.pushed,
19006
+ prUrl: r.prUrl || null,
19007
+ prNumber: r.prNumber || null,
19008
+ error: r.error || null
19009
+ }))
19010
+ });
19011
+ }
18647
19012
  } catch (err) {
19013
+ if (options.json) {
19014
+ outputJson({
19015
+ command: "multi-branch",
19016
+ dryRun: false,
19017
+ success: false,
19018
+ error: String(err)
19019
+ });
19020
+ process.exit(1);
19021
+ }
18648
19022
  log.error(`Multi-branch operation failed: ${err}`);
18649
19023
  if (operationState) {
18650
19024
  log.info(`
@@ -18652,7 +19026,7 @@ Attempting auto-recovery...`);
18652
19026
  const currentState = loadOperationState(operationState.id);
18653
19027
  if (currentState) {
18654
19028
  try {
18655
- const recovered = await performRecovery(currentState, false);
19029
+ const recovered = await performRecovery(currentState, false, { skipDirtyCheck: true });
18656
19030
  if (recovered) {
18657
19031
  log.success("Auto-recovery completed successfully");
18658
19032
  } else {
@@ -18672,7 +19046,11 @@ Manual recovery options:`);
18672
19046
  };
18673
19047
 
18674
19048
  // src/commands/extract.ts
19049
+ var import_cli_table34 = __toESM(require_table(), 1);
18675
19050
  var extract = async (options) => {
19051
+ if (options.json) {
19052
+ setSilent(true);
19053
+ }
18676
19054
  try {
18677
19055
  if (!options.source) {
18678
19056
  log.error("Missing required option: --source");
@@ -18760,8 +19138,82 @@ Only staged files will be processed.`);
18760
19138
  }
18761
19139
  log.info(`Filtered to ${filesToExtract.length} file${filesToExtract.length !== 1 ? "s" : ""}`);
18762
19140
  }
19141
+ if (options.dryRun) {
19142
+ const excludedFiles = changedFiles.filter((f) => !filesToExtract.includes(f));
19143
+ if (options.json) {
19144
+ outputJson({
19145
+ command: "extract",
19146
+ dryRun: true,
19147
+ source: options.source,
19148
+ compareTarget: compareTarget || null,
19149
+ files: filesToExtract,
19150
+ excludedFiles,
19151
+ totalChanged: changedFiles.length,
19152
+ options: {
19153
+ include: options.include || null,
19154
+ pathPattern: options.pathPattern || null,
19155
+ exclusive: options.exclusive || false,
19156
+ coOwned: options.coOwned || false,
19157
+ compareMain: options.compareMain || false
19158
+ }
19159
+ });
19160
+ return;
19161
+ }
19162
+ log.header("Dry Run Preview — extract");
19163
+ console.log("");
19164
+ const detailsTable = new import_cli_table34.default({
19165
+ style: { head: ["cyan"] },
19166
+ wordWrap: true
19167
+ });
19168
+ detailsTable.push({ [source_default.bold("Source")]: options.source }, {
19169
+ [source_default.bold("Compare target")]: compareTarget || "auto-detected"
19170
+ }, {
19171
+ [source_default.bold("Files in source")]: `${changedFiles.length} changed file${changedFiles.length !== 1 ? "s" : ""}`
19172
+ }, {
19173
+ [source_default.bold("Files to extract")]: `${filesToExtract.length} file${filesToExtract.length !== 1 ? "s" : ""}`
19174
+ }, {
19175
+ [source_default.bold("Files excluded")]: `${excludedFiles.length} file${excludedFiles.length !== 1 ? "s" : ""} (filtered out)`
19176
+ });
19177
+ if (options.include) {
19178
+ detailsTable.push({
19179
+ [source_default.bold("Owner filter")]: `${options.include}${options.exclusive ? " (exclusive)" : ""}`
19180
+ });
19181
+ }
19182
+ if (options.pathPattern) {
19183
+ detailsTable.push({
19184
+ [source_default.bold("Path filter")]: options.pathPattern
19185
+ });
19186
+ }
19187
+ if (options.coOwned) {
19188
+ detailsTable.push({
19189
+ [source_default.bold("Co-owned mode")]: "Yes (only files with multiple owners)"
19190
+ });
19191
+ }
19192
+ console.log(detailsTable.toString());
19193
+ console.log(source_default.bold.green(`
19194
+ Files to be extracted (${filesToExtract.length}):`));
19195
+ filesToExtract.forEach((file) => console.log(` ${source_default.green("+")} ${file}`));
19196
+ if (excludedFiles.length > 0) {
19197
+ console.log(source_default.bold.dim(`
19198
+ Excluded files (${excludedFiles.length}):`));
19199
+ excludedFiles.forEach((file) => console.log(` ${source_default.dim("-")} ${source_default.dim(file)}`));
19200
+ }
19201
+ console.log("");
19202
+ return;
19203
+ }
18763
19204
  log.info("Extracting files to working directory...");
18764
19205
  await extractFilesFromRef(options.source, filesToExtract);
19206
+ if (options.json) {
19207
+ outputJson({
19208
+ command: "extract",
19209
+ dryRun: false,
19210
+ source: options.source,
19211
+ compareTarget: compareTarget || null,
19212
+ files: filesToExtract,
19213
+ totalChanged: changedFiles.length
19214
+ });
19215
+ return;
19216
+ }
18765
19217
  log.success(`
18766
19218
  ✓ Extracted ${filesToExtract.length} file${filesToExtract.length !== 1 ? "s" : ""} to working directory (unstaged)`);
18767
19219
  log.info(`
@@ -18773,13 +19225,17 @@ Next steps:`);
18773
19225
  log.info(" - Use 'cg branch' command to create a branch and commit");
18774
19226
  log.info(" - Example: cg branch -i @my-team -b my-branch -m 'Commit message' -p");
18775
19227
  } catch (err) {
19228
+ if (options.json) {
19229
+ outputJson({ command: "extract", error: String(err) });
19230
+ process.exit(1);
19231
+ }
18776
19232
  log.error(`
18777
19233
  ✗ Extraction failed: ${err}`);
18778
19234
  process.exit(1);
18779
19235
  }
18780
19236
  };
18781
19237
  // package.json
18782
- var version = "2.0.1";
19238
+ var version = "2.1.0";
18783
19239
 
18784
19240
  // src/commands/version.ts
18785
19241
  function getVersion() {
@@ -18800,15 +19256,28 @@ Force exiting...`);
18800
19256
 
18801
19257
  Received ${signal}. Gracefully shutting down...`);
18802
19258
  const incompleteOps = getIncompleteOperations();
18803
- if (incompleteOps.length > 0) {
18804
- log.warn(`Found ${incompleteOps.length} incomplete operation(s).`);
19259
+ if (incompleteOps.length === 0) {
19260
+ process.exit(130);
19261
+ return;
19262
+ }
19263
+ const mostRecent = incompleteOps[0];
19264
+ log.warn(`Found ${incompleteOps.length} incomplete operation(s).`);
19265
+ getCurrentBranch().then((currentBranch) => {
19266
+ if (currentBranch !== mostRecent.originalBranch) {
19267
+ log.info(`Returning to original branch: ${mostRecent.originalBranch}...`);
19268
+ return checkout(mostRecent.originalBranch).then(() => {
19269
+ log.success(`Returned to ${mostRecent.originalBranch}`);
19270
+ });
19271
+ }
19272
+ }).catch(() => {
19273
+ log.warn("Could not return to original branch automatically.");
19274
+ }).finally(() => {
18805
19275
  log.info(`
18806
- To recover from incomplete operations, run:`);
18807
- log.info(" codeowners-git recover --list # List all incomplete operations");
19276
+ To fully recover from incomplete operations, run:`);
18808
19277
  log.info(" codeowners-git recover --auto # Auto-recover most recent operation");
18809
- log.info(" codeowners-git recover --id <id> # Recover specific operation");
18810
- }
18811
- process.exit(130);
19278
+ log.info(" codeowners-git recover --list # List all incomplete operations");
19279
+ process.exit(130);
19280
+ });
18812
19281
  };
18813
19282
  process.on("SIGINT", () => handleShutdown("SIGINT"));
18814
19283
  process.on("SIGTERM", () => handleShutdown("SIGTERM"));
@@ -18818,7 +19287,7 @@ To recover from incomplete operations, run:`);
18818
19287
  setupSignalHandlers();
18819
19288
  var program2 = new Command;
18820
19289
  program2.name("codeowners-git (cg)").description("CLI tool for grouping and managing staged files by CODEOWNERS").version(getVersion());
18821
- program2.command("list").description("Lists all git changed files by CODEOWNER").argument("[pattern]", "Path pattern to filter files (micromatch syntax, comma-separated)").option("-i, --include <patterns>", "Filter by owner patterns").option("-g, --group", "Group files by code owner").option("-e, --exclusive", "Only include files where the owner is the sole owner (no co-owners)").option("-c, --co-owned", "Only include files with multiple owners (co-owned files)").action((pattern, options) => {
19290
+ program2.command("list").description("Lists all git changed files by CODEOWNER").argument("[pattern]", "Path pattern to filter files (micromatch syntax, comma-separated)").option("-i, --include <patterns>", "Filter by owner patterns").option("-g, --group", "Group files by code owner").option("-e, --exclusive", "Only include files where the owner is the sole owner (no co-owners)").option("-c, --co-owned", "Only include files with multiple owners (co-owned files)").option("--json", "Output results as JSON (suppresses all other output)").action((pattern, options) => {
18822
19291
  if (options.exclusive && options.coOwned) {
18823
19292
  console.error("Error: Cannot use both --exclusive and --co-owned options");
18824
19293
  process.exit(1);
@@ -18828,7 +19297,7 @@ program2.command("list").description("Lists all git changed files by CODEOWNER")
18828
19297
  pathPattern: pattern
18829
19298
  });
18830
19299
  });
18831
- program2.command("branch").description("Create new branch with codeowner changes").argument("[pattern]", "Path pattern to filter files (micromatch syntax, comma-separated)").requiredOption("-i, --include <patterns>", "Code owner pattern to filter files").requiredOption("-b, --branch <branch>", "Branch name").requiredOption("-m, --message <message>", "Commit message").option("-n, --no-verify", "Skip lint-staged or any other ci checks").option("-p, --push", "Push branch to remote after commit").option("-r, --remote <remote>", "Remote name to push to", "origin").option("-u, --upstream <upstream>", "Upstream branch name (defaults to local branch name)").option("-f, --force", "Force push to remote").option("-k, --keep-branch-on-failure", "Keep the created branch even if operation fails").option("--append", "Add commits to existing branch instead of creating a new one").option("--pr", "Create a pull request after pushing (requires --push)").option("--draft-pr", "Create a draft pull request after pushing (requires --push)").option("-e, --exclusive", "Only include files where the owner is the sole owner (no co-owners)").option("-c, --co-owned", "Only include files with multiple owners (co-owned files)").action((pattern, options) => {
19300
+ program2.command("branch").description("Create new branch with codeowner changes").argument("[pattern]", "Path pattern to filter files (micromatch syntax, comma-separated)").requiredOption("-i, --include <patterns>", "Code owner pattern to filter files").requiredOption("-b, --branch <branch>", "Branch name").requiredOption("-m, --message <message>", "Commit message").option("-n, --no-verify", "Skip lint-staged or any other ci checks").option("-p, --push", "Push branch to remote after commit").option("-r, --remote <remote>", "Remote name to push to", "origin").option("-u, --upstream <upstream>", "Upstream branch name (defaults to local branch name)").option("-f, --force", "Force push to remote").option("-k, --keep-branch-on-failure", "Keep the created branch even if operation fails").option("--append", "Add commits to existing branch instead of creating a new one").option("--pr", "Create a pull request after pushing (requires --push)").option("--draft-pr", "Create a draft pull request after pushing (requires --push)").option("-e, --exclusive", "Only include files where the owner is the sole owner (no co-owners)").option("-c, --co-owned", "Only include files with multiple owners (co-owned files)").option("--dry-run", "Preview the operation without making any changes").option("--json", "Output results as JSON (suppresses all other output)").action((pattern, options) => {
18832
19301
  if (options.exclusive && options.coOwned) {
18833
19302
  console.error("Error: Cannot use both --exclusive and --co-owned options");
18834
19303
  process.exit(1);
@@ -18838,7 +19307,7 @@ program2.command("branch").description("Create new branch with codeowner changes
18838
19307
  pathPattern: pattern
18839
19308
  });
18840
19309
  });
18841
- program2.command("multi-branch").description("Create branches for all codeowners").argument("[pattern]", "Path pattern to filter files (micromatch syntax, comma-separated)").requiredOption("-b, --branch <branch>", "Base branch name (will be suffixed with codeowner name)").requiredOption("-m, --message <message>", "Base commit message (will be suffixed with codeowner name)").option("-n, --no-verify", "Skip lint-staged or any other ci checks").option("-p, --push", "Push branches to remote after commit").option("-r, --remote <remote>", "Remote name to push to", "origin").option("-u, --upstream <upstream>", "Upstream branch name pattern (defaults to local branch name)").option("-f, --force", "Force push to remote").option("-k, --keep-branch-on-failure", "Keep created branches even if operation fails").option("-d, --default-owner <defaultOwner>", "Default owner to use when no codeowners are found for changed files").option("--ignore <patterns>", "Comma-separated patterns to exclude codeowners (e.g., 'team-a,team-b')").option("--include <patterns>", "Comma-separated patterns to include codeowners (e.g., 'team-*,@org/*')").option("--append", "Add commits to existing branches instead of creating new ones").option("--pr", "Create pull requests after pushing (requires --push)").option("--draft-pr", "Create draft pull requests after pushing (requires --push)").option("-e, --exclusive", "Only include files where each owner is the sole owner (no co-owners)").option("-c, --co-owned", "Only include files with multiple owners (co-owned files)").action((pattern, options) => {
19310
+ program2.command("multi-branch").description("Create branches for all codeowners").argument("[pattern]", "Path pattern to filter files (micromatch syntax, comma-separated)").requiredOption("-b, --branch <branch>", "Base branch name (will be suffixed with codeowner name)").requiredOption("-m, --message <message>", "Base commit message (will be suffixed with codeowner name)").option("-n, --no-verify", "Skip lint-staged or any other ci checks").option("-p, --push", "Push branches to remote after commit").option("-r, --remote <remote>", "Remote name to push to", "origin").option("-u, --upstream <upstream>", "Upstream branch name pattern (defaults to local branch name)").option("-f, --force", "Force push to remote").option("-k, --keep-branch-on-failure", "Keep created branches even if operation fails").option("-d, --default-owner <defaultOwner>", "Default owner to use when no codeowners are found for changed files").option("--ignore <patterns>", "Comma-separated patterns to exclude codeowners (e.g., 'team-a,team-b')").option("--include <patterns>", "Comma-separated patterns to include codeowners (e.g., 'team-*,@org/*')").option("--append", "Add commits to existing branches instead of creating new ones").option("--pr", "Create pull requests after pushing (requires --push)").option("--draft-pr", "Create draft pull requests after pushing (requires --push)").option("-e, --exclusive", "Only include files where each owner is the sole owner (no co-owners)").option("-c, --co-owned", "Only include files with multiple owners (co-owned files)").option("--dry-run", "Preview the operation without making any changes").option("--json", "Output results as JSON (suppresses all other output)").action((pattern, options) => {
18842
19311
  if (options.exclusive && options.coOwned) {
18843
19312
  console.error("Error: Cannot use both --exclusive and --co-owned options");
18844
19313
  process.exit(1);
@@ -18848,7 +19317,7 @@ program2.command("multi-branch").description("Create branches for all codeowners
18848
19317
  pathPattern: pattern
18849
19318
  });
18850
19319
  });
18851
- program2.command("extract").description("Extract file changes from a branch or commit to working directory").argument("[pattern]", "Path pattern to filter files (micromatch syntax, comma-separated)").requiredOption("-s, --source <source>", "Source branch or commit to extract from").option("-i, --include <patterns>", "Filter extracted files by code owner pattern").option("--compare-main", "Compare source against main branch instead of detecting merge-base").option("-e, --exclusive", "Only include files where the owner is the sole owner (no co-owners)").option("-c, --co-owned", "Only include files with multiple owners (co-owned files)").action((pattern, options) => {
19320
+ program2.command("extract").description("Extract file changes from a branch or commit to working directory").argument("[pattern]", "Path pattern to filter files (micromatch syntax, comma-separated)").requiredOption("-s, --source <source>", "Source branch or commit to extract from").option("-i, --include <patterns>", "Filter extracted files by code owner pattern").option("--compare-main", "Compare source against main branch instead of detecting merge-base").option("-e, --exclusive", "Only include files where the owner is the sole owner (no co-owners)").option("-c, --co-owned", "Only include files with multiple owners (co-owned files)").option("--dry-run", "Preview the operation without making any changes").option("--json", "Output results as JSON (suppresses all other output)").action((pattern, options) => {
18852
19321
  if (options.exclusive && options.coOwned) {
18853
19322
  console.error("Error: Cannot use both --exclusive and --co-owned options");
18854
19323
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeowners-git",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "module": "src/cli.ts",
5
5
  "type": "module",
6
6
  "private": false,