codeowners-git 2.0.2 → 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 +506 -35
  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();
@@ -16240,7 +16282,8 @@ var pushBranch = async (branchName, {
16240
16282
  remote = "origin",
16241
16283
  upstream,
16242
16284
  force = false,
16243
- noVerify = false
16285
+ noVerify = false,
16286
+ silent = false
16244
16287
  } = {}) => {
16245
16288
  const targetUpstream = upstream || branchName;
16246
16289
  log.info(`Pushing branch "${branchName}" to ${remote}/${targetUpstream}...`);
@@ -16253,8 +16296,9 @@ var pushBranch = async (branchName, {
16253
16296
  if (noVerify) {
16254
16297
  pushArgs.push("--no-verify");
16255
16298
  }
16299
+ const stdioOption = silent ? ["pipe", "pipe", "pipe"] : ["inherit", "inherit", "inherit"];
16256
16300
  const gitProcess = spawn2("git", ["push", ...pushArgs], {
16257
- stdio: ["inherit", "inherit", "inherit"]
16301
+ stdio: stdioOption
16258
16302
  });
16259
16303
  return new Promise((resolve, reject) => {
16260
16304
  gitProcess.on("close", (code) => {
@@ -16430,6 +16474,9 @@ var getOwnerFiles = async (ownerPattern, includeUnowned = false, pathPattern, ex
16430
16474
  // src/commands/list.ts
16431
16475
  var listCodeowners = async (options) => {
16432
16476
  try {
16477
+ if (options.json) {
16478
+ setSilent(true);
16479
+ }
16433
16480
  if (await hasUnstagedChanges()) {
16434
16481
  const unstagedFiles = await getUnstagedFiles();
16435
16482
  log.warn("Warning: Unstaged changes detected (these will be ignored):");
@@ -16462,6 +16509,34 @@ Only staged files will be processed.`);
16462
16509
  return matchFn(owners, patterns);
16463
16510
  });
16464
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
+ }
16465
16540
  if (options.group) {
16466
16541
  const ownerGroups = new Map;
16467
16542
  for (const { file, owners } of filteredFiles) {
@@ -16528,6 +16603,10 @@ Only staged files will be processed.`);
16528
16603
  ]);
16529
16604
  }
16530
16605
  } catch (err) {
16606
+ if (options.json) {
16607
+ outputJson({ command: "list", error: String(err) });
16608
+ process.exit(1);
16609
+ }
16531
16610
  log.error(err);
16532
16611
  process.exit(1);
16533
16612
  }
@@ -16780,6 +16859,9 @@ var branch = async (options) => {
16780
16859
  let operationState = options.operationState || null;
16781
16860
  const isSubOperation = !!options.operationState;
16782
16861
  let autoRecoverySucceeded = false;
16862
+ if (options.json && !isSubOperation) {
16863
+ setSilent(true);
16864
+ }
16783
16865
  try {
16784
16866
  if (!options.branch || !options.message || !options.include) {
16785
16867
  throw new Error("Missing required options for branch creation");
@@ -16829,6 +16911,93 @@ Only staged files will be processed.`);
16829
16911
  log.file(`Files to be committed:
16830
16912
  ${filesToCommit.join(`
16831
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
+ }
16832
17001
  const branchAlreadyExists = await branchExists(options.branch);
16833
17002
  if (branchAlreadyExists && !options.append) {
16834
17003
  throw new Error(`Branch "${options.branch}" already exists. Use --append to add commits to it, or use a different name.`);
@@ -16883,7 +17052,8 @@ Only staged files will be processed.`);
16883
17052
  remote: options.remote,
16884
17053
  upstream: options.upstream,
16885
17054
  force: options.force,
16886
- noVerify: !options.verify
17055
+ noVerify: !options.verify,
17056
+ silent: !!options.json
16887
17057
  });
16888
17058
  pushed = true;
16889
17059
  if (operationState) {
@@ -16949,7 +17119,7 @@ Only staged files will be processed.`);
16949
17119
  Files committed:`);
16950
17120
  filesToCommit.forEach((file) => console.log(` - ${file}`));
16951
17121
  }
16952
- return {
17122
+ const result = {
16953
17123
  success: true,
16954
17124
  branchName: options.branch,
16955
17125
  owner: options.include,
@@ -16958,6 +17128,15 @@ Files committed:`);
16958
17128
  prUrl,
16959
17129
  prNumber
16960
17130
  };
17131
+ if (options.json && !isSubOperation) {
17132
+ outputJson({
17133
+ command: "branch",
17134
+ dryRun: false,
17135
+ ...result,
17136
+ error: null
17137
+ });
17138
+ }
17139
+ return result;
16961
17140
  } catch (operationError) {
16962
17141
  log.error(`Operation failed: ${operationError}`);
16963
17142
  if (operationState && !isSubOperation) {
@@ -17027,6 +17206,21 @@ Files committed:`);
17027
17206
  error: String(err)
17028
17207
  };
17029
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
+ }
17030
17224
  if (operationState && !autoRecoverySucceeded) {
17031
17225
  log.info(`
17032
17226
  Auto-recovery failed. Manual recovery options:`);
@@ -17049,7 +17243,6 @@ Auto-recovery failed. Manual recovery options:`);
17049
17243
  }
17050
17244
  }
17051
17245
  };
17052
-
17053
17246
  // node_modules/@inquirer/core/dist/esm/lib/key.js
17054
17247
  var isUpKey = (key) => key.name === "up" || key.name === "k" || key.ctrl && key.name === "p";
17055
17248
  var isDownKey = (key) => key.name === "down" || key.name === "j" || key.ctrl && key.name === "n";
@@ -18337,9 +18530,21 @@ Operation ID: ${op.id}`);
18337
18530
  log.info(formatBranchState(op));
18338
18531
  }
18339
18532
  };
18340
- var performRecovery = async (state, keepBranches) => {
18533
+ var performRecovery = async (state, keepBranches, options) => {
18341
18534
  log.header(`Recovering from operation ${state.id}`);
18342
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;
18343
18548
  const currentBranch = await getCurrentBranch();
18344
18549
  if (currentBranch !== state.originalBranch) {
18345
18550
  try {
@@ -18370,7 +18575,8 @@ Restoring files from branches...`);
18370
18575
  }
18371
18576
  } catch (error) {
18372
18577
  log.error(`Failed to restore files from ${branch2.name}: ${error}`);
18373
- 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);
18374
18580
  hadWarnings = true;
18375
18581
  }
18376
18582
  }
@@ -18381,6 +18587,12 @@ Restoring files from branches...`);
18381
18587
  Cleaning up created branches...`);
18382
18588
  for (const branch2 of state.branches) {
18383
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
+ }
18384
18596
  try {
18385
18597
  const exists2 = await branchExists(branch2.name);
18386
18598
  if (exists2) {
@@ -18478,6 +18690,9 @@ Operation details:`);
18478
18690
  var import_cli_table33 = __toESM(require_table(), 1);
18479
18691
  var multiBranch = async (options) => {
18480
18692
  let operationState = null;
18693
+ if (options.json) {
18694
+ setSilent(true);
18695
+ }
18481
18696
  try {
18482
18697
  if (!options.branch || !options.message) {
18483
18698
  throw new Error("Missing required options for multi-branch creation");
@@ -18556,6 +18771,137 @@ Only staged files will be processed.`);
18556
18771
  }
18557
18772
  log.info(`Processing ${codeowners2.length} codeowners after filtering: ${codeowners2.join(", ")}`);
18558
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
+ }
18559
18905
  const results = [];
18560
18906
  for (const owner of codeowners2) {
18561
18907
  const sanitizedOwner = owner.replace(/[^a-zA-Z0-9-_@]/g, "-").replace(/^@/, "");
@@ -18579,7 +18925,8 @@ Only staged files will be processed.`);
18579
18925
  operationState: operationState || undefined,
18580
18926
  pathPattern: options.pathPattern,
18581
18927
  exclusive: options.exclusive,
18582
- coOwned: options.coOwned
18928
+ coOwned: options.coOwned,
18929
+ json: options.json
18583
18930
  });
18584
18931
  results.push(result);
18585
18932
  }
@@ -18642,7 +18989,36 @@ Note: ${failureCount} branch(es) failed. Files were auto-restored to working dir
18642
18989
  log.info(`State preserved for reference. Run 'codeowners-git recover --id ${operationState.id}' if needed.`);
18643
18990
  }
18644
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
+ }
18645
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
+ }
18646
19022
  log.error(`Multi-branch operation failed: ${err}`);
18647
19023
  if (operationState) {
18648
19024
  log.info(`
@@ -18650,7 +19026,7 @@ Attempting auto-recovery...`);
18650
19026
  const currentState = loadOperationState(operationState.id);
18651
19027
  if (currentState) {
18652
19028
  try {
18653
- const recovered = await performRecovery(currentState, false);
19029
+ const recovered = await performRecovery(currentState, false, { skipDirtyCheck: true });
18654
19030
  if (recovered) {
18655
19031
  log.success("Auto-recovery completed successfully");
18656
19032
  } else {
@@ -18670,7 +19046,11 @@ Manual recovery options:`);
18670
19046
  };
18671
19047
 
18672
19048
  // src/commands/extract.ts
19049
+ var import_cli_table34 = __toESM(require_table(), 1);
18673
19050
  var extract = async (options) => {
19051
+ if (options.json) {
19052
+ setSilent(true);
19053
+ }
18674
19054
  try {
18675
19055
  if (!options.source) {
18676
19056
  log.error("Missing required option: --source");
@@ -18758,8 +19138,82 @@ Only staged files will be processed.`);
18758
19138
  }
18759
19139
  log.info(`Filtered to ${filesToExtract.length} file${filesToExtract.length !== 1 ? "s" : ""}`);
18760
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
+ }
18761
19204
  log.info("Extracting files to working directory...");
18762
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
+ }
18763
19217
  log.success(`
18764
19218
  ✓ Extracted ${filesToExtract.length} file${filesToExtract.length !== 1 ? "s" : ""} to working directory (unstaged)`);
18765
19219
  log.info(`
@@ -18771,13 +19225,17 @@ Next steps:`);
18771
19225
  log.info(" - Use 'cg branch' command to create a branch and commit");
18772
19226
  log.info(" - Example: cg branch -i @my-team -b my-branch -m 'Commit message' -p");
18773
19227
  } catch (err) {
19228
+ if (options.json) {
19229
+ outputJson({ command: "extract", error: String(err) });
19230
+ process.exit(1);
19231
+ }
18774
19232
  log.error(`
18775
19233
  ✗ Extraction failed: ${err}`);
18776
19234
  process.exit(1);
18777
19235
  }
18778
19236
  };
