git-stack-cli 1.12.0 → 1.13.1

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.
@@ -1,6 +1,6 @@
1
1
  import * as React from "react";
2
2
 
3
- import fs from "node:fs";
3
+ import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
5
 
6
6
  import * as Ink from "ink-cjs";
@@ -11,6 +11,7 @@ import { FormatText } from "~/app/FormatText";
11
11
  import { Store } from "~/app/Store";
12
12
  import { colors } from "~/core/colors";
13
13
  import { invariant } from "~/core/invariant";
14
+ import { safe_exists } from "~/core/safe_exists";
14
15
 
15
16
  export function PreManualRebase() {
16
17
  return <Await fallback={null} function={run} />;
@@ -40,8 +41,8 @@ async function run() {
40
41
  for (const key of PR_TEMPLATE_KEY_LIST) {
41
42
  const pr_template_fn = PR_TEMPLATE[key as keyof typeof PR_TEMPLATE];
42
43
 
43
- if (fs.existsSync(pr_template_fn(repo_root))) {
44
- pr_template_body = fs.readFileSync(pr_template_fn(repo_root), "utf-8");
44
+ if (await safe_exists(pr_template_fn(repo_root))) {
45
+ pr_template_body = await fs.readFile(pr_template_fn(repo_root), "utf-8");
45
46
 
46
47
  actions.output(
47
48
  <FormatText
@@ -59,8 +60,8 @@ async function run() {
59
60
 
60
61
  // ./.github/PULL_REQUEST_TEMPLATE/*.md
61
62
  let pr_templates: Array<string> = [];
62
- if (fs.existsSync(PR_TEMPLATE.TemplateDir(repo_root))) {
63
- pr_templates = fs.readdirSync(PR_TEMPLATE.TemplateDir(repo_root));
63
+ if (await safe_exists(PR_TEMPLATE.TemplateDir(repo_root))) {
64
+ pr_templates = await fs.readdir(PR_TEMPLATE.TemplateDir(repo_root));
64
65
  }
65
66
 
66
67
  // check if repo has multiple pr templates
@@ -1,6 +1,5 @@
1
1
  import * as React from "react";
2
2
 
3
- import fs from "node:fs";
4
3
  import path from "node:path";
5
4
 
6
5
  import * as Ink from "ink-cjs";
@@ -11,6 +10,7 @@ import { Store } from "~/app/Store";
11
10
  import { YesNoPrompt } from "~/app/YesNoPrompt";
12
11
  import { cli } from "~/core/cli";
13
12
  import { colors } from "~/core/colors";
13
+ import { safe_exists } from "~/core/safe_exists";
14
14
 
15
15
  type Props = {
16
16
  children: React.ReactNode;
@@ -74,8 +74,8 @@ export function RebaseCheck(props: Props) {
74
74
  const git_dir = (await cli(`git rev-parse --absolute-git-dir`)).stdout;
75
75
 
76
76
  let is_rebase = false;
77
- is_rebase ||= fs.existsSync(path.join(git_dir, "rebase-apply"));
78
- is_rebase ||= fs.existsSync(path.join(git_dir, "rebase-merge"));
77
+ is_rebase ||= await safe_exists(path.join(git_dir, "rebase-apply"));
78
+ is_rebase ||= await safe_exists(path.join(git_dir, "rebase-merge"));
79
79
 
80
80
  const status = is_rebase ? "prompt" : "done";
81
81
  patch({ status });
package/src/app/Store.tsx CHANGED
@@ -24,6 +24,11 @@ type MutateOutputArgs = {
24
24
  withoutTimestamp?: boolean;
25
25
  };
26
26
 
27
+ type SyncGithubState = {
28
+ commit_range: CommitMetadata.CommitRange;
29
+ rebase_group_index: number;
30
+ };
31
+
27
32
  export type State = {
28
33
  // set immediately in `index.tsx` so no `null` scenario
29
34
  process_argv: Array<string>;
@@ -41,6 +46,7 @@ export type State = {
41
46
  commit_map: null | CommitMap;
42
47
  pr_templates: Array<string>;
43
48
  pr_template_body: null | string;
49
+ sync_github: null | SyncGithubState;
44
50
 
45
51
  step:
46
52
  | "github-api-error"
@@ -52,6 +58,7 @@ export type State = {
52
58
  | "select-commit-ranges"
53
59
  | "pre-manual-rebase"
54
60
  | "manual-rebase"
61
+ | "sync-github"
55
62
  | "post-rebase-status";
56
63
 
57
64
  output: Array<React.ReactNode>;
@@ -105,6 +112,7 @@ const BaseStore = createStore<State>()(
105
112
  commit_map: null,
106
113
  pr_templates: [],
107
114
  pr_template_body: null,
115
+ sync_github: null,
108
116
 
109
117
  step: "loading",
110
118
 
@@ -0,0 +1,322 @@
1
+ import * as React from "react";
2
+
3
+ import * as Ink from "ink-cjs";
4
+ import last from "lodash/last";
5
+
6
+ import { Await } from "~/app/Await";
7
+ import { Store } from "~/app/Store";
8
+ import * as StackSummaryTable from "~/core/StackSummaryTable";
9
+ import { cli } from "~/core/cli";
10
+ import { colors } from "~/core/colors";
11
+ import * as github from "~/core/github";
12
+ import { invariant } from "~/core/invariant";
13
+
14
+ import type * as CommitMetadata from "~/core/CommitMetadata";
15
+
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
+ return (
32
+ <Await
33
+ fallback={<Ink.Text color={colors.yellow}>Syncing…</Ink.Text>}
34
+ function={async function () {
35
+ await run({ abort_handler });
36
+ }}
37
+ />
38
+ );
39
+ }
40
+
41
+ type Args = {
42
+ abort_handler: React.MutableRefObject<() => void>;
43
+ };
44
+
45
+ async function run(args: Args) {
46
+ const state = Store.getState();
47
+ const actions = state.actions;
48
+ const argv = state.argv;
49
+ const branch_name = state.branch_name;
50
+ const commit_map = state.commit_map;
51
+ const master_branch = state.master_branch;
52
+ const repo_root = state.repo_root;
53
+ const sync_github = state.sync_github;
54
+
55
+ invariant(branch_name, "branch_name must exist");
56
+ invariant(commit_map, "commit_map must exist");
57
+ invariant(repo_root, "repo_root must exist");
58
+ invariant(sync_github, "sync_github must exist");
59
+
60
+ const commit_range = sync_github.commit_range;
61
+ const rebase_group_index = sync_github.rebase_group_index;
62
+
63
+ // always listen for SIGINT event and restore pr state
64
+ args.abort_handler.current = function sigint_handler() {
65
+ actions.output(<Ink.Text color={colors.red}>🚨 Abort</Ink.Text>);
66
+ handle_exit(17);
67
+ };
68
+
69
+ let DEFAULT_PR_BODY = "";
70
+ if (state.pr_template_body) {
71
+ DEFAULT_PR_BODY = state.pr_template_body;
72
+ }
73
+
74
+ const push_group_list = get_push_group_list();
75
+
76
+ // for all push targets in push_group_list
77
+ // things that can be done in parallel are grouped by numbers
78
+ //
79
+ // -----------------------------------
80
+ // 1 (before_push) temp mark draft
81
+ // --------------------------------------
82
+ // 2 push simultaneously to github
83
+ // --------------------------------------
84
+ // 2 create PR / edit PR
85
+ // 2 (after_push) undo temp mark draft
86
+ // --------------------------------------
87
+
88
+ try {
89
+ const before_push_tasks = [];
90
+ for (const group of push_group_list) {
91
+ before_push_tasks.push(before_push({ group }));
92
+ }
93
+
94
+ await Promise.all(before_push_tasks);
95
+
96
+ // git push -f origin HEAD~6:OtVX7Qvrw HEAD~3:E63ytp5dj HEAD~2:gs-NBabNSjXA HEAD~1:gs-UGVJdKNoD HEAD~0:gs-6LAx-On4
97
+
98
+ const git_push_command = [`git push -f origin`];
99
+
100
+ if (argv.verify === false) {
101
+ git_push_command.push("--no-verify");
102
+ }
103
+
104
+ for (const group of push_group_list) {
105
+ const last_commit = last(group.commits);
106
+ invariant(last_commit, "last_commit must exist");
107
+ const target = `${last_commit.sha}:${group.id}`;
108
+ git_push_command.push(target);
109
+ }
110
+
111
+ await cli(git_push_command);
112
+
113
+ const pr_url_list = commit_range.group_list.map(get_group_url);
114
+
115
+ const after_push_tasks = [];
116
+ for (const group of push_group_list) {
117
+ after_push_tasks.push(after_push({ group, pr_url_list }));
118
+ }
119
+
120
+ await Promise.all(after_push_tasks);
121
+
122
+ // finally, ensure all prs have the updated stack table from updated pr_url_list
123
+ // this step must come after the after_push since that step may create new PRs
124
+ // we need the urls for all prs at this step so we run it after the after_push
125
+ const update_pr_body_tasks = [];
126
+ for (let i = 0; i < commit_range.group_list.length; i++) {
127
+ const group = commit_range.group_list[i];
128
+
129
+ // use the updated pr_url_list to get the actual selected_url
130
+ const selected_url = pr_url_list[i];
131
+
132
+ const task = update_pr_body({ group, selected_url, pr_url_list });
133
+ update_pr_body_tasks.push(task);
134
+ }
135
+
136
+ await Promise.all(update_pr_body_tasks);
137
+
138
+ actions.set((state) => {
139
+ state.step = "post-rebase-status";
140
+ });
141
+ } catch (err) {
142
+ if (err instanceof Error) {
143
+ actions.error(err.message);
144
+ }
145
+
146
+ actions.error("Unable to sync.");
147
+ if (!argv.verbose) {
148
+ actions.error("Try again with `--verbose` to see more information.");
149
+ }
150
+
151
+ await handle_exit(18);
152
+ }
153
+
154
+ function get_push_group_list() {
155
+ // start from HEAD and work backward to rebase_group_index
156
+ const push_group_list = [];
157
+
158
+ for (let i = 0; i < commit_range.group_list.length; i++) {
159
+ const index = commit_range.group_list.length - 1 - i;
160
+
161
+ // do not go past rebase_group_index
162
+ if (index < rebase_group_index) {
163
+ break;
164
+ }
165
+
166
+ const group = commit_range.group_list[index];
167
+
168
+ push_group_list.unshift(group);
169
+ }
170
+
171
+ return push_group_list;
172
+ }
173
+
174
+ async function before_push(args: { group: CommitMetadataGroup }) {
175
+ const { group } = args;
176
+
177
+ invariant(group.base, "group.base must exist");
178
+
179
+ // we may temporarily mark PR as a draft before editing it
180
+ // if it is not already a draft PR, to avoid notification spam
181
+ let is_temp_draft = !group.pr?.isDraft;
182
+
183
+ // before pushing reset base to master temporarily
184
+ // avoid accidentally pointing to orphaned parent commit
185
+ // should hopefully fix issues where a PR includes a bunch of commits after pushing
186
+ if (group.pr) {
187
+ if (!group.pr.isDraft) {
188
+ is_temp_draft = true;
189
+ }
190
+
191
+ if (is_temp_draft) {
192
+ await github.pr_draft({
193
+ branch: group.id,
194
+ draft: true,
195
+ });
196
+ }
197
+
198
+ await github.pr_edit({
199
+ branch: group.id,
200
+ base: master_branch,
201
+ });
202
+ }
203
+ }
204
+
205
+ async function after_push(args: {
206
+ group: CommitMetadataGroup;
207
+ pr_url_list: Array<string>;
208
+ }) {
209
+ const { group, pr_url_list } = args;
210
+
211
+ invariant(group.base, "group.base must exist");
212
+
213
+ const selected_url = get_group_url(group);
214
+
215
+ if (group.pr) {
216
+ // ensure base matches pr in github
217
+ await github.pr_edit({
218
+ branch: group.id,
219
+ base: group.base,
220
+ body: StackSummaryTable.write({
221
+ body: group.pr.body,
222
+ pr_url_list,
223
+ selected_url,
224
+ }),
225
+ });
226
+
227
+ // we may temporarily mark PR as a draft before editing it
228
+ // if it is not already a draft PR, to avoid notification spam
229
+ let is_temp_draft = !group.pr?.isDraft;
230
+
231
+ if (is_temp_draft) {
232
+ // mark pr as ready for review again
233
+ await github.pr_draft({
234
+ branch: group.id,
235
+ draft: false,
236
+ });
237
+ }
238
+ } else {
239
+ // create pr in github
240
+ const pr_url = await github.pr_create({
241
+ branch: group.id,
242
+ base: group.base,
243
+ title: group.title,
244
+ body: DEFAULT_PR_BODY,
245
+ draft: argv.draft,
246
+ });
247
+
248
+ if (!pr_url) {
249
+ throw new Error("unable to create pr");
250
+ }
251
+
252
+ // update pr_url_list with created pr_url
253
+ for (let i = 0; i < pr_url_list.length; i++) {
254
+ const url = pr_url_list[i];
255
+ if (url === selected_url) {
256
+ pr_url_list[i] = pr_url;
257
+ }
258
+ }
259
+ }
260
+ }
261
+
262
+ async function update_pr_body(args: {
263
+ group: CommitMetadataGroup;
264
+ selected_url: string;
265
+ pr_url_list: Array<string>;
266
+ }) {
267
+ const { group, selected_url, pr_url_list } = args;
268
+
269
+ invariant(group.base, "group.base must exist");
270
+
271
+ const body = group.pr?.body || DEFAULT_PR_BODY;
272
+
273
+ const update_body = StackSummaryTable.write({
274
+ body,
275
+ pr_url_list,
276
+ selected_url,
277
+ });
278
+
279
+ if (update_body === body) {
280
+ actions.debug(`Skipping body update for ${selected_url}`);
281
+ } else {
282
+ actions.debug(`Update body for ${selected_url}`);
283
+
284
+ await github.pr_edit({
285
+ branch: group.id,
286
+ base: group.base,
287
+ body: update_body,
288
+ });
289
+ }
290
+ }
291
+
292
+ function handle_exit(code: number) {
293
+ actions.output(
294
+ <Ink.Text color={colors.yellow}>Restoring PR state…</Ink.Text>
295
+ );
296
+
297
+ for (const group of push_group_list) {
298
+ // we may temporarily mark PR as a draft before editing it
299
+ // if it is not already a draft PR, to avoid notification spam
300
+ let is_temp_draft = !group.pr?.isDraft;
301
+
302
+ // restore PR to non-draft state
303
+ if (is_temp_draft) {
304
+ github
305
+ .pr_draft({
306
+ branch: group.id,
307
+ draft: false,
308
+ })
309
+ .catch(actions.error);
310
+ }
311
+ }
312
+
313
+ actions.output(
314
+ <Ink.Text color={colors.yellow}>Restored PR state.</Ink.Text>
315
+ );
316
+
317
+ actions.exit(code);
318
+ }
319
+ }
320
+
321
+ type CommitMetadataGroup = CommitMetadata.CommitRange["group_list"][number];
322
+ const get_group_url = (group: CommitMetadataGroup) => group.pr?.url || group.id;
package/src/command.ts CHANGED
@@ -52,11 +52,6 @@ export async function command() {
52
52
  );
53
53
  }
54
54
 
55
- const Rebase = Object.freeze({
56
- "git-revise": "git-revise",
57
- "cherry-pick": "cherry-pick",
58
- });
59
-
60
55
  const GlobalOptions = {
61
56
  verbose: {
62
57
  type: "boolean",
@@ -95,17 +90,6 @@ const DefaultOptions = {
95
90
  "Run git hooks such as pre-commit and pre-push, disable with --no-verify",
96
91
  },
97
92
 
98
- "rebase": {
99
- type: "string",
100
- choices: [Rebase["git-revise"], Rebase["cherry-pick"]],
101
- default: Rebase["git-revise"],
102
- description: [
103
- "Strategy used for syncing branches",
104
- `${Rebase["git-revise"]}: perform faster in-memory rebase`,
105
- `${Rebase["cherry-pick"]}: use disk and incrementally rebase each commit`,
106
- ].join(" | "),
107
- },
108
-
109
93
  "update": {
110
94
  type: "boolean",
111
95
  alias: ["u", "upgrade"],
@@ -16,15 +16,35 @@ 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
+
19
33
  return (
20
34
  <Await
21
- function={Rebase.run}
22
35
  fallback={<Ink.Text color={colors.yellow}>Rebasing commits…</Ink.Text>}
36
+ function={async function () {
37
+ await Rebase.run({ abort_handler });
38
+ }}
23
39
  />
24
40
  );
25
41
  }
26
42
 
27
- Rebase.run = async function run() {
43
+ type Args = {
44
+ abort_handler: React.MutableRefObject<() => void>;
45
+ };
46
+
47
+ Rebase.run = async function run(args: Args) {
28
48
  const state = Store.getState();
29
49
  const actions = state.actions;
30
50
  const branch_name = state.branch_name;
@@ -38,7 +58,10 @@ Rebase.run = async function run() {
38
58
  invariant(repo_root, "repo_root must exist");
39
59
 
40
60
  // always listen for SIGINT event and restore git state
41
- process.once("SIGINT", handle_exit);
61
+ args.abort_handler.current = async function sigint_handler() {
62
+ actions.output(<Ink.Text color={colors.red}>🚨 Abort</Ink.Text>);
63
+ handle_exit(19);
64
+ };
42
65
 
43
66
  const temp_branch_name = `${branch_name}_${short_id()}`;
44
67
 
@@ -111,9 +134,8 @@ Rebase.run = async function run() {
111
134
  await cli(`git cherry-pick --keep-redundant-commits ${sha_list}`);
112
135
  }
113
136
 
114
- // after all commits have been cherry-picked and amended
115
- // move the branch pointer to the newly created temporary branch
116
- // now we are locally in sync with github and on the original branch
137
+ // after all commits have been cherry-picked move the pointer
138
+ // of original branch to the newly created temporary branch
117
139
  await cli(`git branch -f ${branch_name} ${temp_branch_name}`);
118
140
 
119
141
  restore_git();
@@ -144,7 +166,7 @@ Rebase.run = async function run() {
144
166
  }
145
167
  }
146
168
 
147
- handle_exit();
169
+ handle_exit(20);
148
170
  }
149
171
 
150
172
  // cleanup git operations if cancelled during manual rebase
@@ -154,9 +176,6 @@ Rebase.run = async function run() {
154
176
  // all children processes receive the SIGINT signal
155
177
  const spawn_options = { ignoreExitCode: true };
156
178
 
157
- // always clean up any patch files
158
- cli.sync(`rm ${PATCH_FILE}`, spawn_options);
159
-
160
179
  // always hard reset and clean to allow subsequent checkout
161
180
  // if there are files checkout will fail and cascade fail subsequent commands
162
181
  cli.sync(`git reset --hard`, spawn_options);
@@ -168,13 +187,6 @@ Rebase.run = async function run() {
168
187
  // ...and cleanup temporary branch
169
188
  cli.sync(`git branch -D ${temp_branch_name}`, spawn_options);
170
189
 
171
- if (commit_range) {
172
- // ...and cleanup pr group branches
173
- for (const group of commit_range.group_list) {
174
- cli.sync(`git branch -D ${group.id}`, spawn_options);
175
- }
176
- }
177
-
178
190
  // restore back to original dir
179
191
  if (fs.existsSync(cwd)) {
180
192
  process.chdir(cwd);
@@ -182,7 +194,7 @@ Rebase.run = async function run() {
182
194
  cli.sync(`pwd`, spawn_options);
183
195
  }
184
196
 
185
- function handle_exit() {
197
+ function handle_exit(code: number) {
186
198
  actions.output(
187
199
  <Ink.Text color={colors.yellow}>
188
200
  Restoring <Brackets>{branch_name}</Brackets>…
@@ -197,8 +209,6 @@ Rebase.run = async function run() {
197
209
  </Ink.Text>
198
210
  );
199
211
 
200
- actions.exit(6);
212
+ actions.exit(code);
201
213
  }
202
214
  };
203
-
204
- const PATCH_FILE = "git-stack-cli-patch.patch";
@@ -1,4 +1,4 @@
1
- import fs from "node:fs";
1
+ import fs from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
 
@@ -120,10 +120,10 @@ GitReviseTodo.execute = async function grt_execute(args: ExecuteArgs) {
120
120
  const GIT_SEQUENCE_EDITOR_SCRIPT = `process.env.GIT_SEQUENCE_EDITOR_SCRIPT`;
121
121
 
122
122
  // write script to temporary path
123
- fs.writeFileSync(tmp_git_sequence_editor_path, GIT_SEQUENCE_EDITOR_SCRIPT);
123
+ await fs.writeFile(tmp_git_sequence_editor_path, GIT_SEQUENCE_EDITOR_SCRIPT);
124
124
 
125
125
  // ensure script is executable
126
- fs.chmodSync(tmp_git_sequence_editor_path, "755");
126
+ await fs.chmod(tmp_git_sequence_editor_path, "755");
127
127
 
128
128
  const git_revise_todo = GitReviseTodo(args);
129
129
 
@@ -1,6 +1,6 @@
1
1
  import * as React from "react";
2
2
 
3
- import fs from "node:fs";
3
+ import fs from "node:fs/promises";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
 
@@ -12,6 +12,7 @@ import { cli } from "~/core/cli";
12
12
  import { colors } from "~/core/colors";
13
13
  import { invariant } from "~/core/invariant";
14
14
  import { safe_quote } from "~/core/safe_quote";
15
+ import { safe_rm } from "~/core/safe_rm";
15
16
 
16
17
  export async function pr_list(): Promise<Array<PullRequest>> {
17
18
  const state = Store.getState();
@@ -148,7 +149,8 @@ export async function pr_edit(args: EditPullRequestArgs) {
148
149
  const command_parts = [`gh pr edit ${args.branch} --base ${args.base}`];
149
150
 
150
151
  if (args.body) {
151
- command_parts.push(`--body-file="${body_file(args.body)}"`);
152
+ const body_file = await write_body_file(args);
153
+ command_parts.push(`--body-file="${body_file}"`);
152
154
  }
153
155
 
154
156
  const command = command_parts.join(" ");
@@ -166,6 +168,10 @@ type DraftPullRequestArgs = {
166
168
  };
167
169
 
168
170
  export async function pr_draft(args: DraftPullRequestArgs) {
171
+ // https://cli.github.com/manual/gh_api
172
+ // https://docs.github.com/en/graphql/reference/mutations#convertpullrequesttodraft
173
+ // https://docs.github.com/en/graphql/reference/mutations#markpullrequestreadyforreview
174
+
169
175
  const mutation_name = args.draft
170
176
  ? "convertPullRequestToDraft"
171
177
  : "markPullRequestReadyForReview";
@@ -220,7 +226,7 @@ async function gh_json<T>(command: string): Promise<T | Error> {
220
226
  }
221
227
 
222
228
  // read from file
223
- const json_str = fs.readFileSync(tmp_pr_json, "utf-8");
229
+ const json_str = await fs.readFile(tmp_pr_json, "utf-8");
224
230
  const json = JSON.parse(json_str);
225
231
  return json;
226
232
  }
@@ -237,13 +243,16 @@ function handle_error(output: string): never {
237
243
  }
238
244
 
239
245
  // convert a string to a file for use via github cli `--body-file`
240
- function body_file(body: string) {
246
+ async function write_body_file(args: EditPullRequestArgs) {
247
+ invariant(args.body, "args.body must exist");
248
+
241
249
  const temp_dir = os.tmpdir();
242
- const temp_path = path.join(temp_dir, "git-stack-body");
243
- if (fs.existsSync(temp_path)) {
244
- fs.rmSync(temp_path);
245
- }
246
- fs.writeFileSync(temp_path, body);
250
+ const temp_path = path.join(temp_dir, `git-stack-body-${args.base}`);
251
+
252
+ await safe_rm(temp_path);
253
+
254
+ await fs.writeFile(temp_path, args.body);
255
+
247
256
  return temp_path;
248
257
  }
249
258
 
@@ -1,8 +1,8 @@
1
- import fs from "node:fs";
1
+ import fs from "node:fs/promises";
2
2
 
3
- export function read_json<T = unknown>(path: string): null | T {
3
+ export async function read_json<T = unknown>(path: string): Promise<null | T> {
4
4
  try {
5
- const file_buffer = fs.readFileSync(path);
5
+ const file_buffer = await fs.readFile(path);
6
6
  const json_str = String(file_buffer);
7
7
  const json = JSON.parse(json_str);
8
8
  return json;
@@ -0,0 +1,10 @@
1
+ import fs from "node:fs/promises";
2
+
3
+ export async function safe_exists(filepath: string) {
4
+ try {
5
+ await fs.access(filepath);
6
+ return true;
7
+ } catch {
8
+ return false;
9
+ }
10
+ }
@@ -0,0 +1,10 @@
1
+ import fs from "node:fs/promises";
2
+
3
+ export async function safe_rm(filepath: string) {
4
+ try {
5
+ await fs.access(filepath);
6
+ await fs.rm(filepath);
7
+ } catch {
8
+ // if access fails there is no file to remove this is safe
9
+ }
10
+ }
package/src/index.tsx CHANGED
@@ -10,7 +10,10 @@ import { command } from "~/command";
10
10
 
11
11
  command()
12
12
  .then((argv) => {
13
- const ink = Ink.render(<App />);
13
+ const ink = Ink.render(<App />, {
14
+ // If true, each update will be rendered as a separate output, without replacing the previous one.
15
+ // debug: true,
16
+ });
14
17
 
15
18
  Store.setState((state) => {
16
19
  state.ink = ink;