@skillsgate/tui 0.1.14 → 0.2.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.
@@ -1,202 +0,0 @@
1
- import { useState, useCallback } from "react"
2
- import { useKeyboard } from "@opentui/react"
3
- import { useStore, useDispatch } from "../store/context.js"
4
- import { useAuth } from "../data/use-auth.js"
5
- import { API_BASE_URL } from "../../../cli/src/constants.js"
6
- import { colors } from "../utils/colors.js"
7
-
8
- type LoginStep = "prompt" | "code" | "exchanging"
9
-
10
- /**
11
- * Login view implementing the device code flow:
12
- * 1. Show instructions with the auth URL
13
- * 2. Prompt to open browser (y/n)
14
- * 3. Show input for device code (XXXX-XXXX)
15
- * 4. Exchange code for token, save auth, navigate back
16
- */
17
- export function LoginView() {
18
- const state = useStore()
19
- const dispatch = useDispatch()
20
- const { auth, login, logout } = useAuth()
21
-
22
- const [step, setStep] = useState<LoginStep>("prompt")
23
- const [error, setError] = useState<string | null>(null)
24
-
25
- const authUrl = `${API_BASE_URL}/cli/auth`
26
-
27
- function openBrowser() {
28
- try {
29
- const { exec } = require("node:child_process")
30
- const cmd = process.platform === "darwin" ? "open" : "xdg-open"
31
- exec(`${cmd} "${authUrl}"`)
32
- dispatch({
33
- type: "SHOW_NOTIFICATION",
34
- notification: { type: "info", message: "Opening browser..." },
35
- })
36
- } catch {
37
- // Best effort
38
- }
39
- }
40
-
41
- // Handle keyboard input
42
- useKeyboard((key) => {
43
- if (state.activeView !== "login") return
44
- if (state.showHelp) return
45
-
46
- // Esc to go back at any step
47
- if (key.name === "escape") {
48
- dispatch({ type: "GO_BACK" })
49
- return
50
- }
51
-
52
- if (step === "prompt") {
53
- // "r" to re-login (clear old auth, open browser, go to code step)
54
- if (key.name === "r") {
55
- logout()
56
- openBrowser()
57
- setStep("code")
58
- return
59
- }
60
-
61
- // "o" to logout only (no re-login)
62
- if (key.name === "o") {
63
- logout()
64
- dispatch({
65
- type: "SHOW_NOTIFICATION",
66
- notification: { type: "success", message: "Signed out" },
67
- })
68
- dispatch({ type: "GO_BACK" })
69
- return
70
- }
71
-
72
- // "y" to open browser and proceed to code input
73
- if (key.name === "y") {
74
- openBrowser()
75
- setStep("code")
76
- return
77
- }
78
-
79
- // "n" to skip browser, go straight to code input
80
- if (key.name === "n") {
81
- setStep("code")
82
- return
83
- }
84
- }
85
- })
86
-
87
- const handleCodeSubmit = useCallback(async (value: string) => {
88
- const code = value.trim()
89
- if (!code) return
90
-
91
- setStep("exchanging")
92
- setError(null)
93
-
94
- const errMsg = await login(code)
95
-
96
- if (errMsg) {
97
- setError(errMsg)
98
- setStep("code") // Let user retry
99
- } else {
100
- // Success - navigate back
101
- dispatch({
102
- type: "SHOW_NOTIFICATION",
103
- notification: { type: "success", message: "Logged in successfully!" },
104
- })
105
- dispatch({ type: "GO_BACK" })
106
- }
107
- }, [login, dispatch])
108
-
109
- // Already logged in -- offer re-login or logout
110
- if (auth && step === "prompt") {
111
- return (
112
- <box style={{ flexDirection: "column", padding: 2 }}>
113
- <text fg={colors.success}>
114
- Logged in as <strong>{auth.user.name}</strong> ({auth.user.email})
115
- </text>
116
- <text>{" "}</text>
117
- <text fg={colors.text}>
118
- If AI search isn't working, your session may have expired.
119
- </text>
120
- <text>{" "}</text>
121
- <text fg={colors.primary}>r</text>
122
- <text fg={colors.text}> Re-login with a fresh token</text>
123
- <text fg={colors.primary}>o</text>
124
- <text fg={colors.text}> Sign out</text>
125
- <text fg={colors.textDim}>Esc</text>
126
- <text fg={colors.text}> Go back</text>
127
- </box>
128
- )
129
- }
130
-
131
- return (
132
- <box style={{ flexDirection: "column", padding: 2 }}>
133
- {/* Title */}
134
- <text fg={colors.primary}>
135
- <strong>Sign in to SkillsGate</strong>
136
- </text>
137
- <text>{" "}</text>
138
-
139
- {/* Instructions */}
140
- <text fg={colors.text}>
141
- Visit the following URL in your browser to get a login code:
142
- </text>
143
- <text>{" "}</text>
144
- <text fg={colors.primary}>
145
- {authUrl}
146
- </text>
147
- <text>{" "}</text>
148
-
149
- {step === "prompt" && (
150
- <>
151
- <text fg={colors.text}>
152
- Open browser? <span fg={colors.textDim}>(y/n)</span>
153
- </text>
154
- </>
155
- )}
156
-
157
- {step === "code" && (
158
- <>
159
- <text fg={colors.text}>
160
- Paste the code from the browser:
161
- </text>
162
- <text>{" "}</text>
163
- <box
164
- style={{
165
- height: 3,
166
- width: 40,
167
- border: true,
168
- borderColor: colors.primary,
169
- paddingLeft: 1,
170
- paddingRight: 1,
171
- }}
172
- title="Code"
173
- >
174
- <input
175
- placeholder="XXXX-XXXX"
176
- focused={state.activeView === "login" && step === "code" && !state.showHelp}
177
- onSubmit={handleCodeSubmit as any}
178
- />
179
- </box>
180
- <text>{" "}</text>
181
- <text fg={colors.textDim}>
182
- Press Enter to submit, Esc to cancel
183
- </text>
184
- </>
185
- )}
186
-
187
- {step === "exchanging" && (
188
- <text fg={colors.primary}>
189
- Verifying code...
190
- </text>
191
- )}
192
-
193
- {/* Error message */}
194
- {error && (
195
- <>
196
- <text>{" "}</text>
197
- <text fg={colors.error}>{error}</text>
198
- </>
199
- )}
200
- </box>
201
- )
202
- }