18779
19237
  // package.json
18780
- var version = "2.0.2";
19238
+ var version = "2.1.0";
18781
19239
 
18782
19240
  // src/commands/version.ts
18783
19241
  function getVersion() {
@@ -18798,15 +19256,28 @@ Force exiting...`);
18798
19256
 
18799
19257
  Received ${signal}. Gracefully shutting down...`);
18800
19258
  const incompleteOps = getIncompleteOperations();
18801
- if (incompleteOps.length > 0) {
18802
- 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(() => {
18803
19275
  log.info(`
18804
- To recover from incomplete operations, run:`);
18805
- log.info(" codeowners-git recover --list # List all incomplete operations");
19276
+ To fully recover from incomplete operations, run:`);
18806
19277
  log.info(" codeowners-git recover --auto # Auto-recover most recent operation");
18807
- log.info(" codeowners-git recover --id <id> # Recover specific operation");
18808
- }
18809
- process.exit(130);
19278
+ log.info(" codeowners-git recover --list # List all incomplete operations");
19279
+ process.exit(130);
19280
+ });
18810
19281
  };
18811
19282
  process.on("SIGINT", () => handleShutdown("SIGINT"));
18812
19283
  process.on("SIGTERM", () => handleShutdown("SIGTERM"));
@@ -18816,7 +19287,7 @@ To recover from incomplete operations, run:`);
18816
19287
  setupSignalHandlers();
18817
19288
  var program2 = new Command;
18818
19289
  program2.name("codeowners-git (cg)").description("CLI tool for grouping and managing staged files by CODEOWNERS").version(getVersion());
18819
- 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) => {
18820
19291
  if (options.exclusive && options.coOwned) {
18821
19292
  console.error("Error: Cannot use both --exclusive and --co-owned options");
18822
19293
  process.exit(1);
@@ -18826,7 +19297,7 @@ program2.command("list").description("Lists all git changed files by CODEOWNER")
18826
19297
  pathPattern: pattern
18827
19298
  });
18828
19299
  });
18829
- 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) => {
18830
19301
  if (options.exclusive && options.coOwned) {
18831
19302
  console.error("Error: Cannot use both --exclusive and --co-owned options");
18832
19303
  process.exit(1);
@@ -18836,7 +19307,7 @@ program2.command("branch").description("Create new branch with codeowner changes
18836
19307
  pathPattern: pattern
18837
19308
  });
18838
19309
  });
18839
- 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) => {
18840
19311
  if (options.exclusive && options.coOwned) {
18841
19312
  console.error("Error: Cannot use both --exclusive and --co-owned options");
18842
19313
  process.exit(1);
@@ -18846,7 +19317,7 @@ program2.command("multi-branch").description("Create branches for all codeowners
18846
19317
  pathPattern: pattern
18847
19318
  });
18848
19319
  });
18849
- 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) => {
18850
19321
  if (options.exclusive && options.coOwned) {
18851
19322
  console.error("Error: Cannot use both --exclusive and --co-owned options");
18852
19323
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeowners-git",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
4
4
  "module": "src/cli.ts",
5
5
  "type": "module",
6
6
  "private": false,