@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.
@@ -0,0 +1,153 @@
1
+ // Walks the user through a multi-step recipe one command at a time.
2
+ import React, { useState } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import TextInput from 'ink-text-input';
5
+ import Spinner from 'ink-spinner';
6
+ import { git } from '../git.js';
7
+ import { recipes } from '../recipes.js';
8
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
9
+ export default function RecipeView({
10
+ recipeId,
11
+ theme,
12
+ config
13
+ }) {
14
+ const recipe = recipes[recipeId];
15
+ const inputs = recipe.inputs || [];
16
+ const [values, setValues] = useState(() => {
17
+ const init = {};
18
+ for (const inp of inputs) init[inp.name] = inp.default || '';
19
+ return init;
20
+ });
21
+ const [idx, setIdx] = useState(0);
22
+ const [phase, setPhase] = useState(inputs.length ? 'input' : 'run'); // input | run | busy | done
23
+ const [step, setStep] = useState(0);
24
+ const [log, setLog] = useState([]); // {title, ok, out}
25
+
26
+ const steps = phase !== 'input' ? recipe.steps(values) : [];
27
+ async function runStep() {
28
+ setPhase('busy');
29
+ const s = steps[step];
30
+ const r = await git(s.args);
31
+ const entry = {
32
+ title: s.title,
33
+ ok: r.ok,
34
+ out: r.stdout || r.stderr,
35
+ cmd: 'git ' + s.args.join(' ')
36
+ };
37
+ const nextLog = [...log, entry];
38
+ setLog(nextLog);
39
+ if (step < steps.length - 1) {
40
+ setStep(step + 1);
41
+ setPhase('run');
42
+ } else {
43
+ setPhase('done');
44
+ }
45
+ }
46
+ function skipStep() {
47
+ const s = steps[step];
48
+ const entry = {
49
+ title: s.title,
50
+ ok: true,
51
+ out: '(skipped)',
52
+ cmd: 'git ' + s.args.join(' '),
53
+ skipped: true
54
+ };
55
+ setLog([...log, entry]);
56
+ if (step < steps.length - 1) setStep(step + 1);else setPhase('done');
57
+ }
58
+ useInput((input, key) => {
59
+ if (phase === 'run') {
60
+ if (key.return) runStep();else if (input.toLowerCase() === 's') skipStep();
61
+ }
62
+ });
63
+ if (phase === 'input') {
64
+ const inp = inputs[idx];
65
+ return /*#__PURE__*/_jsxs(Box, {
66
+ flexDirection: "column",
67
+ children: [/*#__PURE__*/_jsxs(Text, {
68
+ color: theme.secondary,
69
+ bold: true,
70
+ children: [recipe.emoji, " ", recipe.title]
71
+ }), /*#__PURE__*/_jsx(Text, {
72
+ color: theme.muted,
73
+ children: recipe.intro
74
+ }), /*#__PURE__*/_jsxs(Box, {
75
+ marginTop: 1,
76
+ children: [/*#__PURE__*/_jsxs(Text, {
77
+ color: theme.accent,
78
+ children: [inp.label.padEnd(24), " "]
79
+ }), /*#__PURE__*/_jsx(TextInput, {
80
+ value: values[inp.name],
81
+ placeholder: inp.placeholder || '',
82
+ onChange: val => setValues(v => ({
83
+ ...v,
84
+ [inp.name]: val
85
+ })),
86
+ onSubmit: () => {
87
+ if (!values[inp.name].trim() && !inp.default) return;
88
+ if (idx < inputs.length - 1) setIdx(idx + 1);else setPhase('run');
89
+ }
90
+ })]
91
+ })]
92
+ });
93
+ }
94
+ return /*#__PURE__*/_jsxs(Box, {
95
+ flexDirection: "column",
96
+ children: [/*#__PURE__*/_jsxs(Text, {
97
+ color: theme.secondary,
98
+ bold: true,
99
+ children: [recipe.emoji, " ", recipe.title]
100
+ }), log.map((l, i) => /*#__PURE__*/_jsx(Box, {
101
+ children: /*#__PURE__*/_jsxs(Text, {
102
+ color: l.skipped ? theme.muted : l.ok ? theme.success : theme.danger,
103
+ children: [l.skipped ? '↷' : l.ok ? '✓' : '✗', " Step ", i + 1, ": ", l.title]
104
+ })
105
+ }, i)), phase === 'busy' && /*#__PURE__*/_jsxs(Text, {
106
+ color: theme.accent,
107
+ children: [/*#__PURE__*/_jsx(Spinner, {
108
+ type: "dots"
109
+ }), " Running step ", step + 1, "\u2026"]
110
+ }), phase === 'run' && steps[step] && /*#__PURE__*/_jsxs(Box, {
111
+ flexDirection: "column",
112
+ marginTop: 1,
113
+ children: [/*#__PURE__*/_jsxs(Text, {
114
+ color: theme.accent,
115
+ bold: true,
116
+ children: ["Step ", step + 1, " of ", steps.length, ": ", steps[step].title]
117
+ }), /*#__PURE__*/_jsx(Text, {
118
+ color: theme.muted,
119
+ children: steps[step].why
120
+ }), /*#__PURE__*/_jsxs(Box, {
121
+ marginTop: 1,
122
+ borderStyle: "round",
123
+ borderColor: theme.accent,
124
+ paddingX: 1,
125
+ children: [/*#__PURE__*/_jsx(Text, {
126
+ color: theme.muted,
127
+ children: "$ "
128
+ }), /*#__PURE__*/_jsxs(Text, {
129
+ color: theme.secondary,
130
+ bold: true,
131
+ children: ["git ", steps[step].args.map(a => /\s/.test(a) ? `"${a}"` : a).join(' ')]
132
+ })]
133
+ }), /*#__PURE__*/_jsx(Text, {
134
+ color: theme.muted,
135
+ children: "Enter \u2192 run this step \xB7 S \u2192 skip it \xB7 Esc \u2192 leave recipe"
136
+ })]
137
+ }), phase === 'done' && /*#__PURE__*/_jsxs(Box, {
138
+ flexDirection: "column",
139
+ marginTop: 1,
140
+ children: [/*#__PURE__*/_jsx(Text, {
141
+ color: theme.success,
142
+ bold: true,
143
+ children: "\uD83C\uDF89 Recipe complete!"
144
+ }), log.some(l => !l.ok && !l.skipped) && /*#__PURE__*/_jsx(Text, {
145
+ color: theme.danger,
146
+ children: "Some steps failed \u2014 scroll up to review. That's normal while learning!"
147
+ }), /*#__PURE__*/_jsx(Text, {
148
+ color: theme.muted,
149
+ children: "Press Esc / \u2190 to go back."
150
+ })]
151
+ })]
152
+ });
153
+ }
@@ -0,0 +1,66 @@
1
+ // Customisation: pick a theme (with live preview) and toggle UI options.
2
+ import React from 'react';
3
+ import { Box, Text } from 'ink';
4
+ import Gradient from 'ink-gradient';
5
+ import Menu from '../components/Menu.js';
6
+ import { themes, themeOrder } from '../themes.js';
7
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
+ export default function SettingsView({
9
+ theme,
10
+ config,
11
+ onChange
12
+ }) {
13
+ const items = [...themeOrder.map(key => ({
14
+ label: `Theme: ${themes[key].emoji} ${themes[key].label}${key === config.theme ? ' ✓' : ''}`,
15
+ value: 'theme:' + key
16
+ })), {
17
+ label: `Animations: ${config.animations ? 'ON ✨' : 'OFF'}`,
18
+ value: 'toggle:animations'
19
+ }, {
20
+ label: `Confirm dangerous actions: ${config.confirmDanger ? 'ON 🛡' : 'OFF'}`,
21
+ value: 'toggle:confirmDanger'
22
+ }, {
23
+ label: `Show command hints: ${config.showHints ? 'ON 💡' : 'OFF'}`,
24
+ value: 'toggle:showHints'
25
+ }];
26
+ function handle({
27
+ value
28
+ }) {
29
+ const [kind, key] = value.split(':');
30
+ if (kind === 'theme') onChange({
31
+ ...config,
32
+ theme: key
33
+ });else onChange({
34
+ ...config,
35
+ [key]: !config[key]
36
+ });
37
+ }
38
+ return /*#__PURE__*/_jsxs(Box, {
39
+ flexDirection: "column",
40
+ children: [/*#__PURE__*/_jsx(Text, {
41
+ color: theme.muted,
42
+ children: "Pick a theme or toggle options. Changes save automatically."
43
+ }), /*#__PURE__*/_jsxs(Box, {
44
+ marginTop: 1,
45
+ marginBottom: 1,
46
+ children: [themeOrder.map(key => /*#__PURE__*/_jsx(Box, {
47
+ marginRight: 1,
48
+ children: /*#__PURE__*/_jsx(Gradient, {
49
+ colors: themes[key].gradient,
50
+ children: /*#__PURE__*/_jsx(Text, {
51
+ children: key === config.theme ? '◉' : '●'
52
+ })
53
+ })
54
+ }, key)), /*#__PURE__*/_jsx(Text, {
55
+ color: theme.muted,
56
+ children: " live palette"
57
+ })]
58
+ }), /*#__PURE__*/_jsx(Menu, {
59
+ items: items,
60
+ onSelect: handle,
61
+ theme: theme,
62
+ animations: config.animations,
63
+ limit: 12
64
+ })]
65
+ });
66
+ }
@@ -0,0 +1,120 @@
1
+ // One-time identity setup: name, email, default branch = main.
2
+ import React, { useState, useEffect } from 'react';
3
+ import { Box, Text } from 'ink';
4
+ import TextInput from 'ink-text-input';
5
+ import Spinner from 'ink-spinner';
6
+ import { git, getGlobalIdentity } from '../git.js';
7
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
+ export default function SetupWizard({
9
+ theme,
10
+ onDone
11
+ }) {
12
+ const [phase, setPhase] = useState('load'); // load | name | email | running | done
13
+ const [name, setName] = useState('');
14
+ const [email, setEmail] = useState('');
15
+ const [results, setResults] = useState([]);
16
+ useEffect(() => {
17
+ let alive = true;
18
+ getGlobalIdentity().then(id => {
19
+ if (!alive) return;
20
+ setName(id.name);
21
+ setEmail(id.email);
22
+ setPhase('name');
23
+ });
24
+ return () => {
25
+ alive = false;
26
+ };
27
+ }, []);
28
+ async function run() {
29
+ setPhase('running');
30
+ const steps = [{
31
+ label: 'Set your name',
32
+ args: ['config', '--global', 'user.name', name]
33
+ }, {
34
+ label: 'Set your email',
35
+ args: ['config', '--global', 'user.email', email]
36
+ }, {
37
+ label: 'Default branch → main',
38
+ args: ['config', '--global', 'init.defaultBranch', 'main']
39
+ }];
40
+ const out = [];
41
+ for (const s of steps) {
42
+ const r = await git(s.args);
43
+ out.push({
44
+ label: s.label,
45
+ ok: r.ok,
46
+ msg: r.ok ? 'done' : r.stderr
47
+ });
48
+ setResults([...out]);
49
+ }
50
+ setPhase('done');
51
+ }
52
+ if (phase === 'load') {
53
+ return /*#__PURE__*/_jsxs(Text, {
54
+ color: theme.accent,
55
+ children: [/*#__PURE__*/_jsx(Spinner, {
56
+ type: "dots"
57
+ }), " Checking existing config\u2026"]
58
+ });
59
+ }
60
+ return /*#__PURE__*/_jsxs(Box, {
61
+ flexDirection: "column",
62
+ children: [/*#__PURE__*/_jsx(Text, {
63
+ color: theme.muted,
64
+ children: "Tell git who you are. This is stored globally and only needs doing once."
65
+ }), /*#__PURE__*/_jsxs(Box, {
66
+ marginTop: 1,
67
+ children: [/*#__PURE__*/_jsx(Text, {
68
+ color: theme.accent,
69
+ children: "Name "
70
+ }), phase === 'name' ? /*#__PURE__*/_jsx(TextInput, {
71
+ value: name,
72
+ onChange: setName,
73
+ onSubmit: () => name.trim() && setPhase('email'),
74
+ placeholder: "Your Name"
75
+ }) : /*#__PURE__*/_jsx(Text, {
76
+ color: "white",
77
+ children: name
78
+ })]
79
+ }), phase !== 'name' && /*#__PURE__*/_jsxs(Box, {
80
+ children: [/*#__PURE__*/_jsx(Text, {
81
+ color: theme.accent,
82
+ children: "Email "
83
+ }), phase === 'email' ? /*#__PURE__*/_jsx(TextInput, {
84
+ value: email,
85
+ onChange: setEmail,
86
+ onSubmit: () => email.trim() && run(),
87
+ placeholder: "you@example.com"
88
+ }) : /*#__PURE__*/_jsx(Text, {
89
+ color: "white",
90
+ children: email
91
+ })]
92
+ }), (phase === 'running' || phase === 'done') && /*#__PURE__*/_jsxs(Box, {
93
+ flexDirection: "column",
94
+ marginTop: 1,
95
+ children: [results.map((r, i) => /*#__PURE__*/_jsxs(Text, {
96
+ color: r.ok ? theme.success : theme.danger,
97
+ children: [r.ok ? '✓' : '✗', " ", r.label, " ", r.ok ? '' : '— ' + r.msg]
98
+ }, i)), phase === 'done' && /*#__PURE__*/_jsx(Box, {
99
+ marginTop: 1,
100
+ children: /*#__PURE__*/_jsx(Text, {
101
+ color: theme.success,
102
+ bold: true,
103
+ children: "\uD83C\uDF89 All set! Press Esc / \u2190 to go back."
104
+ })
105
+ })]
106
+ }), phase === 'name' && /*#__PURE__*/_jsx(Box, {
107
+ marginTop: 1,
108
+ children: /*#__PURE__*/_jsx(Text, {
109
+ color: theme.muted,
110
+ children: "Type your name, press Enter \u2192"
111
+ })
112
+ }), phase === 'email' && /*#__PURE__*/_jsx(Box, {
113
+ marginTop: 1,
114
+ children: /*#__PURE__*/_jsx(Text, {
115
+ color: theme.muted,
116
+ children: "Type your email, press Enter to save \u2192"
117
+ })
118
+ })]
119
+ });
120
+ }
@@ -0,0 +1,121 @@
1
+ // Colour-coded working-tree dashboard.
2
+ import React, { useState, useEffect } from 'react';
3
+ import { Box, Text } from 'ink';
4
+ import Spinner from 'ink-spinner';
5
+ import { getStatus, getCurrentBranch } from '../git.js';
6
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
7
+ function Row({
8
+ theme,
9
+ file
10
+ }) {
11
+ const color = file.untracked ? theme.warn : file.label === 'Deleted' ? theme.danger : file.staged ? theme.success : theme.secondary;
12
+ const mark = file.staged ? '✓' : file.untracked ? '?' : '•';
13
+ return /*#__PURE__*/_jsxs(Text, {
14
+ children: [/*#__PURE__*/_jsxs(Text, {
15
+ color: color,
16
+ children: [mark, " "]
17
+ }), /*#__PURE__*/_jsxs(Text, {
18
+ color: theme.muted,
19
+ children: ["[", file.label.padEnd(9), "] "]
20
+ }), /*#__PURE__*/_jsx(Text, {
21
+ color: color,
22
+ children: file.name
23
+ })]
24
+ });
25
+ }
26
+ export default function StatusView({
27
+ theme
28
+ }) {
29
+ const [state, setState] = useState({
30
+ loading: true
31
+ });
32
+ const [branch, setBranch] = useState('');
33
+ useEffect(() => {
34
+ let alive = true;
35
+ (async () => {
36
+ const [s, b] = [await getStatus(), await getCurrentBranch()];
37
+ if (!alive) return;
38
+ setState({
39
+ loading: false,
40
+ ...s
41
+ });
42
+ setBranch(b);
43
+ })();
44
+ return () => {
45
+ alive = false;
46
+ };
47
+ }, []);
48
+ if (state.loading) {
49
+ return /*#__PURE__*/_jsxs(Text, {
50
+ color: theme.accent,
51
+ children: [/*#__PURE__*/_jsx(Spinner, {
52
+ type: "dots"
53
+ }), " Reading working tree\u2026"]
54
+ });
55
+ }
56
+ if (!state.ok) {
57
+ return /*#__PURE__*/_jsxs(Text, {
58
+ color: theme.danger,
59
+ children: ["Could not read status: ", state.error]
60
+ });
61
+ }
62
+ const clean = state.files.length === 0;
63
+ return /*#__PURE__*/_jsxs(Box, {
64
+ flexDirection: "column",
65
+ children: [/*#__PURE__*/_jsxs(Box, {
66
+ children: [/*#__PURE__*/_jsxs(Text, {
67
+ color: theme.accent,
68
+ bold: true,
69
+ children: ["On branch ", branch || '(detached)', ' ']
70
+ }), state.ahead > 0 && /*#__PURE__*/_jsxs(Text, {
71
+ color: theme.warn,
72
+ children: ["\u2191", state.ahead, " ahead "]
73
+ }), state.behind > 0 && /*#__PURE__*/_jsxs(Text, {
74
+ color: theme.danger,
75
+ children: ["\u2193", state.behind, " behind "]
76
+ })]
77
+ }), clean ? /*#__PURE__*/_jsx(Box, {
78
+ marginTop: 1,
79
+ children: /*#__PURE__*/_jsx(Text, {
80
+ color: theme.success,
81
+ children: "\u2728 Working tree clean \u2014 nothing to commit."
82
+ })
83
+ }) : /*#__PURE__*/_jsxs(Box, {
84
+ flexDirection: "column",
85
+ marginTop: 1,
86
+ children: [state.staged.length > 0 && /*#__PURE__*/_jsxs(Box, {
87
+ flexDirection: "column",
88
+ marginBottom: 1,
89
+ children: [/*#__PURE__*/_jsxs(Text, {
90
+ color: theme.success,
91
+ bold: true,
92
+ children: ["\u25CF Staged (", state.staged.length, ") \u2014 will be committed"]
93
+ }), state.staged.map((f, i) => /*#__PURE__*/_jsx(Row, {
94
+ theme: theme,
95
+ file: f
96
+ }, 's' + i))]
97
+ }), state.unstaged.length > 0 && /*#__PURE__*/_jsxs(Box, {
98
+ flexDirection: "column",
99
+ marginBottom: 1,
100
+ children: [/*#__PURE__*/_jsxs(Text, {
101
+ color: theme.secondary,
102
+ bold: true,
103
+ children: ["\u25CF Changed but not staged (", state.unstaged.length, ")"]
104
+ }), state.unstaged.map((f, i) => /*#__PURE__*/_jsx(Row, {
105
+ theme: theme,
106
+ file: f
107
+ }, 'u' + i))]
108
+ }), state.untracked.length > 0 && /*#__PURE__*/_jsxs(Box, {
109
+ flexDirection: "column",
110
+ children: [/*#__PURE__*/_jsxs(Text, {
111
+ color: theme.warn,
112
+ bold: true,
113
+ children: ["\u25CF Untracked (", state.untracked.length, ") \u2014 new files git isn't watching yet"]
114
+ }), state.untracked.map((f, i) => /*#__PURE__*/_jsx(Row, {
115
+ theme: theme,
116
+ file: f
117
+ }, 'n' + i))]
118
+ })]
119
+ })]
120
+ });
121
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@manthank/mgit",
3
+ "version": "1.0.0",
4
+ "description": "A friendly, animated terminal UI that teaches beginners how to use git & GitHub.",
5
+ "type": "module",
6
+ "bin": {
7
+ "mgit": "dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "scripts": {
18
+ "build": "babel source --out-dir dist --extensions .js,.jsx --copy-files",
19
+ "dev": "npm run build && node ./dist/cli.js",
20
+ "start": "node ./dist/cli.js",
21
+ "prepare": "npm run build"
22
+ },
23
+ "keywords": [
24
+ "git",
25
+ "github",
26
+ "cli",
27
+ "tui",
28
+ "ink",
29
+ "beginner",
30
+ "interactive",
31
+ "learn-git",
32
+ "terminal",
33
+ "learning"
34
+ ],
35
+ "license": "MIT",
36
+ "dependencies": {
37
+ "execa": "^9.5.2",
38
+ "ink": "^5.1.0",
39
+ "ink-big-text": "^2.0.0",
40
+ "ink-gradient": "^3.0.0",
41
+ "ink-select-input": "^6.0.0",
42
+ "ink-spinner": "^5.0.0",
43
+ "ink-text-input": "^6.0.0",
44
+ "react": "^18.3.1"
45
+ },
46
+ "devDependencies": {
47
+ "@babel/cli": "^7.26.4",
48
+ "@babel/core": "^7.26.0",
49
+ "@babel/preset-react": "^7.26.3",
50
+ "ink-testing-library": "^4.0.0"
51
+ }
52
+ }