pinokiod 7.3.10 → 7.3.12

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.
Files changed (51) hide show
  1. package/kernel/bin/brew.js +12 -2
  2. package/kernel/bin/caddy.js +24 -20
  3. package/kernel/bin/huggingface.js +2 -2
  4. package/kernel/bin/setup.js +2 -3
  5. package/kernel/bin/uv.js +13 -6
  6. package/kernel/connect/index.js +5 -1
  7. package/kernel/connect/providers/huggingface/index.js +213 -75
  8. package/kernel/environment.js +16 -1
  9. package/kernel/router/localhost_home_router.js +7 -0
  10. package/kernel/shell.js +1 -5
  11. package/kernel/util.js +1 -0
  12. package/package.json +1 -1
  13. package/server/index.js +75 -33
  14. package/server/public/common.js +45 -71
  15. package/server/public/install.js +20 -2
  16. package/server/public/layout.js +1 -1
  17. package/server/public/style.css +1432 -511
  18. package/server/public/tab-link-popover.css +162 -18
  19. package/server/public/tab-link-popover.js +230 -21
  20. package/server/public/task-launcher.css +121 -84
  21. package/server/public/terminal-settings.js +121 -45
  22. package/server/public/universal-launcher.css +42 -33
  23. package/server/public/urldropdown.css +284 -0
  24. package/server/views/app.ejs +1608 -321
  25. package/server/views/autolaunch.ejs +4 -5
  26. package/server/views/checkpoints.ejs +223 -50
  27. package/server/views/connect/huggingface.ejs +406 -325
  28. package/server/views/connect.ejs +0 -1
  29. package/server/views/github.ejs +277 -324
  30. package/server/views/index.ejs +53 -1
  31. package/server/views/install.ejs +119 -19
  32. package/server/views/logs.ejs +10 -9
  33. package/server/views/net.ejs +341 -64
  34. package/server/views/network.ejs +85 -63
  35. package/server/views/partials/main_sidebar.ejs +237 -12
  36. package/server/views/plugins.ejs +141 -3
  37. package/server/views/settings.ejs +103 -7
  38. package/server/views/setup.ejs +0 -5
  39. package/server/views/skills.ejs +0 -1
  40. package/server/views/task_list.ejs +0 -1
  41. package/server/views/terminal.ejs +142 -1
  42. package/server/views/terminals.ejs +346 -6
  43. package/server/views/tools.ejs +828 -1691
  44. package/test/caddy-install.test.js +53 -0
  45. package/test/connect-setup.test.js +16 -0
  46. package/test/github-connection.test.js +1 -1
  47. package/test/huggingface-bin.test.js +4 -4
  48. package/test/huggingface-connect.test.js +73 -0
  49. package/test/main-sidebar.test.js +31 -0
  50. package/test/shell-run-template.test.js +5 -1
  51. package/test/uv-bin.test.js +29 -0
@@ -1,350 +1,431 @@
1
- <!DOCTYPE html>
1
+ <!doctype html>
2
2
  <html lang="en">
3
3
  <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Minimal Hugging Face OAuth</title>
