pinokiod 7.5.15 → 7.5.16
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/kernel/api/hf/index.js +295 -1
- package/kernel/connect/index.js +6 -0
- package/kernel/connect/providers/huggingface/index.js +14 -0
- package/package.json +1 -1
- package/server/public/htmlmodal.js +78 -9
- package/server/public/universal-launcher.js +30 -1
- package/test/hf-api.test.js +435 -0
- package/test/huggingface-connect.test.js +33 -0
package/kernel/api/hf/index.js
CHANGED
|
@@ -1,7 +1,301 @@
|
|
|
1
|
-
const path = require('path')
|
|
2
1
|
const unparse = require('yargs-unparser-custom-flag');
|
|
3
2
|
const Shell = require('../shell')
|
|
3
|
+
const Util = require('../../util')
|
|
4
|
+
const HtmlModal = require('../htmlmodal')
|
|
5
|
+
|
|
6
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
7
|
+
const positiveNumber = (value, fallback) => {
|
|
8
|
+
const number = Number(value)
|
|
9
|
+
return Number.isFinite(number) && number > 0 ? number : fallback
|
|
10
|
+
}
|
|
11
|
+
const escapeHtml = (value) => String(value == null ? "" : value)
|
|
12
|
+
.replace(/&/g, "&")
|
|
13
|
+
.replace(/</g, "<")
|
|
14
|
+
.replace(/>/g, ">")
|
|
15
|
+
.replace(/"/g, """)
|
|
16
|
+
.replace(/'/g, "'")
|
|
17
|
+
|
|
4
18
|
class HF {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.htmlModal = new HtmlModal()
|
|
21
|
+
}
|
|
22
|
+
connect(kernel, method) {
|
|
23
|
+
if (!kernel || !kernel.connect || typeof kernel.connect[method] !== "function") {
|
|
24
|
+
throw new Error(`Hugging Face connect ${method} is not available`)
|
|
25
|
+
}
|
|
26
|
+
return kernel.connect
|
|
27
|
+
}
|
|
28
|
+
safeKeys(keys) {
|
|
29
|
+
if (!keys || !keys.access_token) {
|
|
30
|
+
return keys
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
token_path: keys.token_path
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async copyCode(login, ondata, options = {}) {
|
|
37
|
+
if (!login || !login.user_code) {
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
await Util.clipboard({
|
|
42
|
+
type: "copy",
|
|
43
|
+
text: login.user_code
|
|
44
|
+
})
|
|
45
|
+
if (ondata && !options.silent) {
|
|
46
|
+
ondata({ raw: `\r\nHugging Face code copied to clipboard: ${login.user_code}\r\n` })
|
|
47
|
+
}
|
|
48
|
+
return { ok: true }
|
|
49
|
+
} catch (error) {
|
|
50
|
+
const message = error && error.message ? error.message : String(error)
|
|
51
|
+
if (ondata && !options.silent) {
|
|
52
|
+
ondata({ raw: `\r\nCopy this Hugging Face code into the browser: ${login.user_code}\r\n` })
|
|
53
|
+
ondata({ raw: `Clipboard copy failed: ${message}\r\n` })
|
|
54
|
+
}
|
|
55
|
+
return { ok: false, error: message }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
canDispatchModal(req, ondata, kernel) {
|
|
59
|
+
return !!(
|
|
60
|
+
ondata
|
|
61
|
+
&& kernel
|
|
62
|
+
&& kernel.api
|
|
63
|
+
&& typeof kernel.api.wait === "function"
|
|
64
|
+
&& req
|
|
65
|
+
&& req.parent
|
|
66
|
+
&& (req.parent.id || req.parent.path)
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
canUseModal(req, ondata, kernel, url) {
|
|
70
|
+
return !!(url && this.canDispatchModal(req, ondata, kernel))
|
|
71
|
+
}
|
|
72
|
+
modalRequest(req, modalId, params = {}) {
|
|
73
|
+
return {
|
|
74
|
+
params: Object.assign({ id: modalId }, params),
|
|
75
|
+
parent: req.parent,
|
|
76
|
+
cwd: req.cwd
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
modalId(req) {
|
|
80
|
+
const id = req && req.parent ? (req.parent.id || req.parent.path) : "hf-login"
|
|
81
|
+
return `hf-login:${id}`
|
|
82
|
+
}
|
|
83
|
+
loginModalHtml(login, clipboard) {
|
|
84
|
+
const code = escapeHtml(login.user_code || "")
|
|
85
|
+
const expires = login.expires_in
|
|
86
|
+
? `<p class="hf-login-modal-note">Expires in ${escapeHtml(login.expires_in)} seconds.</p>`
|
|
87
|
+
: ""
|
|
88
|
+
let copyText = "Copy this code if Hugging Face asks for it."
|
|
89
|
+
let copyClass = "neutral"
|
|
90
|
+
if (clipboard && clipboard.ok) {
|
|
91
|
+
copyText = "The code has been copied to your clipboard."
|
|
92
|
+
copyClass = "success"
|
|
93
|
+
} else if (clipboard && clipboard.ok === false) {
|
|
94
|
+
copyText = `Clipboard copy failed. The code is displayed below.`
|
|
95
|
+
copyClass = "warning"
|
|
96
|
+
}
|
|
97
|
+
return `
|
|
98
|
+
<div class="hf-login-modal">
|
|
99
|
+
<div class="hf-login-modal-copy ${copyClass}">${escapeHtml(copyText)}</div>
|
|
100
|
+
<div class="hf-login-modal-code" aria-label="Hugging Face device code">${code}</div>
|
|
101
|
+
${expires}
|
|
102
|
+
<p>Open Hugging Face to finish login. Pinokio will continue automatically.</p>
|
|
103
|
+
</div>
|
|
104
|
+
`
|
|
105
|
+
}
|
|
106
|
+
async promptLoginModal(req, ondata, kernel, login, clipboard) {
|
|
107
|
+
const modalId = this.modalId(req)
|
|
108
|
+
const response = await this.htmlModal.open(
|
|
109
|
+
this.modalRequest(req, modalId, {
|
|
110
|
+
title: "Log in to Hugging Face",
|
|
111
|
+
variant: "minimal",
|
|
112
|
+
html: this.loginModalHtml(login, clipboard),
|
|
113
|
+
status: { text: "Waiting for you to open Hugging Face.", waiting: false },
|
|
114
|
+
actions: [{
|
|
115
|
+
id: "open",
|
|
116
|
+
label: "Open Hugging Face",
|
|
117
|
+
type: "submit",
|
|
118
|
+
href: login.verification_uri_complete,
|
|
119
|
+
target: "_blank",
|
|
120
|
+
features: "browser",
|
|
121
|
+
primary: true,
|
|
122
|
+
close: false,
|
|
123
|
+
icon: "fa-solid fa-arrow-up-right-from-square"
|
|
124
|
+
}, {
|
|
125
|
+
id: "cancel",
|
|
126
|
+
label: "Cancel",
|
|
127
|
+
type: "submit",
|
|
128
|
+
variant: "secondary"
|
|
129
|
+
}],
|
|
130
|
+
actionsAlign: "end",
|
|
131
|
+
await: true,
|
|
132
|
+
dismissible: false
|
|
133
|
+
}),
|
|
134
|
+
ondata,
|
|
135
|
+
kernel
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if (!response || response.action === "cancel" || response.action === "dismissed") {
|
|
139
|
+
await this.htmlModal.close(this.modalRequest(req, modalId), ondata, kernel)
|
|
140
|
+
throw new Error("Hugging Face login canceled.")
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
await this.htmlModal.update(
|
|
144
|
+
this.modalRequest(req, modalId, {
|
|
145
|
+
variant: "minimal",
|
|
146
|
+
status: { text: "Waiting for Hugging Face authorization to finish...", waiting: true },
|
|
147
|
+
actions: [{
|
|
148
|
+
id: "open-again",
|
|
149
|
+
label: "Open Again",
|
|
150
|
+
type: "link",
|
|
151
|
+
href: login.verification_uri_complete,
|
|
152
|
+
target: "_blank",
|
|
153
|
+
features: "browser",
|
|
154
|
+
variant: "secondary",
|
|
155
|
+
icon: "fa-solid fa-arrow-up-right-from-square"
|
|
156
|
+
}],
|
|
157
|
+
actionsAlign: "end",
|
|
158
|
+
dismissible: false
|
|
159
|
+
}),
|
|
160
|
+
ondata,
|
|
161
|
+
kernel
|
|
162
|
+
)
|
|
163
|
+
return response
|
|
164
|
+
}
|
|
165
|
+
async closeLoginModal(req, ondata, kernel) {
|
|
166
|
+
if (!this.canDispatchModal(req, ondata, kernel)) {
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
await this.htmlModal.close(this.modalRequest(req, this.modalId(req)), ondata, kernel)
|
|
170
|
+
}
|
|
171
|
+
async cancelLogin(connect, params) {
|
|
172
|
+
if (connect && typeof connect.cancelLogin === "function") {
|
|
173
|
+
await connect.cancelLogin("huggingface", params || {})
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/*
|
|
177
|
+
{
|
|
178
|
+
"method": "hf.login",
|
|
179
|
+
"params": {
|
|
180
|
+
"force": false,
|
|
181
|
+
"open": true,
|
|
182
|
+
"modal": true,
|
|
183
|
+
"clipboard": true,
|
|
184
|
+
"wait": true,
|
|
185
|
+
"timeout": 120000,
|
|
186
|
+
"interval": 2000
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
Starts the existing Hugging Face device login provider. By default it shows a
|
|
191
|
+
blocking modal with the copied code and waits for the user to open the
|
|
192
|
+
verification URL before polling for the managed token.
|
|
193
|
+
*/
|
|
194
|
+
async login(req, ondata, kernel) {
|
|
195
|
+
const connect = this.connect(kernel, "keys")
|
|
196
|
+
const params = req.params || {}
|
|
197
|
+
const force = params.force === true
|
|
198
|
+
const shouldOpen = params.open !== false
|
|
199
|
+
const shouldModal = params.modal !== false
|
|
200
|
+
const shouldCopy = params.clipboard !== false
|
|
201
|
+
const shouldWait = params.wait !== false
|
|
202
|
+
const timeout = positiveNumber(params.timeout, 120000)
|
|
203
|
+
const interval = positiveNumber(params.interval, 2000)
|
|
204
|
+
|
|
205
|
+
if (!force) {
|
|
206
|
+
const existing = await connect.keys("huggingface")
|
|
207
|
+
if (existing && existing.access_token) {
|
|
208
|
+
return {
|
|
209
|
+
status: "success",
|
|
210
|
+
already_logged_in: true,
|
|
211
|
+
...this.safeKeys(existing)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (typeof connect.login !== "function") {
|
|
217
|
+
throw new Error("Hugging Face connect login is not available")
|
|
218
|
+
}
|
|
219
|
+
const response = await connect.login("huggingface", params)
|
|
220
|
+
if (!response || response.status === "error") {
|
|
221
|
+
return response
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const login = response.login || {}
|
|
225
|
+
const url = login.verification_uri_complete
|
|
226
|
+
const useModal = shouldModal && this.canUseModal(req, ondata, kernel, url)
|
|
227
|
+
if (shouldCopy && login.user_code) {
|
|
228
|
+
response.clipboard = await this.copyCode(login, ondata, { silent: useModal })
|
|
229
|
+
} else if (login.user_code && ondata && !useModal) {
|
|
230
|
+
ondata({ raw: `\r\nHugging Face code: ${login.user_code}\r\n` })
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (useModal) {
|
|
234
|
+
try {
|
|
235
|
+
response.open = await this.promptLoginModal(req, ondata, kernel, login, response.clipboard)
|
|
236
|
+
} catch (error) {
|
|
237
|
+
await this.cancelLogin(connect, params)
|
|
238
|
+
throw error
|
|
239
|
+
}
|
|
240
|
+
} else if (shouldOpen && url) {
|
|
241
|
+
if (ondata) {
|
|
242
|
+
if (!shouldModal) {
|
|
243
|
+
ondata({ raw: "\r\nHugging Face login modal disabled.\r\n" })
|
|
244
|
+
}
|
|
245
|
+
const codeHint = response.clipboard && response.clipboard.ok
|
|
246
|
+
? "The code is already on your clipboard; paste it if Hugging Face asks."
|
|
247
|
+
: "If Hugging Face asks for a code, use the code printed above."
|
|
248
|
+
ondata({ raw: `Opening Hugging Face login: ${url}\r\n${codeHint}\r\n` })
|
|
249
|
+
if (response.clipboard && response.clipboard.ok === false) {
|
|
250
|
+
ondata({ raw: "The browser is opening even though clipboard copy failed.\r\n" })
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
response.open = await Util.openURI(url)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (!shouldWait) {
|
|
257
|
+
if (useModal) {
|
|
258
|
+
await this.closeLoginModal(req, ondata, kernel)
|
|
259
|
+
}
|
|
260
|
+
return response
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const startedAt = Date.now()
|
|
264
|
+
while (Date.now() - startedAt < timeout) {
|
|
265
|
+
const keys = await connect.keys("huggingface")
|
|
266
|
+
if (keys && keys.access_token) {
|
|
267
|
+
if (useModal) {
|
|
268
|
+
await this.closeLoginModal(req, ondata, kernel)
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
...response,
|
|
272
|
+
status: "success",
|
|
273
|
+
...this.safeKeys(keys)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
await delay(interval)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (useModal) {
|
|
280
|
+
await this.closeLoginModal(req, ondata, kernel)
|
|
281
|
+
}
|
|
282
|
+
await this.cancelLogin(connect, params)
|
|
283
|
+
return {
|
|
284
|
+
...response,
|
|
285
|
+
status: "timeout",
|
|
286
|
+
error: "Timed out waiting for Hugging Face login to finish."
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/*
|
|
290
|
+
{
|
|
291
|
+
"method": "hf.logout"
|
|
292
|
+
}
|
|
293
|
+
*/
|
|
294
|
+
async logout(req, ondata, kernel) {
|
|
295
|
+
const connect = this.connect(kernel, "logout")
|
|
296
|
+
await connect.logout("huggingface", req.params || {})
|
|
297
|
+
return { status: "success" }
|
|
298
|
+
}
|
|
5
299
|
/*
|
|
6
300
|
{
|
|
7
301
|
"method": "hf.download",
|
package/kernel/connect/index.js
CHANGED
|
@@ -32,6 +32,12 @@ class Connect {
|
|
|
32
32
|
let res = await this.clients[provider].logout(req)
|
|
33
33
|
return res
|
|
34
34
|
}
|
|
35
|
+
async cancelLogin(provider, req) {
|
|
36
|
+
if (this.clients[provider] && this.clients[provider].cancelLogin) {
|
|
37
|
+
return await this.clients[provider].cancelLogin(req)
|
|
38
|
+
}
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
35
41
|
async keys(provider) {
|
|
36
42
|
let res = await this.clients[provider].keys()
|
|
37
43
|
return res
|
|
@@ -227,6 +227,20 @@ class Huggingface {
|
|
|
227
227
|
await this.runHf(["auth", "logout"]).catch(() => {})
|
|
228
228
|
await fs.promises.rm(this.kernel.path("connect", "huggingface"), { recursive: true, force: true }).catch(() => {})
|
|
229
229
|
}
|
|
230
|
+
async cancelLogin() {
|
|
231
|
+
const session = this.loginSession
|
|
232
|
+
if (!session || session.status !== "pending") {
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
session.status = "canceled"
|
|
236
|
+
session.error = "Hugging Face login canceled."
|
|
237
|
+
if (session.child) {
|
|
238
|
+
session.child.kill()
|
|
239
|
+
}
|
|
240
|
+
if (this.loginSession === session) {
|
|
241
|
+
this.loginSession = null
|
|
242
|
+
}
|
|
243
|
+
}
|
|
230
244
|
async logout() {
|
|
231
245
|
await this.destroy()
|
|
232
246
|
}
|
package/package.json
CHANGED
|
@@ -15,21 +15,43 @@
|
|
|
15
15
|
style.id = 'htmlmodal-style'
|
|
16
16
|
style.textContent = `
|
|
17
17
|
.htmlmodal-overlay { position: fixed; inset: 0; width: 100vw; height: 100vh; background: rgba(15, 23, 42, 0.72); backdrop-filter: blur(6px); z-index: 1000000000005; padding: 32px; display: flex; align-items: center; justify-content: center; box-sizing: border-box; opacity: 0; visibility: hidden; pointer-events: none; transition: opacity 0.18s ease, visibility 0.18s ease; }
|
|
18
|
+
.htmlmodal-overlay.minimal { background: rgba(9, 11, 15, 0.34); backdrop-filter: none; padding: 24px; }
|
|
19
|
+
body.dark .htmlmodal-overlay.minimal { background: rgba(0, 0, 0, 0.58); }
|
|
18
20
|
.htmlmodal-overlay.visible { opacity: 1; visibility: visible; pointer-events: auto; }
|
|
19
21
|
.htmlmodal-window { width: min(560px, 100%); max-height: calc(100vh - 64px); background: #0f172a; color: #f8fafc; border-radius: 18px; box-shadow: 0 30px 60px rgba(0,0,0,0.45); display: flex; flex-direction: column; overflow: hidden; border: 1px solid rgba(148,163,184,0.2); }
|
|
22
|
+
.htmlmodal-window.minimal { width: min(420px, calc(100vw - 32px)); max-height: calc(100vh - 48px); background: #ffffff; color: #18181b; border-radius: 8px; box-shadow: 0 18px 50px rgba(15, 23, 42, 0.16); border-color: rgba(24, 24, 27, 0.12); font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
23
|
+
body.dark .htmlmodal-window.minimal { background: #1b1c1d; color: rgba(250, 250, 250, 0.94); border-color: rgba(255, 255, 255, 0.1); box-shadow: 0 24px 72px rgba(0, 0, 0, 0.42); }
|
|
20
24
|
.htmlmodal-header { display: flex; align-items: center; justify-content: space-between; padding: 20px 24px 12px; }
|
|
25
|
+
.htmlmodal-window.minimal .htmlmodal-header { gap: 16px; padding: 20px 20px 12px; }
|
|
21
26
|
.htmlmodal-title { font-size: 1.15rem; font-weight: 600; }
|
|
27
|
+
.htmlmodal-window.minimal .htmlmodal-title { font-size: 16px; line-height: 1.25; letter-spacing: 0; }
|
|
22
28
|
.htmlmodal-close { background: none; border: none; color: #cbd5f5; font-size: 20px; cursor: pointer; padding: 4px 8px; border-radius: 6px; }
|
|
23
29
|
.htmlmodal-close:hover { background: rgba(255,255,255,0.08); }
|
|
30
|
+
.htmlmodal-window.minimal .htmlmodal-close { width: 28px; height: 28px; margin: -6px -6px 0 0; background: transparent; border: 1px solid transparent; color: #71717a; font: inherit; font-size: 18px; line-height: 1; padding: 0; display: inline-flex; align-items: center; justify-content: center; }
|
|
31
|
+
.htmlmodal-window.minimal .htmlmodal-close:hover, .htmlmodal-window.minimal .htmlmodal-close:focus-visible { background: rgba(24, 24, 27, 0.06); color: #18181b; outline: none; }
|
|
32
|
+
body.dark .htmlmodal-window.minimal .htmlmodal-close { color: rgba(229, 231, 235, 0.58); }
|
|
33
|
+
body.dark .htmlmodal-window.minimal .htmlmodal-close:hover, body.dark .htmlmodal-window.minimal .htmlmodal-close:focus-visible { background: rgba(255, 255, 255, 0.07); color: rgba(250, 250, 250, 0.94); }
|
|
24
34
|
.htmlmodal-body { padding: 0 24px 16px; overflow-y: auto; font-size: 0.95rem; line-height: 1.5; }
|
|
35
|
+
.htmlmodal-window.minimal .htmlmodal-body { padding: 0 20px; color: #71717a; font-size: 13px; line-height: 1.45; }
|
|
36
|
+
body.dark .htmlmodal-window.minimal .htmlmodal-body { color: rgba(229, 231, 235, 0.62); }
|
|
25
37
|
.htmlmodal-body p { margin: 0 0 0.85rem; }
|
|
38
|
+
.htmlmodal-window.minimal .htmlmodal-body p { margin: 0 0 10px; }
|
|
26
39
|
.htmlmodal-status { padding: 0 24px 18px; font-size: 0.9rem; color: #cbd5f5; display: flex; align-items: center; gap: 10px; }
|
|
40
|
+
.htmlmodal-window.minimal .htmlmodal-status { padding: 12px 20px 0; font-size: 13px; line-height: 1.4; color: #71717a; gap: 8px; }
|
|
41
|
+
body.dark .htmlmodal-window.minimal .htmlmodal-status { color: rgba(229, 231, 235, 0.62); }
|
|
27
42
|
.htmlmodal-status.hidden { display: none; }
|
|
28
43
|
.htmlmodal-status.error { color: #fecaca; }
|
|
29
44
|
.htmlmodal-status.success { color: #bbf7d0; }
|
|
45
|
+
.htmlmodal-window.minimal .htmlmodal-status.error { color: #b91c1c; }
|
|
46
|
+
.htmlmodal-window.minimal .htmlmodal-status.success { color: #15803d; }
|
|
47
|
+
body.dark .htmlmodal-window.minimal .htmlmodal-status.error { color: #fca5a5; }
|
|
48
|
+
body.dark .htmlmodal-window.minimal .htmlmodal-status.success { color: #86efac; }
|
|
30
49
|
.htmlmodal-status .spinner { width: 18px; height: 18px; border: 2px solid rgba(248, 250, 252, 0.2); border-top-color: #38bdf8; border-radius: 50%; animation: htmlmodal-spin 0.9s linear infinite; }
|
|
50
|
+
.htmlmodal-window.minimal .htmlmodal-status .spinner { width: 14px; height: 14px; border-color: rgba(24, 24, 27, 0.14); border-top-color: #18181b; }
|
|
51
|
+
body.dark .htmlmodal-window.minimal .htmlmodal-status .spinner { border-color: rgba(255, 255, 255, 0.16); border-top-color: rgba(250, 250, 250, 0.94); }
|
|
31
52
|
@keyframes htmlmodal-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
|
32
53
|
.htmlmodal-actions { padding: 0 24px 24px; display: flex; flex-wrap: wrap; gap: 10px; }
|
|
54
|
+
.htmlmodal-window.minimal .htmlmodal-actions { padding: 16px 20px 20px; gap: 8px; justify-content: flex-end; }
|
|
33
55
|
.htmlmodal-actions .btn { border-radius: 999px; border: 1px solid transparent; padding: 10px 18px; font-size: 0.9rem; cursor: pointer; font-weight: 500; display: inline-flex; align-items: center; gap: 8px; text-decoration: none; transition: transform 0.15s ease, background 0.2s ease, color 0.2s ease; }
|
|
34
56
|
.htmlmodal-actions .btn.primary { background: #38bdf8; color: #0f172a; }
|
|
35
57
|
.htmlmodal-actions .btn.primary:hover { background: #0ea5e9; transform: translateY(-1px); }
|
|
@@ -37,7 +59,31 @@
|
|
|
37
59
|
.htmlmodal-actions .btn.secondary:hover { background: rgba(148,163,184,0.3); }
|
|
38
60
|
.htmlmodal-actions .btn.link { background: rgba(56,189,248,0.12); border-color: rgba(56,189,248,0.55); color: #38bdf8; padding: 10px 18px; }
|
|
39
61
|
.htmlmodal-actions .btn.link:hover { background: rgba(56,189,248,0.2); color: #7dd3fc; transform: translateY(-1px); }
|
|
62
|
+
.htmlmodal-window.minimal .htmlmodal-actions .btn { min-height: 32px; border-radius: 6px; border: 1px solid rgba(24, 24, 27, 0.12); padding: 0 12px; background: transparent; color: #52525b; font: inherit; font-size: 13px; font-weight: 600; justify-content: center; box-shadow: none; transition: background 0.16s ease, color 0.16s ease, border-color 0.16s ease; }
|
|
63
|
+
.htmlmodal-window.minimal .htmlmodal-actions .btn.primary { background: #18181b; border-color: #18181b; color: #ffffff; }
|
|
64
|
+
.htmlmodal-window.minimal .htmlmodal-actions .btn.primary:hover, .htmlmodal-window.minimal .htmlmodal-actions .btn.primary:focus-visible { background: #27272a; border-color: #27272a; outline: none; transform: none; }
|
|
65
|
+
.htmlmodal-window.minimal .htmlmodal-actions .btn.secondary:hover, .htmlmodal-window.minimal .htmlmodal-actions .btn.secondary:focus-visible, .htmlmodal-window.minimal .htmlmodal-actions .btn.link:hover, .htmlmodal-window.minimal .htmlmodal-actions .btn.link:focus-visible { background: rgba(24, 24, 27, 0.06); color: #18181b; outline: none; transform: none; }
|
|
66
|
+
.htmlmodal-window.minimal .htmlmodal-actions .btn.link { color: #52525b; }
|
|
67
|
+
body.dark .htmlmodal-window.minimal .htmlmodal-actions .btn { border-color: rgba(255, 255, 255, 0.1); color: rgba(229, 231, 235, 0.7); }
|
|
68
|
+
body.dark .htmlmodal-window.minimal .htmlmodal-actions .btn.primary { background: rgba(250, 250, 250, 0.94); border-color: rgba(250, 250, 250, 0.94); color: #1b1c1d; }
|
|
69
|
+
body.dark .htmlmodal-window.minimal .htmlmodal-actions .btn.primary:hover, body.dark .htmlmodal-window.minimal .htmlmodal-actions .btn.primary:focus-visible { background: #ffffff; border-color: #ffffff; color: #18181b; }
|
|
70
|
+
body.dark .htmlmodal-window.minimal .htmlmodal-actions .btn.secondary:hover, body.dark .htmlmodal-window.minimal .htmlmodal-actions .btn.secondary:focus-visible, body.dark .htmlmodal-window.minimal .htmlmodal-actions .btn.link:hover, body.dark .htmlmodal-window.minimal .htmlmodal-actions .btn.link:focus-visible { background: rgba(255, 255, 255, 0.07); color: rgba(250, 250, 250, 0.94); }
|
|
40
71
|
.htmlmodal-actions .btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
|
72
|
+
.hf-login-modal { display: flex; flex-direction: column; gap: 10px; }
|
|
73
|
+
.hf-login-modal p { margin: 0; }
|
|
74
|
+
.hf-login-modal-copy { font-size: 12px; color: #71717a; }
|
|
75
|
+
.hf-login-modal-copy.warning { color: #b45309; }
|
|
76
|
+
body.dark .hf-login-modal-copy { color: rgba(229, 231, 235, 0.62); }
|
|
77
|
+
body.dark .hf-login-modal-copy.warning { color: #fbbf24; }
|
|
78
|
+
.hf-login-modal-code { display: block; width: 100%; box-sizing: border-box; padding: 10px 12px; border-radius: 6px; background: rgba(24, 24, 27, 0.03); border: 1px solid rgba(24, 24, 27, 0.12); color: #18181b; font: 600 20px/1.2 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; letter-spacing: 0; text-align: center; user-select: all; }
|
|
79
|
+
body.dark .hf-login-modal-code { background: rgba(255, 255, 255, 0.04); border-color: rgba(255, 255, 255, 0.1); color: rgba(250, 250, 250, 0.94); }
|
|
80
|
+
.hf-login-modal-note { font-size: 12px; color: inherit !important; }
|
|
81
|
+
@media only screen and (max-width: 480px) {
|
|
82
|
+
.htmlmodal-overlay.minimal { padding: 16px; }
|
|
83
|
+
.htmlmodal-window.minimal { width: calc(100vw - 32px); }
|
|
84
|
+
.htmlmodal-window.minimal .htmlmodal-actions { flex-direction: column-reverse; align-items: stretch; }
|
|
85
|
+
.htmlmodal-window.minimal .htmlmodal-actions .btn { width: 100%; }
|
|
86
|
+
}
|
|
41
87
|
`
|
|
42
88
|
document.head.appendChild(style)
|
|
43
89
|
}
|
|
@@ -76,7 +122,8 @@
|
|
|
76
122
|
awaiting: null,
|
|
77
123
|
actions: [],
|
|
78
124
|
socket: null,
|
|
79
|
-
dismissible: true
|
|
125
|
+
dismissible: true,
|
|
126
|
+
variant: null
|
|
80
127
|
}
|
|
81
128
|
}
|
|
82
129
|
|
|
@@ -94,11 +141,15 @@
|
|
|
94
141
|
}
|
|
95
142
|
if (action === 'open') {
|
|
96
143
|
this.current.id = payload.id
|
|
144
|
+
this.current.dismissible = true
|
|
145
|
+
this.closeBtn.style.display = 'inline-flex'
|
|
146
|
+
this.setVariant(payload.variant)
|
|
97
147
|
this.show()
|
|
98
148
|
this.render(payload)
|
|
99
149
|
} else if (action === 'update') {
|
|
100
150
|
if (!this.current.id) {
|
|
101
151
|
this.current.id = payload.id
|
|
152
|
+
this.setVariant(payload.variant)
|
|
102
153
|
this.show()
|
|
103
154
|
}
|
|
104
155
|
this.render(payload)
|
|
@@ -121,6 +172,16 @@
|
|
|
121
172
|
this.overlay.classList.remove('visible')
|
|
122
173
|
this.current.id = null
|
|
123
174
|
this.current.actions = []
|
|
175
|
+
this.current.dismissible = true
|
|
176
|
+
this.closeBtn.style.display = 'inline-flex'
|
|
177
|
+
this.setVariant(null)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
setVariant(variant) {
|
|
181
|
+
const name = variant === 'minimal' ? 'minimal' : null
|
|
182
|
+
this.current.variant = name
|
|
183
|
+
this.overlay.classList.toggle('minimal', name === 'minimal')
|
|
184
|
+
this.window.classList.toggle('minimal', name === 'minimal')
|
|
124
185
|
}
|
|
125
186
|
|
|
126
187
|
setTitle(title) {
|
|
@@ -190,8 +251,11 @@
|
|
|
190
251
|
if (action.icon) {
|
|
191
252
|
link.innerHTML = `<i class="${action.icon}"></i> ${link.textContent}`
|
|
192
253
|
}
|
|
193
|
-
link.addEventListener('click', () => {
|
|
194
|
-
|
|
254
|
+
link.addEventListener('click', (event) => {
|
|
255
|
+
if (action.features) {
|
|
256
|
+
event.preventDefault()
|
|
257
|
+
this.handleAction(action)
|
|
258
|
+
}
|
|
195
259
|
})
|
|
196
260
|
return link
|
|
197
261
|
}
|
|
@@ -212,14 +276,13 @@
|
|
|
212
276
|
|
|
213
277
|
buildButtonClass(action, fallback = 'secondary') {
|
|
214
278
|
const classes = ['btn']
|
|
215
|
-
if (action && action.
|
|
279
|
+
if (action && action.primary) {
|
|
280
|
+
classes.push('primary')
|
|
281
|
+
} else if (action && action.variant) {
|
|
216
282
|
classes.push(action.variant)
|
|
217
283
|
} else {
|
|
218
284
|
classes.push(fallback)
|
|
219
285
|
}
|
|
220
|
-
if (action && action.primary) {
|
|
221
|
-
classes.push('primary')
|
|
222
|
-
}
|
|
223
286
|
return classes.join(' ')
|
|
224
287
|
}
|
|
225
288
|
|
|
@@ -232,6 +295,9 @@
|
|
|
232
295
|
window.open(action.href, target, action.features || 'noopener')
|
|
233
296
|
return
|
|
234
297
|
}
|
|
298
|
+
if (action.type === 'submit' && action.href) {
|
|
299
|
+
window.open(action.href, action.target || '_blank', action.features || 'noopener')
|
|
300
|
+
}
|
|
235
301
|
if (action.type === 'submit' && this.current.awaiting && this.current.socket) {
|
|
236
302
|
this.emitResponse({ action: action.id || 'submit', payload: action.payload || null })
|
|
237
303
|
if (action.close !== false) {
|
|
@@ -240,7 +306,7 @@
|
|
|
240
306
|
} else if (action.type === 'button' && this.current.socket && this.current.awaiting) {
|
|
241
307
|
this.emitResponse({ action: action.id || 'button', payload: action.payload || null })
|
|
242
308
|
} else if (action.type === 'button' && action.href) {
|
|
243
|
-
window.open(action.href, action.target || '_blank')
|
|
309
|
+
window.open(action.href, action.target || '_blank', action.features || 'noopener')
|
|
244
310
|
}
|
|
245
311
|
}
|
|
246
312
|
|
|
@@ -282,7 +348,10 @@
|
|
|
282
348
|
this.actions.style.justifyContent = 'flex-start'
|
|
283
349
|
}
|
|
284
350
|
} else {
|
|
285
|
-
this.actions.style.justifyContent = '
|
|
351
|
+
this.actions.style.justifyContent = ''
|
|
352
|
+
}
|
|
353
|
+
if (Object.prototype.hasOwnProperty.call(payload, 'variant')) {
|
|
354
|
+
this.setVariant(payload.variant)
|
|
286
355
|
}
|
|
287
356
|
if (Object.prototype.hasOwnProperty.call(payload, 'dismissible')) {
|
|
288
357
|
this.current.dismissible = Boolean(payload.dismissible)
|
|
@@ -513,6 +513,34 @@
|
|
|
513
513
|
}
|
|
514
514
|
}
|
|
515
515
|
|
|
516
|
+
function ensureSocketReady() {
|
|
517
|
+
if (typeof Socket === 'function') {
|
|
518
|
+
return Promise.resolve(Socket);
|
|
519
|
+
}
|
|
520
|
+
return new Promise((resolve, reject) => {
|
|
521
|
+
const target = document.head || document.body || document.documentElement;
|
|
522
|
+
if (!target) {
|
|
523
|
+
reject(new Error('Socket runtime is unavailable.'));
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const script = document.createElement('script');
|
|
528
|
+
script.src = '/Socket.js';
|
|
529
|
+
script.async = true;
|
|
530
|
+
script.onload = () => {
|
|
531
|
+
if (typeof Socket === 'function') {
|
|
532
|
+
resolve(Socket);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
reject(new Error('Socket runtime failed to load.'));
|
|
536
|
+
};
|
|
537
|
+
script.onerror = () => {
|
|
538
|
+
reject(new Error('Socket runtime failed to load.'));
|
|
539
|
+
};
|
|
540
|
+
target.appendChild(script);
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
516
544
|
async function checkLauncherInstallDestinationExists(relativePath, folderName) {
|
|
517
545
|
const response = await fetch('/pinokio/install/exists', {
|
|
518
546
|
method: 'POST',
|
|
@@ -4257,8 +4285,9 @@
|
|
|
4257
4285
|
throw new Error('Download is unavailable right now.');
|
|
4258
4286
|
}
|
|
4259
4287
|
appendDownloadOutput(ui, `$ ${clone.message}\n\n`);
|
|
4288
|
+
const SocketConstructor = await ensureSocketReady();
|
|
4260
4289
|
await new Promise((resolve, reject) => {
|
|
4261
|
-
const socket = new
|
|
4290
|
+
const socket = new SocketConstructor();
|
|
4262
4291
|
let settled = false;
|
|
4263
4292
|
const settle = (fn, value) => {
|
|
4264
4293
|
if (settled) {
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
const assert = require('node:assert/strict')
|
|
2
|
+
const test = require('node:test')
|
|
3
|
+
|
|
4
|
+
const HF = require('../kernel/api/hf')
|
|
5
|
+
const Util = require('../kernel/util')
|
|
6
|
+
|
|
7
|
+
const login = {
|
|
8
|
+
verification_uri_complete: 'https://huggingface.co/oauth/device?user_code=ABCD-EFGH',
|
|
9
|
+
user_code: 'ABCD-EFGH',
|
|
10
|
+
expires_in: 300
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
test('hf.login shows a modal, waits for the user to open Hugging Face, then closes after saved keys', async () => {
|
|
14
|
+
const hf = new HF()
|
|
15
|
+
const packets = []
|
|
16
|
+
const actions = []
|
|
17
|
+
const originalOpenURI = Util.openURI
|
|
18
|
+
const originalClipboard = Util.clipboard
|
|
19
|
+
let keyReads = 0
|
|
20
|
+
const loginCalls = []
|
|
21
|
+
const waitCalls = []
|
|
22
|
+
const kernel = {
|
|
23
|
+
connect: {
|
|
24
|
+
async keys(provider) {
|
|
25
|
+
assert.equal(provider, 'huggingface')
|
|
26
|
+
keyReads += 1
|
|
27
|
+
if (keyReads < 3) {
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
access_token: 'hf_secret',
|
|
32
|
+
token_path: '/tmp/pinokio/hf-token'
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
async login(provider, params) {
|
|
36
|
+
loginCalls.push({ provider, params })
|
|
37
|
+
return {
|
|
38
|
+
status: 'pending',
|
|
39
|
+
login,
|
|
40
|
+
token_path: '/tmp/pinokio/hf-token'
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
api: {
|
|
45
|
+
async wait(key) {
|
|
46
|
+
waitCalls.push(key)
|
|
47
|
+
return { action: 'open' }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
Util.clipboard = async (req) => {
|
|
53
|
+
actions.push({ type: 'clipboard', req })
|
|
54
|
+
}
|
|
55
|
+
Util.openURI = async (uri) => {
|
|
56
|
+
actions.push({ type: 'openURI', uri })
|
|
57
|
+
return { ok: true, status: 'stubbed' }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const result = await hf.login({
|
|
62
|
+
parent: { id: 'test-script', path: '/pinokio/api/test/hf-login.js' },
|
|
63
|
+
params: {
|
|
64
|
+
timeout: 100,
|
|
65
|
+
interval: 1
|
|
66
|
+
}
|
|
67
|
+
}, (stream, type) => packets.push({ type, stream }), kernel)
|
|
68
|
+
|
|
69
|
+
assert.deepEqual(loginCalls, [{ provider: 'huggingface', params: { timeout: 100, interval: 1 } }])
|
|
70
|
+
assert.deepEqual(waitCalls, ['/pinokio/api/test/hf-login.js'])
|
|
71
|
+
assert.deepEqual(actions, [{
|
|
72
|
+
type: 'clipboard',
|
|
73
|
+
req: {
|
|
74
|
+
type: 'copy',
|
|
75
|
+
text: 'ABCD-EFGH'
|
|
76
|
+
}
|
|
77
|
+
}])
|
|
78
|
+
|
|
79
|
+
assert.equal(packets.length, 3)
|
|
80
|
+
assert.equal(packets[0].type, 'htmlmodal')
|
|
81
|
+
assert.equal(packets[0].stream.action, 'open')
|
|
82
|
+
assert.equal(packets[0].stream.variant, 'minimal')
|
|
83
|
+
assert.equal(packets[0].stream.actionsAlign, 'end')
|
|
84
|
+
assert.equal(packets[0].stream.await, true)
|
|
85
|
+
assert.equal(packets[0].stream.awaitKey, '/pinokio/api/test/hf-login.js')
|
|
86
|
+
assert.equal(packets[0].stream.dismissible, false)
|
|
87
|
+
assert.match(packets[0].stream.html, /ABCD-EFGH/)
|
|
88
|
+
assert.match(packets[0].stream.html, /copied to your clipboard/)
|
|
89
|
+
assert.deepEqual(packets[0].stream.actions[0], {
|
|
90
|
+
id: 'open',
|
|
91
|
+
label: 'Open Hugging Face',
|
|
92
|
+
type: 'submit',
|
|
93
|
+
href: 'https://huggingface.co/oauth/device?user_code=ABCD-EFGH',
|
|
94
|
+
target: '_blank',
|
|
95
|
+
features: 'browser',
|
|
96
|
+
primary: true,
|
|
97
|
+
close: false,
|
|
98
|
+
icon: 'fa-solid fa-arrow-up-right-from-square'
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
assert.equal(packets[1].type, 'htmlmodal')
|
|
102
|
+
assert.equal(packets[1].stream.action, 'update')
|
|
103
|
+
assert.equal(packets[1].stream.variant, 'minimal')
|
|
104
|
+
assert.equal(packets[1].stream.actionsAlign, 'end')
|
|
105
|
+
assert.equal(packets[1].stream.status.waiting, true)
|
|
106
|
+
assert.deepEqual(packets[1].stream.actions[0], {
|
|
107
|
+
id: 'open-again',
|
|
108
|
+
label: 'Open Again',
|
|
109
|
+
type: 'link',
|
|
110
|
+
href: 'https://huggingface.co/oauth/device?user_code=ABCD-EFGH',
|
|
111
|
+
target: '_blank',
|
|
112
|
+
features: 'browser',
|
|
113
|
+
variant: 'secondary',
|
|
114
|
+
icon: 'fa-solid fa-arrow-up-right-from-square'
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
assert.equal(packets[2].type, 'htmlmodal')
|
|
118
|
+
assert.equal(packets[2].stream.action, 'close')
|
|
119
|
+
|
|
120
|
+
assert.deepEqual(result, {
|
|
121
|
+
status: 'success',
|
|
122
|
+
login,
|
|
123
|
+
token_path: '/tmp/pinokio/hf-token',
|
|
124
|
+
clipboard: { ok: true },
|
|
125
|
+
open: { action: 'open' }
|
|
126
|
+
})
|
|
127
|
+
} finally {
|
|
128
|
+
Util.openURI = originalOpenURI
|
|
129
|
+
Util.clipboard = originalClipboard
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('hf.login returns success without opening anything when already logged in', async () => {
|
|
134
|
+
const hf = new HF()
|
|
135
|
+
const opened = []
|
|
136
|
+
const copied = []
|
|
137
|
+
const originalOpenURI = Util.openURI
|
|
138
|
+
const originalClipboard = Util.clipboard
|
|
139
|
+
const kernel = {
|
|
140
|
+
connect: {
|
|
141
|
+
async keys(provider) {
|
|
142
|
+
assert.equal(provider, 'huggingface')
|
|
143
|
+
return {
|
|
144
|
+
access_token: 'hf_secret',
|
|
145
|
+
token_path: '/tmp/pinokio/hf-token'
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
Util.openURI = async (uri) => {
|
|
152
|
+
opened.push(uri)
|
|
153
|
+
return { ok: true, status: 'stubbed' }
|
|
154
|
+
}
|
|
155
|
+
Util.clipboard = async (req) => {
|
|
156
|
+
copied.push(req)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const result = await hf.login({ params: {} }, () => {}, kernel)
|
|
161
|
+
|
|
162
|
+
assert.deepEqual(opened, [])
|
|
163
|
+
assert.deepEqual(copied, [])
|
|
164
|
+
assert.deepEqual(result, {
|
|
165
|
+
status: 'success',
|
|
166
|
+
already_logged_in: true,
|
|
167
|
+
token_path: '/tmp/pinokio/hf-token'
|
|
168
|
+
})
|
|
169
|
+
} finally {
|
|
170
|
+
Util.openURI = originalOpenURI
|
|
171
|
+
Util.clipboard = originalClipboard
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('hf.login can use the non-modal browser fallback when modal is disabled', async () => {
|
|
176
|
+
const hf = new HF()
|
|
177
|
+
const opened = []
|
|
178
|
+
const copied = []
|
|
179
|
+
const output = []
|
|
180
|
+
const originalOpenURI = Util.openURI
|
|
181
|
+
const originalClipboard = Util.clipboard
|
|
182
|
+
const kernel = {
|
|
183
|
+
connect: {
|
|
184
|
+
async keys(provider) {
|
|
185
|
+
assert.equal(provider, 'huggingface')
|
|
186
|
+
return null
|
|
187
|
+
},
|
|
188
|
+
async login(provider, params) {
|
|
189
|
+
assert.equal(provider, 'huggingface')
|
|
190
|
+
assert.deepEqual(params, { modal: false, wait: false })
|
|
191
|
+
return {
|
|
192
|
+
status: 'pending',
|
|
193
|
+
login,
|
|
194
|
+
token_path: '/tmp/pinokio/hf-token'
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
Util.clipboard = async (req) => {
|
|
201
|
+
copied.push(req)
|
|
202
|
+
}
|
|
203
|
+
Util.openURI = async (uri) => {
|
|
204
|
+
opened.push(uri)
|
|
205
|
+
return { ok: true, status: 'stubbed' }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const result = await hf.login({ params: { modal: false, wait: false } }, (chunk) => output.push(chunk), kernel)
|
|
210
|
+
|
|
211
|
+
assert.deepEqual(copied, [{
|
|
212
|
+
type: 'copy',
|
|
213
|
+
text: 'ABCD-EFGH'
|
|
214
|
+
}])
|
|
215
|
+
assert.deepEqual(opened, ['https://huggingface.co/oauth/device?user_code=ABCD-EFGH'])
|
|
216
|
+
assert.deepEqual(output, [
|
|
217
|
+
{ raw: '\r\nHugging Face code copied to clipboard: ABCD-EFGH\r\n' },
|
|
218
|
+
{ raw: '\r\nHugging Face login modal disabled.\r\n' },
|
|
219
|
+
{ raw: 'Opening Hugging Face login: https://huggingface.co/oauth/device?user_code=ABCD-EFGH\r\nThe code is already on your clipboard; paste it if Hugging Face asks.\r\n' }
|
|
220
|
+
])
|
|
221
|
+
assert.deepEqual(result, {
|
|
222
|
+
status: 'pending',
|
|
223
|
+
login,
|
|
224
|
+
token_path: '/tmp/pinokio/hf-token',
|
|
225
|
+
clipboard: { ok: true },
|
|
226
|
+
open: { ok: true, status: 'stubbed' }
|
|
227
|
+
})
|
|
228
|
+
} finally {
|
|
229
|
+
Util.openURI = originalOpenURI
|
|
230
|
+
Util.clipboard = originalClipboard
|
|
231
|
+
}
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
test('hf.login modal displays clipboard failure without opening the browser automatically', async () => {
|
|
235
|
+
const hf = new HF()
|
|
236
|
+
const packets = []
|
|
237
|
+
const opened = []
|
|
238
|
+
const originalOpenURI = Util.openURI
|
|
239
|
+
const originalClipboard = Util.clipboard
|
|
240
|
+
const kernel = {
|
|
241
|
+
connect: {
|
|
242
|
+
async keys(provider) {
|
|
243
|
+
assert.equal(provider, 'huggingface')
|
|
244
|
+
return null
|
|
245
|
+
},
|
|
246
|
+
async login(provider) {
|
|
247
|
+
assert.equal(provider, 'huggingface')
|
|
248
|
+
return {
|
|
249
|
+
status: 'pending',
|
|
250
|
+
login,
|
|
251
|
+
token_path: '/tmp/pinokio/hf-token'
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
api: {
|
|
256
|
+
async wait() {
|
|
257
|
+
return { action: 'open' }
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
Util.clipboard = async () => {
|
|
263
|
+
throw new Error('clipboard denied')
|
|
264
|
+
}
|
|
265
|
+
Util.openURI = async (uri) => {
|
|
266
|
+
opened.push(uri)
|
|
267
|
+
return { ok: true, status: 'stubbed' }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
const result = await hf.login({
|
|
272
|
+
parent: { id: 'test-script' },
|
|
273
|
+
params: { wait: false }
|
|
274
|
+
}, (stream, type) => packets.push({ type, stream }), kernel)
|
|
275
|
+
|
|
276
|
+
assert.deepEqual(opened, [])
|
|
277
|
+
assert.equal(packets.length, 3)
|
|
278
|
+
assert.match(packets[0].stream.html, /Clipboard copy failed/)
|
|
279
|
+
assert.match(packets[0].stream.html, /ABCD-EFGH/)
|
|
280
|
+
assert.equal(packets[1].stream.action, 'update')
|
|
281
|
+
assert.equal(packets[2].stream.action, 'close')
|
|
282
|
+
assert.deepEqual(result, {
|
|
283
|
+
status: 'pending',
|
|
284
|
+
login,
|
|
285
|
+
token_path: '/tmp/pinokio/hf-token',
|
|
286
|
+
clipboard: { ok: false, error: 'clipboard denied' },
|
|
287
|
+
open: { action: 'open' }
|
|
288
|
+
})
|
|
289
|
+
} finally {
|
|
290
|
+
Util.openURI = originalOpenURI
|
|
291
|
+
Util.clipboard = originalClipboard
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
test('hf.login cancels the pending provider login when the modal is canceled', async () => {
|
|
296
|
+
const hf = new HF()
|
|
297
|
+
const packets = []
|
|
298
|
+
const cancelCalls = []
|
|
299
|
+
const originalClipboard = Util.clipboard
|
|
300
|
+
const kernel = {
|
|
301
|
+
connect: {
|
|
302
|
+
async keys(provider) {
|
|
303
|
+
assert.equal(provider, 'huggingface')
|
|
304
|
+
return null
|
|
305
|
+
},
|
|
306
|
+
async login(provider) {
|
|
307
|
+
assert.equal(provider, 'huggingface')
|
|
308
|
+
return {
|
|
309
|
+
status: 'pending',
|
|
310
|
+
login,
|
|
311
|
+
token_path: '/tmp/pinokio/hf-token'
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
async cancelLogin(provider, params) {
|
|
315
|
+
cancelCalls.push({ provider, params })
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
api: {
|
|
319
|
+
async wait() {
|
|
320
|
+
return { action: 'cancel' }
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
Util.clipboard = async () => {}
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
await assert.rejects(
|
|
329
|
+
() => hf.login({
|
|
330
|
+
parent: { id: 'test-script' },
|
|
331
|
+
params: { force: true }
|
|
332
|
+
}, (stream, type) => packets.push({ type, stream }), kernel),
|
|
333
|
+
/Hugging Face login canceled/
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
assert.deepEqual(cancelCalls, [{
|
|
337
|
+
provider: 'huggingface',
|
|
338
|
+
params: { force: true }
|
|
339
|
+
}])
|
|
340
|
+
assert.equal(packets.at(-1).stream.action, 'close')
|
|
341
|
+
} finally {
|
|
342
|
+
Util.clipboard = originalClipboard
|
|
343
|
+
}
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
test('hf.login cancels the pending provider login after timeout', async () => {
|
|
347
|
+
const hf = new HF()
|
|
348
|
+
const cancelCalls = []
|
|
349
|
+
const originalClipboard = Util.clipboard
|
|
350
|
+
const kernel = {
|
|
351
|
+
connect: {
|
|
352
|
+
async keys(provider) {
|
|
353
|
+
assert.equal(provider, 'huggingface')
|
|
354
|
+
return null
|
|
355
|
+
},
|
|
356
|
+
async login(provider) {
|
|
357
|
+
assert.equal(provider, 'huggingface')
|
|
358
|
+
return {
|
|
359
|
+
status: 'pending',
|
|
360
|
+
login,
|
|
361
|
+
token_path: '/tmp/pinokio/hf-token'
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
async cancelLogin(provider, params) {
|
|
365
|
+
cancelCalls.push({ provider, params })
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
api: {
|
|
369
|
+
async wait() {
|
|
370
|
+
return { action: 'open' }
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
Util.clipboard = async () => {}
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
const result = await hf.login({
|
|
379
|
+
parent: { id: 'test-script' },
|
|
380
|
+
params: { timeout: 1, interval: 1 }
|
|
381
|
+
}, () => {}, kernel)
|
|
382
|
+
|
|
383
|
+
assert.equal(result.status, 'timeout')
|
|
384
|
+
assert.deepEqual(cancelCalls, [{
|
|
385
|
+
provider: 'huggingface',
|
|
386
|
+
params: { timeout: 1, interval: 1 }
|
|
387
|
+
}])
|
|
388
|
+
} finally {
|
|
389
|
+
Util.clipboard = originalClipboard
|
|
390
|
+
}
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
test('hf.logout delegates to the Hugging Face connect provider and returns success', async () => {
|
|
394
|
+
const hf = new HF()
|
|
395
|
+
const req = { params: {} }
|
|
396
|
+
const calls = []
|
|
397
|
+
const kernel = {
|
|
398
|
+
connect: {
|
|
399
|
+
async logout(provider, params) {
|
|
400
|
+
calls.push({ method: 'logout', provider, params })
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const result = await hf.logout(req, () => {}, kernel)
|
|
406
|
+
|
|
407
|
+
assert.deepEqual(result, { status: 'success' })
|
|
408
|
+
assert.deepEqual(calls, [{ method: 'logout', provider: 'huggingface', params: req.params }])
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
test('hf.login reports unavailable Hugging Face connect provider', async () => {
|
|
412
|
+
const hf = new HF()
|
|
413
|
+
|
|
414
|
+
await assert.rejects(
|
|
415
|
+
() => hf.login({ params: {} }, () => {}, {}),
|
|
416
|
+
/Hugging Face connect keys is not available/
|
|
417
|
+
)
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
test('hf.login reports unavailable Hugging Face login starter after checking existing keys', async () => {
|
|
421
|
+
const hf = new HF()
|
|
422
|
+
const kernel = {
|
|
423
|
+
connect: {
|
|
424
|
+
async keys(provider) {
|
|
425
|
+
assert.equal(provider, 'huggingface')
|
|
426
|
+
return null
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
await assert.rejects(
|
|
432
|
+
() => hf.login({ params: {} }, () => {}, kernel),
|
|
433
|
+
/Hugging Face connect login is not available/
|
|
434
|
+
)
|
|
435
|
+
})
|
|
@@ -71,3 +71,36 @@ test('Hugging Face connect uses shared HF_TOKEN_PATH and ignores ambient HF_TOKE
|
|
|
71
71
|
await fs.rm(root, { recursive: true, force: true })
|
|
72
72
|
}
|
|
73
73
|
})
|
|
74
|
+
|
|
75
|
+
test('Hugging Face connect cancelLogin only stops a pending login session', async () => {
|
|
76
|
+
const provider = new HuggingfaceConnect(createKernel('/tmp/pinokio'), {})
|
|
77
|
+
let killed = false
|
|
78
|
+
provider.loginSession = {
|
|
79
|
+
status: 'pending',
|
|
80
|
+
error: null,
|
|
81
|
+
child: {
|
|
82
|
+
kill() {
|
|
83
|
+
killed = true
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
await provider.cancelLogin()
|
|
89
|
+
|
|
90
|
+
assert.equal(killed, true)
|
|
91
|
+
assert.equal(provider.loginSession, null)
|
|
92
|
+
|
|
93
|
+
const completeSession = {
|
|
94
|
+
status: 'success',
|
|
95
|
+
child: {
|
|
96
|
+
kill() {
|
|
97
|
+
throw new Error('should not kill completed sessions')
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
provider.loginSession = completeSession
|
|
102
|
+
|
|
103
|
+
await provider.cancelLogin()
|
|
104
|
+
|
|
105
|
+
assert.equal(provider.loginSession, completeSession)
|
|
106
|
+
})
|