opencode-account-manager 0.6.4 → 0.6.6
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 +235 -216
- package/README_VI.md +235 -216
- package/dist/cli.js +83 -0
- package/dist/cli.js.map +1 -1
- package/dist/core/config-store.d.ts +12 -0
- package/dist/core/config-store.d.ts.map +1 -1
- package/dist/core/config-store.js +98 -0
- package/dist/core/config-store.js.map +1 -1
- package/dist/core/health-log.d.ts +9 -0
- package/dist/core/health-log.d.ts.map +1 -0
- package/dist/core/health-log.js +154 -0
- package/dist/core/health-log.js.map +1 -0
- package/dist/core/health-oauth.d.ts +5 -0
- package/dist/core/health-oauth.d.ts.map +1 -0
- package/dist/core/health-oauth.js +147 -0
- package/dist/core/health-oauth.js.map +1 -0
- package/dist/core/health-orchestrator.d.ts +32 -0
- package/dist/core/health-orchestrator.d.ts.map +1 -0
- package/dist/core/health-orchestrator.js +148 -0
- package/dist/core/health-orchestrator.js.map +1 -0
- package/dist/core/health-utils.d.ts +15 -0
- package/dist/core/health-utils.d.ts.map +1 -0
- package/dist/core/health-utils.js +60 -0
- package/dist/core/health-utils.js.map +1 -0
- package/dist/core/paths.d.ts +1 -0
- package/dist/core/paths.d.ts.map +1 -1
- package/dist/core/paths.js +4 -0
- package/dist/core/paths.js.map +1 -1
- package/dist/core/types.d.ts +26 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/tui/Dashboard.d.ts.map +1 -1
- package/dist/tui/Dashboard.js +69 -2
- package/dist/tui/Dashboard.js.map +1 -1
- package/dist/tui/components/AccountList.d.ts +5 -3
- package/dist/tui/components/AccountList.d.ts.map +1 -1
- package/dist/tui/components/AccountList.js +9 -3
- package/dist/tui/components/AccountList.js.map +1 -1
- package/dist/tui/components/DashboardView.d.ts +3 -2
- package/dist/tui/components/DashboardView.d.ts.map +1 -1
- package/dist/tui/components/DashboardView.js +102 -17
- package/dist/tui/components/DashboardView.js.map +1 -1
- package/dist/tui/components/HealthBadge.d.ts +9 -0
- package/dist/tui/components/HealthBadge.d.ts.map +1 -0
- package/dist/tui/components/HealthBadge.js +56 -0
- package/dist/tui/components/HealthBadge.js.map +1 -0
- package/dist/tui/components/StatusBadge.d.ts +2 -1
- package/dist/tui/components/StatusBadge.d.ts.map +1 -1
- package/dist/tui/components/StatusBadge.js +30 -2
- package/dist/tui/components/StatusBadge.js.map +1 -1
- package/dist/tui/components/index.d.ts +1 -0
- package/dist/tui/components/index.d.ts.map +1 -1
- package/dist/tui/components/index.js +3 -1
- package/dist/tui/components/index.js.map +1 -1
- package/docs/BLUEPRINT.md +476 -476
- package/docs/ROADMAP.md +125 -107
- package/package.json +38 -38
- package/src/cli.ts +139 -38
- package/src/core/config-store.ts +278 -171
- package/src/core/crypto.ts +162 -162
- package/src/core/health-log.ts +173 -0
- package/src/core/health-oauth.ts +190 -0
- package/src/core/health-orchestrator.ts +224 -0
- package/src/core/importers/amExport.ts +177 -177
- package/src/core/opencode-config.ts +217 -217
- package/src/core/paths.ts +10 -6
- package/src/core/types.ts +193 -147
- package/src/tui/Dashboard.tsx +557 -478
- package/src/tui/components/AccountList.tsx +122 -104
- package/src/tui/components/ActionPalette.tsx +117 -117
- package/src/tui/components/Box.tsx +7 -7
- package/src/tui/components/DashboardView.tsx +324 -220
- package/src/tui/components/ExportModal.tsx +255 -255
- package/src/tui/components/FileBrowser.tsx +393 -393
- package/src/tui/components/Header.tsx +26 -26
- package/src/tui/components/HealthBadge.tsx +64 -0
- package/src/tui/components/ImportModal.tsx +334 -334
- package/src/tui/components/McpServerList.tsx +67 -67
- package/src/tui/components/Menu.tsx +61 -61
- package/src/tui/components/PasswordInput.tsx +159 -159
- package/src/tui/components/ProviderList.tsx +59 -59
- package/src/tui/components/SectionBox.tsx +35 -35
- package/src/tui/components/StatsRow.tsx +33 -33
- package/src/tui/components/StatusBadge.tsx +36 -3
- package/src/tui/components/index.ts +15 -14
- package/test-minimal.js +26 -26
- package/test-with-accounts.js +58 -58
|
@@ -1,67 +1,67 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { Box, Text } from "ink";
|
|
3
|
-
import { McpServerInfo } from "../../core/opencode-config";
|
|
4
|
-
|
|
5
|
-
interface McpServerListProps {
|
|
6
|
-
servers: McpServerInfo[];
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function McpServerList({ servers }: McpServerListProps) {
|
|
10
|
-
if (servers.length === 0) {
|
|
11
|
-
return (
|
|
12
|
-
<Box paddingX={1}>
|
|
13
|
-
<Text dimColor>No MCP servers configured</Text>
|
|
14
|
-
</Box>
|
|
15
|
-
);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
return (
|
|
19
|
-
<Box flexDirection="column" paddingX={1}>
|
|
20
|
-
{/* Header */}
|
|
21
|
-
<Box>
|
|
22
|
-
<Box width={20}>
|
|
23
|
-
<Text bold dimColor>SERVER</Text>
|
|
24
|
-
</Box>
|
|
25
|
-
<Box width={10}>
|
|
26
|
-
<Text bold dimColor>STATUS</Text>
|
|
27
|
-
</Box>
|
|
28
|
-
<Box width={8}>
|
|
29
|
-
<Text bold dimColor>ENV</Text>
|
|
30
|
-
</Box>
|
|
31
|
-
<Box>
|
|
32
|
-
<Text bold dimColor>COMMAND</Text>
|
|
33
|
-
</Box>
|
|
34
|
-
</Box>
|
|
35
|
-
|
|
36
|
-
{/* Rows */}
|
|
37
|
-
{servers.map((server) => (
|
|
38
|
-
<Box key={server.id}>
|
|
39
|
-
<Box width={20}>
|
|
40
|
-
<Text>{truncate(server.id, 18)}</Text>
|
|
41
|
-
</Box>
|
|
42
|
-
<Box width={10}>
|
|
43
|
-
{server.enabled ? (
|
|
44
|
-
<Text>enabled</Text>
|
|
45
|
-
) : (
|
|
46
|
-
<Text dimColor>disabled</Text>
|
|
47
|
-
)}
|
|
48
|
-
</Box>
|
|
49
|
-
<Box width={8}>
|
|
50
|
-
{server.hasEnvVars ? (
|
|
51
|
-
<Text>{server.envVarCount}</Text>
|
|
52
|
-
) : (
|
|
53
|
-
<Text dimColor>-</Text>
|
|
54
|
-
)}
|
|
55
|
-
</Box>
|
|
56
|
-
<Box>
|
|
57
|
-
<Text dimColor>{truncate(server.command, 40)}</Text>
|
|
58
|
-
</Box>
|
|
59
|
-
</Box>
|
|
60
|
-
))}
|
|
61
|
-
</Box>
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function truncate(str: string, len: number): string {
|
|
66
|
-
return str.length > len ? str.slice(0, len - 1) + "…" : str;
|
|
67
|
-
}
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { McpServerInfo } from "../../core/opencode-config";
|
|
4
|
+
|
|
5
|
+
interface McpServerListProps {
|
|
6
|
+
servers: McpServerInfo[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function McpServerList({ servers }: McpServerListProps) {
|
|
10
|
+
if (servers.length === 0) {
|
|
11
|
+
return (
|
|
12
|
+
<Box paddingX={1}>
|
|
13
|
+
<Text dimColor>No MCP servers configured</Text>
|
|
14
|
+
</Box>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Box flexDirection="column" paddingX={1}>
|
|
20
|
+
{/* Header */}
|
|
21
|
+
<Box>
|
|
22
|
+
<Box width={20}>
|
|
23
|
+
<Text bold dimColor>SERVER</Text>
|
|
24
|
+
</Box>
|
|
25
|
+
<Box width={10}>
|
|
26
|
+
<Text bold dimColor>STATUS</Text>
|
|
27
|
+
</Box>
|
|
28
|
+
<Box width={8}>
|
|
29
|
+
<Text bold dimColor>ENV</Text>
|
|
30
|
+
</Box>
|
|
31
|
+
<Box>
|
|
32
|
+
<Text bold dimColor>COMMAND</Text>
|
|
33
|
+
</Box>
|
|
34
|
+
</Box>
|
|
35
|
+
|
|
36
|
+
{/* Rows */}
|
|
37
|
+
{servers.map((server) => (
|
|
38
|
+
<Box key={server.id}>
|
|
39
|
+
<Box width={20}>
|
|
40
|
+
<Text>{truncate(server.id, 18)}</Text>
|
|
41
|
+
</Box>
|
|
42
|
+
<Box width={10}>
|
|
43
|
+
{server.enabled ? (
|
|
44
|
+
<Text>enabled</Text>
|
|
45
|
+
) : (
|
|
46
|
+
<Text dimColor>disabled</Text>
|
|
47
|
+
)}
|
|
48
|
+
</Box>
|
|
49
|
+
<Box width={8}>
|
|
50
|
+
{server.hasEnvVars ? (
|
|
51
|
+
<Text>{server.envVarCount}</Text>
|
|
52
|
+
) : (
|
|
53
|
+
<Text dimColor>-</Text>
|
|
54
|
+
)}
|
|
55
|
+
</Box>
|
|
56
|
+
<Box>
|
|
57
|
+
<Text dimColor>{truncate(server.command, 40)}</Text>
|
|
58
|
+
</Box>
|
|
59
|
+
</Box>
|
|
60
|
+
))}
|
|
61
|
+
</Box>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function truncate(str: string, len: number): string {
|
|
66
|
+
return str.length > len ? str.slice(0, len - 1) + "…" : str;
|
|
67
|
+
}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Box, Text, useInput } from "ink";
|
|
3
3
|
|
|
4
|
-
export type MenuAction =
|
|
5
|
-
| "refresh"
|
|
6
|
-
| "export"
|
|
7
|
-
| "export-selected"
|
|
8
|
-
| "import-file"
|
|
9
|
-
| "import-am"
|
|
10
|
-
| "toggle-select-mode"
|
|
11
|
-
| "select-all"
|
|
12
|
-
| "select-none"
|
|
13
|
-
| "enable-selected"
|
|
14
|
-
| "disable-selected"
|
|
15
|
-
| "delete-selected"
|
|
16
|
-
| "quit";
|
|
4
|
+
export type MenuAction =
|
|
5
|
+
| "refresh"
|
|
6
|
+
| "export"
|
|
7
|
+
| "export-selected"
|
|
8
|
+
| "import-file"
|
|
9
|
+
| "import-am"
|
|
10
|
+
| "toggle-select-mode"
|
|
11
|
+
| "select-all"
|
|
12
|
+
| "select-none"
|
|
13
|
+
| "enable-selected"
|
|
14
|
+
| "disable-selected"
|
|
15
|
+
| "delete-selected"
|
|
16
|
+
| "quit";
|
|
17
17
|
|
|
18
18
|
interface MenuItem {
|
|
19
19
|
label: string;
|
|
@@ -23,21 +23,21 @@ interface MenuItem {
|
|
|
23
23
|
normalModeOnly?: boolean;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
const MENU_ITEMS: MenuItem[] = [
|
|
27
|
-
{ label: "Refresh", key: "R", action: "refresh" },
|
|
28
|
-
{ label: "Export", key: "E", action: "export", normalModeOnly: true },
|
|
29
|
-
{ label: "Import", key: "I", action: "import-file", normalModeOnly: true },
|
|
30
|
-
{ label: "AM Import", key: "A", action: "import-am", normalModeOnly: true },
|
|
31
|
-
{ label: "Select Mode", key: "S", action: "toggle-select-mode", normalModeOnly: true },
|
|
32
|
-
{ label: "Exit Select", key: "S", action: "toggle-select-mode", selectModeOnly: true },
|
|
33
|
-
{ label: "All", key: "A", action: "select-all", selectModeOnly: true },
|
|
34
|
-
{ label: "None", key: "N", action: "select-none", selectModeOnly: true },
|
|
35
|
-
{ label: "Enable", key: "E", action: "enable-selected", selectModeOnly: true },
|
|
36
|
-
{ label: "Disable", key: "D", action: "disable-selected", selectModeOnly: true },
|
|
37
|
-
{ label: "Export", key: "X", action: "export-selected", selectModeOnly: true },
|
|
38
|
-
{ label: "Delete", key: "DEL", action: "delete-selected", selectModeOnly: true },
|
|
39
|
-
{ label: "Quit", key: "Q", action: "quit" },
|
|
40
|
-
];
|
|
26
|
+
const MENU_ITEMS: MenuItem[] = [
|
|
27
|
+
{ label: "Refresh", key: "R", action: "refresh" },
|
|
28
|
+
{ label: "Export", key: "E", action: "export", normalModeOnly: true },
|
|
29
|
+
{ label: "Import", key: "I", action: "import-file", normalModeOnly: true },
|
|
30
|
+
{ label: "AM Import", key: "A", action: "import-am", normalModeOnly: true },
|
|
31
|
+
{ label: "Select Mode", key: "S", action: "toggle-select-mode", normalModeOnly: true },
|
|
32
|
+
{ label: "Exit Select", key: "S", action: "toggle-select-mode", selectModeOnly: true },
|
|
33
|
+
{ label: "All", key: "A", action: "select-all", selectModeOnly: true },
|
|
34
|
+
{ label: "None", key: "N", action: "select-none", selectModeOnly: true },
|
|
35
|
+
{ label: "Enable", key: "E", action: "enable-selected", selectModeOnly: true },
|
|
36
|
+
{ label: "Disable", key: "D", action: "disable-selected", selectModeOnly: true },
|
|
37
|
+
{ label: "Export", key: "X", action: "export-selected", selectModeOnly: true },
|
|
38
|
+
{ label: "Delete", key: "DEL", action: "delete-selected", selectModeOnly: true },
|
|
39
|
+
{ label: "Quit", key: "Q", action: "quit" },
|
|
40
|
+
];
|
|
41
41
|
|
|
42
42
|
interface MenuBarProps {
|
|
43
43
|
onSelect: (action: MenuAction) => void;
|
|
@@ -46,30 +46,30 @@ interface MenuBarProps {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
export function MenuBar({ onSelect, selectMode = false, selectedCount = 0 }: MenuBarProps) {
|
|
49
|
-
useInput((input, key) => {
|
|
50
|
-
const lower = input.toLowerCase();
|
|
51
|
-
|
|
52
|
-
if (selectMode) {
|
|
53
|
-
// Select mode keys
|
|
54
|
-
if (lower === "s" || key.escape) onSelect("toggle-select-mode");
|
|
55
|
-
if (lower === "a") onSelect("select-all");
|
|
56
|
-
if (lower === "n") onSelect("select-none");
|
|
57
|
-
if (lower === "e") onSelect("enable-selected");
|
|
58
|
-
if (lower === "d") onSelect("disable-selected");
|
|
59
|
-
if (lower === "x") onSelect("export-selected");
|
|
60
|
-
if (key.delete || lower === "backspace") onSelect("delete-selected");
|
|
61
|
-
if (lower === "r") onSelect("refresh");
|
|
62
|
-
if (lower === "q") onSelect("quit");
|
|
63
|
-
} else {
|
|
64
|
-
// Normal mode keys
|
|
65
|
-
if (lower === "r") onSelect("refresh");
|
|
66
|
-
if (lower === "e") onSelect("export");
|
|
67
|
-
if (lower === "i") onSelect("import-file");
|
|
68
|
-
if (lower === "a") onSelect("import-am");
|
|
69
|
-
if (lower === "s") onSelect("toggle-select-mode");
|
|
70
|
-
if (lower === "q" || key.escape) onSelect("quit");
|
|
71
|
-
}
|
|
72
|
-
});
|
|
49
|
+
useInput((input, key) => {
|
|
50
|
+
const lower = input.toLowerCase();
|
|
51
|
+
|
|
52
|
+
if (selectMode) {
|
|
53
|
+
// Select mode keys
|
|
54
|
+
if (lower === "s" || key.escape) onSelect("toggle-select-mode");
|
|
55
|
+
if (lower === "a") onSelect("select-all");
|
|
56
|
+
if (lower === "n") onSelect("select-none");
|
|
57
|
+
if (lower === "e") onSelect("enable-selected");
|
|
58
|
+
if (lower === "d") onSelect("disable-selected");
|
|
59
|
+
if (lower === "x") onSelect("export-selected");
|
|
60
|
+
if (key.delete || lower === "backspace") onSelect("delete-selected");
|
|
61
|
+
if (lower === "r") onSelect("refresh");
|
|
62
|
+
if (lower === "q") onSelect("quit");
|
|
63
|
+
} else {
|
|
64
|
+
// Normal mode keys
|
|
65
|
+
if (lower === "r") onSelect("refresh");
|
|
66
|
+
if (lower === "e") onSelect("export");
|
|
67
|
+
if (lower === "i") onSelect("import-file");
|
|
68
|
+
if (lower === "a") onSelect("import-am");
|
|
69
|
+
if (lower === "s") onSelect("toggle-select-mode");
|
|
70
|
+
if (lower === "q" || key.escape) onSelect("quit");
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
73
|
|
|
74
74
|
const visibleItems = MENU_ITEMS.filter(item => {
|
|
75
75
|
if (selectMode && item.normalModeOnly) return false;
|
|
@@ -78,17 +78,17 @@ export function MenuBar({ onSelect, selectMode = false, selectedCount = 0 }: Men
|
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
return (
|
|
81
|
-
<Box flexDirection="column">
|
|
82
|
-
{selectMode && (
|
|
83
|
-
<Box marginBottom={1} paddingX={1}>
|
|
84
|
-
<Text dimColor bold>
|
|
85
|
-
SELECT MODE - {selectedCount} selected | ↑↓ navigate | SPACE toggle | ←→ switch section
|
|
86
|
-
</Text>
|
|
87
|
-
</Box>
|
|
88
|
-
)}
|
|
81
|
+
<Box flexDirection="column">
|
|
82
|
+
{selectMode && (
|
|
83
|
+
<Box marginBottom={1} paddingX={1}>
|
|
84
|
+
<Text dimColor bold>
|
|
85
|
+
SELECT MODE - {selectedCount} selected | ↑↓ navigate | SPACE toggle | ←→ switch section
|
|
86
|
+
</Text>
|
|
87
|
+
</Box>
|
|
88
|
+
)}
|
|
89
89
|
<Box
|
|
90
90
|
borderStyle="single"
|
|
91
|
-
borderColor={selectMode ? "white" : "gray"}
|
|
91
|
+
borderColor={selectMode ? "white" : "gray"}
|
|
92
92
|
paddingX={1}
|
|
93
93
|
justifyContent="space-between"
|
|
94
94
|
>
|
|
@@ -1,159 +1,159 @@
|
|
|
1
|
-
import React, { useState } from "react";
|
|
2
|
-
import { Box, Text, useInput } from "ink";
|
|
3
|
-
|
|
4
|
-
interface PasswordInputProps {
|
|
5
|
-
mode: "single" | "confirm";
|
|
6
|
-
title?: string;
|
|
7
|
-
subtitle?: string;
|
|
8
|
-
warning?: string;
|
|
9
|
-
onSubmit: (password: string) => void;
|
|
10
|
-
onCancel: () => void;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function PasswordInput({
|
|
14
|
-
mode,
|
|
15
|
-
title = "Enter Password",
|
|
16
|
-
subtitle,
|
|
17
|
-
warning,
|
|
18
|
-
onSubmit,
|
|
19
|
-
onCancel,
|
|
20
|
-
}: PasswordInputProps) {
|
|
21
|
-
const [password, setPassword] = useState("");
|
|
22
|
-
const [confirmPassword, setConfirmPassword] = useState("");
|
|
23
|
-
const [activeField, setActiveField] = useState<"password" | "confirm">("password");
|
|
24
|
-
const [error, setError] = useState<string | null>(null);
|
|
25
|
-
|
|
26
|
-
useInput((input, key) => {
|
|
27
|
-
// Handle escape
|
|
28
|
-
if (key.escape) {
|
|
29
|
-
onCancel();
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Handle tab to switch fields (in confirm mode)
|
|
34
|
-
if (key.tab && mode === "confirm") {
|
|
35
|
-
setActiveField(prev => prev === "password" ? "confirm" : "password");
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Handle enter
|
|
40
|
-
if (key.return) {
|
|
41
|
-
if (mode === "confirm" && activeField === "password") {
|
|
42
|
-
// Move to confirm field
|
|
43
|
-
setActiveField("confirm");
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Validate
|
|
48
|
-
if (password.length === 0) {
|
|
49
|
-
setError("Password cannot be empty");
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (mode === "confirm" && password !== confirmPassword) {
|
|
54
|
-
setError("Passwords do not match");
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
onSubmit(password);
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Handle backspace
|
|
63
|
-
if (key.backspace || key.delete) {
|
|
64
|
-
if (activeField === "password") {
|
|
65
|
-
setPassword(prev => prev.slice(0, -1));
|
|
66
|
-
} else {
|
|
67
|
-
setConfirmPassword(prev => prev.slice(0, -1));
|
|
68
|
-
}
|
|
69
|
-
setError(null);
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Handle regular input (printable characters)
|
|
74
|
-
if (input && input.length === 1 && !key.ctrl && !key.meta) {
|
|
75
|
-
if (activeField === "password") {
|
|
76
|
-
setPassword(prev => prev + input);
|
|
77
|
-
} else {
|
|
78
|
-
setConfirmPassword(prev => prev + input);
|
|
79
|
-
}
|
|
80
|
-
setError(null);
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
const maskPassword = (pwd: string): string => {
|
|
85
|
-
return "•".repeat(pwd.length);
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
return (
|
|
89
|
-
<Box
|
|
90
|
-
flexDirection="column"
|
|
91
|
-
borderStyle="round"
|
|
92
|
-
borderColor="gray"
|
|
93
|
-
paddingX={2}
|
|
94
|
-
paddingY={1}
|
|
95
|
-
>
|
|
96
|
-
{/* Title */}
|
|
97
|
-
<Box marginBottom={1}>
|
|
98
|
-
<Text bold>{title}</Text>
|
|
99
|
-
{subtitle && <Text dimColor> - {subtitle}</Text>}
|
|
100
|
-
</Box>
|
|
101
|
-
|
|
102
|
-
{/* Password field */}
|
|
103
|
-
<Box>
|
|
104
|
-
<Text dimColor>Password: </Text>
|
|
105
|
-
<Box
|
|
106
|
-
borderStyle={activeField === "password" ? "single" : undefined}
|
|
107
|
-
borderColor="yellow"
|
|
108
|
-
paddingX={1}
|
|
109
|
-
minWidth={30}
|
|
110
|
-
>
|
|
111
|
-
<Text color={activeField === "password" ? "white" : "gray"}>
|
|
112
|
-
{maskPassword(password)}
|
|
113
|
-
{activeField === "password" && <Text color="yellow">▌</Text>}
|
|
114
|
-
</Text>
|
|
115
|
-
</Box>
|
|
116
|
-
</Box>
|
|
117
|
-
|
|
118
|
-
{/* Confirm field (only in confirm mode) */}
|
|
119
|
-
{mode === "confirm" && (
|
|
120
|
-
<Box marginTop={1}>
|
|
121
|
-
<Text dimColor>Confirm: </Text>
|
|
122
|
-
<Box
|
|
123
|
-
borderStyle={activeField === "confirm" ? "single" : undefined}
|
|
124
|
-
borderColor="yellow"
|
|
125
|
-
paddingX={1}
|
|
126
|
-
minWidth={30}
|
|
127
|
-
>
|
|
128
|
-
<Text color={activeField === "confirm" ? "white" : "gray"}>
|
|
129
|
-
{maskPassword(confirmPassword)}
|
|
130
|
-
{activeField === "confirm" && <Text color="yellow">▌</Text>}
|
|
131
|
-
</Text>
|
|
132
|
-
</Box>
|
|
133
|
-
</Box>
|
|
134
|
-
)}
|
|
135
|
-
|
|
136
|
-
{/* Warning */}
|
|
137
|
-
{warning && (
|
|
138
|
-
<Box marginTop={1}>
|
|
139
|
-
<Text color="yellow">⚠️ {warning}</Text>
|
|
140
|
-
</Box>
|
|
141
|
-
)}
|
|
142
|
-
|
|
143
|
-
{/* Error */}
|
|
144
|
-
{error && (
|
|
145
|
-
<Box marginTop={1}>
|
|
146
|
-
<Text color="red">✗ {error}</Text>
|
|
147
|
-
</Box>
|
|
148
|
-
)}
|
|
149
|
-
|
|
150
|
-
{/* Help */}
|
|
151
|
-
<Box marginTop={1}>
|
|
152
|
-
<Text dimColor>
|
|
153
|
-
{mode === "confirm" && "[Tab] Switch field "}
|
|
154
|
-
[Enter] Confirm [Esc] Cancel
|
|
155
|
-
</Text>
|
|
156
|
-
</Box>
|
|
157
|
-
</Box>
|
|
158
|
-
);
|
|
159
|
-
}
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
|
|
4
|
+
interface PasswordInputProps {
|
|
5
|
+
mode: "single" | "confirm";
|
|
6
|
+
title?: string;
|
|
7
|
+
subtitle?: string;
|
|
8
|
+
warning?: string;
|
|
9
|
+
onSubmit: (password: string) => void;
|
|
10
|
+
onCancel: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function PasswordInput({
|
|
14
|
+
mode,
|
|
15
|
+
title = "Enter Password",
|
|
16
|
+
subtitle,
|
|
17
|
+
warning,
|
|
18
|
+
onSubmit,
|
|
19
|
+
onCancel,
|
|
20
|
+
}: PasswordInputProps) {
|
|
21
|
+
const [password, setPassword] = useState("");
|
|
22
|
+
const [confirmPassword, setConfirmPassword] = useState("");
|
|
23
|
+
const [activeField, setActiveField] = useState<"password" | "confirm">("password");
|
|
24
|
+
const [error, setError] = useState<string | null>(null);
|
|
25
|
+
|
|
26
|
+
useInput((input, key) => {
|
|
27
|
+
// Handle escape
|
|
28
|
+
if (key.escape) {
|
|
29
|
+
onCancel();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Handle tab to switch fields (in confirm mode)
|
|
34
|
+
if (key.tab && mode === "confirm") {
|
|
35
|
+
setActiveField(prev => prev === "password" ? "confirm" : "password");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Handle enter
|
|
40
|
+
if (key.return) {
|
|
41
|
+
if (mode === "confirm" && activeField === "password") {
|
|
42
|
+
// Move to confirm field
|
|
43
|
+
setActiveField("confirm");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Validate
|
|
48
|
+
if (password.length === 0) {
|
|
49
|
+
setError("Password cannot be empty");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (mode === "confirm" && password !== confirmPassword) {
|
|
54
|
+
setError("Passwords do not match");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
onSubmit(password);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Handle backspace
|
|
63
|
+
if (key.backspace || key.delete) {
|
|
64
|
+
if (activeField === "password") {
|
|
65
|
+
setPassword(prev => prev.slice(0, -1));
|
|
66
|
+
} else {
|
|
67
|
+
setConfirmPassword(prev => prev.slice(0, -1));
|
|
68
|
+
}
|
|
69
|
+
setError(null);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Handle regular input (printable characters)
|
|
74
|
+
if (input && input.length === 1 && !key.ctrl && !key.meta) {
|
|
75
|
+
if (activeField === "password") {
|
|
76
|
+
setPassword(prev => prev + input);
|
|
77
|
+
} else {
|
|
78
|
+
setConfirmPassword(prev => prev + input);
|
|
79
|
+
}
|
|
80
|
+
setError(null);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const maskPassword = (pwd: string): string => {
|
|
85
|
+
return "•".repeat(pwd.length);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<Box
|
|
90
|
+
flexDirection="column"
|
|
91
|
+
borderStyle="round"
|
|
92
|
+
borderColor="gray"
|
|
93
|
+
paddingX={2}
|
|
94
|
+
paddingY={1}
|
|
95
|
+
>
|
|
96
|
+
{/* Title */}
|
|
97
|
+
<Box marginBottom={1}>
|
|
98
|
+
<Text bold>{title}</Text>
|
|
99
|
+
{subtitle && <Text dimColor> - {subtitle}</Text>}
|
|
100
|
+
</Box>
|
|
101
|
+
|
|
102
|
+
{/* Password field */}
|
|
103
|
+
<Box>
|
|
104
|
+
<Text dimColor>Password: </Text>
|
|
105
|
+
<Box
|
|
106
|
+
borderStyle={activeField === "password" ? "single" : undefined}
|
|
107
|
+
borderColor="yellow"
|
|
108
|
+
paddingX={1}
|
|
109
|
+
minWidth={30}
|
|
110
|
+
>
|
|
111
|
+
<Text color={activeField === "password" ? "white" : "gray"}>
|
|
112
|
+
{maskPassword(password)}
|
|
113
|
+
{activeField === "password" && <Text color="yellow">▌</Text>}
|
|
114
|
+
</Text>
|
|
115
|
+
</Box>
|
|
116
|
+
</Box>
|
|
117
|
+
|
|
118
|
+
{/* Confirm field (only in confirm mode) */}
|
|
119
|
+
{mode === "confirm" && (
|
|
120
|
+
<Box marginTop={1}>
|
|
121
|
+
<Text dimColor>Confirm: </Text>
|
|
122
|
+
<Box
|
|
123
|
+
borderStyle={activeField === "confirm" ? "single" : undefined}
|
|
124
|
+
borderColor="yellow"
|
|
125
|
+
paddingX={1}
|
|
126
|
+
minWidth={30}
|
|
127
|
+
>
|
|
128
|
+
<Text color={activeField === "confirm" ? "white" : "gray"}>
|
|
129
|
+
{maskPassword(confirmPassword)}
|
|
130
|
+
{activeField === "confirm" && <Text color="yellow">▌</Text>}
|
|
131
|
+
</Text>
|
|
132
|
+
</Box>
|
|
133
|
+
</Box>
|
|
134
|
+
)}
|
|
135
|
+
|
|
136
|
+
{/* Warning */}
|
|
137
|
+
{warning && (
|
|
138
|
+
<Box marginTop={1}>
|
|
139
|
+
<Text color="yellow">⚠️ {warning}</Text>
|
|
140
|
+
</Box>
|
|
141
|
+
)}
|
|
142
|
+
|
|
143
|
+
{/* Error */}
|
|
144
|
+
{error && (
|
|
145
|
+
<Box marginTop={1}>
|
|
146
|
+
<Text color="red">✗ {error}</Text>
|
|
147
|
+
</Box>
|
|
148
|
+
)}
|
|
149
|
+
|
|
150
|
+
{/* Help */}
|
|
151
|
+
<Box marginTop={1}>
|
|
152
|
+
<Text dimColor>
|
|
153
|
+
{mode === "confirm" && "[Tab] Switch field "}
|
|
154
|
+
[Enter] Confirm [Esc] Cancel
|
|
155
|
+
</Text>
|
|
156
|
+
</Box>
|
|
157
|
+
</Box>
|
|
158
|
+
);
|
|
159
|
+
}
|