pinokiod 8.0.6 → 8.0.8
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 +2 -0
- package/package.json +1 -1
- package/server/index.js +0 -4
- package/server/public/htmlmodal.js +59 -4
- package/test/github-connection.test.js +17 -12
- package/test/hf-api.test.js +2 -0
- package/test/htmlmodal-clipboard.test.js +115 -0
package/kernel/api/hf/index.js
CHANGED
|
@@ -118,6 +118,8 @@ class HF {
|
|
|
118
118
|
href: login.verification_uri_complete,
|
|
119
119
|
target: "_blank",
|
|
120
120
|
features: "browser",
|
|
121
|
+
copyText: login.user_code,
|
|
122
|
+
copyFeedbackSelector: ".hf-login-modal-copy",
|
|
121
123
|
primary: true,
|
|
122
124
|
close: false,
|
|
123
125
|
icon: "fa-solid fa-arrow-up-right-from-square"
|
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -1546,15 +1546,11 @@ class Server {
|
|
|
1546
1546
|
github_login_params() {
|
|
1547
1547
|
const doneMarker = "PINOKIO_GITHUB_LOGIN_DONE"
|
|
1548
1548
|
const delimiter = this.kernel.platform === "win32" ? " && " : " ; "
|
|
1549
|
-
const verifyCommand = this.kernel.platform === "win32"
|
|
1550
|
-
? "powershell.exe -NoProfile -Command \"$env:GIT_TERMINAL_PROMPT='0'; $env:GCM_INTERACTIVE='never'; @('protocol=https','host=github.com','') | git credential fill > $null; exit $LASTEXITCODE\""
|
|
1551
|
-
: "printf 'protocol=https\\nhost=github.com\\n\\n' | GIT_TERMINAL_PROMPT=0 GCM_INTERACTIVE=never git credential fill >/dev/null"
|
|
1552
1549
|
const doneCommand = this.kernel.platform === "win32"
|
|
1553
1550
|
? "echo P^INOKIO_GITHUB_LOGIN_DONE"
|
|
1554
1551
|
: "(GCM_DONE=INOKIO_GITHUB_LOGIN_DONE; printf 'P%s\\n' \"$GCM_DONE\")"
|
|
1555
1552
|
const loginCommand = [
|
|
1556
1553
|
"git credential-manager github login --device --force",
|
|
1557
|
-
verifyCommand,
|
|
1558
1554
|
doneCommand
|
|
1559
1555
|
].join(" && ")
|
|
1560
1556
|
|
|
@@ -286,17 +286,59 @@
|
|
|
286
286
|
return classes.join(' ')
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
async copyActionText(action) {
|
|
290
|
+
if (!action || typeof action.copyText !== 'string') {
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
const feedback = action.copyFeedbackSelector
|
|
294
|
+
? this.body.querySelector(action.copyFeedbackSelector)
|
|
295
|
+
: null
|
|
296
|
+
try {
|
|
297
|
+
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
|
298
|
+
await navigator.clipboard.writeText(action.copyText)
|
|
299
|
+
} else {
|
|
300
|
+
const input = createElement('textarea')
|
|
301
|
+
try {
|
|
302
|
+
input.value = action.copyText
|
|
303
|
+
input.setAttribute('readonly', '')
|
|
304
|
+
input.style.position = 'fixed'
|
|
305
|
+
input.style.opacity = '0'
|
|
306
|
+
document.body.appendChild(input)
|
|
307
|
+
input.select()
|
|
308
|
+
if (!document.execCommand('copy')) {
|
|
309
|
+
throw new Error('Clipboard copy was rejected')
|
|
310
|
+
}
|
|
311
|
+
} finally {
|
|
312
|
+
input.remove()
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (feedback) {
|
|
316
|
+
feedback.textContent = 'The code has been copied to your clipboard.'
|
|
317
|
+
feedback.classList.remove('warning')
|
|
318
|
+
feedback.classList.add('success')
|
|
319
|
+
}
|
|
320
|
+
} catch (_) {
|
|
321
|
+
if (feedback) {
|
|
322
|
+
feedback.textContent = 'Clipboard copy failed. Copy the displayed code manually.'
|
|
323
|
+
feedback.classList.remove('success')
|
|
324
|
+
feedback.classList.add('warning')
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
289
329
|
handleAction(action) {
|
|
290
330
|
if (!action) {
|
|
291
331
|
return
|
|
292
332
|
}
|
|
333
|
+
if (typeof action.copyText === 'string') {
|
|
334
|
+
this.copyActionText(action)
|
|
335
|
+
}
|
|
293
336
|
if (action.type === 'link' && action.href) {
|
|
294
|
-
|
|
295
|
-
window.open(action.href, target, action.features || 'noopener')
|
|
337
|
+
this.openActionUrl(action)
|
|
296
338
|
return
|
|
297
339
|
}
|
|
298
340
|
if (action.type === 'submit' && action.href) {
|
|
299
|
-
|
|
341
|
+
this.openActionUrl(action)
|
|
300
342
|
}
|
|
301
343
|
if (action.type === 'submit' && this.current.awaiting && this.current.socket) {
|
|
302
344
|
this.emitResponse({ action: action.id || 'submit', payload: action.payload || null })
|
|
@@ -306,8 +348,21 @@
|
|
|
306
348
|
} else if (action.type === 'button' && this.current.socket && this.current.awaiting) {
|
|
307
349
|
this.emitResponse({ action: action.id || 'button', payload: action.payload || null })
|
|
308
350
|
} else if (action.type === 'button' && action.href) {
|
|
309
|
-
|
|
351
|
+
this.openActionUrl(action)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
openActionUrl(action) {
|
|
356
|
+
if (action.features && action.features.includes('browser')) {
|
|
357
|
+
const agent = document.body.getAttribute('data-agent')
|
|
358
|
+
if (agent === 'electron') {
|
|
359
|
+
window.open(action.href, action.target || '_blank', 'browser')
|
|
360
|
+
} else {
|
|
361
|
+
window.open(action.href, action.target || '_blank')
|
|
362
|
+
}
|
|
363
|
+
return
|
|
310
364
|
}
|
|
365
|
+
window.open(action.href, action.target || '_blank', action.features || 'noopener')
|
|
311
366
|
}
|
|
312
367
|
|
|
313
368
|
emitResponse(data) {
|
|
@@ -73,24 +73,29 @@ test('GitHub connection coalesces only concurrent GCM checks', async () => {
|
|
|
73
73
|
assert.equal(calls, 2)
|
|
74
74
|
})
|
|
75
75
|
|
|
76
|
-
test('GitHub login
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
76
|
+
test('GitHub login completes directly from GCM success on macOS and Linux', () => {
|
|
77
|
+
for (const platform of ['darwin', 'linux']) {
|
|
78
|
+
const { doneMarker, message } = Server.prototype.github_login_params.call({
|
|
79
|
+
kernel: { platform }
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
assert.equal(doneMarker, 'PINOKIO_GITHUB_LOGIN_DONE')
|
|
83
|
+
assert.match(message, /git credential-manager github login --device --force && \(GCM_DONE=INOKIO_GITHUB_LOGIN_DONE; printf 'P%s\\n' "\$GCM_DONE"\)/)
|
|
84
|
+
assert.doesNotMatch(message, /credential fill/)
|
|
85
|
+
assert.doesNotMatch(message, /git credential-manager github login --device --force\s*;/)
|
|
86
|
+
assert.doesNotMatch(message, /--web/)
|
|
87
|
+
assert.doesNotMatch(message, /PINOKIO_GITHUB_LOGIN_DONE/)
|
|
88
|
+
}
|
|
86
89
|
})
|
|
87
90
|
|
|
88
|
-
test('GitHub login
|
|
91
|
+
test('GitHub login completes directly from GCM success on Windows', () => {
|
|
89
92
|
const { message } = Server.prototype.github_login_params.call({
|
|
90
93
|
kernel: { platform: 'win32' }
|
|
91
94
|
})
|
|
92
95
|
|
|
93
|
-
assert.match(message, /git credential-manager github login --device --force &&
|
|
96
|
+
assert.match(message, /git credential-manager github login --device --force && echo P\^INOKIO_GITHUB_LOGIN_DONE/)
|
|
97
|
+
assert.doesNotMatch(message, /credential fill/)
|
|
98
|
+
assert.doesNotMatch(message, /powershell\.exe/)
|
|
94
99
|
assert.doesNotMatch(message, /--web/)
|
|
95
100
|
assert.doesNotMatch(message, /PINOKIO_GITHUB_LOGIN_DONE/)
|
|
96
101
|
})
|
package/test/hf-api.test.js
CHANGED
|
@@ -108,6 +108,8 @@ test('hf.login shows a modal, waits for the user to open Hugging Face, then clos
|
|
|
108
108
|
href: 'https://huggingface.co/oauth/device?user_code=ABCD-EFGH',
|
|
109
109
|
target: '_blank',
|
|
110
110
|
features: 'browser',
|
|
111
|
+
copyText: 'ABCD-EFGH',
|
|
112
|
+
copyFeedbackSelector: '.hf-login-modal-copy',
|
|
111
113
|
primary: true,
|
|
112
114
|
close: false,
|
|
113
115
|
icon: 'fa-solid fa-arrow-up-right-from-square'
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
const assert = require('node:assert/strict')
|
|
2
|
+
const fs = require('node:fs/promises')
|
|
3
|
+
const path = require('node:path')
|
|
4
|
+
const test = require('node:test')
|
|
5
|
+
const { JSDOM } = require('jsdom')
|
|
6
|
+
|
|
7
|
+
const htmlModalPath = path.resolve(__dirname, '../server/public/htmlmodal.js')
|
|
8
|
+
|
|
9
|
+
async function createModalDom(options = {}) {
|
|
10
|
+
const script = await fs.readFile(htmlModalPath, 'utf8')
|
|
11
|
+
const opened = []
|
|
12
|
+
const dom = new JSDOM(`<!doctype html><html><head></head><body><script>${script}</script></body></html>`, {
|
|
13
|
+
url: 'http://127.0.0.1:42000/',
|
|
14
|
+
runScripts: 'dangerously',
|
|
15
|
+
pretendToBeVisual: true,
|
|
16
|
+
beforeParse(window) {
|
|
17
|
+
if (options.clipboard) {
|
|
18
|
+
Object.defineProperty(window.navigator, 'clipboard', {
|
|
19
|
+
configurable: true,
|
|
20
|
+
value: options.clipboard
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
if (options.execCommand) {
|
|
24
|
+
window.document.execCommand = options.execCommand
|
|
25
|
+
}
|
|
26
|
+
window.open = (...args) => {
|
|
27
|
+
opened.push(args)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
if (!dom.window.HtmlModal) {
|
|
33
|
+
await new Promise((resolve) => dom.window.addEventListener('DOMContentLoaded', resolve, { once: true }))
|
|
34
|
+
}
|
|
35
|
+
if (options.agent) {
|
|
36
|
+
dom.window.document.body.setAttribute('data-agent', options.agent)
|
|
37
|
+
}
|
|
38
|
+
return { dom, manager: dom.window.HtmlModal, opened }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function openCopyModal(manager) {
|
|
42
|
+
manager.handle({
|
|
43
|
+
id: 'packet',
|
|
44
|
+
data: {
|
|
45
|
+
action: 'open',
|
|
46
|
+
id: 'hf-login:test',
|
|
47
|
+
html: '<div class="hf-login-modal-copy custom-class">Copy this code.</div>',
|
|
48
|
+
actions: [{
|
|
49
|
+
id: 'open',
|
|
50
|
+
label: 'Open Hugging Face',
|
|
51
|
+
type: 'submit',
|
|
52
|
+
href: 'https://huggingface.co/oauth/device?user_code=ABCD-EFGH',
|
|
53
|
+
target: '_blank',
|
|
54
|
+
features: 'browser',
|
|
55
|
+
copyText: 'ABCD-EFGH',
|
|
56
|
+
copyFeedbackSelector: '.hf-login-modal-copy'
|
|
57
|
+
}]
|
|
58
|
+
}
|
|
59
|
+
}, null)
|
|
60
|
+
manager.actions.querySelector('button').click()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
test('html modal copies action text from the user click and opens a client-side web tab', async () => {
|
|
64
|
+
const copied = []
|
|
65
|
+
const { manager, opened } = await createModalDom({
|
|
66
|
+
clipboard: {
|
|
67
|
+
async writeText(value) {
|
|
68
|
+
copied.push(value)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
openCopyModal(manager)
|
|
73
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
74
|
+
|
|
75
|
+
assert.deepEqual(copied, ['ABCD-EFGH'])
|
|
76
|
+
assert.deepEqual(opened, [[
|
|
77
|
+
'https://huggingface.co/oauth/device?user_code=ABCD-EFGH',
|
|
78
|
+
'_blank'
|
|
79
|
+
]])
|
|
80
|
+
const feedback = manager.body.querySelector('.hf-login-modal-copy')
|
|
81
|
+
assert.equal(feedback.textContent, 'The code has been copied to your clipboard.')
|
|
82
|
+
assert.equal(feedback.classList.contains('custom-class'), true)
|
|
83
|
+
assert.equal(feedback.classList.contains('success'), true)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('html modal keeps the Electron external-browser hint', async () => {
|
|
87
|
+
const { manager, opened } = await createModalDom({
|
|
88
|
+
agent: 'electron',
|
|
89
|
+
clipboard: { async writeText() {} }
|
|
90
|
+
})
|
|
91
|
+
openCopyModal(manager)
|
|
92
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
93
|
+
|
|
94
|
+
assert.deepEqual(opened, [[
|
|
95
|
+
'https://huggingface.co/oauth/device?user_code=ABCD-EFGH',
|
|
96
|
+
'_blank',
|
|
97
|
+
'browser'
|
|
98
|
+
]])
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('html modal reports fallback copy failure and removes its temporary textarea', async () => {
|
|
102
|
+
const { dom, manager } = await createModalDom({
|
|
103
|
+
execCommand() {
|
|
104
|
+
throw new Error('copy denied')
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
openCopyModal(manager)
|
|
108
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
109
|
+
|
|
110
|
+
const feedback = manager.body.querySelector('.hf-login-modal-copy')
|
|
111
|
+
assert.equal(feedback.textContent, 'Clipboard copy failed. Copy the displayed code manually.')
|
|
112
|
+
assert.equal(feedback.classList.contains('custom-class'), true)
|
|
113
|
+
assert.equal(feedback.classList.contains('warning'), true)
|
|
114
|
+
assert.equal(dom.window.document.querySelectorAll('body > textarea').length, 0)
|
|
115
|
+
})
|