tree-swing 1.0.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.
- package/README.md +30 -0
- package/config.txt +2 -0
- package/core/input/keycodes.ts +8 -0
- package/core/input/menu.ts +112 -0
- package/core/input/question.ts +29 -0
- package/core/input/text.ts +8 -0
- package/core/terminal/colors.ts +15 -0
- package/core/terminal/cursor.ts +7 -0
- package/core/terminal/screen.ts +9 -0
- package/main.ts +5 -0
- package/menus/configManager/index.ts +47 -0
- package/menus/createConfig/index.ts +94 -0
- package/menus/deleteConfig/index.ts +72 -0
- package/menus/start/index.ts +134 -0
- package/navigate/index.ts +14 -0
- package/package.json +26 -0
- package/services/configService.ts +38 -0
- package/state/index.ts +9 -0
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Tree Swing
|
|
2
|
+
|
|
3
|
+
A CLI tool for git branch management. Quickly create feature branches from origin branches with configurable prefixes.
|
|
4
|
+
|
|
5
|
+
> **Requires [Bun](https://bun.sh/) installed.**
|
|
6
|
+
|
|
7
|
+
## Install globally
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# From the project directory
|
|
11
|
+
npm install -g .
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Or using `npm link` for development:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm link
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Then run from anywhere:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
tree-swing
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Uninstall
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm uninstall -g tree-swing
|
|
30
|
+
```
|
package/config.txt
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { COLORS } from "../terminal/colors";
|
|
2
|
+
import { hideCursor, showCursor } from "../terminal/cursor";
|
|
3
|
+
import { clearScreen } from "../terminal/screen";
|
|
4
|
+
import { KeyCode } from "./keycodes";
|
|
5
|
+
import { renderColor } from "./text";
|
|
6
|
+
|
|
7
|
+
interface InitMenuOptions {
|
|
8
|
+
hideCursor?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const initMenu = (options?: InitMenuOptions) => {
|
|
12
|
+
const shouldHideCursor = options?.hideCursor ?? true;
|
|
13
|
+
|
|
14
|
+
process.stdin.removeAllListeners("data");
|
|
15
|
+
|
|
16
|
+
clearScreen();
|
|
17
|
+
if (shouldHideCursor) {
|
|
18
|
+
hideCursor();
|
|
19
|
+
}
|
|
20
|
+
process.stdin.setRawMode(true);
|
|
21
|
+
process.stdin.resume();
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export enum MENU_STATE {
|
|
25
|
+
MAIN = 1,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const chooseOption = (
|
|
29
|
+
selected: number,
|
|
30
|
+
options: { id: number; label: string; value: string; isGoBack?: boolean }[],
|
|
31
|
+
props?: { title?: string },
|
|
32
|
+
) => {
|
|
33
|
+
const title = props?.title ?? "Select an option:";
|
|
34
|
+
|
|
35
|
+
process.stdout.write(
|
|
36
|
+
`${renderColor(title, [COLORS.CYAN, COLORS.BOLD])} ${renderColor(
|
|
37
|
+
"(use arrow keys, press Enter to confirm)",
|
|
38
|
+
COLORS.DIM,
|
|
39
|
+
)}\n\n`,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
options.forEach((option) => {
|
|
43
|
+
if (option.isGoBack) {
|
|
44
|
+
renderIsGoBackOption(option.label, option.id === selected);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (option.id === selected) {
|
|
49
|
+
process.stdout.write(
|
|
50
|
+
`${renderColor(" ▶ " + option.label, [COLORS.BG_GREEN, COLORS.BLACK, COLORS.BOLD])}\n`,
|
|
51
|
+
);
|
|
52
|
+
} else {
|
|
53
|
+
process.stdout.write(
|
|
54
|
+
renderColor(" " + option.label, COLORS.DIM) + "\n",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const handleUpdateOptionsMenu =
|
|
61
|
+
(
|
|
62
|
+
selectedOption: number,
|
|
63
|
+
options: {
|
|
64
|
+
id: number;
|
|
65
|
+
label: string;
|
|
66
|
+
value: string;
|
|
67
|
+
action: () => Promise<void> | void;
|
|
68
|
+
}[],
|
|
69
|
+
title?: string,
|
|
70
|
+
) =>
|
|
71
|
+
async (key: Buffer) => {
|
|
72
|
+
if (key[2] === KeyCode.DOWN_ARROW) {
|
|
73
|
+
if (selectedOption < options.length) {
|
|
74
|
+
selectedOption++;
|
|
75
|
+
} else {
|
|
76
|
+
selectedOption = 1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
clearScreen();
|
|
80
|
+
chooseOption(selectedOption, options, { title });
|
|
81
|
+
} else if (key[2] === KeyCode.UP_ARROW) {
|
|
82
|
+
if (selectedOption > 1) {
|
|
83
|
+
selectedOption--;
|
|
84
|
+
} else {
|
|
85
|
+
selectedOption = options.length;
|
|
86
|
+
}
|
|
87
|
+
clearScreen();
|
|
88
|
+
chooseOption(selectedOption, options, { title });
|
|
89
|
+
} else if (key[0] === KeyCode.ENTER) {
|
|
90
|
+
clearScreen();
|
|
91
|
+
process.stdin.removeAllListeners("data");
|
|
92
|
+
|
|
93
|
+
await options.find((option) => option.id === selectedOption)?.action();
|
|
94
|
+
} else if (key[0] === KeyCode.CTRL_C) {
|
|
95
|
+
showCursor();
|
|
96
|
+
clearScreen();
|
|
97
|
+
process.exit();
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const renderIsGoBackOption = (label: string, isSelected: boolean) => {
|
|
102
|
+
if (isSelected) {
|
|
103
|
+
process.stdout.write(
|
|
104
|
+
`${renderColor(" ← " + label, [COLORS.BG_RED, COLORS.BOLD])}${COLORS.RESET}\n`,
|
|
105
|
+
);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
process.stdout.write(
|
|
110
|
+
`${renderColor(" ← " + label, COLORS.RED)}${COLORS.RESET}\n`,
|
|
111
|
+
);
|
|
112
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { showCursor } from "../terminal/cursor";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
|
|
4
|
+
export const makeQuestion = (question: string): Promise<string> => {
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
showCursor();
|
|
7
|
+
process.stdin.removeAllListeners("data");
|
|
8
|
+
process.stdin.removeAllListeners("close");
|
|
9
|
+
process.stdin.removeAllListeners("end");
|
|
10
|
+
|
|
11
|
+
if (process.stdin.isPaused()) {
|
|
12
|
+
process.stdin.resume();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
process.stdin.setRawMode(false);
|
|
16
|
+
|
|
17
|
+
const rl = createInterface({
|
|
18
|
+
input: process.stdin,
|
|
19
|
+
output: process.stdout,
|
|
20
|
+
terminal: false,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
rl.question(`${question}\n > `, (answer) => {
|
|
24
|
+
rl.close();
|
|
25
|
+
rl.removeAllListeners();
|
|
26
|
+
resolve(answer);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { COLORS } from "../terminal/colors";
|
|
2
|
+
|
|
3
|
+
export const renderColor = (text: string, colorCode: string | string[]) => {
|
|
4
|
+
if (Array.isArray(colorCode)) {
|
|
5
|
+
return `${colorCode.join("")}${text}${COLORS.RESET}`;
|
|
6
|
+
}
|
|
7
|
+
return `${colorCode}${text}${COLORS.RESET}`;
|
|
8
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const COLORS = {
|
|
2
|
+
RESET: "\x1b[0m",
|
|
3
|
+
BOLD: "\x1b[1m",
|
|
4
|
+
DIM: "\x1b[2m",
|
|
5
|
+
CYAN: "\x1b[36m",
|
|
6
|
+
GREEN: "\x1b[32m",
|
|
7
|
+
YELLOW: "\x1b[33m",
|
|
8
|
+
BLUE: "\x1b[34m",
|
|
9
|
+
MAGENTA: "\x1b[35m",
|
|
10
|
+
RED: "\x1b[31m",
|
|
11
|
+
BG_CYAN: "\x1b[46m",
|
|
12
|
+
BG_GREEN: "\x1b[42m",
|
|
13
|
+
BG_RED: "\x1b[41m",
|
|
14
|
+
BLACK: "\x1b[30m",
|
|
15
|
+
};
|
package/main.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chooseOption,
|
|
3
|
+
handleUpdateOptionsMenu,
|
|
4
|
+
initMenu,
|
|
5
|
+
} from "../../core/input/menu";
|
|
6
|
+
import { showMenuStart } from "../start";
|
|
7
|
+
import { showCreateConfig } from "../createConfig";
|
|
8
|
+
import { showDeleteConfig } from "../deleteConfig";
|
|
9
|
+
|
|
10
|
+
const handleGoBack = () => {
|
|
11
|
+
process.stdin.removeAllListeners("data");
|
|
12
|
+
showMenuStart();
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const MENU_OPTIONS = [
|
|
16
|
+
{
|
|
17
|
+
id: 1,
|
|
18
|
+
label: "Create new config",
|
|
19
|
+
value: "create",
|
|
20
|
+
action: showCreateConfig,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: 2,
|
|
24
|
+
label: "Delete config",
|
|
25
|
+
value: "delete",
|
|
26
|
+
action: showDeleteConfig,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 3,
|
|
30
|
+
label: "Go back",
|
|
31
|
+
value: "back",
|
|
32
|
+
isGoBack: true,
|
|
33
|
+
action: handleGoBack,
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const currentOption = 1;
|
|
38
|
+
|
|
39
|
+
export const showConfigManager = () => {
|
|
40
|
+
initMenu();
|
|
41
|
+
|
|
42
|
+
chooseOption(currentOption, MENU_OPTIONS);
|
|
43
|
+
process.stdin.on(
|
|
44
|
+
"data",
|
|
45
|
+
handleUpdateOptionsMenu(currentOption, MENU_OPTIONS),
|
|
46
|
+
);
|
|
47
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { initMenu } from "../../core/input/menu";
|
|
2
|
+
import { makeQuestion } from "../../core/input/question";
|
|
3
|
+
import { renderColor } from "../../core/input/text";
|
|
4
|
+
import { COLORS } from "../../core/terminal/colors";
|
|
5
|
+
import { ConfigService } from "../../services/configService";
|
|
6
|
+
import { showConfigManager } from "../configManager";
|
|
7
|
+
|
|
8
|
+
const isValidBranchName = (name: string): boolean => {
|
|
9
|
+
return /^[a-zA-Z0-9\-_/]+$/.test(name);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const handleCreateConfig = async () => {
|
|
13
|
+
const originBranch = await makeQuestion(
|
|
14
|
+
renderColor("Origin branch name (e.g. develop): ", COLORS.CYAN),
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
if (!originBranch.trim()) {
|
|
18
|
+
process.stdout.write(
|
|
19
|
+
renderColor("\n⚠️ Origin branch name cannot be empty.\n", COLORS.RED),
|
|
20
|
+
);
|
|
21
|
+
setTimeout(() => showConfigManager(), 1000);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!isValidBranchName(originBranch.trim())) {
|
|
26
|
+
process.stdout.write(
|
|
27
|
+
renderColor(
|
|
28
|
+
"\n⚠️ Invalid branch name. Only letters, numbers, - / _ are allowed.\n",
|
|
29
|
+
COLORS.RED,
|
|
30
|
+
),
|
|
31
|
+
);
|
|
32
|
+
setTimeout(() => showConfigManager(), 1000);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
process.stdout.write("\n");
|
|
37
|
+
|
|
38
|
+
const prefix = await makeQuestion(
|
|
39
|
+
renderColor("Prefix for new branches (e.g. for-dev): ", COLORS.CYAN),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
if (!prefix.trim()) {
|
|
43
|
+
process.stdout.write(
|
|
44
|
+
renderColor("\n⚠️ Prefix cannot be empty.\n", COLORS.RED),
|
|
45
|
+
);
|
|
46
|
+
setTimeout(() => showConfigManager(), 1000);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!isValidBranchName(prefix.trim())) {
|
|
51
|
+
process.stdout.write(
|
|
52
|
+
renderColor(
|
|
53
|
+
"\n⚠️ Invalid prefix. Only letters, numbers, - / _ are allowed.\n",
|
|
54
|
+
COLORS.RED,
|
|
55
|
+
),
|
|
56
|
+
);
|
|
57
|
+
setTimeout(() => showConfigManager(), 1000);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
ConfigService.addConfig(originBranch.trim(), prefix.trim());
|
|
62
|
+
|
|
63
|
+
process.stdout.write("\n");
|
|
64
|
+
process.stdout.write(
|
|
65
|
+
renderColor("✅ Config added successfully!\n", [COLORS.BOLD, COLORS.GREEN]),
|
|
66
|
+
);
|
|
67
|
+
process.stdout.write(
|
|
68
|
+
renderColor(` ${originBranch} → ${prefix}\n\n`, COLORS.DIM),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
setTimeout(() => showConfigManager(), 1500);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const showCreateConfig = () => {
|
|
75
|
+
initMenu({ hideCursor: false });
|
|
76
|
+
|
|
77
|
+
const existingConfigs = ConfigService.getConfig();
|
|
78
|
+
|
|
79
|
+
process.stdout.write(
|
|
80
|
+
renderColor("📋 Current configurations:\n\n", [COLORS.BOLD, COLORS.CYAN]),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
existingConfigs.forEach((config) => {
|
|
84
|
+
process.stdout.write(renderColor(` • ${config.label}`, COLORS.GREEN));
|
|
85
|
+
process.stdout.write(renderColor(` → ${config.value}\n`, COLORS.DIM));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
process.stdout.write("\n");
|
|
89
|
+
process.stdout.write(
|
|
90
|
+
renderColor(" Create new config\n\n", [COLORS.BOLD, COLORS.YELLOW]),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
handleCreateConfig();
|
|
94
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chooseOption,
|
|
3
|
+
handleUpdateOptionsMenu,
|
|
4
|
+
initMenu,
|
|
5
|
+
} from "../../core/input/menu";
|
|
6
|
+
import { renderColor } from "../../core/input/text";
|
|
7
|
+
import { COLORS } from "../../core/terminal/colors";
|
|
8
|
+
import { clearScreen } from "../../core/terminal/screen";
|
|
9
|
+
import { ConfigService } from "../../services/configService";
|
|
10
|
+
import { showConfigManager } from "../configManager";
|
|
11
|
+
|
|
12
|
+
export const showDeleteConfig = () => {
|
|
13
|
+
initMenu();
|
|
14
|
+
|
|
15
|
+
const existingConfigs = ConfigService.getConfig();
|
|
16
|
+
|
|
17
|
+
if (existingConfigs.length === 0) {
|
|
18
|
+
process.stdout.write(
|
|
19
|
+
renderColor("⚠️ No configurations to delete.\n", COLORS.YELLOW),
|
|
20
|
+
);
|
|
21
|
+
setTimeout(() => showConfigManager(), 1500);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const menuOptions = [
|
|
26
|
+
...existingConfigs.map((config) => ({
|
|
27
|
+
id: config.id,
|
|
28
|
+
label: `${config.label} → ${config.value}`,
|
|
29
|
+
value: config.label,
|
|
30
|
+
action: handleDelete(config.label),
|
|
31
|
+
})),
|
|
32
|
+
{
|
|
33
|
+
id: existingConfigs.length + 1,
|
|
34
|
+
label: "Go back",
|
|
35
|
+
value: "back",
|
|
36
|
+
isGoBack: true,
|
|
37
|
+
action: () => {
|
|
38
|
+
process.stdin.removeAllListeners("data");
|
|
39
|
+
showConfigManager();
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const currentOption = 1;
|
|
45
|
+
|
|
46
|
+
const title = renderColor("🗑️ Delete config", [COLORS.BOLD, COLORS.RED]);
|
|
47
|
+
|
|
48
|
+
chooseOption(currentOption, menuOptions, { title });
|
|
49
|
+
process.stdin.on(
|
|
50
|
+
"data",
|
|
51
|
+
handleUpdateOptionsMenu(currentOption, menuOptions, title),
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const handleDelete = (originBranch: string) => () => {
|
|
56
|
+
process.stdin.removeAllListeners("data");
|
|
57
|
+
clearScreen();
|
|
58
|
+
|
|
59
|
+
ConfigService.deleteConfig(originBranch);
|
|
60
|
+
|
|
61
|
+
process.stdout.write(
|
|
62
|
+
renderColor("✅ Config deleted successfully!\n", [
|
|
63
|
+
COLORS.BOLD,
|
|
64
|
+
COLORS.GREEN,
|
|
65
|
+
]),
|
|
66
|
+
);
|
|
67
|
+
process.stdout.write(
|
|
68
|
+
renderColor(` Removed: ${originBranch}\n\n`, COLORS.DIM),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
setTimeout(() => showConfigManager(), 1500);
|
|
72
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chooseOption,
|
|
3
|
+
handleUpdateOptionsMenu,
|
|
4
|
+
initMenu,
|
|
5
|
+
} from "../../core/input/menu";
|
|
6
|
+
import { $ } from "bun";
|
|
7
|
+
import { showCursor } from "../../core/terminal/cursor";
|
|
8
|
+
import { renderColor } from "../../core/input/text";
|
|
9
|
+
import { COLORS } from "../../core/terminal/colors";
|
|
10
|
+
import { ConfigService } from "../../services/configService";
|
|
11
|
+
import { showConfigManager } from "../configManager";
|
|
12
|
+
|
|
13
|
+
const currentOption = 1;
|
|
14
|
+
|
|
15
|
+
export const showMenuStart = () => {
|
|
16
|
+
initMenu();
|
|
17
|
+
|
|
18
|
+
const menuOptions = ConfigService.getConfig();
|
|
19
|
+
const otherOption = {
|
|
20
|
+
id: menuOptions.length + 1,
|
|
21
|
+
label: "other",
|
|
22
|
+
value: "other",
|
|
23
|
+
isGoBack: true,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const options = [
|
|
27
|
+
...menuOptions.map((option) => ({
|
|
28
|
+
...option,
|
|
29
|
+
action: handleCreateNewBranch(option.value, option.label),
|
|
30
|
+
})),
|
|
31
|
+
{ ...otherOption, action: handleOther },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
chooseOption(currentOption, [...menuOptions, otherOption]);
|
|
35
|
+
process.stdin.on("data", handleUpdateOptionsMenu(currentOption, options));
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const handleOther = () => {
|
|
39
|
+
process.stdin.removeAllListeners("data");
|
|
40
|
+
showConfigManager();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const handleCreateNewBranch = (prefix: string, branch: string) => async () => {
|
|
44
|
+
const currentBranch = (
|
|
45
|
+
await $`git rev-parse --abbrev-ref HEAD`.text()
|
|
46
|
+
).trim();
|
|
47
|
+
|
|
48
|
+
if (currentBranch.includes(prefix)) {
|
|
49
|
+
process.stdout.write(renderColor(`⚠️ The current branch:`, COLORS.YELLOW));
|
|
50
|
+
process.stdout.write(
|
|
51
|
+
renderColor(` ${currentBranch}\n`, [COLORS.BOLD, COLORS.MAGENTA]),
|
|
52
|
+
);
|
|
53
|
+
process.stdout.write(
|
|
54
|
+
renderColor(`Already includes the prefix`, COLORS.YELLOW),
|
|
55
|
+
);
|
|
56
|
+
process.stdout.write(
|
|
57
|
+
renderColor(` ${prefix}\n\n`, [COLORS.BOLD, COLORS.MAGENTA]),
|
|
58
|
+
);
|
|
59
|
+
process.stdout.write(
|
|
60
|
+
renderColor(
|
|
61
|
+
`❌ Please switch to a branch that doesn't include the prefix "${prefix}" before running this tool.\n`,
|
|
62
|
+
COLORS.RED,
|
|
63
|
+
),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
showCursor();
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
process.stdout.write(
|
|
71
|
+
renderColor("⏳ Fetching and preparing...\n", COLORS.CYAN),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
await $`git fetch origin "${branch}:${branch}" --force`.quiet();
|
|
76
|
+
} catch {
|
|
77
|
+
process.stdout.write(
|
|
78
|
+
renderColor(
|
|
79
|
+
`⚠️ Failed to fetch branch "${branch}". Please check if it exists on the remote.\n`,
|
|
80
|
+
COLORS.RED,
|
|
81
|
+
),
|
|
82
|
+
);
|
|
83
|
+
showCursor();
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const newBranch = `${prefix}/${currentBranch}`;
|
|
88
|
+
process.stdout.write(renderColor(`📌 New branch: \n`, COLORS.GREEN));
|
|
89
|
+
process.stdout.write(renderColor(` ${newBranch}\n\n`, COLORS.DIM));
|
|
90
|
+
|
|
91
|
+
process.stdout.write(
|
|
92
|
+
renderColor("🗑️ Cleaning up old branch...\n", COLORS.GREEN),
|
|
93
|
+
);
|
|
94
|
+
try {
|
|
95
|
+
await $`git branch -D "${newBranch}"`.quiet();
|
|
96
|
+
process.stdout.write(
|
|
97
|
+
renderColor(`✅ Old branch "${newBranch}" deleted.\n\n`, COLORS.DIM),
|
|
98
|
+
);
|
|
99
|
+
} catch {
|
|
100
|
+
process.stdout.write(
|
|
101
|
+
renderColor(`⚠️ No existing branch to delete.\n\n`, COLORS.DIM),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
process.stdout.write(
|
|
106
|
+
renderColor("🔀 Switching to new branch...\n", COLORS.GREEN),
|
|
107
|
+
);
|
|
108
|
+
await $`git switch -c "${newBranch}"`.quiet();
|
|
109
|
+
process.stdout.write(
|
|
110
|
+
renderColor(`✅ Switched to new branch "${newBranch}".\n\n`, COLORS.DIM),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
process.stdout.write(
|
|
114
|
+
renderColor(`🔗 Merging origin/${branch}...\n`, COLORS.GREEN),
|
|
115
|
+
);
|
|
116
|
+
try {
|
|
117
|
+
await $`git merge "origin/${branch}"`.quiet();
|
|
118
|
+
process.stdout.write(
|
|
119
|
+
renderColor("✅ Success!\n", [COLORS.BOLD, COLORS.GREEN]),
|
|
120
|
+
);
|
|
121
|
+
} catch {
|
|
122
|
+
process.stdout.write(
|
|
123
|
+
renderColor(
|
|
124
|
+
`⚠️ Merge conflicts detected. Please resolve them manually.\n`,
|
|
125
|
+
COLORS.RED,
|
|
126
|
+
),
|
|
127
|
+
);
|
|
128
|
+
showCursor();
|
|
129
|
+
process.exit(0);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
showCursor();
|
|
133
|
+
process.exit(0);
|
|
134
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { MENU_STATE } from "../core/input/menu";
|
|
2
|
+
import { hideCursor } from "../core/terminal/cursor";
|
|
3
|
+
import { setMenuState } from "../state";
|
|
4
|
+
|
|
5
|
+
const menuLoaders = {
|
|
6
|
+
[MENU_STATE.MAIN]: () =>
|
|
7
|
+
import("../menus/start").then((mod) => mod.showMenuStart()),
|
|
8
|
+
} as const;
|
|
9
|
+
|
|
10
|
+
export const navigateToMenu = (menu: MENU_STATE) => {
|
|
11
|
+
setMenuState(menu);
|
|
12
|
+
hideCursor();
|
|
13
|
+
menuLoaders[menu]();
|
|
14
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tree-swing",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Git branch management CLI tool",
|
|
5
|
+
"module": "main.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"tree-swing": "./main.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"main.ts",
|
|
12
|
+
"core/**",
|
|
13
|
+
"menus/**",
|
|
14
|
+
"navigate/**",
|
|
15
|
+
"state/**",
|
|
16
|
+
"services/**",
|
|
17
|
+
"config.txt"
|
|
18
|
+
],
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/bun": "latest"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"typescript": "^5"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {}
|
|
26
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { appendFileSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join, dirname } from "node:path";
|
|
3
|
+
|
|
4
|
+
const CONFIG_FILE = join(dirname(import.meta.dir), "config.txt");
|
|
5
|
+
|
|
6
|
+
interface Config {
|
|
7
|
+
label: string;
|
|
8
|
+
value: string;
|
|
9
|
+
id: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const ConfigService = {
|
|
13
|
+
getConfig: () => {
|
|
14
|
+
const fileContent = readFileSync(CONFIG_FILE, "utf-8")
|
|
15
|
+
.split("\n")
|
|
16
|
+
.filter((line) => line.trim() !== "");
|
|
17
|
+
|
|
18
|
+
const config = fileContent.map((line, idx) => {
|
|
19
|
+
const [originBranch, prefix] = line.split("=").map((part) => part.trim());
|
|
20
|
+
return { label: originBranch, value: prefix, id: idx + 1 };
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return config as Config[];
|
|
24
|
+
},
|
|
25
|
+
addConfig: (originBranch: string, prefix: string) => {
|
|
26
|
+
const newConfigLine = `${originBranch}=${prefix}\n`;
|
|
27
|
+
appendFileSync(CONFIG_FILE, newConfigLine);
|
|
28
|
+
},
|
|
29
|
+
deleteConfig: (originBranch: string) => {
|
|
30
|
+
const fileContent = readFileSync(CONFIG_FILE, "utf-8")
|
|
31
|
+
.split("\n")
|
|
32
|
+
.filter(
|
|
33
|
+
(line) => line.trim() !== "" && !line.startsWith(originBranch + "="),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
writeFileSync(CONFIG_FILE, fileContent.join("\n") + "\n");
|
|
37
|
+
},
|
|
38
|
+
};
|