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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "8.0.6",
3
+ "version": "8.0.8",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
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
- const target = action.target || '_blank'
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
- window.open(action.href, action.target || '_blank', action.features || 'noopener')
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
- window.open(action.href, action.target || '_blank', action.features || 'noopener')
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 only emits completion marker after credential verification', () => {
77
- const { doneMarker, message } = Server.prototype.github_login_params.call({
78
- kernel: { platform: 'darwin' }
79
- })
80
-
81
- assert.equal(doneMarker, 'PINOKIO_GITHUB_LOGIN_DONE')
82
- assert.match(message, /git credential-manager github login --device --force && printf 'protocol=https\\nhost=github\.com\\n\\n' \| GIT_TERMINAL_PROMPT=0 GCM_INTERACTIVE=never git credential fill >\/dev\/null && \(GCM_DONE=INOKIO_GITHUB_LOGIN_DONE; printf 'P%s\\n' "\$GCM_DONE"\)/)
83
- assert.doesNotMatch(message, /git credential-manager github login --device --force\s*;/)
84
- assert.doesNotMatch(message, /--web/)
85
- assert.doesNotMatch(message, /PINOKIO_GITHUB_LOGIN_DONE/)
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 uses success-only credential verification on Windows', () => {
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 && powershell\.exe -NoProfile -Command "\$env:GIT_TERMINAL_PROMPT='0'; \$env:GCM_INTERACTIVE='never'; @\('protocol=https','host=github\.com',''\) \| git credential fill > \$null; exit \$LASTEXITCODE" && echo P\^INOKIO_GITHUB_LOGIN_DONE/)
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
  })
@@ -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
+ })