7
- <style>
8
- body {
9
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
10
- background: #f8fafc;
11
- }
12
- .container {
13
- background: white;
14
- padding: 30px;
15
- border-radius: 10px;
16
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
17
- }
18
- h1 {
19
- color: #1f2937;
20
- margin-bottom: 20px;
21
- }
22
- .btn {
23
- background: #ff6b35;
24
- color: white;
25
- border: none;
26
- padding: 12px 24px;
27
- border-radius: 6px;
28
- cursor: pointer;
29
- font-size: 16px;
30
- margin: 10px 5px 10px 0;
31
- }
32
- .btn:hover {
33
- background: #e55a2b;
34
- }
35
- .btn.secondary {
36
- background: #6b7280;
37
- }
38
- .btn.secondary:hover {
39
- background: #4b5563;
40
- }
41
- .user-info {
42
- background: #f0f9ff;
43
- padding: 15px;
44
- border-radius: 6px;
45
- border-left: 4px solid #3b82f6;
46
- margin-top: 20px;
47
- }
48
- .status {
49
- padding: 12px;
50
- margin: 10px 0;
51
- border-radius: 6px;
52
- font-weight: 500;
53
- }
54
- .status.success { background: #d1fae5; color: #065f46; }
55
- .status.error { background: #fee2e2; color: #991b1b; }
56
- .status.warning { background: #fef3c7; color: #92400e; }
57
- .hidden { display: none; }
58
- </style>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Hugging Face Login</title>
7
+ <link href="/css/fontawesome.min.css" rel="stylesheet">
8
+ <link href="/css/solid.min.css" rel="stylesheet">
9
+ <link href="/css/regular.min.css" rel="stylesheet">
10
+ <link href="/css/brands.min.css" rel="stylesheet">
11
+ <link href="/noty.css" rel="stylesheet">
12
+ <link href="/style.css" rel="stylesheet">
13
+ <link href="/task-launcher.css" rel="stylesheet">
14
+ <% if (agent === "electron") { %>
15
+ <link href="/electron.css" rel="stylesheet"/>
16
+ <% } %>
17
+ <style>
18
+ .hf-login-shell {
19
+ max-width: 980px;
20
+ }
21
+ .hf-state-panel {
22
+ display: flex;
23
+ align-items: flex-start;
24
+ gap: 14px;
25
+ }
26
+ .hf-state-icon {
27
+ width: 36px;
28
+ height: 36px;
29
+ border-radius: 8px;
30
+ display: inline-flex;
31
+ align-items: center;
32
+ justify-content: center;
33
+ flex: 0 0 auto;
34
+ background: var(--task-chip-bg, var(--task-soft));
35
+ border: 1px solid var(--task-border);
36
+ color: var(--task-text);
37
+ }
38
+ .hf-state-copy {
39
+ min-width: 0;
40
+ }
41
+ .hf-state-title {
42
+ margin: 0;
43
+ color: var(--task-text);
44
+ font-size: 18px;
45
+ font-weight: 800;
46
+ line-height: 1.25;
47
+ letter-spacing: 0;
48
+ }
49
+ .hf-state-detail {
50
+ margin: 8px 0 0;
51
+ color: var(--task-muted);
52
+ font-size: 14px;
53
+ line-height: 1.55;
54
+ }
55
+ .hf-action-row {
56
+ display: flex;
57
+ flex-wrap: wrap;
58
+ gap: 10px;
59
+ margin-top: 20px;
60
+ }
61
+ .hf-code-panel {
62
+ display: grid;
63
+ gap: 10px;
64
+ max-width: 520px;
65
+ margin-top: 18px;
66
+ padding: 14px;
67
+ border: 1px solid var(--task-border);
68
+ border-radius: 8px;
69
+ background: var(--task-panel);
70
+ }
71
+ .hf-code-panel.hidden {
72
+ display: none;
73
+ }
74
+ .hf-code-label {
75
+ margin: 0;
76
+ color: var(--task-muted);
77
+ font-size: 13px;
78
+ line-height: 1.45;
79
+ }
80
+ .hf-code-row {
81
+ display: flex;
82
+ flex-wrap: wrap;
83
+ align-items: center;
84
+ gap: 10px;
85
+ }
86
+ .hf-code-value {
87
+ display: inline-flex;
88
+ align-items: center;
89
+ padding: 9px 11px;
90
+ border: 1px solid var(--task-border);
91
+ border-radius: 6px;
92
+ color: var(--task-text);
93
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
94
+ font-size: 15px;
95
+ font-weight: 800;
96
+ letter-spacing: 0.08em;
97
+ }
98
+ .hf-error {
99
+ margin-top: 16px;
100
+ }
101
+ </style>
59
102
  </head>
60
- <body>
61
- <div class="container">
62
- <h1>🤗 Hugging Face OAuth</h1>
63
-
64
- <div id="status" class="status warning">
65
- Checking authentication status...
66
- </div>
67
-
68
- <!-- Login Section -->
69
- <div id="login-section">
70
- <p>Click below to authenticate with Hugging Face:</p>
71
- <button class="btn" onclick="login()">Login with Hugging Face</button>
72
- </div>
73
-
74
- <!-- User Section -->
75
- <div id="user-section" class="hidden">
76
- <div class="user-info">
77
- <h3>Welcome!</h3>
78
- <div id="user-details"></div>
79
- <button class="btn secondary" onclick="logout()">Logout</button>
103
+ <%
104
+ const selected = "connect";
105
+ %>
106
+ <body class="<%= theme %> main-sidebar-page task-launcher-page task-page setup-page" data-agent="<%= agent %>" data-page="huggingface-connect">
107
+ <%- include('../partials/app_navheader', { agent }) %>
108
+ <main>
109
+ <div class="task-container">
110
+ <section class="task-shell hf-login-shell">
111
+ <header class="task-shell-header">
112
+ <div class="task-shell-header-main">
113
+ <div class="task-shell-topline">
114
+ <p class="task-eyebrow">Login / Hugging Face</p>
115
+ </div>
116
+ <h1 class="task-title">Authorize Hugging Face</h1>
117
+ <p class="task-description">Copy the device code, open Hugging Face, then approve the login. Pinokio will continue when the token is saved.</p>
118
+ <div class="task-badge-row task-header-badges">
119
+ <span class="task-badge task-badge-accent" id="hf-badge">Starting</span>
120
+ <span class="task-badge task-badge-neutral">Managed by huggingface_hub</span>
80
121
  </div>
122
+ </div>
123
+ </header>
124
+
125
+ <div class="task-shell-body">
126
+ <section class="task-section task-section-subtle">
127
+ <div class="hf-state-panel">
128
+ <div class="hf-state-icon" aria-hidden="true">
129
+ <i class="fa-solid fa-circle-notch fa-spin" id="hf-icon"></i>
130
+ </div>
131
+ <div class="hf-state-copy">
132
+ <h2 class="hf-state-title" id="hf-title">Checking Hugging Face login</h2>
133
+ <p class="hf-state-detail" id="hf-detail">Pinokio is checking whether a Hugging Face token is already saved.</p>
134
+ <div class="hf-action-row">
135
+ <button class="task-button primary hidden" id="hf-login" type="button">
136
+ <i class="fa-solid fa-key" aria-hidden="true"></i>
137
+ <span>Log In</span>
138
+ </button>
139
+ <a class="task-button hidden" id="hf-done" href="/connect">
140
+ <i class="fa-solid fa-check" aria-hidden="true"></i>
141
+ <span>Done</span>
142
+ </a>
143
+ <button class="task-button danger hidden" id="hf-logout" type="button">
144
+ <i class="fa-solid fa-circle-xmark" aria-hidden="true"></i>
145
+ <span>Logout</span>
146
+ </button>
147
+ </div>
148
+ <div class="hf-code-panel hidden" id="hf-code">
149
+ <p class="hf-code-label">Hugging Face is asking for this device code.</p>
150
+ <div class="hf-code-row">
151
+ <div class="hf-code-value" id="hf-code-value"></div>
152
+ <button class="task-button primary" id="hf-open" type="button">
153
+ <i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i>
154
+ <span>Copy and Open Hugging Face</span>
155
+ </button>
156
+ </div>
157
+ </div>
158
+ <div class="task-status error hidden hf-error" id="hf-error"></div>
159
+ </div>
160
+ </div>
161
+ </section>
81
162
  </div>
163
+ </section>
82
164
  </div>
165
+ <%- include('../partials/main_sidebar', { selected }) %>
166
+ </main>
83
167
 
84
- <script>
85
- // Configuration
86
- const CLIENT_ID = 'e90d4a4d-68a6-4c12-ae71-64756b5918de';
87
- const REDIRECT_URI = 'https://pinokio.localhost/connect/huggingface';
88
- const HF_OAUTH_URL = 'https://huggingface.co/oauth/authorize';
89
- const HF_TOKEN_URL = 'https://huggingface.co/oauth/token';
90
- const HF_API_URL = 'https://huggingface.co/api/whoami-v2';
168
+ <script>
169
+ const LOGIN_URL = "/connect/huggingface/login"
170
+ const LOGOUT_URL = "/connect/huggingface/logout"
171
+ const KEYS_URL = "/connect/huggingface/keys"
172
+ const PROFILE_URL = "/connect/huggingface/profile"
173
+ const POLL_INTERVAL = 2000
174
+ const POLL_TIMEOUT = 120000
91
175
 
92
- // Utility functions
93
- function setStatus(message, type) {
94
- const status = document.getElementById('status');
95
- status.textContent = message;
96
- status.className = `status ${type}`;
97
- }
176
+ const badgeElement = document.getElementById("hf-badge")
177
+ const iconElement = document.getElementById("hf-icon")
178
+ const titleElement = document.getElementById("hf-title")
179
+ const detailElement = document.getElementById("hf-detail")
180
+ const openButton = document.getElementById("hf-open")
181
+ const loginButton = document.getElementById("hf-login")
182
+ const doneLink = document.getElementById("hf-done")
183
+ const logoutButton = document.getElementById("hf-logout")
184
+ const codeElement = document.getElementById("hf-code")
185
+ const codeValueElement = document.getElementById("hf-code-value")
186
+ const errorElement = document.getElementById("hf-error")
98
187
 
99
- function generateRandomString(length) {
100
- const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
101
- let result = '';
102
- for (let i = 0; i < length; i++) {
103
- result += chars.charAt(Math.floor(Math.random() * chars.length));
104
- }
105
- return result;
106
- }
188
+ let loginUrl = null
189
+ let pollTimer = null
190
+ let timeoutTimer = null
107
191
 
108
- // PKCE functions
109
- function generateCodeVerifier() {
110
- const array = new Uint8Array(32);
111
- crypto.getRandomValues(array);
112
- return base64URLEncode(array);
113
- }
192
+ function setState({ badge, icon, title, detail, done = false }) {
193
+ if (badge) badgeElement.textContent = badge
194
+ if (icon) iconElement.className = icon
195
+ if (title) titleElement.textContent = title
196
+ if (detail) detailElement.textContent = detail
197
+ doneLink.classList.toggle("hidden", !done)
198
+ }
114
199
 
115
- async function generateCodeChallenge(verifier) {
116
- const encoder = new TextEncoder();
117
- const data = encoder.encode(verifier);
118
- const digest = await crypto.subtle.digest('SHA-256', data);
119
- return base64URLEncode(new Uint8Array(digest));
120
- }
200
+ function setLoginLabel(label) {
201
+ const labelElement = loginButton.querySelector("span")
202
+ if (labelElement) labelElement.textContent = label
203
+ }
121
204
 
122
- function base64URLEncode(array) {
123
- return btoa(String.fromCharCode.apply(null, array))
124
- .replace(/\+/g, '-')
125
- .replace(/\//g, '_')
126
- .replace(/=/g, '');
127
- }
205
+ function showError(message, options = {}) {
206
+ const allowLogin = options.allowLogin !== false
207
+ errorElement.textContent = message
208
+ errorElement.classList.remove("hidden")
209
+ setLoginLabel("Try Again")
210
+ loginButton.classList.toggle("hidden", !allowLogin)
211
+ logoutButton.classList.add("hidden")
212
+ setState({
213
+ badge: "Needs attention",
214
+ icon: "fa-solid fa-triangle-exclamation",
215
+ title: "Login needs attention",
216
+ detail: "Pinokio could not complete the Hugging Face browser login."
217
+ })
218
+ }
128
219
 
129
- // Token management with automatic refresh
130
- async function ensureValidToken() {
131
- const res = await fetch('/connect/huggingface/keys', {
132
- method: 'POST',
133
- headers: { 'Content-Type': 'application/json' },
134
- body: JSON.stringify({})
135
- });
136
- const json = await res.json();
137
- console.log({ json })
138
- if (json.error) {
139
- return null
140
- } else if (json.access_token) {
141
- return json.access_token
142
- } else {
143
- return null
144
- }
145
- }
220
+ function clearError() {
221
+ errorElement.textContent = ""
222
+ errorElement.classList.add("hidden")
223
+ loginButton.classList.add("hidden")
224
+ }
146
225
 
147
- // OAuth functions
148
- async function login() {
149
- try {
150
- // Clear existing data
151
- localStorage.removeItem('oauth_state');
152
- localStorage.removeItem('code_verifier');
153
-
154
- // Generate PKCE parameters
155
- const state = generateRandomString(32);
156
- const codeVerifier = generateCodeVerifier();
157
- const codeChallenge = await generateCodeChallenge(codeVerifier);
158
-
159
- // Store for later
160
- localStorage.setItem('oauth_state', state);
161
- localStorage.setItem('code_verifier', codeVerifier);
162
-
163
- // Build auth URL
164
- const params = new URLSearchParams({
165
- client_id: CLIENT_ID,
166
- redirect_uri: REDIRECT_URI,
167
- response_type: 'code',
168
- scope: 'openid profile email read-repos write-repos manage-repos',
169
- state: state,
170
- code_challenge: codeChallenge,
171
- code_challenge_method: 'S256'
172
- });
173
-
174
- const authUrl = HF_OAUTH_URL + '?' + params.toString();
175
-
176
- // Redirect
177
- window.location.href = authUrl;
178
-
179
- } catch (error) {
180
- console.error('Login error:', error);
181
- setStatus('Failed to start login', 'error');
182
- }
183
- }
226
+ function stopPolling() {
227
+ if (pollTimer) {
228
+ clearInterval(pollTimer)
229
+ pollTimer = null
230
+ }
231
+ if (timeoutTimer) {
232
+ clearTimeout(timeoutTimer)
233
+ timeoutTimer = null
234
+ }
235
+ }
184
236
 
185
- async function handleOAuthCallback(code) {
186
- try {
187
- // Verify state
188
- const urlParams = new URLSearchParams(window.location.search);
189
- const returnedState = urlParams.get('state');
190
- const storedState = localStorage.getItem('oauth_state');
191
-
192
- if (returnedState !== storedState) {
193
- throw new Error('Invalid state parameter');
194
- }
195
-
196
- // Get code verifier
197
- const codeVerifier = localStorage.getItem('code_verifier');
198
- if (!codeVerifier) {
199
- throw new Error('Code verifier not found');
200
- }
201
-
202
- // Exchange code for tokens
203
- const response = await fetch(HF_TOKEN_URL, {
204
- method: 'POST',
205
- headers: {
206
- 'Content-Type': 'application/x-www-form-urlencoded',
207
- },
208
- body: new URLSearchParams({
209
- client_id: CLIENT_ID,
210
- code: code,
211
- redirect_uri: REDIRECT_URI,
212
- grant_type: 'authorization_code',
213
- code_verifier: codeVerifier
214
- })
215
- });
216
-
217
- if (!response.ok) {
218
- const errorText = await response.text();
219
- throw new Error(`Token exchange failed: ${response.status} - ${errorText}`);
220
- }
221
-
222
- const tokenData = await response.json();
223
- console.log('Token response:', tokenData);
224
-
225
- // Store tokens
226
- if (tokenData.access_token) {
237
+ function showLoggedOut(options = {}) {
238
+ stopPolling()
239
+ clearError()
240
+ loginUrl = null
241
+ codeElement.classList.add("hidden")
242
+ codeValueElement.textContent = ""
243
+ logoutButton.classList.add("hidden")
244
+ setLoginLabel(options.loginLabel || "Log In")
245
+ loginButton.classList.remove("hidden")
246
+ setState({
247
+ badge: options.badge || "Not connected",
248
+ icon: options.icon || "fa-regular fa-circle",
249
+ title: options.title || "Hugging Face not connected",
250
+ detail: options.detail || "Log in when you need Hugging Face access for gated models or private repositories.",
251
+ done: Boolean(options.done)
252
+ })
253
+ }
227
254
 
228
- const res = await fetch('/connect/huggingface/login', {
229
- method: 'POST',
230
- headers: { 'Content-Type': 'application/json' },
231
- body: JSON.stringify(tokenData)
232
- });
233
- const response = await res.json();
234
- // Cleanup
235
- localStorage.removeItem('oauth_state');
236
- localStorage.removeItem('code_verifier');
237
- window.history.replaceState({}, document.title, window.location.pathname);
238
-
239
- // Get user info
240
- await fetchUserInfo();
241
- } else {
242
- throw new Error('No access token received');
243
- }
244
-
245
- } catch (error) {
246
- console.error('OAuth callback error:', error);
247
- setStatus('Authentication failed: ' + error.message, 'error');
248
- }
249
- }
255
+ async function postJSON(url, body = {}) {
256
+ const res = await fetch(url, {
257
+ method: "POST",
258
+ headers: { "Content-Type": "application/json" },
259
+ body: JSON.stringify(body)
260
+ })
261
+ return res.json()
262
+ }
250
263
 
251
- async function fetchUserInfo() {
252
- try {
253
- // Use ensureValidToken to automatically refresh if needed
254
- const token = await ensureValidToken();
255
- if (!token) {
256
- throw new Error('No valid token available');
257
- }
258
-
259
- const response = await fetch(HF_API_URL, {
260
- headers: { 'Authorization': 'Bearer ' + token }
261
- });
262
-
263
- if (!response.ok) {
264
- throw new Error(`API call failed: ${response.status}`);
265
- }
266
-
267
- const userInfo = await response.json();
268
- displayUserInfo(userInfo);
269
-
270
- } catch (error) {
271
- console.error('Error fetching user info:', error);
272
- setStatus('Failed to fetch user info: ' + error.message, 'error');
273
- logout();
274
- }
275
- }
264
+ async function readToken() {
265
+ const json = await postJSON(KEYS_URL)
266
+ return json && json.access_token && !json.error ? json.access_token : null
267
+ }
276
268
 
277
- function displayUserInfo(userInfo) {
278
- const userDetails = document.getElementById('user-details');
279
- userDetails.innerHTML = `
280
- <p><strong>Username:</strong> ${userInfo.name || 'N/A'}</p>
281
- <p><strong>Full Name:</strong> ${userInfo.fullname || 'N/A'}</p>
282
- <p><strong>Email:</strong> ${userInfo.email || 'N/A'}</p>
283
- <p><strong>Avatar:</strong> <img src="${userInfo.avatarUrl || ''}" alt="Avatar" style="width: 40px; height: 40px; border-radius: 50%; vertical-align: middle;"></p>
284
- `;
285
-
286
- document.getElementById('login-section').className = 'hidden';
287
- document.getElementById('user-section').className = '';
288
- setStatus(`Logged in as ${userInfo.name}`, 'success');
289
- }
269
+ async function showConnected() {
270
+ stopPolling()
271
+ clearError()
272
+ codeElement.classList.add("hidden")
273
+ loginButton.classList.add("hidden")
274
+ logoutButton.classList.remove("hidden")
275
+ try {
276
+ const res = await fetch(PROFILE_URL)
277
+ const profile = await res.json()
278
+ const nameRow = profile && Array.isArray(profile.items) ? profile.items.find((item) => item.key === "username") : null
279
+ const username = nameRow && nameRow.val ? ` as ${nameRow.val}` : ""
280
+ setState({
281
+ badge: "Connected",
282
+ icon: "fa-solid fa-circle-check",
283
+ title: "Hugging Face connected",
284
+ detail: `Pinokio is signed in${username}.`,
285
+ done: true
286
+ })
287
+ } catch (_) {
288
+ setState({
289
+ badge: "Connected",
290
+ icon: "fa-solid fa-circle-check",
291
+ title: "Hugging Face connected",
292
+ detail: "Pinokio is signed in.",
293
+ done: true
294
+ })
295
+ }
296
+ }
290
297
 
291
- async function logout() {
292
- document.getElementById('login-section').className = '';
293
- document.getElementById('user-section').className = 'hidden';
294
- setStatus('Logged out', 'warning');
295
- const res = await fetch('/connect/huggingface/logout', {
296
- method: 'POST',
297
- headers: { 'Content-Type': 'application/json' },
298
- body: JSON.stringify({})
299
- });
300
- const json = await res.json();
301
- location.href = location.href
298
+ async function logout() {
299
+ stopPolling()
300
+ clearError()
301
+ logoutButton.disabled = true
302
+ logoutButton.classList.add("is-busy")
303
+ try {
304
+ const response = await postJSON(LOGOUT_URL)
305
+ if (response.error) {
306
+ throw new Error(response.error)
302
307
  }
308
+ loginUrl = null
309
+ codeElement.classList.add("hidden")
310
+ codeValueElement.textContent = ""
311
+ showLoggedOut({
312
+ badge: "Signed out",
313
+ icon: "fa-solid fa-circle-xmark",
314
+ title: "Hugging Face signed out",
315
+ detail: "Pinokio removed the saved Hugging Face token.",
316
+ done: true,
317
+ loginLabel: "Log In Again"
318
+ })
319
+ } catch (error) {
320
+ showError(error.message, { allowLogin: false })
321
+ logoutButton.classList.remove("hidden")
322
+ } finally {
323
+ logoutButton.disabled = false
324
+ logoutButton.classList.remove("is-busy")
325
+ }
326
+ }
303
327
 
304
- // Utility function for making authenticated API calls with automatic refresh
305
- async function makeAuthenticatedRequest(url, options = {}) {
306
- const token = await ensureValidToken();
307
- if (!token) {
308
- throw new Error('No valid token available');
309
- }
310
-
311
- return fetch(url, {
312
- ...options,
313
- headers: {
314
- ...options.headers,
315
- 'Authorization': 'Bearer ' + token
316
- }
317
- });
318
- }
328
+ async function pollLoginStatus() {
329
+ const token = await readToken()
330
+ if (token) {
331
+ await showConnected()
332
+ }
333
+ }
334
+
335
+ function startPolling() {
336
+ stopPolling()
337
+ pollLoginStatus()
338
+ pollTimer = setInterval(pollLoginStatus, POLL_INTERVAL)
339
+ timeoutTimer = setTimeout(() => {
340
+ stopPolling()
341
+ setLoginLabel("Try Again")
342
+ loginButton.classList.remove("hidden")
343
+ setState({
344
+ badge: "Still waiting",
345
+ icon: "fa-regular fa-clock",
346
+ title: "Still waiting for Hugging Face",
347
+ detail: "The browser login did not finish in time."
348
+ })
349
+ }, POLL_TIMEOUT)
350
+ }
351
+
352
+ async function copyAndOpen() {
353
+ if (!loginUrl) return
354
+ const value = codeValueElement.textContent.trim()
355
+ if (value) {
356
+ try {
357
+ await navigator.clipboard.writeText(value)
358
+ } catch (_) {}
359
+ }
360
+ await fetch("/pinokio/open", {
361
+ method: "POST",
362
+ headers: { "Content-Type": "application/json" },
363
+ body: JSON.stringify({ url: loginUrl, surface: "browser" })
364
+ }).catch(() => {})
365
+ openButton.querySelector("span").textContent = "Copied and Opened"
366
+ setTimeout(() => {
367
+ openButton.querySelector("span").textContent = "Copy and Open Hugging Face"
368
+ }, 1500)
369
+ }
370
+
371
+ async function startLogin() {
372
+ stopPolling()
373
+ clearError()
374
+ loginUrl = null
375
+ doneLink.classList.add("hidden")
376
+ logoutButton.classList.add("hidden")
377
+ loginButton.classList.add("hidden")
378
+ setLoginLabel("Try Again")
379
+ codeElement.classList.add("hidden")
380
+ codeValueElement.textContent = ""
381
+ setState({
382
+ badge: "Preparing",
383
+ icon: "fa-solid fa-circle-notch fa-spin",
384
+ title: "Preparing Hugging Face",
385
+ detail: "Pinokio is requesting a Hugging Face device code."
386
+ })
319
387
 
320
- // Initialize on page load
321
- window.addEventListener('load', async () => {
322
- const urlParams = new URLSearchParams(window.location.search);
323
- const code = urlParams.get('code');
324
- const error = urlParams.get('error');
325
-
326
- if (error) {
327
- setStatus('OAuth error: ' + error, 'error');
328
- return;
329
- }
330
-
331
- if (code) {
332
- await handleOAuthCallback(code);
333
- return;
334
- }
335
-
336
- // Check existing session with automatic refresh
337
- const token = await ensureValidToken();
338
- if (token) {
339
- await fetchUserInfo();
340
- } else {
341
- setStatus('Not authenticated', 'warning');
342
- }
343
- });
388
+ const response = await postJSON(LOGIN_URL)
389
+ if (response.error || response.status === "error") {
390
+ showError(response.error || "Unknown Hugging Face login error.")
391
+ return
392
+ }
393
+ if (!response.login || !response.login.verification_uri_complete) {
394
+ showError("Hugging Face did not return a browser login URL.")
395
+ return
396
+ }
344
397
 
345
- // Export makeAuthenticatedRequest for external use
346
- window.makeAuthenticatedRequest = makeAuthenticatedRequest;
347
- window.ensureValidToken = ensureValidToken;
348
- </script>
398
+ loginUrl = response.login.verification_uri_complete
399
+ if (response.login.user_code) {
400
+ codeValueElement.textContent = response.login.user_code
401
+ codeElement.classList.remove("hidden")
402
+ }
403
+ setState({
404
+ badge: "Waiting",
405
+ icon: "fa-solid fa-circle-notch fa-spin",
406
+ title: "Authorize Hugging Face",
407
+ detail: "Use the button to copy the code and open Hugging Face. Keep this page open while Pinokio waits."
408
+ })
409
+ startPolling()
410
+ }
411
+
412
+ openButton.addEventListener("click", copyAndOpen)
413
+ loginButton.addEventListener("click", startLogin)
414
+ logoutButton.addEventListener("click", logout)
415
+
416
+ window.addEventListener("load", async () => {
417
+ try {
418
+ const token = await readToken()
419
+ if (token) {
420
+ await showConnected()
421
+ return
422
+ }
423
+ showLoggedOut()
424
+ } catch (error) {
425
+ showError(error.message)
426
+ }
427
+ })
428
+ </script>
429
+ <%- include('../partials/app_common_scripts') %>
349
430
  </body>
350
431
  </html>