git-stack-cli 0.3.1 → 0.5.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.
@@ -14,19 +14,29 @@ async function run(args) {
14
14
  const commit_range = Store.getState().commit_range;
15
15
  invariant(commit_range, "commit_range must exist");
16
16
  actions.output(React.createElement(StatusTable, null));
17
+ let needs_rebase = false;
17
18
  let needs_update = false;
18
19
  for (const group of commit_range.group_list) {
19
20
  if (group.dirty) {
20
21
  needs_update = true;
21
- break;
22
+ }
23
+ if (group.pr?.state === "MERGED") {
24
+ needs_rebase = true;
25
+ }
26
+ }
27
+ for (let i = 0; i < commit_range.commit_list.length; i++) {
28
+ const commit = commit_range.commit_list[i];
29
+ const commit_pr = commit_range.pr_map.get(commit.branch_id || "");
30
+ if (commit.branch_id && !commit_pr) {
31
+ needs_rebase = true;
22
32
  }
23
33
  }
24
34
  if (args.argv.check) {
25
35
  actions.exit(0);
26
36
  }
27
- else if (args.argv.force) {
37
+ else if (needs_rebase) {
28
38
  Store.setState((state) => {
29
- state.step = "select-commit-ranges";
39
+ state.step = "pre-local-merge-rebase";
30
40
  });
31
41
  }
32
42
  else if (needs_update) {
@@ -34,6 +44,11 @@ async function run(args) {
34
44
  state.step = "pre-select-commit-ranges";
35
45
  });
36
46
  }
47
+ else if (args.argv.force) {
48
+ Store.setState((state) => {
49
+ state.step = "select-commit-ranges";
50
+ });
51
+ }
37
52
  else {
38
53
  actions.output(React.createElement(Ink.Text, null, "\u2705 Everything up to date."));
39
54
  actions.output(React.createElement(Ink.Text, { color: "gray" },
@@ -1,6 +1,5 @@
1
1
  import * as React from "react";
2
2
  import * as Ink from "ink";
3
- import { clamp } from "../core/clamp.js";
4
3
  import { invariant } from "../core/invariant.js";
5
4
  import { Store } from "./Store.js";
6
5
  export function StatusTable() {
@@ -49,7 +48,8 @@ export function StatusTable() {
49
48
  row_list.push(row);
50
49
  }
51
50
  if (!row_list.length) {
52
- return React.createElement(Ink.Text, { dimColor: true }, "No data found.");
51
+ return (React.createElement(Container, null,
52
+ React.createElement(Ink.Text, { dimColor: true }, "No data found.")));
53
53
  }
54
54
  // walk data and discover max width for each column
55
55
  const sample_row = row_list[0];
@@ -67,9 +67,8 @@ export function StatusTable() {
67
67
  const { stdout } = Ink.useStdout();
68
68
  const available_width = stdout.columns;
69
69
  const columnGap = 2;
70
- const breathing_room = 10;
71
- const max_title_width = Math.min(max_col_width.title, MAX_TITLE_LENGTH);
72
- const remaining_space = clamp(available_width -
70
+ const breathing_room = 0;
71
+ const remaining_space = available_width -
73
72
  // icon
74
73
  max_col_width.icon -
75
74
  // status
@@ -78,28 +77,33 @@ export function StatusTable() {
78
77
  max_col_width.count -
79
78
  // url
80
79
  max_col_width.url -
81
- // gap * col count
82
- columnGap * col_list.length -
80
+ // gap * col count (minus one for row.id which is not shown but used at key)
81
+ columnGap * (col_list.length - 1) -
83
82
  // remove some extra space
84
- breathing_room, 0, max_title_width);
85
- const title_width = remaining_space;
86
- return (React.createElement(Ink.Box, { flexDirection: "column", width: available_width },
83
+ breathing_room;
84
+ // add one for ellipsis character
85
+ const title_width = Math.min(max_col_width.title, remaining_space + 1);
86
+ // prettier-ignore
87
+ // console.debug({ available_width, remaining_space, title_width, max_col_width });
88
+ return (React.createElement(Container, null, row_list.map((row) => {
89
+ return (React.createElement(Ink.Box, { key: row.id,
90
+ // borderStyle="round"
91
+ flexDirection: "row", columnGap: columnGap, width: available_width },
92
+ React.createElement(Ink.Box, { width: max_col_width.icon },
93
+ React.createElement(Ink.Text, null, row.icon)),
94
+ React.createElement(Ink.Box, { width: max_col_width.status },
95
+ React.createElement(Ink.Text, null, row.status)),
96
+ React.createElement(Ink.Box, { width: max_col_width.count },
97
+ React.createElement(Ink.Text, null, row.count)),
98
+ React.createElement(Ink.Box, { width: title_width },
99
+ React.createElement(Ink.Text, { wrap: "truncate-end" }, row.title)),
100
+ React.createElement(Ink.Box, { width: max_col_width.url },
101
+ React.createElement(Ink.Text, null, row.url))));
102
+ })));
103
+ }
104
+ function Container(props) {
105
+ return (React.createElement(Ink.Box, { flexDirection: "column" },
87
106
  React.createElement(Ink.Box, { height: 1 }),
88
- row_list.map((row) => {
89
- return (React.createElement(Ink.Box, { key: row.id,
90
- // borderStyle="round"
91
- flexDirection: "row", columnGap: columnGap, width: available_width },
92
- React.createElement(Ink.Box, { width: max_col_width.icon },
93
- React.createElement(Ink.Text, null, row.icon)),
94
- React.createElement(Ink.Box, { width: max_col_width.status },
95
- React.createElement(Ink.Text, null, row.status)),
96
- React.createElement(Ink.Box, { width: max_col_width.count },
97
- React.createElement(Ink.Text, null, row.count)),
98
- React.createElement(Ink.Box, { width: title_width },
99
- React.createElement(Ink.Text, { wrap: "truncate-end" }, row.title)),
100
- React.createElement(Ink.Box, { width: max_col_width.url },
101
- React.createElement(Ink.Text, null, row.url))));
102
- }),
107
+ props.children,
103
108
  React.createElement(Ink.Box, { height: 1 })));
104
109
  }
105
- const MAX_TITLE_LENGTH = 50;
package/dist/app/Store.js CHANGED
@@ -31,7 +31,17 @@ const BaseStore = createStore()(immer((set, get) => ({
31
31
  },
32
32
  newline() {
33
33
  set((state) => {
34
- state.mutate.output(state, React.createElement(Ink.Text, null, "\u200E"));
34
+ state.mutate.output(state, "");
35
+ });
36
+ },
37
+ json(value) {
38
+ set((state) => {
39
+ state.mutate.output(state, JSON.stringify(value, null, 2));
40
+ });
41
+ },
42
+ error(message) {
43
+ set((state) => {
44
+ state.mutate.output(state, React.createElement(Ink.Text, { color: "#ef4444" }, message));
35
45
  });
36
46
  },
37
47
  output(node) {
@@ -40,11 +50,15 @@ const BaseStore = createStore()(immer((set, get) => ({
40
50
  });
41
51
  },
42
52
  debug(node) {
43
- set((state) => {
44
- if (state.argv?.debug) {
45
- state.mutate.output(state, node);
46
- }
47
- });
53
+ if (get().actions.isDebug()) {
54
+ set((state) => {
55
+ state.mutate.output(state, React.createElement(Ink.Text, { dimColor: true }, node));
56
+ });
57
+ }
58
+ },
59
+ isDebug() {
60
+ const state = get();
61
+ return state.select.debug(state);
48
62
  },
49
63
  reset_pr() {
50
64
  set((state) => {
@@ -59,9 +73,21 @@ const BaseStore = createStore()(immer((set, get) => ({
59
73
  },
60
74
  mutate: {
61
75
  output(state, node) {
76
+ switch (typeof node) {
77
+ case "boolean":
78
+ case "number":
79
+ case "string":
80
+ state.output.push(React.createElement(Ink.Text, null, String(node)));
81
+ return;
82
+ }
62
83
  state.output.push(node);
63
84
  },
64
85
  },
86
+ select: {
87
+ debug(state) {
88
+ return state.argv?.verbose || false;
89
+ },
90
+ },
65
91
  })));
66
92
  function useState(selector) {
67
93
  return useStore(BaseStore, selector);
@@ -0,0 +1,37 @@
1
+ import * as React from "react";
2
+ import * as Ink from "ink";
3
+ export function TextInput(props) {
4
+ const [value, set_value] = React.useState(get_value(props));
5
+ React.useEffect(function sync_value_prop() {
6
+ set_value(get_value(props));
7
+ }, [props.value]);
8
+ Ink.useInput((input, key) => {
9
+ let next_value = value;
10
+ // console.debug("[useInput]", { input, key });
11
+ if (key.backspace || key.delete) {
12
+ next_value = value.slice(0, -1);
13
+ }
14
+ else if (key.return) {
15
+ props.onSubmit?.(next_value);
16
+ }
17
+ else {
18
+ switch (input) {
19
+ case "\r":
20
+ if (props.multiline) {
21
+ next_value = `${value}\n`;
22
+ }
23
+ break;
24
+ default:
25
+ next_value = `${value}${input}`;
26
+ }
27
+ }
28
+ set_value(next_value);
29
+ props.onChange?.(next_value);
30
+ });
31
+ // console.debug("[TextInput]", { value });
32
+ return (React.createElement(Ink.Box, { borderStyle: "single", minHeight: 1, borderColor: "yellow", borderDimColor: true },
33
+ React.createElement(Ink.Text, null, value || "‎")));
34
+ }
35
+ function get_value(props) {
36
+ return props.value || "";
37
+ }
@@ -32,7 +32,7 @@ export function YesNoPrompt(props) {
32
32
  }
33
33
  return (React.createElement(Ink.Box, { flexDirection: "column" },
34
34
  React.createElement(Ink.Box, null,
35
- React.createElement(Ink.Text, { color: "yellow" }, props.message),
35
+ typeof props.message === "object" ? (props.message) : (React.createElement(Ink.Text, { color: "yellow" }, props.message)),
36
36
  React.createElement(Ink.Text, null, " "),
37
37
  React.createElement(Parens, null,
38
38
  React.createElement(Ink.Text, { color: "gray" }, choices)))));
package/dist/app/main.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import * as React from "react";
2
2
  import { assertNever } from "../core/assertNever.js";
3
3
  import { GithubApiError } from "./GithubApiError.js";
4
+ import { LocalMergeRebase } from "./LocalMergeRebase.js";
4
5
  import { ManualRebase } from "./ManualRebase.js";
5
6
  import { PostRebaseStatus } from "./PostRebaseStatus.js";
7
+ import { PreLocalMergeRebase } from "./PreLocalMergeRebase.js";
6
8
  import { PreSelectCommitRanges } from "./PreSelectCommitRanges.js";
7
9
  import { SelectCommitRanges } from "./SelectCommitRanges.js";
8
10
  import { Status } from "./Status.js";
@@ -16,6 +18,10 @@ export function Main() {
16
18
  return null;
17
19
  case "status":
18
20
  return React.createElement(Status, null);
21
+ case "local-merge-rebase":
22
+ return React.createElement(LocalMergeRebase, null);
23
+ case "pre-local-merge-rebase":
24
+ return React.createElement(PreLocalMergeRebase, null);
19
25
  case "pre-select-commit-ranges":
20
26
  return React.createElement(PreSelectCommitRanges, null);
21
27
  case "select-commit-ranges":
package/dist/command.js CHANGED
@@ -1,62 +1,43 @@
1
1
  import yargs from "yargs";
2
2
  import { hideBin } from "yargs/helpers";
3
3
  export async function command() {
4
- const debug_argv = await yargs(hideBin(process.argv))
5
- .option("debug", {
6
- type: "boolean",
7
- description: "Enable debug mode with more options for debugging",
8
- })
9
- .help(false).argv;
10
- if (!debug_argv.debug) {
11
- return NormalMode();
12
- }
13
- return DebugMode();
14
- }
15
- function NormalMode() {
16
- return (yargs(hideBin(process.argv))
17
- .option("force", {
18
- type: "boolean",
19
- description: "Force sync even if no changes are detected",
20
- })
21
- .option("check", {
22
- type: "boolean",
23
- description: "Print status table without syncing",
24
- })
25
- .option("debug", {
26
- type: "boolean",
27
- description: "Enable debug mode with more options for debugging",
28
- })
29
- // disallow unknown options
30
- .strict()
31
- .help().argv);
32
- }
33
- function DebugMode() {
4
+ // https://yargs.js.org/docs/#api-reference-optionkey-opt
34
5
  return (yargs(hideBin(process.argv))
6
+ .usage("Usage: git stack [options]")
35
7
  .option("force", {
36
8
  type: "boolean",
9
+ alias: ["f"],
37
10
  description: "Force sync even if no changes are detected",
38
11
  })
39
12
  .option("check", {
40
13
  type: "boolean",
14
+ alias: ["c"],
41
15
  description: "Print status table without syncing",
42
16
  })
43
- .option("debug", {
17
+ .option("no-verify", {
44
18
  type: "boolean",
45
- description: "Enable debug mode with more options for debugging",
19
+ description: "Disable the pre-push hook, bypassing it completely",
46
20
  })
47
21
  .option("verbose", {
48
22
  type: "boolean",
49
- description: "Log extra information during execution",
23
+ alias: ["v"],
24
+ description: "Enable verbose mode with more detailed output for debugging",
50
25
  })
51
26
  .option("write-state-json", {
27
+ hidden: true,
52
28
  type: "boolean",
53
29
  description: "Write state to local json file for debugging",
54
30
  })
55
31
  .option("mock-metadata", {
32
+ hidden: true,
56
33
  type: "boolean",
57
34
  description: "Mock local store metadata for testing",
58
35
  })
36
+ // do not wrap to 80 columns (yargs default)
37
+ // .wrap(yargs().terminalWidth()) will fill terminal (maximuize)
38
+ .wrap(null)
59
39
  // disallow unknown options
60
40
  .strict()
61
- .help().argv);
41
+ .version()
42
+ .help("help").argv);
62
43
  }
@@ -1,18 +1,24 @@
1
1
  import * as Metadata from "./Metadata.js";
2
2
  import { cli } from "./cli.js";
3
3
  import * as github from "./github.js";
4
- export async function range(commit_map) {
4
+ export async function range(commit_group_map) {
5
5
  // gather all open prs in repo first
6
6
  // cheaper query to populate cache
7
7
  await github.pr_list();
8
8
  const commit_list = await get_commit_list();
9
+ const pr_map = new Map();
9
10
  let invalid = false;
10
11
  const group_map = new Map();
11
12
  for (const commit of commit_list) {
12
13
  let id = commit.branch_id;
14
+ let title = id;
13
15
  // use commit map if provided (via select commit ranges)
14
- if (commit_map) {
15
- id = commit_map[commit.sha];
16
+ if (commit_group_map) {
17
+ const group = commit_group_map[commit.sha];
18
+ if (group) {
19
+ id = group.id;
20
+ title = group.title;
21
+ }
16
22
  }
17
23
  if (!id) {
18
24
  // console.debug("INVALID", "MISSING ID", commit.message);
@@ -33,8 +39,12 @@ export async function range(commit_map) {
33
39
  invalid = true;
34
40
  id = UNASSIGNED;
35
41
  }
42
+ if (!title) {
43
+ title = id;
44
+ }
36
45
  const group = group_map.get(id) || {
37
46
  id,
47
+ title,
38
48
  pr: null,
39
49
  base: null,
40
50
  dirty: false,
@@ -53,6 +63,7 @@ export async function range(commit_map) {
53
63
  const pr_result = await github.pr_status(group.id);
54
64
  if (pr_result && pr_result.state !== "CLOSED") {
55
65
  group.pr = pr_result;
66
+ pr_map.set(group.id, pr_result);
56
67
  }
57
68
  }
58
69
  // console.debug("group", group.pr?.title.substring(0, 40));
@@ -108,10 +119,13 @@ export async function range(commit_map) {
108
119
  if (unassigned_group) {
109
120
  group_list.unshift(unassigned_group);
110
121
  }
111
- return { invalid, group_list, commit_list, UNASSIGNED };
122
+ return { invalid, group_list, commit_list, pr_map, UNASSIGNED };
112
123
  }
113
124
  async function get_commit_list() {
114
125
  const log_result = await cli(`git log master..HEAD --oneline --format=%H --color=never`);
126
+ if (!log_result.stdout) {
127
+ return [];
128
+ }
115
129
  const sha_list = lines(log_result.stdout).reverse();
116
130
  const commit_metadata_list = [];
117
131
  for (let i = 0; i < sha_list.length; i++) {
@@ -1,8 +1,9 @@
1
1
  import { invariant } from "../core/invariant.js";
2
+ import { safe_quote } from "../core/safe_quote.js";
2
3
  export function write(message, branch_id) {
3
4
  let result = message;
4
5
  // escape double-quote for cli
5
- result = result.replace(RE.all_double_quote, '\\"');
6
+ result = safe_quote(result);
6
7
  // remove any previous metadata lines
7
8
  result = remove(result);
8
9
  const line_list = [result, "", TEMPLATE.branch_id(branch_id)];
@@ -27,10 +28,10 @@ export function remove(message) {
27
28
  }
28
29
  const TEMPLATE = {
29
30
  branch_id(id) {
30
- return `git-multi-diff-id: ${id}`;
31
+ return `git-stack-id: ${id}`;
31
32
  },
32
33
  };
33
34
  const RE = {
34
35
  all_double_quote: /"/g,
35
- branch_id: new RegExp(TEMPLATE.branch_id("(?<id>[a-z0-9-]+)")),
36
+ branch_id: new RegExp(TEMPLATE.branch_id("(?<id>[a-z0-9-+]+)"), "i"),
36
37
  };
package/dist/core/cli.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import * as child from "node:child_process";
2
+ import { Store } from "../app/Store.js";
2
3
  export async function cli(command, unsafe_options) {
4
+ const state = Store.getState();
3
5
  const options = Object.assign({}, unsafe_options);
4
6
  return new Promise((resolve, reject) => {
5
7
  const childProcess = child.spawn("sh", ["-c", command], options);
@@ -26,6 +28,8 @@ export async function cli(command, unsafe_options) {
26
28
  stderr: stderr.trimEnd(),
27
29
  output: output.trimEnd(),
28
30
  };
31
+ state.actions.debug(`$ ${command}`);
32
+ state.actions.debug(result.output);
29
33
  resolve(result);
30
34
  }
31
35
  });
@@ -4,6 +4,7 @@ import { Brackets } from "../app/Brackets.js";
4
4
  import { Store } from "../app/Store.js";
5
5
  import { cli } from "./cli.js";
6
6
  import { invariant } from "./invariant.js";
7
+ import { safe_quote } from "./safe_quote.js";
7
8
  // prettier-ignore
8
9
  const JSON_FIELDS = "--json number,state,baseRefName,headRefName,commits,title,url";
9
10
  export async function pr_list() {
@@ -20,13 +21,15 @@ export async function pr_list() {
20
21
  handle_error(cli_result.output);
21
22
  }
22
23
  const result_pr_list = JSON.parse(cli_result.stdout);
23
- actions.debug(React.createElement(Ink.Text, { dimColor: true },
24
- React.createElement(Ink.Text, null, "Github cache "),
25
- React.createElement(Ink.Text, { bold: true, color: "yellow" }, result_pr_list.length),
26
- React.createElement(Ink.Text, null, " open PRs from "),
27
- React.createElement(Brackets, null, repo_path),
28
- React.createElement(Ink.Text, null, " authored by "),
29
- React.createElement(Brackets, null, username)));
24
+ if (actions.isDebug()) {
25
+ actions.output(React.createElement(Ink.Text, { dimColor: true },
26
+ React.createElement(Ink.Text, null, "Github cache "),
27
+ React.createElement(Ink.Text, { bold: true, color: "yellow" }, result_pr_list.length),
28
+ React.createElement(Ink.Text, null, " open PRs from "),
29
+ React.createElement(Brackets, null, repo_path),
30
+ React.createElement(Ink.Text, null, " authored by "),
31
+ React.createElement(Brackets, null, username)));
32
+ }
30
33
  actions.set((state) => {
31
34
  for (const pr of result_pr_list) {
32
35
  state.pr[pr.headRefName] = pr;
@@ -43,20 +46,24 @@ export async function pr_status(branch) {
43
46
  invariant(repo_path, "repo_path must exist");
44
47
  const cache = state.pr[branch];
45
48
  if (cache) {
46
- actions.debug(React.createElement(Ink.Text, null,
49
+ if (actions.isDebug()) {
50
+ actions.output(React.createElement(Ink.Text, null,
51
+ React.createElement(Ink.Text, { dimColor: true }, "Github pr_status cache"),
52
+ React.createElement(Ink.Text, null, " "),
53
+ React.createElement(Ink.Text, { bold: true, color: "#22c55e" }, "HIT "),
54
+ React.createElement(Ink.Text, null, " "),
55
+ React.createElement(Ink.Text, { dimColor: true }, branch)));
56
+ }
57
+ return cache;
58
+ }
59
+ if (actions.isDebug()) {
60
+ actions.output(React.createElement(Ink.Text, null,
47
61
  React.createElement(Ink.Text, { dimColor: true }, "Github pr_status cache"),
48
62
  React.createElement(Ink.Text, null, " "),
49
- React.createElement(Ink.Text, { bold: true, color: "#22c55e" }, "HIT "),
63
+ React.createElement(Ink.Text, { bold: true, color: "#ef4444" }, "MISS"),
50
64
  React.createElement(Ink.Text, null, " "),
51
65
  React.createElement(Ink.Text, { dimColor: true }, branch)));
52
- return cache;
53
66
  }
54
- actions.debug(React.createElement(Ink.Text, null,
55
- React.createElement(Ink.Text, { dimColor: true }, "Github pr_status cache"),
56
- React.createElement(Ink.Text, null, " "),
57
- React.createElement(Ink.Text, { bold: true, color: "#ef4444" }, "MISS"),
58
- React.createElement(Ink.Text, null, " "),
59
- React.createElement(Ink.Text, { dimColor: true }, branch)));
60
67
  const cli_result = await cli(`gh pr view ${branch} --repo ${repo_path} ${JSON_FIELDS}`, {
61
68
  ignoreExitCode: true,
62
69
  });
@@ -70,8 +77,9 @@ export async function pr_status(branch) {
70
77
  });
71
78
  return pr;
72
79
  }
73
- export async function pr_create(branch, base) {
74
- const cli_result = await cli(`gh pr create --fill --head ${branch} --base ${base}`);
80
+ export async function pr_create(args) {
81
+ const title = safe_quote(args.title);
82
+ const cli_result = await cli(`gh pr create --fill --head ${args.branch} --base ${args.base} --title="${title}"`);
75
83
  if (cli_result.code !== 0) {
76
84
  handle_error(cli_result.output);
77
85
  }
@@ -0,0 +1,61 @@
1
+ import crypto from "node:crypto";
2
+ // console.log(id());
3
+ export function id() {
4
+ const timestamp = Date.now();
5
+ // 9 223 372 036 854 775 808
6
+ // 9 trillion possible values
7
+ // (2^53) * (2^10) = 2^63 = 9,223,372,036,854,775,808
8
+ const js_max_bits = 53;
9
+ const timestamp_bits = Math.floor(Math.log2(timestamp)) + 1;
10
+ // padding needed to reach 53 bits
11
+ const padding_bits = js_max_bits - timestamp_bits;
12
+ // random between 0 and 2^padding_bits - 1
13
+ const random = crypto.randomInt(0, Math.pow(2, padding_bits));
14
+ // combine timestamp and random value
15
+ const combined = interleave_bits(timestamp, random);
16
+ // console.debug({ combined, timestamp, random, padding_bits, timestamp_bits });
17
+ return encode(combined);
18
+ }
19
+ function binary(value) {
20
+ return BigInt(value).toString(2);
21
+ }
22
+ function rand_index(list) {
23
+ return Math.floor(Math.random() * list.length);
24
+ }
25
+ function interleave_bits(a, b) {
26
+ const a_binary = binary(a).split("");
27
+ const b_binary = binary(b).split("");
28
+ while (b_binary.length) {
29
+ // pull random bit out of b_binary
30
+ const b_index = rand_index(b_binary);
31
+ const [selected] = b_binary.splice(b_index, 1);
32
+ // insert random bit into a_binary
33
+ const a_index = rand_index(a_binary);
34
+ a_binary.splice(a_index, 0, selected);
35
+ }
36
+ // convert binary list back to integer
37
+ const a_value = parseInt(a_binary.join(""), 2);
38
+ return a_value;
39
+ }
40
+ function encode(value) {
41
+ // base64 encode (64 characters)
42
+ // max character necessary to encode is equal to maximum number
43
+ // of bits in value divided by bits per character in encoding
44
+ //
45
+ // Example
46
+ // in base64 each characters can represent 6 bits (2^6 = 64)
47
+ // 53 bits / 6 bits = 8.833333333333334 characters (9 characters)
48
+ //
49
+ // prettier-ignore
50
+ const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-+";
51
+ const bits_per_char = Math.log2(chars.length);
52
+ const max_value_bits = 53;
53
+ const max_char_size = Math.ceil(max_value_bits / bits_per_char);
54
+ let result = "";
55
+ while (value > 0) {
56
+ result = chars[value % chars.length] + result;
57
+ value = Math.floor(value / chars.length);
58
+ }
59
+ // pad the result to necessary characters
60
+ return result.padStart(max_char_size, "=");
61
+ }
@@ -0,0 +1,3 @@
1
+ export async function sleep(time) {
2
+ return new Promise((resolve) => setTimeout(resolve, time));
3
+ }
@@ -0,0 +1,12 @@
1
+ import fs from "node:fs";
2
+ export function read_json(path) {
3
+ try {
4
+ const file_buffer = fs.readFileSync(path);
5
+ const json_str = String(file_buffer);
6
+ const json = JSON.parse(json_str);
7
+ return json;
8
+ }
9
+ catch (error) {
10
+ return null;
11
+ }
12
+ }
@@ -0,0 +1,9 @@
1
+ // escape double-quote for cli
2
+ export function safe_quote(value) {
3
+ let result = value;
4
+ result = result.replace(RE.all_double_quote, '\\"');
5
+ return result;
6
+ }
7
+ const RE = {
8
+ all_double_quote: /"/g,
9
+ };