@manthank/mgit 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/LICENSE +21 -0
- package/README.md +96 -0
- package/dist/app.js +468 -0
- package/dist/catalog.js +585 -0
- package/dist/cli.js +48 -0
- package/dist/components/Footer.js +38 -0
- package/dist/components/Header.js +46 -0
- package/dist/components/Menu.js +56 -0
- package/dist/config.js +40 -0
- package/dist/git.js +263 -0
- package/dist/hooks.js +23 -0
- package/dist/recipes.js +96 -0
- package/dist/themes.js +89 -0
- package/dist/views/ActionRunner.js +175 -0
- package/dist/views/BranchView.js +84 -0
- package/dist/views/ConfigView.js +60 -0
- package/dist/views/ContribView.js +80 -0
- package/dist/views/GitignoreView.js +85 -0
- package/dist/views/LogGraphView.js +116 -0
- package/dist/views/RecipeView.js +153 -0
- package/dist/views/SettingsView.js +66 -0
- package/dist/views/SetupWizard.js +120 -0
- package/dist/views/StatusView.js +121 -0
- package/package.json +52 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// Drives a single catalog action: collect inputs, preview the git command
|
|
2
|
+
// (teaching moment), confirm, run it, then show the result.
|
|
3
|
+
import React, { useState, useEffect } from 'react';
|
|
4
|
+
import { Box, Text, useInput } from 'ink';
|
|
5
|
+
import TextInput from 'ink-text-input';
|
|
6
|
+
import Spinner from 'ink-spinner';
|
|
7
|
+
import { git } from '../git.js';
|
|
8
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
9
|
+
export default function ActionRunner({
|
|
10
|
+
action,
|
|
11
|
+
theme,
|
|
12
|
+
config
|
|
13
|
+
}) {
|
|
14
|
+
const inputs = action.inputs || [];
|
|
15
|
+
const [values, setValues] = useState(() => {
|
|
16
|
+
const init = {};
|
|
17
|
+
for (const inp of inputs) init[inp.name] = inp.default || '';
|
|
18
|
+
return init;
|
|
19
|
+
});
|
|
20
|
+
const [idx, setIdx] = useState(0);
|
|
21
|
+
const [phase, setPhase] = useState(inputs.length ? 'input' : 'preview');
|
|
22
|
+
const [result, setResult] = useState(null);
|
|
23
|
+
const args = phase !== 'input' ? safeBuild(action, values) : [];
|
|
24
|
+
const commandStr = 'git ' + (args || []).map(a => /\s/.test(a) ? `"${a}"` : a).join(' ');
|
|
25
|
+
async function execute() {
|
|
26
|
+
setPhase('running');
|
|
27
|
+
const r = await git(args);
|
|
28
|
+
setResult(r);
|
|
29
|
+
setPhase('result');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Preview/confirm key handling.
|
|
33
|
+
useInput((input, key) => {
|
|
34
|
+
if (phase === 'preview') {
|
|
35
|
+
if (action.danger && config.confirmDanger) {
|
|
36
|
+
if (input.toLowerCase() === 'y') execute();
|
|
37
|
+
} else if (key.return) {
|
|
38
|
+
execute();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
if (phase === 'input') {
|
|
43
|
+
const inp = inputs[idx];
|
|
44
|
+
return /*#__PURE__*/_jsxs(Box, {
|
|
45
|
+
flexDirection: "column",
|
|
46
|
+
children: [config.showHints && /*#__PURE__*/_jsx(Text, {
|
|
47
|
+
color: theme.muted,
|
|
48
|
+
children: action.hint
|
|
49
|
+
}), /*#__PURE__*/_jsx(Box, {
|
|
50
|
+
marginTop: 1,
|
|
51
|
+
flexDirection: "column",
|
|
52
|
+
children: inputs.map((f, i) => /*#__PURE__*/_jsxs(Box, {
|
|
53
|
+
children: [/*#__PURE__*/_jsxs(Text, {
|
|
54
|
+
color: i === idx ? theme.accent : theme.muted,
|
|
55
|
+
bold: i === idx,
|
|
56
|
+
children: [f.label.padEnd(20), ' ']
|
|
57
|
+
}), i === idx ? /*#__PURE__*/_jsx(TextInput, {
|
|
58
|
+
value: values[f.name],
|
|
59
|
+
placeholder: f.placeholder || '',
|
|
60
|
+
onChange: val => setValues(v => ({
|
|
61
|
+
...v,
|
|
62
|
+
[f.name]: val
|
|
63
|
+
})),
|
|
64
|
+
onSubmit: () => {
|
|
65
|
+
if (!f.optional && !values[f.name].trim()) return;
|
|
66
|
+
if (idx < inputs.length - 1) setIdx(idx + 1);else setPhase('preview');
|
|
67
|
+
}
|
|
68
|
+
}) : /*#__PURE__*/_jsx(Text, {
|
|
69
|
+
color: "white",
|
|
70
|
+
children: values[f.name] || /*#__PURE__*/_jsx(Text, {
|
|
71
|
+
color: theme.muted,
|
|
72
|
+
children: "(blank)"
|
|
73
|
+
})
|
|
74
|
+
})]
|
|
75
|
+
}, f.name))
|
|
76
|
+
}), /*#__PURE__*/_jsx(Box, {
|
|
77
|
+
marginTop: 1,
|
|
78
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
79
|
+
color: theme.muted,
|
|
80
|
+
children: "Enter \u2192 next \xB7 fields marked optional can be left blank"
|
|
81
|
+
})
|
|
82
|
+
})]
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return /*#__PURE__*/_jsxs(Box, {
|
|
86
|
+
flexDirection: "column",
|
|
87
|
+
children: [config.showHints && /*#__PURE__*/_jsx(Text, {
|
|
88
|
+
color: theme.muted,
|
|
89
|
+
children: action.hint
|
|
90
|
+
}), /*#__PURE__*/_jsxs(Box, {
|
|
91
|
+
marginTop: 1,
|
|
92
|
+
borderStyle: "round",
|
|
93
|
+
borderColor: action.danger ? theme.danger : theme.accent,
|
|
94
|
+
paddingX: 1,
|
|
95
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
96
|
+
color: theme.muted,
|
|
97
|
+
children: "$ "
|
|
98
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
99
|
+
color: action.danger ? theme.danger : theme.secondary,
|
|
100
|
+
bold: true,
|
|
101
|
+
children: commandStr
|
|
102
|
+
})]
|
|
103
|
+
}), phase === 'preview' && /*#__PURE__*/_jsx(Box, {
|
|
104
|
+
marginTop: 1,
|
|
105
|
+
flexDirection: "column",
|
|
106
|
+
children: action.danger && config.confirmDanger ? /*#__PURE__*/_jsx(Text, {
|
|
107
|
+
color: theme.danger,
|
|
108
|
+
bold: true,
|
|
109
|
+
children: "\u26A0 This can change or delete work. Press Y to run, Esc to cancel."
|
|
110
|
+
}) : /*#__PURE__*/_jsx(Text, {
|
|
111
|
+
color: theme.accent,
|
|
112
|
+
children: "Press Enter to run \xB7 Esc to cancel"
|
|
113
|
+
})
|
|
114
|
+
}), phase === 'running' && /*#__PURE__*/_jsx(Box, {
|
|
115
|
+
marginTop: 1,
|
|
116
|
+
children: /*#__PURE__*/_jsxs(Text, {
|
|
117
|
+
color: theme.accent,
|
|
118
|
+
children: [/*#__PURE__*/_jsx(Spinner, {
|
|
119
|
+
type: "dots"
|
|
120
|
+
}), " Running\u2026"]
|
|
121
|
+
})
|
|
122
|
+
}), phase === 'result' && result && /*#__PURE__*/_jsxs(Box, {
|
|
123
|
+
flexDirection: "column",
|
|
124
|
+
marginTop: 1,
|
|
125
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
126
|
+
color: result.ok ? theme.success : theme.danger,
|
|
127
|
+
bold: true,
|
|
128
|
+
children: result.ok ? '✓ Success' : `✗ Failed (exit ${result.code})`
|
|
129
|
+
}), /*#__PURE__*/_jsx(Output, {
|
|
130
|
+
text: result.stdout,
|
|
131
|
+
color: "white",
|
|
132
|
+
theme: theme
|
|
133
|
+
}), /*#__PURE__*/_jsx(Output, {
|
|
134
|
+
text: result.stderr,
|
|
135
|
+
color: result.ok ? theme.muted : theme.danger,
|
|
136
|
+
theme: theme
|
|
137
|
+
}), result.ok && !result.stdout && !result.stderr && /*#__PURE__*/_jsx(Text, {
|
|
138
|
+
color: theme.muted,
|
|
139
|
+
children: "(no output \u2014 command completed quietly)"
|
|
140
|
+
}), /*#__PURE__*/_jsx(Box, {
|
|
141
|
+
marginTop: 1,
|
|
142
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
143
|
+
color: theme.muted,
|
|
144
|
+
children: "Press Esc / \u2190 to go back to the menu."
|
|
145
|
+
})
|
|
146
|
+
})]
|
|
147
|
+
})]
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
function Output({
|
|
151
|
+
text,
|
|
152
|
+
color,
|
|
153
|
+
theme
|
|
154
|
+
}) {
|
|
155
|
+
if (!text) return null;
|
|
156
|
+
const lines = text.split('\n');
|
|
157
|
+
const shown = lines.slice(0, 200);
|
|
158
|
+
return /*#__PURE__*/_jsxs(Box, {
|
|
159
|
+
flexDirection: "column",
|
|
160
|
+
children: [shown.map((l, i) => /*#__PURE__*/_jsx(Text, {
|
|
161
|
+
color: color,
|
|
162
|
+
children: l || ' '
|
|
163
|
+
}, i)), lines.length > shown.length && /*#__PURE__*/_jsxs(Text, {
|
|
164
|
+
color: theme.muted,
|
|
165
|
+
children: ["\u2026", lines.length - shown.length, " more lines"]
|
|
166
|
+
})]
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
function safeBuild(action, values) {
|
|
170
|
+
try {
|
|
171
|
+
return action.build ? action.build(values) : [];
|
|
172
|
+
} catch {
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Visual branch map: each branch as a coloured node with its latest commit.
|
|
2
|
+
import React, { useState, useEffect } from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import Spinner from 'ink-spinner';
|
|
5
|
+
import { getBranches } from '../git.js';
|
|
6
|
+
import { useFrame } from '../hooks.js';
|
|
7
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
8
|
+
export default function BranchView({
|
|
9
|
+
theme,
|
|
10
|
+
animations
|
|
11
|
+
}) {
|
|
12
|
+
const [state, setState] = useState({
|
|
13
|
+
loading: true
|
|
14
|
+
});
|
|
15
|
+
const frame = useFrame(animations, 4);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
let alive = true;
|
|
18
|
+
getBranches().then(s => alive && setState({
|
|
19
|
+
loading: false,
|
|
20
|
+
...s
|
|
21
|
+
}));
|
|
22
|
+
return () => {
|
|
23
|
+
alive = false;
|
|
24
|
+
};
|
|
25
|
+
}, []);
|
|
26
|
+
if (state.loading) {
|
|
27
|
+
return /*#__PURE__*/_jsxs(Text, {
|
|
28
|
+
color: theme.accent,
|
|
29
|
+
children: [/*#__PURE__*/_jsx(Spinner, {
|
|
30
|
+
type: "dots"
|
|
31
|
+
}), " Reading branches\u2026"]
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
if (!state.ok) return /*#__PURE__*/_jsxs(Text, {
|
|
35
|
+
color: theme.danger,
|
|
36
|
+
children: ["Error: ", state.error]
|
|
37
|
+
});
|
|
38
|
+
if (state.branches.length === 0) {
|
|
39
|
+
return /*#__PURE__*/_jsx(Text, {
|
|
40
|
+
color: theme.muted,
|
|
41
|
+
children: "No branches yet \u2014 make your first commit!"
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
const colors = theme.branchColors;
|
|
45
|
+
return /*#__PURE__*/_jsxs(Box, {
|
|
46
|
+
flexDirection: "column",
|
|
47
|
+
children: [/*#__PURE__*/_jsxs(Text, {
|
|
48
|
+
color: theme.muted,
|
|
49
|
+
children: [state.branches.length, " local branch", state.branches.length === 1 ? '' : 'es', " \u2014 the \u2605 marks where you are."]
|
|
50
|
+
}), /*#__PURE__*/_jsx(Box, {
|
|
51
|
+
marginTop: 1,
|
|
52
|
+
flexDirection: "column",
|
|
53
|
+
children: state.branches.map((b, i) => {
|
|
54
|
+
const color = colors[i % colors.length];
|
|
55
|
+
const isTop = i === 0;
|
|
56
|
+
const isBottom = i === state.branches.length - 1;
|
|
57
|
+
const connector = isTop ? '┌' : isBottom ? '└' : '├';
|
|
58
|
+
const node = b.current && animations ? frame % 2 ? '◉' : '●' : b.current ? '◉' : '○';
|
|
59
|
+
return /*#__PURE__*/_jsxs(Box, {
|
|
60
|
+
children: [/*#__PURE__*/_jsxs(Text, {
|
|
61
|
+
color: color,
|
|
62
|
+
children: [connector, "\u2500", node, "\u2500 "]
|
|
63
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
64
|
+
color: b.current ? theme.accent : 'white',
|
|
65
|
+
bold: b.current,
|
|
66
|
+
children: b.name.padEnd(24)
|
|
67
|
+
}), /*#__PURE__*/_jsxs(Text, {
|
|
68
|
+
color: theme.muted,
|
|
69
|
+
children: [" ", b.sha, " "]
|
|
70
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
71
|
+
color: theme.muted,
|
|
72
|
+
children: truncate(b.subject, 40)
|
|
73
|
+
}), b.current && /*#__PURE__*/_jsx(Text, {
|
|
74
|
+
color: theme.accent,
|
|
75
|
+
children: " \u2605 HEAD"
|
|
76
|
+
})]
|
|
77
|
+
}, b.name);
|
|
78
|
+
})
|
|
79
|
+
})]
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function truncate(s, n) {
|
|
83
|
+
return s.length > n ? s.slice(0, n - 1) + '…' : s;
|
|
84
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Read-only view of `git config --list`, grouped by section.
|
|
2
|
+
import React, { useState, useEffect } from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import Spinner from 'ink-spinner';
|
|
5
|
+
import { getConfigList } from '../git.js';
|
|
6
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
7
|
+
export default function ConfigView({
|
|
8
|
+
theme
|
|
9
|
+
}) {
|
|
10
|
+
const [state, setState] = useState({
|
|
11
|
+
loading: true
|
|
12
|
+
});
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
let alive = true;
|
|
15
|
+
getConfigList().then(s => alive && setState({
|
|
16
|
+
loading: false,
|
|
17
|
+
...s
|
|
18
|
+
}));
|
|
19
|
+
return () => {
|
|
20
|
+
alive = false;
|
|
21
|
+
};
|
|
22
|
+
}, []);
|
|
23
|
+
if (state.loading) {
|
|
24
|
+
return /*#__PURE__*/_jsxs(Text, {
|
|
25
|
+
color: theme.accent,
|
|
26
|
+
children: [/*#__PURE__*/_jsx(Spinner, {
|
|
27
|
+
type: "dots"
|
|
28
|
+
}), " Reading git config\u2026"]
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (!state.ok) return /*#__PURE__*/_jsxs(Text, {
|
|
32
|
+
color: theme.danger,
|
|
33
|
+
children: ["Error: ", state.error]
|
|
34
|
+
});
|
|
35
|
+
return /*#__PURE__*/_jsxs(Box, {
|
|
36
|
+
flexDirection: "column",
|
|
37
|
+
children: [/*#__PURE__*/_jsxs(Text, {
|
|
38
|
+
color: theme.muted,
|
|
39
|
+
children: [state.entries.length, " settings currently active"]
|
|
40
|
+
}), /*#__PURE__*/_jsxs(Box, {
|
|
41
|
+
marginTop: 1,
|
|
42
|
+
flexDirection: "column",
|
|
43
|
+
children: [state.entries.slice(0, 40).map((e, i) => /*#__PURE__*/_jsxs(Text, {
|
|
44
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
45
|
+
color: theme.secondary,
|
|
46
|
+
children: e.key
|
|
47
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
48
|
+
color: theme.muted,
|
|
49
|
+
children: " = "
|
|
50
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
51
|
+
color: "white",
|
|
52
|
+
children: e.value
|
|
53
|
+
})]
|
|
54
|
+
}, i)), state.entries.length > 40 && /*#__PURE__*/_jsxs(Text, {
|
|
55
|
+
color: theme.muted,
|
|
56
|
+
children: ["\u2026and ", state.entries.length - 40, " more"]
|
|
57
|
+
})]
|
|
58
|
+
})]
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Horizontal bar chart of commits per author, with an animated fill sweep.
|
|
2
|
+
import React, { useState, useEffect } from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import Spinner from 'ink-spinner';
|
|
5
|
+
import { getShortstat } from '../git.js';
|
|
6
|
+
import { useFrame } from '../hooks.js';
|
|
7
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
8
|
+
export default function ContribView({
|
|
9
|
+
theme,
|
|
10
|
+
animations
|
|
11
|
+
}) {
|
|
12
|
+
const [state, setState] = useState({
|
|
13
|
+
loading: true
|
|
14
|
+
});
|
|
15
|
+
const frame = useFrame(animations, 20);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
let alive = true;
|
|
18
|
+
getShortstat().then(s => alive && setState({
|
|
19
|
+
loading: false,
|
|
20
|
+
...s
|
|
21
|
+
}));
|
|
22
|
+
return () => {
|
|
23
|
+
alive = false;
|
|
24
|
+
};
|
|
25
|
+
}, []);
|
|
26
|
+
if (state.loading) {
|
|
27
|
+
return /*#__PURE__*/_jsxs(Text, {
|
|
28
|
+
color: theme.accent,
|
|
29
|
+
children: [/*#__PURE__*/_jsx(Spinner, {
|
|
30
|
+
type: "dots"
|
|
31
|
+
}), " Counting contributions\u2026"]
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
if (!state.ok) return /*#__PURE__*/_jsxs(Text, {
|
|
35
|
+
color: theme.danger,
|
|
36
|
+
children: ["Error: ", state.error]
|
|
37
|
+
});
|
|
38
|
+
if (state.authors.length === 0) {
|
|
39
|
+
return /*#__PURE__*/_jsx(Text, {
|
|
40
|
+
color: theme.muted,
|
|
41
|
+
children: "No commits yet \u2014 nothing to chart."
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
const max = Math.max(...state.authors.map(a => a.count));
|
|
45
|
+
const width = 30;
|
|
46
|
+
const colors = theme.branchColors;
|
|
47
|
+
// Animate a "grow-in" the first time, capped at full width.
|
|
48
|
+
const grow = animations ? Math.min(1, frame / 8) : 1;
|
|
49
|
+
return /*#__PURE__*/_jsxs(Box, {
|
|
50
|
+
flexDirection: "column",
|
|
51
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
52
|
+
color: theme.muted,
|
|
53
|
+
children: "Commits per author (all branches)"
|
|
54
|
+
}), /*#__PURE__*/_jsx(Box, {
|
|
55
|
+
marginTop: 1,
|
|
56
|
+
flexDirection: "column",
|
|
57
|
+
children: state.authors.slice(0, 12).map((a, i) => {
|
|
58
|
+
const full = Math.round(a.count / max * width * grow);
|
|
59
|
+
const color = colors[i % colors.length];
|
|
60
|
+
const bar = '█'.repeat(full) + '░'.repeat(width - full);
|
|
61
|
+
return /*#__PURE__*/_jsxs(Box, {
|
|
62
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
63
|
+
color: "white",
|
|
64
|
+
children: truncate(a.name, 18).padEnd(19)
|
|
65
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
66
|
+
color: color,
|
|
67
|
+
children: bar
|
|
68
|
+
}), /*#__PURE__*/_jsxs(Text, {
|
|
69
|
+
color: theme.accent,
|
|
70
|
+
bold: true,
|
|
71
|
+
children: [' ', a.count]
|
|
72
|
+
})]
|
|
73
|
+
}, a.name);
|
|
74
|
+
})
|
|
75
|
+
})]
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function truncate(s, n) {
|
|
79
|
+
return s.length > n ? s.slice(0, n - 1) + '…' : s;
|
|
80
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Writes a sensible starter .gitignore into the current folder (with confirm).
|
|
2
|
+
import React, { useState, useEffect } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
7
|
+
const TEMPLATE = `# Dependencies
|
|
8
|
+
node_modules/
|
|
9
|
+
|
|
10
|
+
# Build output
|
|
11
|
+
build/
|
|
12
|
+
dist/
|
|
13
|
+
|
|
14
|
+
# Logs
|
|
15
|
+
*.log
|
|
16
|
+
|
|
17
|
+
# Environment / secrets
|
|
18
|
+
.env
|
|
19
|
+
.env.local
|
|
20
|
+
|
|
21
|
+
# Editor
|
|
22
|
+
.vscode/
|
|
23
|
+
.idea/
|
|
24
|
+
.DS_Store
|
|
25
|
+
`;
|
|
26
|
+
export default function GitignoreView({
|
|
27
|
+
theme
|
|
28
|
+
}) {
|
|
29
|
+
const path = join(process.cwd(), '.gitignore');
|
|
30
|
+
const [exists] = useState(() => existsSync(path));
|
|
31
|
+
const [phase, setPhase] = useState('confirm'); // confirm | done | error
|
|
32
|
+
const [error, setError] = useState('');
|
|
33
|
+
function write() {
|
|
34
|
+
try {
|
|
35
|
+
writeFileSync(path, TEMPLATE, 'utf8');
|
|
36
|
+
setPhase('done');
|
|
37
|
+
} catch (e) {
|
|
38
|
+
setError(e.message);
|
|
39
|
+
setPhase('error');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
useInput((input, key) => {
|
|
43
|
+
if (phase !== 'confirm') return;
|
|
44
|
+
if (input.toLowerCase() === 'y' || key.return) write();
|
|
45
|
+
});
|
|
46
|
+
return /*#__PURE__*/_jsxs(Box, {
|
|
47
|
+
flexDirection: "column",
|
|
48
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
49
|
+
color: theme.muted,
|
|
50
|
+
children: "This will create a starter .gitignore in:"
|
|
51
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
52
|
+
color: theme.secondary,
|
|
53
|
+
children: path
|
|
54
|
+
}), /*#__PURE__*/_jsx(Box, {
|
|
55
|
+
marginTop: 1,
|
|
56
|
+
flexDirection: "column",
|
|
57
|
+
borderStyle: "round",
|
|
58
|
+
borderColor: theme.muted,
|
|
59
|
+
paddingX: 1,
|
|
60
|
+
children: TEMPLATE.trimEnd().split('\n').map((l, i) => /*#__PURE__*/_jsx(Text, {
|
|
61
|
+
color: l.startsWith('#') ? theme.muted : 'white',
|
|
62
|
+
children: l
|
|
63
|
+
}, i))
|
|
64
|
+
}), phase === 'confirm' && /*#__PURE__*/_jsx(Box, {
|
|
65
|
+
marginTop: 1,
|
|
66
|
+
children: exists ? /*#__PURE__*/_jsx(Text, {
|
|
67
|
+
color: theme.danger,
|
|
68
|
+
children: "\u26A0 .gitignore already exists \u2014 press Y / Enter to OVERWRITE, Esc to cancel."
|
|
69
|
+
}) : /*#__PURE__*/_jsx(Text, {
|
|
70
|
+
color: theme.accent,
|
|
71
|
+
children: "Press Y / Enter to create it, Esc to cancel."
|
|
72
|
+
})
|
|
73
|
+
}), phase === 'done' && /*#__PURE__*/_jsx(Box, {
|
|
74
|
+
marginTop: 1,
|
|
75
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
76
|
+
color: theme.success,
|
|
77
|
+
bold: true,
|
|
78
|
+
children: "\u2713 .gitignore written. Press Esc / \u2190 to go back."
|
|
79
|
+
})
|
|
80
|
+
}), phase === 'error' && /*#__PURE__*/_jsxs(Text, {
|
|
81
|
+
color: theme.danger,
|
|
82
|
+
children: ["\u2717 Could not write file: ", error]
|
|
83
|
+
})]
|
|
84
|
+
});
|
|
85
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// A stylised, colourful commit graph. Renders commits top-to-bottom with a
|
|
2
|
+
// travelling pulse and ref badges (branches/tags) highlighted.
|
|
3
|
+
import React, { useState, useEffect } from 'react';
|
|
4
|
+
import { Box, Text } from 'ink';
|
|
5
|
+
import Spinner from 'ink-spinner';
|
|
6
|
+
import { getLog } from '../git.js';
|
|
7
|
+
import { useFrame } from '../hooks.js';
|
|
8
|
+
import { jsxs as _jsxs, Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
9
|
+
function RefBadges({
|
|
10
|
+
refs,
|
|
11
|
+
theme
|
|
12
|
+
}) {
|
|
13
|
+
if (!refs) return null;
|
|
14
|
+
const parts = refs.split(',').map(s => s.trim()).filter(Boolean);
|
|
15
|
+
return /*#__PURE__*/_jsx(_Fragment, {
|
|
16
|
+
children: parts.map((r, i) => {
|
|
17
|
+
const isHead = r.includes('HEAD');
|
|
18
|
+
const isTag = r.startsWith('tag:');
|
|
19
|
+
const color = isHead ? theme.accent : isTag ? theme.warn : theme.success;
|
|
20
|
+
const text = r.replace('tag:', '🏷 ').replace('HEAD ->', '➤');
|
|
21
|
+
return /*#__PURE__*/_jsxs(Text, {
|
|
22
|
+
color: color,
|
|
23
|
+
bold: isHead,
|
|
24
|
+
children: [' ', /*#__PURE__*/_jsxs(Text, {
|
|
25
|
+
backgroundColor: undefined,
|
|
26
|
+
children: ["[", text, "]"]
|
|
27
|
+
})]
|
|
28
|
+
}, i);
|
|
29
|
+
})
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export default function LogGraphView({
|
|
33
|
+
theme,
|
|
34
|
+
animations
|
|
35
|
+
}) {
|
|
36
|
+
const [state, setState] = useState({
|
|
37
|
+
loading: true
|
|
38
|
+
});
|
|
39
|
+
const frame = useFrame(animations, 6);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
let alive = true;
|
|
42
|
+
getLog(20, true).then(s => alive && setState({
|
|
43
|
+
loading: false,
|
|
44
|
+
...s
|
|
45
|
+
}));
|
|
46
|
+
return () => {
|
|
47
|
+
alive = false;
|
|
48
|
+
};
|
|
49
|
+
}, []);
|
|
50
|
+
if (state.loading) {
|
|
51
|
+
return /*#__PURE__*/_jsxs(Text, {
|
|
52
|
+
color: theme.accent,
|
|
53
|
+
children: [/*#__PURE__*/_jsx(Spinner, {
|
|
54
|
+
type: "dots"
|
|
55
|
+
}), " Building commit graph\u2026"]
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (!state.ok) return /*#__PURE__*/_jsxs(Text, {
|
|
59
|
+
color: theme.danger,
|
|
60
|
+
children: ["Error: ", state.error]
|
|
61
|
+
});
|
|
62
|
+
if (state.commits.length === 0) {
|
|
63
|
+
return /*#__PURE__*/_jsx(Text, {
|
|
64
|
+
color: theme.muted,
|
|
65
|
+
children: "No commits yet. Make your first commit to see the graph!"
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const colors = theme.branchColors;
|
|
69
|
+
const pulse = state.commits.length ? frame % state.commits.length : 0;
|
|
70
|
+
return /*#__PURE__*/_jsxs(Box, {
|
|
71
|
+
flexDirection: "column",
|
|
72
|
+
children: [/*#__PURE__*/_jsxs(Text, {
|
|
73
|
+
color: theme.muted,
|
|
74
|
+
children: ["Newest at the top \xB7 ", state.commits.length, " commits across all branches"]
|
|
75
|
+
}), /*#__PURE__*/_jsx(Box, {
|
|
76
|
+
marginTop: 1,
|
|
77
|
+
flexDirection: "column",
|
|
78
|
+
children: state.commits.map((c, i) => {
|
|
79
|
+
const color = colors[i % colors.length];
|
|
80
|
+
const active = animations && i === pulse;
|
|
81
|
+
const node = active ? '◉' : '●';
|
|
82
|
+
const line = i === state.commits.length - 1 ? ' ' : '│';
|
|
83
|
+
return /*#__PURE__*/_jsxs(Box, {
|
|
84
|
+
flexDirection: "column",
|
|
85
|
+
children: [/*#__PURE__*/_jsxs(Box, {
|
|
86
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
87
|
+
color: color,
|
|
88
|
+
bold: active,
|
|
89
|
+
children: node
|
|
90
|
+
}), /*#__PURE__*/_jsxs(Text, {
|
|
91
|
+
color: theme.muted,
|
|
92
|
+
children: [" ", c.hash, " "]
|
|
93
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
94
|
+
color: "white",
|
|
95
|
+
children: truncate(c.subject, 48)
|
|
96
|
+
}), /*#__PURE__*/_jsx(RefBadges, {
|
|
97
|
+
refs: c.refs,
|
|
98
|
+
theme: theme
|
|
99
|
+
})]
|
|
100
|
+
}), /*#__PURE__*/_jsxs(Box, {
|
|
101
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
102
|
+
color: color,
|
|
103
|
+
children: line
|
|
104
|
+
}), /*#__PURE__*/_jsxs(Text, {
|
|
105
|
+
color: theme.muted,
|
|
106
|
+
children: [" ", c.author, " \xB7 ", c.date]
|
|
107
|
+
})]
|
|
108
|
+
})]
|
|
109
|
+
}, c.hash + i);
|
|
110
|
+
})
|
|
111
|
+
})]
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function truncate(s, n) {
|
|
115
|
+
return s.length > n ? s.slice(0, n - 1) + '…' : s;
|
|
116
|
+
}
|