git-personas 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/dist/cli.js +5 -0
- package/dist/components/BackButton.js +5 -0
- package/dist/components/DeleteScreen.js +28 -0
- package/dist/components/EditSelectScreen.js +25 -0
- package/dist/components/MainScreen.js +34 -0
- package/dist/components/PersonaForm.js +93 -0
- package/dist/components/SuccessScreen.js +21 -0
- package/dist/components/SwitchScreen.js +43 -0
- package/dist/index.js +62 -0
- package/dist/store.js +136 -0
- package/dist/types.js +1 -0
- package/package.json +66 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import SelectInput from 'ink-select-input';
|
|
4
|
+
import { deletePersona, saveStore } from '../store.js';
|
|
5
|
+
import BackButton from './BackButton.js';
|
|
6
|
+
export default function DeleteScreen({ store, onScreenChange }) {
|
|
7
|
+
const items = store.personas.map((p) => ({
|
|
8
|
+
label: `🗑️ ${p.name} — ${p.user} <${p.email}>`,
|
|
9
|
+
value: p.name,
|
|
10
|
+
}));
|
|
11
|
+
const onSelect = (item) => {
|
|
12
|
+
const updated = deletePersona(item.value, store);
|
|
13
|
+
saveStore(updated);
|
|
14
|
+
onScreenChange({ type: 'success', message: `Deleted persona "${item.value}"` });
|
|
15
|
+
};
|
|
16
|
+
useInput((_input, key) => {
|
|
17
|
+
if (key.escape)
|
|
18
|
+
onScreenChange({ type: 'main' });
|
|
19
|
+
});
|
|
20
|
+
if (store.personas.length === 0) {
|
|
21
|
+
useInput((_input, key) => {
|
|
22
|
+
if (key.return || key.escape)
|
|
23
|
+
onScreenChange({ type: 'main' });
|
|
24
|
+
});
|
|
25
|
+
return (_jsxs(Box, { padding: 1, children: [_jsx(Text, { children: "No personas to delete." }), _jsx(Text, { dimColor: true, children: "Press Enter or Esc to go back" })] }));
|
|
26
|
+
}
|
|
27
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, children: "\uD83D\uDDD1\uFE0F Select persona to delete" }), _jsx(Box, { marginTop: 1 }), _jsx(SelectInput, { items: items, onSelect: onSelect }), _jsx(BackButton, {})] }));
|
|
28
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import SelectInput from 'ink-select-input';
|
|
4
|
+
import BackButton from './BackButton.js';
|
|
5
|
+
export default function EditSelectScreen({ store, onScreenChange }) {
|
|
6
|
+
const items = store.personas.map((p) => ({
|
|
7
|
+
label: `${p.name} — ${p.user} <${p.email}>`,
|
|
8
|
+
value: p.name,
|
|
9
|
+
}));
|
|
10
|
+
const onSelect = (item) => {
|
|
11
|
+
onScreenChange({ type: 'edit', name: item.value });
|
|
12
|
+
};
|
|
13
|
+
useInput((_input, key) => {
|
|
14
|
+
if (key.escape)
|
|
15
|
+
onScreenChange({ type: 'main' });
|
|
16
|
+
});
|
|
17
|
+
if (store.personas.length === 0) {
|
|
18
|
+
useInput((_input, key) => {
|
|
19
|
+
if (key.return || key.escape)
|
|
20
|
+
onScreenChange({ type: 'main' });
|
|
21
|
+
});
|
|
22
|
+
return (_jsxs(Box, { padding: 1, children: [_jsx(Text, { children: "No personas to edit. Create one first." }), _jsx(Text, { dimColor: true, children: "Press Enter or Esc to go back" })] }));
|
|
23
|
+
}
|
|
24
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, children: "\u270F\uFE0F Select persona to edit" }), _jsx(Box, { marginTop: 1 }), _jsx(SelectInput, { items: items, onSelect: onSelect }), _jsx(BackButton, {})] }));
|
|
25
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import SelectInput from 'ink-select-input';
|
|
4
|
+
import { getPersona } from '../store.js';
|
|
5
|
+
export default function MainScreen({ store, onScreenChange, onQuit }) {
|
|
6
|
+
const activePersona = store.active ? getPersona(store.active, store) : null;
|
|
7
|
+
const menuItems = [
|
|
8
|
+
{ label: '➕ Create Persona', value: 'create' },
|
|
9
|
+
{ label: '✏️ Edit Persona', value: 'edit' },
|
|
10
|
+
{ label: '🗑️ Delete Persona', value: 'delete' },
|
|
11
|
+
{ label: '🔄 Switch Persona', value: 'switch' },
|
|
12
|
+
{ label: '🚪 Quit', value: 'quit' },
|
|
13
|
+
];
|
|
14
|
+
const onSelect = (item) => {
|
|
15
|
+
switch (item.value) {
|
|
16
|
+
case 'create':
|
|
17
|
+
onScreenChange({ type: 'create' });
|
|
18
|
+
break;
|
|
19
|
+
case 'edit':
|
|
20
|
+
onScreenChange({ type: 'edit', name: '' });
|
|
21
|
+
break;
|
|
22
|
+
case 'delete':
|
|
23
|
+
onScreenChange({ type: 'delete' });
|
|
24
|
+
break;
|
|
25
|
+
case 'switch':
|
|
26
|
+
onScreenChange({ type: 'switch' });
|
|
27
|
+
break;
|
|
28
|
+
case 'quit':
|
|
29
|
+
onQuit();
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, children: "\uD83E\uDDD1\u200D\uD83D\uDCBC Git Personas" }), _jsx(Box, { marginTop: 1 }), activePersona ? (_jsxs(Box, { children: [_jsx(Text, { color: "green", children: "\u25CF Active: " }), _jsx(Text, { bold: true, children: activePersona.name }), _jsxs(Text, { children: [" (", activePersona.user, " <", activePersona.email, ">)"] })] })) : (_jsx(Text, { color: "yellow", children: "\u25CF No active persona" })), _jsx(Box, { marginTop: 1 }), store.personas.length > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Your personas:" }), store.personas.map((p) => (_jsx(Box, { children: _jsxs(Text, { children: [' ', p.name === store.active ? '🟢' : '⚪', " ", p.name, _jsxs(Text, { dimColor: true, children: [" \u2014 ", p.user, " <", p.email, ">"] })] }) }, p.name))), _jsx(Box, { marginTop: 1 })] })) : (_jsx(Text, { dimColor: true, children: "No personas yet. Create one to get started." })), _jsx(Box, { marginTop: 1 }), _jsx(SelectInput, { items: menuItems, onSelect: onSelect })] }));
|
|
34
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import SelectInput from 'ink-select-input';
|
|
5
|
+
import TextInput from 'ink-text-input';
|
|
6
|
+
import { detectGpgKeys, detectSshKeys } from '../store.js';
|
|
7
|
+
import BackButton from './BackButton.js';
|
|
8
|
+
export default function PersonaForm({ initial, onSubmit, onCancel, title }) {
|
|
9
|
+
const gpgKeys = detectGpgKeys();
|
|
10
|
+
const sshKeys = detectSshKeys();
|
|
11
|
+
const [name, setName] = useState(initial?.name || '');
|
|
12
|
+
const [user, setUser] = useState(initial?.user || '');
|
|
13
|
+
const [email, setEmail] = useState(initial?.email || '');
|
|
14
|
+
const [gpgKey, setGpgKey] = useState(initial?.gpgKey || '');
|
|
15
|
+
const [sshKey, setSshKey] = useState(initial?.sshKey || '');
|
|
16
|
+
const [defaultBranch, setDefaultBranch] = useState(initial?.defaultBranch || '');
|
|
17
|
+
const [step, setStep] = useState(0);
|
|
18
|
+
const isEdit = !!initial?.name;
|
|
19
|
+
const steps = isEdit
|
|
20
|
+
? ['user', 'email', 'gpg', 'ssh', 'branch', 'confirm']
|
|
21
|
+
: ['name', 'user', 'email', 'gpg', 'ssh', 'branch', 'confirm'];
|
|
22
|
+
const currentStep = steps[step];
|
|
23
|
+
// Global Escape handler: go back a step or cancel
|
|
24
|
+
useInput((_input, key) => {
|
|
25
|
+
if (key.escape && !['gpg', 'ssh'].includes(currentStep)) {
|
|
26
|
+
if (step > 0) {
|
|
27
|
+
setStep(step - 1);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
onCancel();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
const handleGpgSelect = (item) => {
|
|
35
|
+
setGpgKey(item.value);
|
|
36
|
+
setStep(step + 1);
|
|
37
|
+
};
|
|
38
|
+
const handleSshSelect = (item) => {
|
|
39
|
+
setSshKey(item.value);
|
|
40
|
+
setStep(step + 1);
|
|
41
|
+
};
|
|
42
|
+
const handleSubmit = () => {
|
|
43
|
+
if (!name.trim() || !user.trim() || !email.trim())
|
|
44
|
+
return;
|
|
45
|
+
onSubmit({
|
|
46
|
+
name: name.trim(),
|
|
47
|
+
user: user.trim(),
|
|
48
|
+
email: email.trim(),
|
|
49
|
+
gpgKey: gpgKey || undefined,
|
|
50
|
+
sshKey: sshKey || undefined,
|
|
51
|
+
defaultBranch: defaultBranch.trim() || undefined,
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
// Confirm step: Enter submits, Escape goes back
|
|
55
|
+
useInput((_input, key) => {
|
|
56
|
+
if (currentStep === 'confirm') {
|
|
57
|
+
if (key.return)
|
|
58
|
+
handleSubmit();
|
|
59
|
+
if (key.escape)
|
|
60
|
+
setStep(step - 1);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
// Escape handler for GPG/SSH select screens
|
|
64
|
+
useInput((_input, key) => {
|
|
65
|
+
if (key.escape && ['gpg', 'ssh'].includes(currentStep)) {
|
|
66
|
+
setStep(step - 1);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
if (currentStep === 'gpg') {
|
|
70
|
+
const items = [
|
|
71
|
+
{ label: '(none — skip GPG signing)', value: '' },
|
|
72
|
+
...gpgKeys.map((k) => ({
|
|
73
|
+
label: `${k.uid} [${k.id}]`,
|
|
74
|
+
value: k.id,
|
|
75
|
+
})),
|
|
76
|
+
];
|
|
77
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, children: "\uD83D\uDD11 Select GPG Signing Key" }), _jsxs(Text, { dimColor: true, children: ["Current: ", gpgKey || '(none)'] }), _jsx(Box, { marginTop: 1 }), _jsx(SelectInput, { items: items, onSelect: handleGpgSelect }), _jsx(BackButton, {})] }));
|
|
78
|
+
}
|
|
79
|
+
if (currentStep === 'ssh') {
|
|
80
|
+
const items = [
|
|
81
|
+
{ label: '(none — use default SSH)', value: '' },
|
|
82
|
+
...sshKeys.map((k) => ({
|
|
83
|
+
label: `${k.comment} (${k.path})`,
|
|
84
|
+
value: k.path,
|
|
85
|
+
})),
|
|
86
|
+
];
|
|
87
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, children: "\uD83D\uDD11 Select SSH Key" }), _jsxs(Text, { dimColor: true, children: ["Current: ", sshKey || '(none)'] }), _jsx(Box, { marginTop: 1 }), _jsx(SelectInput, { items: items, onSelect: handleSshSelect }), _jsx(BackButton, {})] }));
|
|
88
|
+
}
|
|
89
|
+
if (currentStep === 'confirm') {
|
|
90
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, children: title }), _jsx(Box, { marginTop: 1 }), _jsxs(Text, { children: [" Name: ", _jsx(Text, { bold: true, children: name })] }), _jsxs(Text, { children: [" User: ", user] }), _jsxs(Text, { children: [" Email: ", email] }), _jsxs(Text, { children: [" GPG Key: ", gpgKey || '(none)'] }), _jsxs(Text, { children: [" SSH Key: ", sshKey || '(none)'] }), _jsxs(Text, { children: [" Default Branch: ", defaultBranch || '(system default)'] }), _jsx(Box, { marginTop: 1 }), _jsx(Text, { dimColor: true, children: "Enter to confirm \u00B7 Esc to go back" })] }));
|
|
91
|
+
}
|
|
92
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, children: title }), _jsxs(Text, { dimColor: true, children: ["Step ", step + 1, " of ", steps.length] }), _jsx(Box, { marginTop: 1 }), currentStep === 'name' && (_jsxs(Box, { children: [_jsx(Text, { children: "Name: " }), _jsx(TextInput, { value: name, onChange: setName, onSubmit: () => setStep(1), placeholder: "e.g. work, personal" }), _jsx(Text, { dimColor: true, children: "Press Enter to continue" })] })), currentStep === 'user' && (_jsxs(Box, { children: [_jsx(Text, { children: "Git User Name: " }), _jsx(TextInput, { value: user, onChange: setUser, onSubmit: () => setStep(step + 1), placeholder: "e.g. Phil" }), _jsx(Text, { dimColor: true, children: "Press Enter to continue" })] })), currentStep === 'email' && (_jsxs(Box, { children: [_jsx(Text, { children: "Git Email: " }), _jsx(TextInput, { value: email, onChange: setEmail, onSubmit: () => setStep(step + 1), placeholder: "e.g. phil@example.com" }), _jsx(Text, { dimColor: true, children: "Press Enter to continue" })] })), currentStep === 'branch' && (_jsxs(Box, { children: [_jsx(Text, { children: "Default Branch: " }), _jsx(TextInput, { value: defaultBranch, onChange: setDefaultBranch, onSubmit: () => setStep(step + 1), placeholder: "e.g. main (leave empty for default)" }), _jsx(Text, { dimColor: true, children: "Press Enter to continue (leave blank to skip)" })] })), _jsx(BackButton, {})] }));
|
|
93
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import SelectInput from 'ink-select-input';
|
|
4
|
+
export default function SuccessScreen({ message, onContinue, onQuit }) {
|
|
5
|
+
useInput((_input, key) => {
|
|
6
|
+
if (key.return)
|
|
7
|
+
onContinue();
|
|
8
|
+
if (key.escape)
|
|
9
|
+
onQuit();
|
|
10
|
+
});
|
|
11
|
+
const items = [
|
|
12
|
+
{ label: 'Continue', value: 'continue' },
|
|
13
|
+
{ label: '🚪 Quit', value: 'quit' },
|
|
14
|
+
];
|
|
15
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Text, { color: "green", children: ["\u2713 ", message] }), _jsx(Box, { marginTop: 1 }), _jsx(SelectInput, { items: items, onSelect: (item) => {
|
|
16
|
+
if (item.value === 'continue')
|
|
17
|
+
onContinue();
|
|
18
|
+
if (item.value === 'quit')
|
|
19
|
+
onQuit();
|
|
20
|
+
} })] }));
|
|
21
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import SelectInput from 'ink-select-input';
|
|
4
|
+
import { getPersona, applyPersona, clearActivePersona, setActive, saveStore } from '../store.js';
|
|
5
|
+
import BackButton from './BackButton.js';
|
|
6
|
+
export default function SwitchScreen({ store, onScreenChange }) {
|
|
7
|
+
const items = [
|
|
8
|
+
{ label: '⚪ (none — clear active persona)', value: '__none__' },
|
|
9
|
+
...store.personas.map((p) => ({
|
|
10
|
+
label: `${p.name === store.active ? '🟢' : '⚪'} ${p.name} — ${p.user} <${p.email}>`,
|
|
11
|
+
value: p.name,
|
|
12
|
+
})),
|
|
13
|
+
];
|
|
14
|
+
const onSelect = (item) => {
|
|
15
|
+
if (item.value === '__none__') {
|
|
16
|
+
clearActivePersona();
|
|
17
|
+
const updated = setActive(null, store);
|
|
18
|
+
saveStore(updated);
|
|
19
|
+
onScreenChange({ type: 'success', message: 'Cleared active persona' });
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
const persona = getPersona(item.value, store);
|
|
23
|
+
if (persona) {
|
|
24
|
+
applyPersona(persona);
|
|
25
|
+
const updated = setActive(item.value, store);
|
|
26
|
+
saveStore(updated);
|
|
27
|
+
onScreenChange({ type: 'success', message: `Switched to "${item.value}"` });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
useInput((_input, key) => {
|
|
32
|
+
if (key.escape)
|
|
33
|
+
onScreenChange({ type: 'main' });
|
|
34
|
+
});
|
|
35
|
+
if (store.personas.length === 0) {
|
|
36
|
+
useInput((_input, key) => {
|
|
37
|
+
if (key.return || key.escape)
|
|
38
|
+
onScreenChange({ type: 'main' });
|
|
39
|
+
});
|
|
40
|
+
return (_jsxs(Box, { padding: 1, children: [_jsx(Text, { children: "No personas to switch to." }), _jsx(Text, { dimColor: true, children: "Press Enter or Esc to go back" })] }));
|
|
41
|
+
}
|
|
42
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, children: "\uD83D\uDD04 Switch Active Persona" }), _jsx(Box, { marginTop: 1 }), _jsx(SelectInput, { items: items, onSelect: onSelect }), _jsx(BackButton, {})] }));
|
|
43
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { useInput, useApp } from 'ink';
|
|
4
|
+
import { loadStore, saveStore, ensureConfigDir, addPersona, updatePersona, getPersona, applyPersona } from './store.js';
|
|
5
|
+
import MainScreen from './components/MainScreen.js';
|
|
6
|
+
import PersonaForm from './components/PersonaForm.js';
|
|
7
|
+
import EditSelectScreen from './components/EditSelectScreen.js';
|
|
8
|
+
import DeleteScreen from './components/DeleteScreen.js';
|
|
9
|
+
import SwitchScreen from './components/SwitchScreen.js';
|
|
10
|
+
import SuccessScreen from './components/SuccessScreen.js';
|
|
11
|
+
export default function App() {
|
|
12
|
+
const { exit } = useApp();
|
|
13
|
+
const [screen, setScreen] = useState({ type: 'main' });
|
|
14
|
+
// Global: Escape on main screen quits the app
|
|
15
|
+
useInput((_input, key) => {
|
|
16
|
+
if (key.escape && screen.type === 'main') {
|
|
17
|
+
exit();
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
const store = loadStore();
|
|
21
|
+
switch (screen.type) {
|
|
22
|
+
case 'main':
|
|
23
|
+
return (_jsx(MainScreen, { store: store, onScreenChange: setScreen, onQuit: exit }));
|
|
24
|
+
case 'create': {
|
|
25
|
+
return (_jsx(PersonaForm, { title: "\u2795 Create Persona", onSubmit: (persona) => {
|
|
26
|
+
try {
|
|
27
|
+
ensureConfigDir();
|
|
28
|
+
const updated = addPersona(persona, store);
|
|
29
|
+
saveStore(updated);
|
|
30
|
+
setScreen({ type: 'success', message: `Created persona "${persona.name}"` });
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
34
|
+
setScreen({ type: 'success', message: `Error: ${msg}` });
|
|
35
|
+
}
|
|
36
|
+
}, onCancel: () => setScreen({ type: 'main' }) }));
|
|
37
|
+
}
|
|
38
|
+
case 'edit': {
|
|
39
|
+
if (!screen.name) {
|
|
40
|
+
return (_jsx(EditSelectScreen, { store: store, onScreenChange: setScreen }));
|
|
41
|
+
}
|
|
42
|
+
const persona = getPersona(screen.name, store);
|
|
43
|
+
if (!persona) {
|
|
44
|
+
return (_jsx(SuccessScreen, { message: "Persona not found.", onContinue: () => setScreen({ type: 'main' }), onQuit: exit }));
|
|
45
|
+
}
|
|
46
|
+
return (_jsx(PersonaForm, { title: `✏️ Edit "${persona.name}"`, initial: persona, onSubmit: (updated) => {
|
|
47
|
+
const newStore = updatePersona(screen.name, updated, store);
|
|
48
|
+
saveStore(newStore);
|
|
49
|
+
if (store.active === screen.name) {
|
|
50
|
+
applyPersona(updated);
|
|
51
|
+
}
|
|
52
|
+
setScreen({ type: 'success', message: `Updated persona "${persona.name}"` });
|
|
53
|
+
}, onCancel: () => setScreen({ type: 'main' }) }));
|
|
54
|
+
}
|
|
55
|
+
case 'delete':
|
|
56
|
+
return _jsx(DeleteScreen, { store: store, onScreenChange: setScreen });
|
|
57
|
+
case 'switch':
|
|
58
|
+
return _jsx(SwitchScreen, { store: store, onScreenChange: setScreen });
|
|
59
|
+
case 'success':
|
|
60
|
+
return (_jsx(SuccessScreen, { message: screen.message, onContinue: () => setScreen({ type: 'main' }), onQuit: exit }));
|
|
61
|
+
}
|
|
62
|
+
}
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
const CONFIG_DIR = `${os.homedir()}/.config/git-personas`;
|
|
5
|
+
const CONFIG_FILE = `${CONFIG_DIR}/personas.json`;
|
|
6
|
+
export function ensureConfigDir() {
|
|
7
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
8
|
+
}
|
|
9
|
+
export function loadStore() {
|
|
10
|
+
try {
|
|
11
|
+
const data = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
12
|
+
return JSON.parse(data);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return { personas: [], active: null };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function saveStore(store) {
|
|
19
|
+
ensureConfigDir();
|
|
20
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(store, null, 2));
|
|
21
|
+
}
|
|
22
|
+
export function getPersona(name, store) {
|
|
23
|
+
return store.personas.find((p) => p.name === name);
|
|
24
|
+
}
|
|
25
|
+
export function addPersona(persona, store) {
|
|
26
|
+
if (store.personas.find((p) => p.name === persona.name)) {
|
|
27
|
+
throw new Error(`Persona "${persona.name}" already exists`);
|
|
28
|
+
}
|
|
29
|
+
return { ...store, personas: [...store.personas, persona] };
|
|
30
|
+
}
|
|
31
|
+
export function updatePersona(name, updates, store) {
|
|
32
|
+
return {
|
|
33
|
+
...store,
|
|
34
|
+
personas: store.personas.map((p) => p.name === name ? { ...p, ...updates } : p),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function deletePersona(name, store) {
|
|
38
|
+
return {
|
|
39
|
+
...store,
|
|
40
|
+
personas: store.personas.filter((p) => p.name !== name),
|
|
41
|
+
active: store.active === name ? null : store.active,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function setActive(name, store) {
|
|
45
|
+
return { ...store, active: name };
|
|
46
|
+
}
|
|
47
|
+
export function detectGpgKeys() {
|
|
48
|
+
try {
|
|
49
|
+
const output = execSync('gpg --list-keys --with-colons 2>/dev/null', {
|
|
50
|
+
encoding: 'utf-8',
|
|
51
|
+
});
|
|
52
|
+
const keys = [];
|
|
53
|
+
const lines = output.split('\n');
|
|
54
|
+
for (const line of lines) {
|
|
55
|
+
if (line.startsWith('pub:')) {
|
|
56
|
+
const parts = line.split(':');
|
|
57
|
+
const keyId = parts[4];
|
|
58
|
+
const uid = parts[9] || '';
|
|
59
|
+
if (keyId && uid) {
|
|
60
|
+
keys.push({
|
|
61
|
+
id: keyId,
|
|
62
|
+
uid: uid.substring(0, 60),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return keys;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export function detectSshKeys() {
|
|
74
|
+
try {
|
|
75
|
+
const sshDir = `${os.homedir()}/.ssh`;
|
|
76
|
+
const keys = [];
|
|
77
|
+
const files = fs.readdirSync(sshDir);
|
|
78
|
+
for (const file of files) {
|
|
79
|
+
if (file.startsWith('id_') && !file.endsWith('.pub') && !file.endsWith('.pem')) {
|
|
80
|
+
const pubFile = `${sshDir}/${file}.pub`;
|
|
81
|
+
try {
|
|
82
|
+
const comment = fs.readFileSync(pubFile, 'utf-8').trim().split(' ').slice(2).join(' ');
|
|
83
|
+
keys.push({ path: `${sshDir}/${file}`, comment: comment || file });
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
keys.push({ path: `${sshDir}/${file}`, comment: file });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return keys;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export function applyPersona(persona) {
|
|
97
|
+
const commands = [
|
|
98
|
+
`git config --global user.name "${persona.user}"`,
|
|
99
|
+
`git config --global user.email "${persona.email}"`,
|
|
100
|
+
];
|
|
101
|
+
if (persona.gpgKey) {
|
|
102
|
+
commands.push(`git config --global user.signingkey ${persona.gpgKey}`, `git config --global commit.gpgsign true`);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
commands.push(`git config --global --unset commit.gpgsign || true`);
|
|
106
|
+
commands.push(`git config --global --unset user.signingkey || true`);
|
|
107
|
+
}
|
|
108
|
+
if (persona.sshKey) {
|
|
109
|
+
commands.push(`git config --global core.sshCommand "ssh -i ${persona.sshKey} -o IdentitiesOnly=yes"`);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
commands.push(`git config --global --unset core.sshCommand || true`);
|
|
113
|
+
}
|
|
114
|
+
if (persona.defaultBranch) {
|
|
115
|
+
commands.push(`git config --global init.defaultBranch ${persona.defaultBranch}`);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
commands.push(`git config --global --unset init.defaultBranch || true`);
|
|
119
|
+
}
|
|
120
|
+
for (const cmd of commands) {
|
|
121
|
+
execSync(cmd, { stdio: 'pipe' });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export function clearActivePersona() {
|
|
125
|
+
const commands = [
|
|
126
|
+
`git config --global --unset user.name || true`,
|
|
127
|
+
`git config --global --unset user.email || true`,
|
|
128
|
+
`git config --global --unset user.signingkey || true`,
|
|
129
|
+
`git config --global --unset commit.gpgsign || true`,
|
|
130
|
+
`git config --global --unset core.sshCommand || true`,
|
|
131
|
+
`git config --global --unset init.defaultBranch || true`,
|
|
132
|
+
];
|
|
133
|
+
for (const cmd of commands) {
|
|
134
|
+
execSync(cmd, { stdio: 'pipe' });
|
|
135
|
+
}
|
|
136
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "git-personas",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Terminal UI for managing git personas — switch between git identities with ease",
|
|
5
|
+
"main": "dist/cli.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"git-personas": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"dev": "tsx src/cli.tsx",
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"start": "node dist/cli.js",
|
|
16
|
+
"lint": "eslint src/",
|
|
17
|
+
"lint:fix": "eslint src/ --fix",
|
|
18
|
+
"typecheck": "tsc --noEmit",
|
|
19
|
+
"check": "npm run typecheck && npm run lint",
|
|
20
|
+
"prepublishOnly": "npm run check && npm run build",
|
|
21
|
+
"prepare": "simple-git-hooks"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"git",
|
|
25
|
+
"persona",
|
|
26
|
+
"identity",
|
|
27
|
+
"config",
|
|
28
|
+
"cli",
|
|
29
|
+
"terminal",
|
|
30
|
+
"tui",
|
|
31
|
+
"ink",
|
|
32
|
+
"switch",
|
|
33
|
+
"profile"
|
|
34
|
+
],
|
|
35
|
+
"author": "Phil Oliver",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"type": "module",
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18"
|
|
40
|
+
},
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": ""
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"ink": "^5.2.0",
|
|
47
|
+
"ink-select-input": "^6.0.0",
|
|
48
|
+
"ink-text-input": "^6.0.0",
|
|
49
|
+
"react": "^18.3.1"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@eslint/js": "^10.0.1",
|
|
53
|
+
"@types/node": "^22.15.21",
|
|
54
|
+
"@types/react": "^18.3.30",
|
|
55
|
+
"@typescript-eslint/parser": "^8.60.1",
|
|
56
|
+
"eslint": "^10.0.1",
|
|
57
|
+
"react-dom": "^18.3.1",
|
|
58
|
+
"simple-git-hooks": "^2.13.1",
|
|
59
|
+
"tsx": "^4.19.4",
|
|
60
|
+
"typescript": "^5.8.3",
|
|
61
|
+
"typescript-eslint": "^8.60.1"
|
|
62
|
+
},
|
|
63
|
+
"simple-git-hooks": {
|
|
64
|
+
"pre-commit": "npm run check"
|
|
65
|
+
}
|
|
66
|
+
}
|