git-stack-cli 1.14.0 → 1.15.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.
@@ -18487,15 +18487,41 @@ const immerImpl = (initializer) => (set, get, store) => {
18487
18487
  };
18488
18488
  const immer = immerImpl;
18489
18489
 
18490
+ async function sleep(time) {
18491
+ return new Promise((resolve) => setTimeout(resolve, time));
18492
+ }
18493
+
18490
18494
  function Exit(props) {
18491
- const actions = Store.useActions();
18492
18495
  reactExports.useEffect(() => {
18493
- if (props.clear) {
18494
- actions.clear();
18496
+ // immediately handle exit on mount
18497
+ handle_exit().catch((err) => {
18498
+ // eslint-disable-next-line no-console
18499
+ console.error(err);
18500
+ });
18501
+ async function handle_exit() {
18502
+ const state = Store.getState();
18503
+ const actions = state.actions;
18504
+ actions.debug(`[Exit] handle_exit ${JSON.stringify(props)}`);
18505
+ let exit_code = props.code;
18506
+ // run abort_handler if it exists
18507
+ if (state.abort_handler) {
18508
+ exit_code = await state.abort_handler();
18509
+ }
18510
+ // restore git stash if necessary
18511
+ if (state.is_dirty_check_stash) {
18512
+ await cli("git stash pop");
18513
+ actions.output(reactExports.createElement(Text, { color: colors.green }, "\u2705 Changes restored from stash"));
18514
+ }
18515
+ // ensure output has a chance to render
18516
+ await sleep(1);
18517
+ // finally handle the actual app and process exit
18518
+ if (props.clear) {
18519
+ actions.clear();
18520
+ }
18521
+ actions.unmount();
18522
+ process.exitCode = exit_code;
18523
+ process.exit();
18495
18524
  }
18496
- actions.unmount();
18497
- process.exitCode = props.code;
18498
- process.exit();
18499
18525
  }, [props.clear, props.code]);
18500
18526
  return null;
18501
18527
  }
@@ -26197,6 +26223,10 @@ function LogTimestamp() {
26197
26223
  return (reactExports.createElement(Text, { dimColor: true }, DateTime.now().toFormat("[yyyy-MM-dd HH:mm:ss.SSS] ")));
26198
26224
  }
26199
26225
 
26226
+ function pretty_json(input) {
26227
+ return JSON.stringify(input, null, 2);
26228
+ }
26229
+
26200
26230
  const BaseStore = createStore()(immer((set, get) => ({
26201
26231
  // set immediately in `index.tsx` so no `null` scenario
26202
26232
  process_argv: [],
@@ -26214,6 +26244,8 @@ const BaseStore = createStore()(immer((set, get) => ({
26214
26244
  pr_templates: [],
26215
26245
  pr_template_body: null,
26216
26246
  sync_github: null,
26247
+ is_dirty_check_stash: false,
26248
+ abort_handler: null,
26217
26249
  step: "loading",
26218
26250
  output: [],
26219
26251
  pending_output: {},
@@ -26239,7 +26271,7 @@ const BaseStore = createStore()(immer((set, get) => ({
26239
26271
  },
26240
26272
  json(value) {
26241
26273
  set((state) => {
26242
- const node = JSON.stringify(value, null, 2);
26274
+ const node = pretty_json(value);
26243
26275
  state.mutate.output(state, { node });
26244
26276
  });
26245
26277
  },
@@ -26276,6 +26308,16 @@ const BaseStore = createStore()(immer((set, get) => ({
26276
26308
  state.pr = {};
26277
26309
  });
26278
26310
  },
26311
+ register_abort_handler(abort_handler) {
26312
+ set((state) => {
26313
+ state.abort_handler = abort_handler;
26314
+ });
26315
+ },
26316
+ unregister_abort_handler() {
26317
+ set((state) => {
26318
+ state.abort_handler = null;
26319
+ });
26320
+ },
26279
26321
  set(setter) {
26280
26322
  set((state) => {
26281
26323
  setter(state);
@@ -26561,10 +26603,6 @@ function semver_compare(version_a, version_b) {
26561
26603
  return 0;
26562
26604
  }
26563
26605
 
26564
- async function sleep(time) {
26565
- return new Promise((resolve) => setTimeout(resolve, time));
26566
- }
26567
-
26568
26606
  function reducer$4(state, patch) {
26569
26607
  return { ...state, ...patch };
26570
26608
  }
@@ -26898,7 +26936,7 @@ function Debug() {
26898
26936
  const output_file = path.join(state.cwd, "git-stack-state.json");
26899
26937
  await safe_rm(output_file);
26900
26938
  const serialized = serialize(state);
26901
- const content = JSON.stringify(serialized, null, 2);
26939
+ const content = pretty_json(serialized);
26902
26940
  await fs$1.writeFile(output_file, content);
26903
26941
  }
26904
26942
  }, [argv, state]);
@@ -29807,14 +29845,19 @@ async function pr_create(args) {
29807
29845
  }
29808
29846
  async function pr_edit(args) {
29809
29847
  const command_parts = [`gh pr edit ${args.branch} --base ${args.base}`];
29848
+ let body_file;
29810
29849
  if (args.body) {
29811
- const body_file = await write_body_file(args);
29850
+ body_file = await write_body_file(args);
29812
29851
  command_parts.push(`--body-file="${body_file}"`);
29813
29852
  }
29814
29853
  const cli_result = await cli(command_parts);
29815
29854
  if (cli_result.code !== 0) {
29816
29855
  handle_error(cli_result.output);
29817
29856
  }
29857
+ // cleanup body_file
29858
+ if (body_file) {
29859
+ await safe_rm(body_file);
29860
+ }
29818
29861
  }
29819
29862
  async function pr_draft(args) {
29820
29863
  // https://cli.github.com/manual/gh_api
@@ -30186,6 +30229,8 @@ echo "$GIT_REVISE_TODO" > "$git_revise_todo_path"
30186
30229
  // change to pipe to see output temporarily
30187
30230
  // https://github.com/magus/git-stack-cli/commit/f9f10e3ac3cd9a35ee75d3e0851a48391967a23f
30188
30231
  await cli(command, { stdio: ["ignore", "ignore", "ignore"] });
30232
+ // cleanup tmp_git_sequence_editor_path
30233
+ await safe_rm(tmp_git_sequence_editor_path);
30189
30234
  };
30190
30235
 
30191
30236
  function reducer$2(state, patch) {
@@ -30302,19 +30347,23 @@ function DirtyCheck(props) {
30302
30347
  case "done":
30303
30348
  return props.children;
30304
30349
  case "prompt":
30305
- return (reactExports.createElement(YesNoPrompt, { message: reactExports.createElement(Box, { flexDirection: "column" },
30306
- reactExports.createElement(FormatText, { wrapper: reactExports.createElement(Text, { color: colors.yellow }), message: "{git} repo has uncommitted changes.", values: {
30307
- git: reactExports.createElement(Command, null, "git"),
30308
- git_stack: reactExports.createElement(Command, null, "git stack"),
30309
- } }),
30310
- reactExports.createElement(FormatText, { wrapper: reactExports.createElement(Text, { color: colors.yellow }), message: "Changes may be lost during {git_stack}, are you sure you want to proceed?", values: {
30311
- git: reactExports.createElement(Command, null, "git"),
30312
- git_stack: reactExports.createElement(Command, null, "git stack"),
30313
- } })), onYes: async () => {
30314
- patch({ status: "done" });
30315
- }, onNo: async () => {
30316
- actions.exit(0);
30317
- } }));
30350
+ return (reactExports.createElement(Box, { flexDirection: "column" },
30351
+ reactExports.createElement(FormatText, { wrapper: reactExports.createElement(Text, { color: colors.yellow }), message: "\u26A0\uFE0F Uncommitted changes detected. {git_stack} needs a clean working tree.", values: {
30352
+ git: reactExports.createElement(Command, null, "git"),
30353
+ git_stack: reactExports.createElement(Command, null, "git stack"),
30354
+ } }),
30355
+ reactExports.createElement(YesNoPrompt, { message: reactExports.createElement(FormatText, { wrapper: reactExports.createElement(Text, { color: colors.yellow }), message: "{git_stash} changes to proceed?", values: {
30356
+ git_stash: reactExports.createElement(Command, null, "git stash"),
30357
+ } }), onYes: async () => {
30358
+ await cli("git stash --include-untracked");
30359
+ actions.output(reactExports.createElement(Text, null, "\uD83D\uDCE6 Changes saved to stash"));
30360
+ actions.set((state) => {
30361
+ state.is_dirty_check_stash = true;
30362
+ });
30363
+ patch({ status: "done" });
30364
+ }, onNo: async () => {
30365
+ actions.exit(0);
30366
+ } })));
30318
30367
  default:
30319
30368
  return (reactExports.createElement(Await, { function: run, fallback: reactExports.createElement(Text, { color: colors.yellow },
30320
30369
  "Ensuring ",
@@ -30325,8 +30374,12 @@ function DirtyCheck(props) {
30325
30374
  const actions = Store.getState().actions;
30326
30375
  try {
30327
30376
  const git_dirty = (await cli(`git status --porcelain`)).stdout;
30328
- const status = git_dirty ? "prompt" : "done";
30329
- patch({ status });
30377
+ if (!git_dirty) {
30378
+ patch({ status: "done" });
30379
+ }
30380
+ else {
30381
+ patch({ status: "prompt" });
30382
+ }
30330
30383
  }
30331
30384
  catch (err) {
30332
30385
  actions.error("Must be run from within a git repository.");
@@ -30476,6 +30529,32 @@ async function run$9() {
30476
30529
  reactExports.createElement(Text, { bold: true, color: colors.yellow }, time_until))));
30477
30530
  }
30478
30531
 
30532
+ function HandleCtrlCSigint() {
30533
+ const actions = Store.useActions();
30534
+ const [exiting, set_exiting] = reactExports.useState(false);
30535
+ useInput((input, key) => {
30536
+ handle_input().catch((err) => {
30537
+ // eslint-disable-next-line no-console
30538
+ console.error(err);
30539
+ });
30540
+ async function handle_input() {
30541
+ if (input === "c" && key.ctrl) {
30542
+ actions.clear();
30543
+ actions.output(reactExports.createElement(Text, { color: colors.red },
30544
+ reactExports.createElement(FormatText, { message: "\uD83D\uDEA8 Ctrl+C detected" })));
30545
+ set_exiting(true);
30546
+ await sleep(1);
30547
+ actions.exit(235);
30548
+ }
30549
+ }
30550
+ });
30551
+ if (exiting) {
30552
+ return (reactExports.createElement(Text, { color: colors.red },
30553
+ reactExports.createElement(FormatText, { message: "\uD83D\uDEA8 Exiting\u2026" })));
30554
+ }
30555
+ return null;
30556
+ }
30557
+
30479
30558
  function LocalCommitStatus(props) {
30480
30559
  const argv = Store.useState((state) => state.argv);
30481
30560
  const fallback = (reactExports.createElement(Text, { color: colors.yellow }, "Fetching PR status from Github\u2026"));
@@ -30572,21 +30651,9 @@ function encode(value) {
30572
30651
  }
30573
30652
 
30574
30653
  function Rebase() {
30575
- const abort_handler = reactExports.useRef(() => { });
30576
- reactExports.useEffect(function listen_sigint() {
30577
- process.once("SIGINT", sigint_handler);
30578
- return function cleanup() {
30579
- process.removeListener("SIGINT", sigint_handler);
30580
- };
30581
- function sigint_handler() {
30582
- abort_handler.current();
30583
- }
30584
- }, []);
30585
- return (reactExports.createElement(Await, { fallback: reactExports.createElement(Text, { color: colors.yellow }, "Rebasing commits\u2026"), function: async function () {
30586
- await Rebase.run({ abort_handler });
30587
- } }));
30654
+ return (reactExports.createElement(Await, { fallback: reactExports.createElement(Text, { color: colors.yellow }, "Rebasing commits\u2026"), function: Rebase.run }));
30588
30655
  }
30589
- Rebase.run = async function run(args) {
30656
+ Rebase.run = async function run() {
30590
30657
  const state = Store.getState();
30591
30658
  const actions = state.actions;
30592
30659
  const branch_name = state.branch_name;
@@ -30597,11 +30664,12 @@ Rebase.run = async function run(args) {
30597
30664
  invariant(branch_name, "branch_name must exist");
30598
30665
  invariant(commit_range, "commit_range must exist");
30599
30666
  invariant(repo_root, "repo_root must exist");
30600
- // always listen for SIGINT event and restore git state
30601
- args.abort_handler.current = async function sigint_handler() {
30667
+ // immediately register abort_handler in case of ctrl+c exit
30668
+ actions.register_abort_handler(async function abort_rebase() {
30602
30669
  actions.output(reactExports.createElement(Text, { color: colors.red }, "\uD83D\uDEA8 Abort"));
30603
- handle_exit(19);
30604
- };
30670
+ handle_exit();
30671
+ return 19;
30672
+ });
30605
30673
  const temp_branch_name = `${branch_name}_${short_id()}`;
30606
30674
  try {
30607
30675
  // actions.debug(`commit_range=${JSON.stringify(commit_range, null, 2)}`);
@@ -30652,6 +30720,7 @@ Rebase.run = async function run(args) {
30652
30720
  branch_name: reactExports.createElement(Brackets, null, branch_name),
30653
30721
  origin_branch: reactExports.createElement(Brackets, null, `origin/${master_branch}`),
30654
30722
  } }));
30723
+ actions.unregister_abort_handler();
30655
30724
  actions.set((state) => {
30656
30725
  state.commit_range = next_commit_range;
30657
30726
  state.step = "status";
@@ -30664,7 +30733,8 @@ Rebase.run = async function run(args) {
30664
30733
  actions.error(err.message);
30665
30734
  }
30666
30735
  }
30667
- handle_exit(20);
30736
+ handle_exit();
30737
+ actions.exit(20);
30668
30738
  }
30669
30739
  // cleanup git operations if cancelled during manual rebase
30670
30740
  function restore_git() {
@@ -30686,7 +30756,7 @@ Rebase.run = async function run(args) {
30686
30756
  }
30687
30757
  cli.sync(`pwd`, spawn_options);
30688
30758
  }
30689
- function handle_exit(code) {
30759
+ function handle_exit() {
30690
30760
  actions.output(reactExports.createElement(Text, { color: colors.yellow },
30691
30761
  "Restoring ",
30692
30762
  reactExports.createElement(Brackets, null, branch_name),
@@ -30696,7 +30766,6 @@ Rebase.run = async function run(args) {
30696
30766
  "Restored ",
30697
30767
  reactExports.createElement(Brackets, null, branch_name),
30698
30768
  "."));
30699
- actions.exit(code);
30700
30769
  }
30701
30770
  };
30702
30771
 
@@ -30705,21 +30774,9 @@ function LocalMergeRebase() {
30705
30774
  }
30706
30775
 
30707
30776
  function ManualRebase() {
30708
- const abort_handler = reactExports.useRef(() => { });
30709
- reactExports.useEffect(function listen_sigint() {
30710
- process.once("SIGINT", sigint_handler);
30711
- return function cleanup() {
30712
- process.removeListener("SIGINT", sigint_handler);
30713
- };
30714
- async function sigint_handler() {
30715
- abort_handler.current();
30716
- }
30717
- }, []);
30718
- return (reactExports.createElement(Await, { fallback: reactExports.createElement(Text, { color: colors.yellow }, "Rebasing commits\u2026"), function: async function () {
30719
- await run$7({ abort_handler });
30720
- } }));
30777
+ return (reactExports.createElement(Await, { fallback: reactExports.createElement(Text, { color: colors.yellow }, "Rebasing commits\u2026"), function: run$7 }));
30721
30778
  }
30722
- async function run$7(args) {
30779
+ async function run$7() {
30723
30780
  const state = Store.getState();
30724
30781
  const actions = state.actions;
30725
30782
  const argv = state.argv;
@@ -30731,11 +30788,12 @@ async function run$7(args) {
30731
30788
  invariant(branch_name, "branch_name must exist");
30732
30789
  invariant(commit_map, "commit_map must exist");
30733
30790
  invariant(repo_root, "repo_root must exist");
30734
- // always listen for SIGINT event and restore git state
30735
- args.abort_handler.current = function sigint_handler() {
30791
+ // immediately register abort_handler in case of ctrl+c exit
30792
+ actions.register_abort_handler(async function abort_manual_rebase() {
30736
30793
  actions.output(reactExports.createElement(Text, { color: colors.red }, "\uD83D\uDEA8 Abort"));
30737
- handle_exit(15);
30738
- };
30794
+ handle_exit();
30795
+ return 15;
30796
+ });
30739
30797
  const temp_branch_name = `${branch_name}_${short_id()}`;
30740
30798
  try {
30741
30799
  // get latest merge_base relative to local master
@@ -30790,6 +30848,7 @@ async function run$7(args) {
30790
30848
  // of original branch to the newly created temporary branch
30791
30849
  await cli(`git branch -f ${branch_name} ${temp_branch_name}`);
30792
30850
  restore_git();
30851
+ actions.unregister_abort_handler();
30793
30852
  if (argv.sync) {
30794
30853
  actions.set((state) => {
30795
30854
  state.step = "sync-github";
@@ -30810,7 +30869,8 @@ async function run$7(args) {
30810
30869
  if (!argv.verbose) {
30811
30870
  actions.error("Try again with `--verbose` to see more information.");
30812
30871
  }
30813
- handle_exit(16);
30872
+ handle_exit();
30873
+ actions.exit(16);
30814
30874
  }
30815
30875
  // cleanup git operations if cancelled during manual rebase
30816
30876
  function restore_git() {
@@ -30832,7 +30892,7 @@ async function run$7(args) {
30832
30892
  }
30833
30893
  cli.sync(`pwd`, spawn_options);
30834
30894
  }
30835
- function handle_exit(code) {
30895
+ function handle_exit() {
30836
30896
  actions.output(reactExports.createElement(Text, { color: colors.yellow },
30837
30897
  "Restoring ",
30838
30898
  reactExports.createElement(Brackets, null, branch_name),
@@ -30842,7 +30902,6 @@ async function run$7(args) {
30842
30902
  "Restored ",
30843
30903
  reactExports.createElement(Brackets, null, branch_name),
30844
30904
  "."));
30845
- actions.exit(code);
30846
30905
  }
30847
30906
  }
30848
30907
 
@@ -31757,21 +31816,9 @@ const RE = {
31757
31816
  };
31758
31817
 
31759
31818
  function SyncGithub() {
31760
- const abort_handler = reactExports.useRef(() => { });
31761
- reactExports.useEffect(function listen_sigint() {
31762
- process.once("SIGINT", sigint_handler);
31763
- return function cleanup() {
31764
- process.removeListener("SIGINT", sigint_handler);
31765
- };
31766
- function sigint_handler() {
31767
- abort_handler.current();
31768
- }
31769
- }, []);
31770
- return (reactExports.createElement(Await, { fallback: reactExports.createElement(Text, { color: colors.yellow }, "Syncing\u2026"), function: async function () {
31771
- await run$3({ abort_handler });
31772
- } }));
31819
+ return (reactExports.createElement(Await, { fallback: reactExports.createElement(Text, { color: colors.yellow }, "Syncing\u2026"), function: run$3 }));
31773
31820
  }
31774
- async function run$3(args) {
31821
+ async function run$3() {
31775
31822
  const state = Store.getState();
31776
31823
  const actions = state.actions;
31777
31824
  const argv = state.argv;
@@ -31786,11 +31833,12 @@ async function run$3(args) {
31786
31833
  invariant(sync_github, "sync_github must exist");
31787
31834
  const commit_range = sync_github.commit_range;
31788
31835
  const rebase_group_index = sync_github.rebase_group_index;
31789
- // always listen for SIGINT event and restore pr state
31790
- args.abort_handler.current = function sigint_handler() {
31836
+ // immediately register abort_handler in case of ctrl+c exit
31837
+ actions.register_abort_handler(async function abort_sync_github() {
31791
31838
  actions.output(reactExports.createElement(Text, { color: colors.red }, "\uD83D\uDEA8 Abort"));
31792
- handle_exit(17);
31793
- };
31839
+ handle_exit();
31840
+ return 17;
31841
+ });
31794
31842
  let DEFAULT_PR_BODY = "";
31795
31843
  if (state.pr_template_body) {
31796
31844
  DEFAULT_PR_BODY = state.pr_template_body;
@@ -31859,6 +31907,7 @@ async function run$3(args) {
31859
31907
  update_pr_body_tasks.push(task);
31860
31908
  }
31861
31909
  await Promise.all(update_pr_body_tasks);
31910
+ actions.unregister_abort_handler();
31862
31911
  actions.set((state) => {
31863
31912
  state.step = "post-rebase-status";
31864
31913
  });
@@ -31871,7 +31920,8 @@ async function run$3(args) {
31871
31920
  if (!argv.verbose) {
31872
31921
  actions.error("Try again with `--verbose` to see more information.");
31873
31922
  }
31874
- await handle_exit(18);
31923
+ handle_exit();
31924
+ actions.exit(18);
31875
31925
  }
31876
31926
  function get_push_group_list() {
31877
31927
  // start from HEAD and work backward to rebase_group_index
@@ -31980,7 +32030,7 @@ async function run$3(args) {
31980
32030
  });
31981
32031
  }
31982
32032
  }
31983
- function handle_exit(code) {
32033
+ function handle_exit() {
31984
32034
  actions.output(reactExports.createElement(Text, { color: colors.yellow }, "Restoring PR state\u2026"));
31985
32035
  for (const group of push_group_list) {
31986
32036
  // we may temporarily mark PR as a draft before editing it
@@ -31996,7 +32046,6 @@ async function run$3(args) {
31996
32046
  }
31997
32047
  }
31998
32048
  actions.output(reactExports.createElement(Text, { color: colors.yellow }, "Restored PR state."));
31999
- actions.exit(code);
32000
32049
  }
32001
32050
  }
32002
32051
  const get_group_url = (group) => group.pr?.url || group.id;
@@ -32179,7 +32228,7 @@ async function run$1() {
32179
32228
  });
32180
32229
  if (diff_cmd.code) {
32181
32230
  save_stash = true;
32182
- await cli("git stash -q");
32231
+ await cli("git stash --include-untracked");
32183
32232
  actions.output(reactExports.createElement(Text, null, "\uD83D\uDCE6 Changes saved to stash"));
32184
32233
  }
32185
32234
  try {
@@ -32199,7 +32248,7 @@ async function run$1() {
32199
32248
  }
32200
32249
  finally {
32201
32250
  if (save_stash) {
32202
- await cli("git stash pop -q");
32251
+ await cli("git stash pop");
32203
32252
  actions.output(reactExports.createElement(Text, { color: colors.green }, "\u2705 Changes restored from stash"));
32204
32253
  }
32205
32254
  }
@@ -32278,7 +32327,8 @@ function App() {
32278
32327
  reactExports.createElement(DependencyCheck, null,
32279
32328
  reactExports.createElement(RebaseCheck, null,
32280
32329
  reactExports.createElement(CherryPickCheck, null,
32281
- reactExports.createElement(MaybeMain, null))))))));
32330
+ reactExports.createElement(MaybeMain, null)))))),
32331
+ reactExports.createElement(HandleCtrlCSigint, null)));
32282
32332
  }
32283
32333
  function MaybeMain() {
32284
32334
  const argv = Store.useState((state) => state.argv);
@@ -37620,7 +37670,7 @@ async function command() {
37620
37670
  .wrap(123)
37621
37671
  // disallow unknown options
37622
37672
  .strict()
37623
- .version("1.14.0" )
37673
+ .version("1.15.0" )
37624
37674
  .showHidden("show-hidden", "Show hidden options via `git stack help --show-hidden`")
37625
37675
  .help("help", "Show usage via `git stack help`")
37626
37676
  .argv;
@@ -37708,22 +37758,35 @@ const FixupOptions = {
37708
37758
  },
37709
37759
  };
37710
37760
 
37711
- command()
37712
- .then((argv) => {
37713
- const ink = render(reactExports.createElement(App, null), {
37714
- // If true, each update will be rendered as a separate output, without replacing the previous one.
37715
- // debug: true,
37716
- });
37717
- Store.setState((state) => {
37718
- state.ink = ink;
37719
- state.process_argv = process.argv;
37720
- state.argv = argv;
37721
- state.cwd = process.cwd();
37722
- });
37723
- Store.getState().actions.debug(JSON.stringify(argv, null, 2));
37724
- })
37761
+ (async function main() {
37762
+ try {
37763
+ const argv = await command();
37764
+ const ink = render(reactExports.createElement(App, null), {
37765
+ // If true, each update will be rendered as a separate output, without replacing the previous one.
37766
+ // debug: true,
37767
+ //
37768
+ // Configure whether Ink should listen to Ctrl+C keyboard input and exit the app.
37769
+ // We intentionally handle this ourselves in `<Exit />`
37770
+ exitOnCtrlC: false,
37771
+ });
37772
+ Store.setState((state) => {
37773
+ state.ink = ink;
37774
+ state.process_argv = process.argv;
37775
+ state.argv = argv;
37776
+ state.cwd = process.cwd();
37777
+ });
37778
+ Store.getState().actions.debug(pretty_json(argv));
37779
+ await ink.waitUntilExit();
37780
+ }
37781
+ catch (err) {
37782
+ // eslint-disable-next-line no-console
37783
+ console.error(err);
37784
+ process.exit(235);
37785
+ }
37786
+ })().catch((err) => {
37725
37787
  // eslint-disable-next-line no-console
37726
- .catch(console.error);
37788
+ console.error(err);
37789
+ });
37727
37790
 
37728
37791
  // prettier-ignore
37729
37792
  const METADATA = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-stack-cli",
3
- "version": "1.14.0",
3
+ "version": "1.15.0",
4
4
  "description": "",
5
5
  "author": "magus",
6
6
  "license": "MIT",
package/src/app/App.tsx CHANGED
@@ -8,6 +8,7 @@ import { DetectInitialPR } from "~/app/DetectInitialPR";
8
8
  import { DirtyCheck } from "~/app/DirtyCheck";
9
9
  import { GatherMetadata } from "~/app/GatherMetadata";
10
10
  import { GithubApiError } from "~/app/GithubApiError";
11
+ import { HandleCtrlCSigint } from "~/app/HandleCtrlCSigint";
11
12
  import { LocalCommitStatus } from "~/app/LocalCommitStatus";
12
13
  import { Main } from "~/app/Main";
13
14
  import { Output } from "~/app/Output";
@@ -65,6 +66,8 @@ export function App() {
65
66
  </DependencyCheck>
66
67
  </VerboseDebugInfo>
67
68
  </AutoUpdate>
69
+
70
+ <HandleCtrlCSigint />
68
71
  </Providers>
69
72
  );
70
73
  }
package/src/app/Debug.tsx CHANGED
@@ -8,6 +8,7 @@ import * as Ink from "ink-cjs";
8
8
  import { Store } from "~/app/Store";
9
9
  import { colors } from "~/core/colors";
10
10
  import * as json from "~/core/json";
11
+ import { pretty_json } from "~/core/pretty_json";
11
12
  import { safe_rm } from "~/core/safe_rm";
12
13
 
13
14
  export function Debug() {
@@ -41,7 +42,7 @@ export function Debug() {
41
42
  await safe_rm(output_file);
42
43
 
43
44
  const serialized = json.serialize(state);
44
- const content = JSON.stringify(serialized, null, 2);
45
+ const content = pretty_json(serialized);
45
46
  await fs.writeFile(output_file, content);
46
47
  }
47
48
  },
@@ -15,7 +15,7 @@ type Props = {
15
15
  };
16
16
 
17
17
  type State = {
18
- status: "init" | "prompt" | "done";
18
+ status: "init" | "prompt" | "stash" | "done";
19
19
  };
20
20
 
21
21
  function reducer(state: State, patch: Partial<State>) {
@@ -35,34 +35,42 @@ export function DirtyCheck(props: Props) {
35
35
 
36
36
  case "prompt":
37
37
  return (
38
- <YesNoPrompt
39
- message={
40
- <Ink.Box flexDirection="column">
41
- <FormatText
42
- wrapper={<Ink.Text color={colors.yellow} />}
43
- message="{git} repo has uncommitted changes."
44
- values={{
45
- git: <Command>git</Command>,
46
- git_stack: <Command>git stack</Command>,
47
- }}
48
- />
38
+ <Ink.Box flexDirection="column">
39
+ <FormatText
40
+ wrapper={<Ink.Text color={colors.yellow} />}
41
+ message="⚠️ Uncommitted changes detected. {git_stack} needs a clean working tree."
42
+ values={{
43
+ git: <Command>git</Command>,
44
+ git_stack: <Command>git stack</Command>,
45
+ }}
46
+ />
47
+
48
+ <YesNoPrompt
49
+ message={
49
50
  <FormatText
50
51
  wrapper={<Ink.Text color={colors.yellow} />}
51
- message="Changes may be lost during {git_stack}, are you sure you want to proceed?"
52
+ message="{git_stash} changes to proceed?"
52
53
  values={{
53
- git: <Command>git</Command>,
54
- git_stack: <Command>git stack</Command>,
54
+ git_stash: <Command>git stash</Command>,
55
55
  }}
56
56
  />
57
- </Ink.Box>
58
- }
59
- onYes={async () => {
60
- patch({ status: "done" });
61
- }}
62
- onNo={async () => {
63
- actions.exit(0);
64
- }}
65
- />
57
+ }
58
+ onYes={async () => {
59
+ await cli("git stash --include-untracked");
60
+
61
+ actions.output(<Ink.Text>📦 Changes saved to stash</Ink.Text>);
62
+
63
+ actions.set((state) => {
64
+ state.is_dirty_check_stash = true;
65
+ });
66
+
67
+ patch({ status: "done" });
68
+ }}
69
+ onNo={async () => {
70
+ actions.exit(0);
71
+ }}
72
+ />
73
+ </Ink.Box>
66
74
  );
67
75
 
68
76
  default:
@@ -84,8 +92,11 @@ export function DirtyCheck(props: Props) {
84
92
  try {
85
93
  const git_dirty = (await cli(`git status --porcelain`)).stdout;
86
94
 
87
- const status = git_dirty ? "prompt" : "done";
88
- patch({ status });
95
+ if (!git_dirty) {
96
+ patch({ status: "done" });
97
+ } else {
98
+ patch({ status: "prompt" });
99
+ }
89
100
  } catch (err) {
90
101
  actions.error("Must be run from within a git repository.");
91
102
 
package/src/app/Exit.tsx CHANGED
@@ -1,6 +1,11 @@
1
1
  import * as React from "react";
2
2
 
3
+ import * as Ink from "ink-cjs";
4
+
3
5
  import { Store } from "~/app/Store";
6
+ import { cli } from "~/core/cli";
7
+ import { colors } from "~/core/colors";
8
+ import { sleep } from "~/core/sleep";
4
9
 
5
10
  type Props = {
6
11
  clear: boolean;
@@ -8,17 +13,49 @@ type Props = {
8
13
  };
9
14
 
10
15
  export function Exit(props: Props) {
11
- const actions = Store.useActions();
12
-
13
16
  React.useEffect(() => {
14
- if (props.clear) {
15
- actions.clear();
16
- }
17
+ // immediately handle exit on mount
18
+ handle_exit().catch((err) => {
19
+ // eslint-disable-next-line no-console
20
+ console.error(err);
21
+ });
22
+
23
+ async function handle_exit() {
24
+ const state = Store.getState();
25
+ const actions = state.actions;
26
+
27
+ actions.debug(`[Exit] handle_exit ${JSON.stringify(props)}`);
28
+
29
+ let exit_code = props.code;
17
30
 
18
- actions.unmount();
31
+ // run abort_handler if it exists
32
+ if (state.abort_handler) {
33
+ exit_code = await state.abort_handler();
34
+ }
19
35
 
20
- process.exitCode = props.code;
21
- process.exit();
36
+ // restore git stash if necessary
37
+ if (state.is_dirty_check_stash) {
38
+ await cli("git stash pop");
39
+ actions.output(
40
+ <Ink.Text color={colors.green}>
41
+ ✅ Changes restored from stash
42
+ </Ink.Text>
43
+ );
44
+ }
45
+
46
+ // ensure output has a chance to render
47
+ await sleep(1);
48
+
49
+ // finally handle the actual app and process exit
50
+ if (props.clear) {
51
+ actions.clear();
52
+ }
53
+
54
+ actions.unmount();
55
+
56
+ process.exitCode = exit_code;
57
+ process.exit();
58
+ }
22
59
  }, [props.clear, props.code]);
23
60
 
24
61
  return null;
@@ -0,0 +1,47 @@
1
+ import * as React from "react";
2
+
3
+ import * as Ink from "ink-cjs";
4
+
5
+ import { FormatText } from "~/app/FormatText";
6
+ import { Store } from "~/app/Store";
7
+ import { colors } from "~/core/colors";
8
+ import { sleep } from "~/core/sleep";
9
+
10
+ export function HandleCtrlCSigint() {
11
+ const actions = Store.useActions();
12
+
13
+ const [exiting, set_exiting] = React.useState(false);
14
+
15
+ Ink.useInput((input, key) => {
16
+ handle_input().catch((err) => {
17
+ // eslint-disable-next-line no-console
18
+ console.error(err);
19
+ });
20
+
21
+ async function handle_input() {
22
+ if (input === "c" && key.ctrl) {
23
+ actions.clear();
24
+
25
+ actions.output(
26
+ <Ink.Text color={colors.red}>
27
+ <FormatText message="🚨 Ctrl+C detected" />
28
+ </Ink.Text>
29
+ );
30
+
31
+ set_exiting(true);
32
+ await sleep(1);
33
+ actions.exit(235);
34
+ }
35
+ }
36
+ });
37
+
38
+ if (exiting) {
39
+ return (
40
+ <Ink.Text color={colors.red}>
41
+ <FormatText message="🚨 Exiting…" />
42
+ </Ink.Text>
43
+ );
44
+ }
45
+
46
+ return null;
47
+ }
@@ -15,35 +15,15 @@ import { invariant } from "~/core/invariant";
15
15
  import { short_id } from "~/core/short_id";
16
16
 
17
17
  export function ManualRebase() {
18
- const abort_handler = React.useRef(() => {});
19
-
20
- React.useEffect(function listen_sigint() {
21
- process.once("SIGINT", sigint_handler);
22
-
23
- return function cleanup() {
24
- process.removeListener("SIGINT", sigint_handler);
25
- };
26
-
27
- async function sigint_handler() {
28
- abort_handler.current();
29
- }
30
- }, []);
31
-
32
18
  return (
33
19
  <Await
34
20
  fallback={<Ink.Text color={colors.yellow}>Rebasing commits…</Ink.Text>}
35
- function={async function () {
36
- await run({ abort_handler });
37
- }}
21
+ function={run}
38
22
  />
39
23
  );
40
24
  }
41
25
 
42
- type Args = {
43
- abort_handler: React.MutableRefObject<() => void>;
44
- };
45
-
46
- async function run(args: Args) {
26
+ async function run() {
47
27
  const state = Store.getState();
48
28
  const actions = state.actions;
49
29
  const argv = state.argv;
@@ -57,11 +37,12 @@ async function run(args: Args) {
57
37
  invariant(commit_map, "commit_map must exist");
58
38
  invariant(repo_root, "repo_root must exist");
59
39
 
60
- // always listen for SIGINT event and restore git state
61
- args.abort_handler.current = function sigint_handler() {
40
+ // immediately register abort_handler in case of ctrl+c exit
41
+ actions.register_abort_handler(async function abort_manual_rebase() {
62
42
  actions.output(<Ink.Text color={colors.red}>🚨 Abort</Ink.Text>);
63
- handle_exit(15);
64
- };
43
+ handle_exit();
44
+ return 15;
45
+ });
65
46
 
66
47
  const temp_branch_name = `${branch_name}_${short_id()}`;
67
48
 
@@ -137,6 +118,8 @@ async function run(args: Args) {
137
118
 
138
119
  restore_git();
139
120
 
121
+ actions.unregister_abort_handler();
122
+
140
123
  if (argv.sync) {
141
124
  actions.set((state) => {
142
125
  state.step = "sync-github";
@@ -157,7 +140,8 @@ async function run(args: Args) {
157
140
  actions.error("Try again with `--verbose` to see more information.");
158
141
  }
159
142
 
160
- handle_exit(16);
143
+ handle_exit();
144
+ actions.exit(16);
161
145
  }
162
146
 
163
147
  // cleanup git operations if cancelled during manual rebase
@@ -185,7 +169,7 @@ async function run(args: Args) {
185
169
  cli.sync(`pwd`, spawn_options);
186
170
  }
187
171
 
188
- function handle_exit(code: number) {
172
+ function handle_exit() {
189
173
  actions.output(
190
174
  <Ink.Text color={colors.yellow}>
191
175
  Restoring <Brackets>{branch_name}</Brackets>…
@@ -199,7 +183,5 @@ async function run(args: Args) {
199
183
  Restored <Brackets>{branch_name}</Brackets>.
200
184
  </Ink.Text>
201
185
  );
202
-
203
- actions.exit(code);
204
186
  }
205
187
  }
package/src/app/Store.tsx CHANGED
@@ -7,6 +7,7 @@ import { immer } from "zustand/middleware/immer";
7
7
  import { Exit } from "~/app/Exit";
8
8
  import { LogTimestamp } from "~/app/LogTimestamp";
9
9
  import { colors } from "~/core/colors";
10
+ import { pretty_json } from "~/core/pretty_json";
10
11
 
11
12
  import type { Instance as InkInstance } from "ink-cjs";
12
13
  import type { Argv } from "~/command";
@@ -29,6 +30,9 @@ type SyncGithubState = {
29
30
  rebase_group_index: number;
30
31
  };
31
32
 
33
+ // async function that returns exit code
34
+ type AbortHandler = () => Promise<number>;
35
+
32
36
  export type State = {
33
37
  // set immediately in `index.tsx` so no `null` scenario
34
38
  process_argv: Array<string>;
@@ -47,6 +51,8 @@ export type State = {
47
51
  pr_templates: Array<string>;
48
52
  pr_template_body: null | string;
49
53
  sync_github: null | SyncGithubState;
54
+ is_dirty_check_stash: boolean;
55
+ abort_handler: null | AbortHandler;
50
56
 
51
57
  step:
52
58
  | "github-api-error"
@@ -71,7 +77,7 @@ export type State = {
71
77
  clear(): void;
72
78
  unmount(): void;
73
79
  newline(): void;
74
- json(value: object): void;
80
+ json(value: pretty_json.JSONValue): void;
75
81
  error(message: string): void;
76
82
  output(node: React.ReactNode): void;
77
83
  debug(node: React.ReactNode, id?: string): void;
@@ -79,6 +85,8 @@ export type State = {
79
85
  isDebug(): boolean;
80
86
 
81
87
  reset_pr(): void;
88
+ register_abort_handler(abort_handler: AbortHandler): void;
89
+ unregister_abort_handler(): void;
82
90
 
83
91
  set(setter: Setter): void;
84
92
  };
@@ -113,6 +121,8 @@ const BaseStore = createStore<State>()(
113
121
  pr_templates: [],
114
122
  pr_template_body: null,
115
123
  sync_github: null,
124
+ is_dirty_check_stash: false,
125
+ abort_handler: null,
116
126
 
117
127
  step: "loading",
118
128
 
@@ -146,7 +156,7 @@ const BaseStore = createStore<State>()(
146
156
 
147
157
  json(value) {
148
158
  set((state) => {
149
- const node = JSON.stringify(value, null, 2);
159
+ const node = pretty_json(value);
150
160
  state.mutate.output(state, { node });
151
161
  });
152
162
  },
@@ -189,6 +199,18 @@ const BaseStore = createStore<State>()(
189
199
  });
190
200
  },
191
201
 
202
+ register_abort_handler(abort_handler) {
203
+ set((state) => {
204
+ state.abort_handler = abort_handler;
205
+ });
206
+ },
207
+
208
+ unregister_abort_handler() {
209
+ set((state) => {
210
+ state.abort_handler = null;
211
+ });
212
+ },
213
+
192
214
  set(setter) {
193
215
  set((state) => {
194
216
  setter(state);
@@ -14,35 +14,15 @@ import { invariant } from "~/core/invariant";
14
14
  import type * as CommitMetadata from "~/core/CommitMetadata";
15
15
 
16
16
  export function SyncGithub() {
17
- const abort_handler = React.useRef(() => {});
18
-
19
- React.useEffect(function listen_sigint() {
20
- process.once("SIGINT", sigint_handler);
21
-
22
- return function cleanup() {
23
- process.removeListener("SIGINT", sigint_handler);
24
- };
25
-
26
- function sigint_handler() {
27
- abort_handler.current();
28
- }
29
- }, []);
30
-
31
17
  return (
32
18
  <Await
33
19
  fallback={<Ink.Text color={colors.yellow}>Syncing…</Ink.Text>}
34
- function={async function () {
35
- await run({ abort_handler });
36
- }}
20
+ function={run}
37
21
  />
38
22
  );
39
23
  }
40
24
 
41
- type Args = {
42
- abort_handler: React.MutableRefObject<() => void>;
43
- };
44
-
45
- async function run(args: Args) {
25
+ async function run() {
46
26
  const state = Store.getState();
47
27
  const actions = state.actions;
48
28
  const argv = state.argv;
@@ -60,11 +40,12 @@ async function run(args: Args) {
60
40
  const commit_range = sync_github.commit_range;
61
41
  const rebase_group_index = sync_github.rebase_group_index;
62
42
 
63
- // always listen for SIGINT event and restore pr state
64
- args.abort_handler.current = function sigint_handler() {
43
+ // immediately register abort_handler in case of ctrl+c exit
44
+ actions.register_abort_handler(async function abort_sync_github() {
65
45
  actions.output(<Ink.Text color={colors.red}>🚨 Abort</Ink.Text>);
66
- handle_exit(17);
67
- };
46
+ handle_exit();
47
+ return 17;
48
+ });
68
49
 
69
50
  let DEFAULT_PR_BODY = "";
70
51
  if (state.pr_template_body) {
@@ -151,6 +132,8 @@ async function run(args: Args) {
151
132
 
152
133
  await Promise.all(update_pr_body_tasks);
153
134
 
135
+ actions.unregister_abort_handler();
136
+
154
137
  actions.set((state) => {
155
138
  state.step = "post-rebase-status";
156
139
  });
@@ -164,7 +147,8 @@ async function run(args: Args) {
164
147
  actions.error("Try again with `--verbose` to see more information.");
165
148
  }
166
149
 
167
- await handle_exit(18);
150
+ handle_exit();
151
+ actions.exit(18);
168
152
  }
169
153
 
170
154
  function get_push_group_list() {
@@ -305,7 +289,7 @@ async function run(args: Args) {
305
289
  }
306
290
  }
307
291
 
308
- function handle_exit(code: number) {
292
+ function handle_exit() {
309
293
  actions.output(
310
294
  <Ink.Text color={colors.yellow}>Restoring PR state…</Ink.Text>
311
295
  );
@@ -329,8 +313,6 @@ async function run(args: Args) {
329
313
  actions.output(
330
314
  <Ink.Text color={colors.yellow}>Restored PR state.</Ink.Text>
331
315
  );
332
-
333
- actions.exit(code);
334
316
  }
335
317
  }
336
318
 
@@ -97,7 +97,7 @@ async function run() {
97
97
  if (diff_cmd.code) {
98
98
  save_stash = true;
99
99
 
100
- await cli("git stash -q");
100
+ await cli("git stash --include-untracked");
101
101
 
102
102
  actions.output(<Ink.Text>📦 Changes saved to stash</Ink.Text>);
103
103
  }
@@ -118,7 +118,7 @@ async function run() {
118
118
  await cli("git reset --soft HEAD~1");
119
119
  } finally {
120
120
  if (save_stash) {
121
- await cli("git stash pop -q");
121
+ await cli("git stash pop");
122
122
 
123
123
  actions.output(
124
124
  <Ink.Text color={colors.green}>✅ Changes restored from stash</Ink.Text>
@@ -16,35 +16,15 @@ import { invariant } from "~/core/invariant";
16
16
  import { short_id } from "~/core/short_id";
17
17
 
18
18
  export function Rebase() {
19
- const abort_handler = React.useRef(() => {});
20
-
21
- React.useEffect(function listen_sigint() {
22
- process.once("SIGINT", sigint_handler);
23
-
24
- return function cleanup() {
25
- process.removeListener("SIGINT", sigint_handler);
26
- };
27
-
28
- function sigint_handler() {
29
- abort_handler.current();
30
- }
31
- }, []);
32
-
33
19
  return (
34
20
  <Await
35
21
  fallback={<Ink.Text color={colors.yellow}>Rebasing commits…</Ink.Text>}
36
- function={async function () {
37
- await Rebase.run({ abort_handler });
38
- }}
22
+ function={Rebase.run}
39
23
  />
40
24
  );
41
25
  }
42
26
 
43
- type Args = {
44
- abort_handler: React.MutableRefObject<() => void>;
45
- };
46
-
47
- Rebase.run = async function run(args: Args) {
27
+ Rebase.run = async function run() {
48
28
  const state = Store.getState();
49
29
  const actions = state.actions;
50
30
  const branch_name = state.branch_name;
@@ -57,11 +37,12 @@ Rebase.run = async function run(args: Args) {
57
37
  invariant(commit_range, "commit_range must exist");
58
38
  invariant(repo_root, "repo_root must exist");
59
39
 
60
- // always listen for SIGINT event and restore git state
61
- args.abort_handler.current = async function sigint_handler() {
40
+ // immediately register abort_handler in case of ctrl+c exit
41
+ actions.register_abort_handler(async function abort_rebase() {
62
42
  actions.output(<Ink.Text color={colors.red}>🚨 Abort</Ink.Text>);
63
- handle_exit(19);
64
- };
43
+ handle_exit();
44
+ return 19;
45
+ });
65
46
 
66
47
  const temp_branch_name = `${branch_name}_${short_id()}`;
67
48
 
@@ -153,6 +134,8 @@ Rebase.run = async function run(args: Args) {
153
134
  />
154
135
  );
155
136
 
137
+ actions.unregister_abort_handler();
138
+
156
139
  actions.set((state) => {
157
140
  state.commit_range = next_commit_range;
158
141
  state.step = "status";
@@ -166,7 +149,8 @@ Rebase.run = async function run(args: Args) {
166
149
  }
167
150
  }
168
151
 
169
- handle_exit(20);
152
+ handle_exit();
153
+ actions.exit(20);
170
154
  }
171
155
 
172
156
  // cleanup git operations if cancelled during manual rebase
@@ -194,7 +178,7 @@ Rebase.run = async function run(args: Args) {
194
178
  cli.sync(`pwd`, spawn_options);
195
179
  }
196
180
 
197
- function handle_exit(code: number) {
181
+ function handle_exit() {
198
182
  actions.output(
199
183
  <Ink.Text color={colors.yellow}>
200
184
  Restoring <Brackets>{branch_name}</Brackets>…
@@ -208,7 +192,5 @@ Rebase.run = async function run(args: Args) {
208
192
  Restored <Brackets>{branch_name}</Brackets>.
209
193
  </Ink.Text>
210
194
  );
211
-
212
- actions.exit(code);
213
195
  }
214
196
  };
@@ -5,6 +5,7 @@ import path from "node:path";
5
5
  import * as Metadata from "~/core/Metadata";
6
6
  import { cli } from "~/core/cli";
7
7
  import { invariant } from "~/core/invariant";
8
+ import { safe_rm } from "~/core/safe_rm";
8
9
 
9
10
  import type * as CommitMetadata from "~/core/CommitMetadata";
10
11
 
@@ -140,6 +141,9 @@ GitReviseTodo.execute = async function grt_execute(args: ExecuteArgs) {
140
141
  // change to pipe to see output temporarily
141
142
  // https://github.com/magus/git-stack-cli/commit/f9f10e3ac3cd9a35ee75d3e0851a48391967a23f
142
143
  await cli(command, { stdio: ["ignore", "ignore", "ignore"] });
144
+
145
+ // cleanup tmp_git_sequence_editor_path
146
+ await safe_rm(tmp_git_sequence_editor_path);
143
147
  };
144
148
 
145
149
  type ExecuteArgs = {
@@ -161,8 +161,10 @@ type EditPullRequestArgs = {
161
161
  export async function pr_edit(args: EditPullRequestArgs) {
162
162
  const command_parts = [`gh pr edit ${args.branch} --base ${args.base}`];
163
163
 
164
+ let body_file: string | undefined;
165
+
164
166
  if (args.body) {
165
- const body_file = await write_body_file(args);
167
+ body_file = await write_body_file(args);
166
168
  command_parts.push(`--body-file="${body_file}"`);
167
169
  }
168
170
 
@@ -171,6 +173,11 @@ export async function pr_edit(args: EditPullRequestArgs) {
171
173
  if (cli_result.code !== 0) {
172
174
  handle_error(cli_result.output);
173
175
  }
176
+
177
+ // cleanup body_file
178
+ if (body_file) {
179
+ await safe_rm(body_file);
180
+ }
174
181
  }
175
182
 
176
183
  type DraftPullRequestArgs = {
@@ -0,0 +1,12 @@
1
+ export namespace pretty_json {
2
+ export type JSONValue =
3
+ | null
4
+ | number
5
+ | string
6
+ | boolean
7
+ | { [key: string]: JSONValue };
8
+ }
9
+
10
+ export function pretty_json<T extends pretty_json.JSONValue>(input: T): string {
11
+ return JSON.stringify(input, null, 2);
12
+ }
package/src/index.tsx CHANGED
@@ -7,12 +7,19 @@ import * as Ink from "ink-cjs";
7
7
  import { App } from "~/app/App";
8
8
  import { Store } from "~/app/Store";
9
9
  import { command } from "~/command";
10
+ import { pretty_json } from "~/core/pretty_json";
11
+
12
+ (async function main() {
13
+ try {
14
+ const argv = await command();
10
15
 
11
- command()
12
- .then((argv) => {
13
16
  const ink = Ink.render(<App />, {
14
17
  // If true, each update will be rendered as a separate output, without replacing the previous one.
15
18
  // debug: true,
19
+ //
20
+ // Configure whether Ink should listen to Ctrl+C keyboard input and exit the app.
21
+ // We intentionally handle this ourselves in `<Exit />`
22
+ exitOnCtrlC: false,
16
23
  });
17
24
 
18
25
  Store.setState((state) => {
@@ -22,7 +29,15 @@ command()
22
29
  state.cwd = process.cwd();
23
30
  });
24
31
 
25
- Store.getState().actions.debug(JSON.stringify(argv, null, 2));
26
- })
32
+ Store.getState().actions.debug(pretty_json(argv as any));
33
+
34
+ await ink.waitUntilExit();
35
+ } catch (err) {
36
+ // eslint-disable-next-line no-console
37
+ console.error(err);
38
+ process.exit(235);
39
+ }
40
+ })().catch((err) => {
27
41
  // eslint-disable-next-line no-console
28
- .catch(console.error);
42
+ console.error(err);
43
+ });