pinokiod 8.2.24 → 8.2.26
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/exec/index.js +2 -0
- package/kernel/connect/providers/huggingface/index.js +2 -6
- package/kernel/environment.js +14 -2
- package/kernel/shell.js +2 -1
- package/package.json +1 -1
- package/server/views/anywhere.ejs +64 -0
- package/test/anywhere-clipboard.test.js +184 -0
- package/test/huggingface-connect.test.js +8 -10
- package/test/huggingface-managed-env.test.js +124 -0
- package/test/huggingface-token-validity.test.js +4 -4
- package/test/shell-run-template.test.js +3 -3
package/kernel/api/exec/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const child_process = require('child_process')
|
|
2
|
+
const Environment = require('../../environment')
|
|
2
3
|
module.exports = async (req, ondata, kernel) => {
|
|
3
4
|
/*
|
|
4
5
|
req := {
|
|
@@ -50,6 +51,7 @@ module.exports = async (req, ondata, kernel) => {
|
|
|
50
51
|
req.params.cwd = req.params.path
|
|
51
52
|
}
|
|
52
53
|
req.params.env = Object.assign({}, kernel.envs, req.params.env)
|
|
54
|
+
Environment.applyHfTokenPath(req.params.env, kernel)
|
|
53
55
|
console.log("env", JSON.stringify(req.params.env, null, 2))
|
|
54
56
|
ondata({ raw: `██ Exec: ${req.params.message}\r\n` })
|
|
55
57
|
let response = await new Promise((resolve, reject) => {
|
|
@@ -46,7 +46,6 @@ class Huggingface {
|
|
|
46
46
|
let env
|
|
47
47
|
if (context.parentPath) {
|
|
48
48
|
env = await Environment.get2(context.parentPath, this.kernel)
|
|
49
|
-
if (env.HF_TOKEN_PATH === this.kernel.path("cache", "HF_AUTH", "anonymous")) env.HF_TOKEN_PATH = this.defaultTokenPath()
|
|
50
49
|
} else {
|
|
51
50
|
let systemEnv = {}
|
|
52
51
|
try {
|
|
@@ -60,11 +59,8 @@ class Huggingface {
|
|
|
60
59
|
env = Object.assign(env, context.env)
|
|
61
60
|
}
|
|
62
61
|
env = managedPythonEnv(env)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
} else if (!path.isAbsolute(env.HF_TOKEN_PATH)) {
|
|
66
|
-
env.HF_TOKEN_PATH = path.resolve(this.requestCwd(context), env.HF_TOKEN_PATH)
|
|
67
|
-
}
|
|
62
|
+
// Login and all auth operations use the same Pinokio-managed credential file.
|
|
63
|
+
Environment.applyHfTokenPath(env, this.kernel, { auth: true })
|
|
68
64
|
if (!env.HF_HUB_DISABLE_UPDATE_CHECK) {
|
|
69
65
|
env.HF_HUB_DISABLE_UPDATE_CHECK = "1"
|
|
70
66
|
}
|
package/kernel/environment.js
CHANGED
|
@@ -11,6 +11,17 @@ const SCRIPT_AUTOLAUNCH_KEY = "PINOKIO_SCRIPT_AUTOLAUNCH"
|
|
|
11
11
|
const SCRIPT_AUTOLAUNCH_ENABLED_KEY = "PINOKIO_SCRIPT_AUTOLAUNCH_ENABLED"
|
|
12
12
|
const SCRIPT_REQUIREMENTS_KEY = "PINOKIO_SCRIPT_REQUIRES"
|
|
13
13
|
|
|
14
|
+
const applyHfTokenPath = (env, kernel, { auth = false } = {}) => {
|
|
15
|
+
for (const key of Object.keys(env)) {
|
|
16
|
+
if (key.toUpperCase() === "HF_TOKEN_PATH") delete env[key]
|
|
17
|
+
}
|
|
18
|
+
const authDir = path.resolve(kernel.homedir, "cache", "HF_AUTH")
|
|
19
|
+
const anonymousPath = path.join(authDir, "anonymous")
|
|
20
|
+
env.HF_TOKEN_PATH = !auth && kernel.envs?.HF_TOKEN_PATH === anonymousPath
|
|
21
|
+
? anonymousPath
|
|
22
|
+
: path.join(authDir, "token")
|
|
23
|
+
}
|
|
24
|
+
|
|
14
25
|
const parseBooleanFlag = (value) => {
|
|
15
26
|
if (typeof value !== "string") {
|
|
16
27
|
return null
|
|
@@ -602,7 +613,8 @@ const ENV = async (type, homedir, kernel) => {
|
|
|
602
613
|
for(let key in system_env) {
|
|
603
614
|
if (!keys.has(key)) {
|
|
604
615
|
// the key has not been processed, need to add to the lines
|
|
605
|
-
|
|
616
|
+
// HF_TOKEN_PATH is supplied by Pinokio at runtime, never copied into apps.
|
|
617
|
+
if (irrelevant_keys.includes(key) || key.toUpperCase() === "HF_TOKEN_PATH") {
|
|
606
618
|
} else {
|
|
607
619
|
let val = system_env[key]
|
|
608
620
|
let kv = `${key}=${val}`
|
|
@@ -1021,4 +1033,4 @@ const init = async (options, kernel) => {
|
|
|
1021
1033
|
env_path: current
|
|
1022
1034
|
}
|
|
1023
1035
|
}
|
|
1024
|
-
module.exports = { ENV, get, get2, init_folders, ensurePinokioCacheDirs, requirements, init, get_root, SCRIPT_AUTOLAUNCH_KEY, SCRIPT_AUTOLAUNCH_ENABLED_KEY, SCRIPT_REQUIREMENTS_KEY, getScriptAutolaunch, getScriptAutolaunchEnabled, parseAutolaunchList, formatAutolaunchList, getScriptRequirements }
|
|
1036
|
+
module.exports = { ENV, get, get2, applyHfTokenPath, init_folders, ensurePinokioCacheDirs, requirements, init, get_root, SCRIPT_AUTOLAUNCH_KEY, SCRIPT_AUTOLAUNCH_ENABLED_KEY, SCRIPT_REQUIREMENTS_KEY, getScriptAutolaunch, getScriptAutolaunchEnabled, parseAutolaunchList, formatAutolaunchList, getScriptRequirements }
|
package/kernel/shell.js
CHANGED
|
@@ -307,7 +307,6 @@ class Shell {
|
|
|
307
307
|
}
|
|
308
308
|
setDefaultEnvValue(this.env, "UV_HTTP_TIMEOUT", "60")
|
|
309
309
|
setDefaultEnvValue(this.env, "HF_HUB_DISABLE_UPDATE_CHECK", "1")
|
|
310
|
-
setDefaultEnvValue(this.env, "HF_TOKEN_PATH", this.kernel.envs?.HF_TOKEN_PATH || path.resolve(this.kernel.homedir, "cache", "HF_AUTH", "token"))
|
|
311
310
|
|
|
312
311
|
// Point every app's Python HTTPS trust (httpx, requests, pip, the ssl module) at the
|
|
313
312
|
// managed CA bundle instead of the certifi copy frozen inside each environment.
|
|
@@ -350,6 +349,8 @@ class Shell {
|
|
|
350
349
|
targetPath: params && params.path ? params.path : this.kernel.homedir,
|
|
351
350
|
})
|
|
352
351
|
}
|
|
352
|
+
// Apply last so config overrides and -env cannot disconnect apps from login.
|
|
353
|
+
Environment.applyHfTokenPath(this.env, this.kernel)
|
|
353
354
|
}
|
|
354
355
|
isCmdShell(shellName=this.shell) {
|
|
355
356
|
const name = (shellName || '').toLowerCase()
|
package/package.json
CHANGED
|
@@ -49,6 +49,14 @@ body.dark .anywhere-card { border-color: rgba(255,255,255,0.1); background: rgba
|
|
|
49
49
|
}
|
|
50
50
|
body.dark .anywhere-address { background: rgba(255,255,255,0.08); }
|
|
51
51
|
.anywhere-address a { color: inherit; text-decoration: none; }
|
|
52
|
+
.anywhere-copy { min-height: 44px; }
|
|
53
|
+
.anywhere-copy:focus-visible { outline: 2px solid currentColor; outline-offset: 3px; }
|
|
54
|
+
.anywhere-copy-manual { width: 100%; min-width: 0; }
|
|
55
|
+
.anywhere-copy-manual input {
|
|
56
|
+
box-sizing: border-box; width: 100%; min-height: 44px; padding: 8px;
|
|
57
|
+
font-size: 16px; color: inherit; background: transparent; border: 1px solid currentColor;
|
|
58
|
+
border-radius: 8px; user-select: text; -webkit-user-select: text;
|
|
59
|
+
}
|
|
52
60
|
.anywhere-btn {
|
|
53
61
|
display: inline-flex; align-items: center; gap: 8px;
|
|
54
62
|
padding: 10px 16px; border-radius: 8px; border: none; cursor: pointer;
|
|
@@ -201,6 +209,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
201
209
|
<div class='anywhere-qr'>
|
|
202
210
|
<img id='anywhere-qr' alt='QR code to connect' src=''>
|
|
203
211
|
<div class='anywhere-address'><a id='anywhere-address' href='#' target='_blank' rel='noopener'></a></div>
|
|
212
|
+
<button type='button' class='anywhere-btn secondary anywhere-copy' id='anywhere-copy' disabled><i class='fa-regular fa-copy' aria-hidden='true'></i> Copy URL</button>
|
|
213
|
+
<div id='anywhere-copy-status' class='anywhere-muted' role='status' aria-live='polite'></div>
|
|
214
|
+
<div id='anywhere-copy-manual' class='anywhere-copy-manual' hidden>
|
|
215
|
+
<input id='anywhere-copy-value' type='text' readonly aria-label='Remote URL' aria-describedby='anywhere-copy-status'>
|
|
216
|
+
</div>
|
|
204
217
|
<div class='anywhere-muted'>signed in as <b id='anywhere-account'></b></div>
|
|
205
218
|
<div class='anywhere-waiting' id='anywhere-waiting'><i class="fa-solid fa-circle-notch fa-spin"></i> Waiting for another device to join…</div>
|
|
206
219
|
</div>
|
|
@@ -254,8 +267,54 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
254
267
|
const switchEl = document.getElementById('anywhere-switch')
|
|
255
268
|
const switchLabel = document.getElementById('anywhere-switch-label')
|
|
256
269
|
const agent = document.body.getAttribute('data-agent')
|
|
270
|
+
const copyButton = document.getElementById('anywhere-copy')
|
|
271
|
+
const copyStatus = document.getElementById('anywhere-copy-status')
|
|
272
|
+
const copyManual = document.getElementById('anywhere-copy-manual')
|
|
273
|
+
const copyValue = document.getElementById('anywhere-copy-value')
|
|
257
274
|
let current = <%- JSON.stringify(status || {}).replace(/</g, '\\u003c') %>
|
|
258
275
|
let busy = false
|
|
276
|
+
let copying = false
|
|
277
|
+
|
|
278
|
+
copyButton.addEventListener('click', async () => {
|
|
279
|
+
const url = current.address
|
|
280
|
+
if (copying || current.phase !== 'ready' || !url) return
|
|
281
|
+
const previousFocus = document.activeElement
|
|
282
|
+
copying = true
|
|
283
|
+
copyButton.disabled = true
|
|
284
|
+
copyStatus.textContent = 'Copying…'
|
|
285
|
+
const stillCurrent = () => current.phase === 'ready' && current.address === url
|
|
286
|
+
let copied = false
|
|
287
|
+
try {
|
|
288
|
+
// Call directly from the click: Safari requires a user gesture.
|
|
289
|
+
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
|
290
|
+
await navigator.clipboard.writeText(url)
|
|
291
|
+
copied = true
|
|
292
|
+
}
|
|
293
|
+
} catch (_) { /* Try selection-based copying when clipboard access is denied. */ }
|
|
294
|
+
if (stillCurrent()) {
|
|
295
|
+
if (!copied) {
|
|
296
|
+
copyValue.value = url
|
|
297
|
+
copyManual.hidden = false
|
|
298
|
+
copyStatus.textContent = 'Automatic copy is unavailable. Touch and hold the URL to copy it, or press Ctrl+C / ⌘C.'
|
|
299
|
+
try {
|
|
300
|
+
copyValue.focus({ preventScroll: true })
|
|
301
|
+
copyValue.select()
|
|
302
|
+
copyValue.setSelectionRange(0, url.length)
|
|
303
|
+
copied = document.execCommand('copy') === true
|
|
304
|
+
} catch (_) { /* Leave the visible URL available for manual copying. */ }
|
|
305
|
+
}
|
|
306
|
+
if (copied) {
|
|
307
|
+
copyManual.hidden = true
|
|
308
|
+
copyStatus.textContent = 'URL copied.'
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
copying = false
|
|
312
|
+
copyButton.disabled = current.phase !== 'ready' || !current.address
|
|
313
|
+
if (copied && stillCurrent() && previousFocus &&
|
|
314
|
+
(document.activeElement === copyValue || document.activeElement === document.body)) {
|
|
315
|
+
previousFocus.focus({ preventScroll: true })
|
|
316
|
+
}
|
|
317
|
+
})
|
|
259
318
|
|
|
260
319
|
const esc = (value) => String(value == null ? '' : value).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]))
|
|
261
320
|
const openExternal = (href) => {
|
|
@@ -292,7 +351,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
292
351
|
}
|
|
293
352
|
|
|
294
353
|
const render = (status) => {
|
|
354
|
+
if (current.address !== (status || {}).address || current.phase !== (status || {}).phase) {
|
|
355
|
+
copyManual.hidden = true
|
|
356
|
+
copyStatus.textContent = ''
|
|
357
|
+
}
|
|
295
358
|
current = status || {}
|
|
359
|
+
copyButton.disabled = copying || current.phase !== 'ready' || !current.address
|
|
296
360
|
const phase = current.phase && current.phase !== 'setup' ? current.phase : 'starting'
|
|
297
361
|
const on = !!current.enabled && phase !== 'off' && phase !== 'setup'
|
|
298
362
|
switchEl.dataset.on = on ? 'true' : 'false'
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
const assert = require('node:assert/strict')
|
|
2
|
+
const fs = require('node:fs')
|
|
3
|
+
const path = require('node:path')
|
|
4
|
+
const test = require('node:test')
|
|
5
|
+
const ejs = require('ejs')
|
|
6
|
+
const { JSDOM, VirtualConsole } = require('jsdom')
|
|
7
|
+
|
|
8
|
+
const filename = path.resolve(__dirname, '../server/views/anywhere.ejs')
|
|
9
|
+
const template = fs.readFileSync(filename, 'utf8')
|
|
10
|
+
const address = 'https://pinokio-1.tail123.ts.net'
|
|
11
|
+
const tick = () => new Promise((resolve) => setImmediate(resolve))
|
|
12
|
+
|
|
13
|
+
function createHarness(t, options = {}) {
|
|
14
|
+
const state = { status: { phase: 'ready', enabled: true, address }, timers: [], opened: [] }
|
|
15
|
+
Object.assign(state.status, options.status)
|
|
16
|
+
const html = ejs.render(template, {
|
|
17
|
+
theme: 'light', agent: options.agent || 'browser', status: state.status
|
|
18
|
+
}, { filename, includer: () => ({ template: ' ' }) })
|
|
19
|
+
const virtualConsole = new VirtualConsole()
|
|
20
|
+
virtualConsole.on('jsdomError', (error) => { throw error })
|
|
21
|
+
const dom = new JSDOM(html, {
|
|
22
|
+
url: options.origin || 'http://127.0.0.1:42000/',
|
|
23
|
+
runScripts: 'dangerously', pretendToBeVisual: true, virtualConsole,
|
|
24
|
+
beforeParse(window) {
|
|
25
|
+
if (options.clipboard) {
|
|
26
|
+
Object.defineProperty(window.navigator, 'clipboard', { value: options.clipboard })
|
|
27
|
+
}
|
|
28
|
+
window.document.execCommand = options.execCommand
|
|
29
|
+
window.setTimeout = (callback, delay) => state.timers.push({ callback, delay })
|
|
30
|
+
window.fetch = async (url) => {
|
|
31
|
+
assert.equal(url, '/anywhere/status', 'copying must not use the server clipboard')
|
|
32
|
+
return { ok: true, json: async () => ({ ...state.status }) }
|
|
33
|
+
}
|
|
34
|
+
window.open = (...args) => state.opened.push(args)
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
t.after(() => dom.window.close())
|
|
38
|
+
const document = dom.window.document
|
|
39
|
+
return {
|
|
40
|
+
state, document,
|
|
41
|
+
button: document.getElementById('anywhere-copy'),
|
|
42
|
+
feedback: document.getElementById('anywhere-copy-status'),
|
|
43
|
+
manual: document.getElementById('anywhere-copy-manual'),
|
|
44
|
+
input: document.getElementById('anywhere-copy-value'),
|
|
45
|
+
poll: () => state.timers.shift().callback()
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const agent of ['browser', 'electron']) {
|
|
50
|
+
test(`Remote copies the full URL directly from the ${agent} click without opening a window`, async (t) => {
|
|
51
|
+
const copied = []
|
|
52
|
+
const h = createHarness(t, {
|
|
53
|
+
agent,
|
|
54
|
+
clipboard: { writeText: async (text) => { copied.push(text) } },
|
|
55
|
+
execCommand: () => assert.fail('modern clipboard succeeded')
|
|
56
|
+
})
|
|
57
|
+
h.button.focus()
|
|
58
|
+
h.button.click()
|
|
59
|
+
assert.deepEqual(copied, [address], 'clipboard is invoked synchronously in the click')
|
|
60
|
+
assert.equal(h.button.disabled, true)
|
|
61
|
+
h.button.click()
|
|
62
|
+
await tick()
|
|
63
|
+
assert.deepEqual(copied, [address], 'pending copies are not duplicated')
|
|
64
|
+
assert.equal(h.feedback.textContent, 'URL copied.')
|
|
65
|
+
assert.equal(h.manual.hidden, true)
|
|
66
|
+
assert.equal(h.button.disabled, false)
|
|
67
|
+
assert.equal(h.document.activeElement, h.button)
|
|
68
|
+
assert.deepEqual(h.state.opened, [])
|
|
69
|
+
assert.equal(h.document.getElementById('anywhere-address').getAttribute('href'), address)
|
|
70
|
+
assert.equal(h.feedback.getAttribute('role'), 'status')
|
|
71
|
+
if (agent === 'electron') {
|
|
72
|
+
h.document.getElementById('anywhere-address').click()
|
|
73
|
+
assert.deepEqual(h.state.opened, [[address + '/', '_blank', 'browser']])
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const scenario of ['insecure LAN', 'denied', 'synchronous failure', 'electron']) {
|
|
79
|
+
test(`Remote falls back to selected text copying: ${scenario}`, async (t) => {
|
|
80
|
+
const copied = []
|
|
81
|
+
let h
|
|
82
|
+
h = createHarness(t, {
|
|
83
|
+
agent: scenario === 'electron' ? 'electron' : 'browser',
|
|
84
|
+
origin: scenario === 'insecure LAN' ? 'http://192.168.1.10:42000/' : 'https://pinokio.tail123.ts.net/',
|
|
85
|
+
clipboard: scenario === 'denied'
|
|
86
|
+
? { writeText: async () => { throw new Error('NotAllowedError') } }
|
|
87
|
+
: scenario === 'synchronous failure'
|
|
88
|
+
? { writeText: () => { throw new Error('clipboard unavailable') } }
|
|
89
|
+
: undefined,
|
|
90
|
+
execCommand(command) {
|
|
91
|
+
assert.equal(command, 'copy')
|
|
92
|
+
assert.equal(h.document.activeElement, h.input)
|
|
93
|
+
assert.equal(h.input.readOnly, true)
|
|
94
|
+
assert.equal(h.manual.hidden, false)
|
|
95
|
+
copied.push(h.input.value.slice(h.input.selectionStart, h.input.selectionEnd))
|
|
96
|
+
return true
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
h.button.focus()
|
|
100
|
+
h.button.click()
|
|
101
|
+
await tick()
|
|
102
|
+
assert.deepEqual(copied, [address])
|
|
103
|
+
assert.equal(h.feedback.textContent, 'URL copied.')
|
|
104
|
+
assert.equal(h.manual.hidden, true)
|
|
105
|
+
assert.equal(h.document.activeElement, h.button)
|
|
106
|
+
assert.deepEqual(h.state.opened, [])
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const failure of ['false', 'undefined', 'throws', 'unavailable']) {
|
|
111
|
+
test(`Remote offers selected manual copy without false success when execCommand ${failure}`, async (t) => {
|
|
112
|
+
const h = createHarness(t, {
|
|
113
|
+
clipboard: { writeText: async () => { throw new Error('denied') } },
|
|
114
|
+
execCommand: failure === 'unavailable' ? undefined : () => {
|
|
115
|
+
if (failure === 'throws') throw new Error('denied')
|
|
116
|
+
return failure === 'false' ? false : undefined
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
h.button.click()
|
|
120
|
+
await tick()
|
|
121
|
+
assert.equal(h.manual.hidden, false)
|
|
122
|
+
assert.equal(h.input.value, address)
|
|
123
|
+
assert.equal(h.input.selectionStart, 0)
|
|
124
|
+
assert.equal(h.input.selectionEnd, address.length)
|
|
125
|
+
assert.equal(h.document.activeElement, h.input)
|
|
126
|
+
assert.match(h.feedback.textContent, /Touch and hold.*Ctrl\+C \/ ⌘C/)
|
|
127
|
+
assert.doesNotMatch(h.feedback.textContent, /URL copied/)
|
|
128
|
+
assert.equal(h.button.disabled, false)
|
|
129
|
+
await h.poll()
|
|
130
|
+
assert.equal(h.manual.hidden, false, 'routine polling preserves the manual copy field')
|
|
131
|
+
assert.equal(h.input.selectionEnd, address.length)
|
|
132
|
+
h.state.status.address = 'https://renamed.tail123.ts.net'
|
|
133
|
+
await h.poll()
|
|
134
|
+
assert.equal(h.manual.hidden, true, 'a changed URL clears the stale manual field')
|
|
135
|
+
assert.equal(h.feedback.textContent, '')
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
test('Remote does not copy a placeholder or an unavailable address', async (t) => {
|
|
140
|
+
const h = createHarness(t, {
|
|
141
|
+
status: { address: '' },
|
|
142
|
+
clipboard: { writeText: () => assert.fail('no URL to copy') },
|
|
143
|
+
execCommand: () => assert.fail('no URL to copy')
|
|
144
|
+
})
|
|
145
|
+
assert.equal(h.button.disabled, true)
|
|
146
|
+
h.button.click()
|
|
147
|
+
await tick()
|
|
148
|
+
assert.equal(h.feedback.textContent, '')
|
|
149
|
+
h.state.status.address = address
|
|
150
|
+
await h.poll()
|
|
151
|
+
assert.equal(h.button.disabled, false)
|
|
152
|
+
h.state.status.phase = 'off'
|
|
153
|
+
await h.poll()
|
|
154
|
+
assert.equal(h.button.disabled, true)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
test('Remote restores keyboard focus after disabling the button during modern copy', async (t) => {
|
|
158
|
+
let h
|
|
159
|
+
h = createHarness(t, {
|
|
160
|
+
clipboard: { writeText: async () => { h.button.blur() } }
|
|
161
|
+
})
|
|
162
|
+
h.button.focus()
|
|
163
|
+
h.button.click()
|
|
164
|
+
await tick()
|
|
165
|
+
assert.equal(h.document.activeElement, h.button)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test('Remote ignores a pending copy result if the address changes or Remote stops', async (t) => {
|
|
169
|
+
for (const change of [{ address: 'https://new.tail123.ts.net' }, { phase: 'off' }]) {
|
|
170
|
+
let rejectCopy
|
|
171
|
+
const h = createHarness(t, {
|
|
172
|
+
clipboard: { writeText: () => new Promise((resolve, reject) => { rejectCopy = reject }) },
|
|
173
|
+
execCommand: () => assert.fail('must not select a stale URL')
|
|
174
|
+
})
|
|
175
|
+
h.button.click()
|
|
176
|
+
Object.assign(h.state.status, change)
|
|
177
|
+
await h.poll()
|
|
178
|
+
rejectCopy(new Error('denied'))
|
|
179
|
+
await tick()
|
|
180
|
+
assert.equal(h.manual.hidden, true)
|
|
181
|
+
assert.equal(h.feedback.textContent, '')
|
|
182
|
+
assert.equal(h.button.disabled, change.phase === 'off')
|
|
183
|
+
}
|
|
184
|
+
})
|
|
@@ -56,7 +56,7 @@ test('Hugging Face connect parses managed hf agent login instructions', () => {
|
|
|
56
56
|
})
|
|
57
57
|
})
|
|
58
58
|
|
|
59
|
-
test('Hugging Face connect preserves
|
|
59
|
+
test('Hugging Face connect preserves HF_HOME but uses managed credentials', async () => {
|
|
60
60
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-hf-connect-'))
|
|
61
61
|
await fs.writeFile(path.join(root, 'ENVIRONMENT'), 'HF_HOME=./custom/hf-home\nHF_TOKEN_PATH=./custom/hf-token\n')
|
|
62
62
|
const oldToken = process.env.HF_TOKEN
|
|
@@ -66,7 +66,7 @@ test('Hugging Face connect preserves configured HF paths and ignores ambient HF_
|
|
|
66
66
|
const env = await provider.authEnv()
|
|
67
67
|
|
|
68
68
|
assert.equal(env.HF_HOME, path.join(root, 'custom', 'hf-home'))
|
|
69
|
-
assert.equal(env.HF_TOKEN_PATH, path.join(root, '
|
|
69
|
+
assert.equal(env.HF_TOKEN_PATH, path.join(root, 'cache', 'HF_AUTH', 'token'))
|
|
70
70
|
assert.equal(env.HF_TOKEN, undefined)
|
|
71
71
|
assert.equal(env.HF_HUB_DISABLE_UPDATE_CHECK, '1')
|
|
72
72
|
} finally {
|
|
@@ -114,11 +114,11 @@ test('Hugging Face connect isolates its managed Python from external packages',
|
|
|
114
114
|
}
|
|
115
115
|
})
|
|
116
116
|
|
|
117
|
-
test('Hugging Face connect
|
|
117
|
+
test('Hugging Face connect ignores app and command token paths in its managed CLI', async () => {
|
|
118
118
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-hf-connect-app-env-'))
|
|
119
119
|
const appDir = path.join(root, 'api', 'demo')
|
|
120
120
|
const scriptPath = path.join(appDir, 'start.js')
|
|
121
|
-
const tokenPath = path.join(
|
|
121
|
+
const tokenPath = path.join(root, 'cache', 'HF_AUTH', 'token')
|
|
122
122
|
await fs.mkdir(appDir, { recursive: true })
|
|
123
123
|
await fs.writeFile(scriptPath, '')
|
|
124
124
|
await fs.writeFile(
|
|
@@ -148,10 +148,7 @@ test('Hugging Face connect uses the app token path for its managed CLI', async (
|
|
|
148
148
|
assert.equal(env.HF_HOME, path.join(appDir, 'cache', 'huggingface'))
|
|
149
149
|
assert.equal(env.HF_TOKEN_PATH, tokenPath)
|
|
150
150
|
assert.equal(await fs.readFile(tokenPath, 'utf8'), 'hf_app_token')
|
|
151
|
-
await assert.rejects(
|
|
152
|
-
fs.access(path.join(root, 'cache', 'HF_AUTH', 'token')),
|
|
153
|
-
{ code: 'ENOENT' }
|
|
154
|
-
)
|
|
151
|
+
await assert.rejects(fs.access(path.join(appDir, 'auth')), { code: 'ENOENT' })
|
|
155
152
|
|
|
156
153
|
const commandTokenPath = path.join(appDir, 'command-auth', 'token')
|
|
157
154
|
const commandContext = {
|
|
@@ -164,8 +161,9 @@ test('Hugging Face connect uses the app token path for its managed CLI', async (
|
|
|
164
161
|
'require("node:fs").writeFileSync(process.env.HF_TOKEN_PATH, "hf_command_token")'
|
|
165
162
|
], {}, commandContext)
|
|
166
163
|
|
|
167
|
-
assert.equal(commandResult.env.HF_TOKEN_PATH,
|
|
168
|
-
assert.equal(await fs.readFile(
|
|
164
|
+
assert.equal(commandResult.env.HF_TOKEN_PATH, tokenPath)
|
|
165
|
+
assert.equal(await fs.readFile(tokenPath, 'utf8'), 'hf_command_token')
|
|
166
|
+
await assert.rejects(fs.access(commandTokenPath), { code: 'ENOENT' })
|
|
169
167
|
} finally {
|
|
170
168
|
await fs.rm(root, { recursive: true, force: true })
|
|
171
169
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
const assert = require('node:assert/strict')
|
|
2
|
+
const fs = require('node:fs/promises')
|
|
3
|
+
const os = require('node:os')
|
|
4
|
+
const path = require('node:path')
|
|
5
|
+
const test = require('node:test')
|
|
6
|
+
const Environment = require('../kernel/environment')
|
|
7
|
+
const Huggingface = require('../kernel/connect/providers/huggingface')
|
|
8
|
+
const Shell = require('../kernel/shell')
|
|
9
|
+
const Exec = require('../kernel/api/exec')
|
|
10
|
+
const dotenv = require('dotenv')
|
|
11
|
+
|
|
12
|
+
async function withHome(fn) {
|
|
13
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-hf-managed-'))
|
|
14
|
+
const kernel = {
|
|
15
|
+
homedir: root,
|
|
16
|
+
platform: process.platform,
|
|
17
|
+
path: (...parts) => path.join(root, ...parts),
|
|
18
|
+
exists: target => fs.access(target).then(() => true, () => false),
|
|
19
|
+
which: () => null,
|
|
20
|
+
envs: {},
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
await fn(kernel)
|
|
24
|
+
} finally {
|
|
25
|
+
await fs.rm(root, { recursive: true, force: true })
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
test('app generation excludes token paths without changing global configuration', async (t) => {
|
|
30
|
+
for (const eol of ['\n', '\r\n']) {
|
|
31
|
+
await t.test(JSON.stringify(eol), () => withHome(async kernel => {
|
|
32
|
+
const content = [
|
|
33
|
+
'\uFEFF# HF_TOKEN_PATH and all original content stay in the global file',
|
|
34
|
+
'HF_TOKEN_PATH=./cache/HF_AUTH/token',
|
|
35
|
+
' export HF_TOKEN_PATH = "/custom/token" # duplicate',
|
|
36
|
+
'Hf_Token_Path=another-token',
|
|
37
|
+
'CUSTOM=keep',
|
|
38
|
+
'HF_TOKEN_PATH_EXTRA=keep',
|
|
39
|
+
'HF_HOME=./cache/HF_HOME',
|
|
40
|
+
'FREEFORM="line one',
|
|
41
|
+
'line two"',
|
|
42
|
+
].join(eol)
|
|
43
|
+
const envPath = kernel.path('ENVIRONMENT')
|
|
44
|
+
await fs.writeFile(envPath, content)
|
|
45
|
+
const generated = await Environment.ENV('app', kernel.homedir, kernel)
|
|
46
|
+
const appEnv = dotenv.parse(generated)
|
|
47
|
+
assert.equal(appEnv.HF_TOKEN_PATH, undefined)
|
|
48
|
+
assert.equal(appEnv.Hf_Token_Path, undefined)
|
|
49
|
+
assert.equal(appEnv.CUSTOM, 'keep')
|
|
50
|
+
assert.equal(appEnv.HF_TOKEN_PATH_EXTRA, 'keep')
|
|
51
|
+
assert.equal(appEnv.HF_HOME, './cache/HF_HOME')
|
|
52
|
+
assert.equal(await fs.readFile(envPath, 'utf8'), content)
|
|
53
|
+
// Exclusion is limited to inheritance; ordinary global reads are unchanged.
|
|
54
|
+
assert.equal((await Environment.get(kernel.homedir, kernel)).HF_TOKEN_PATH, '/custom/token')
|
|
55
|
+
}))
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('stale global/app/command paths cannot separate downloads from managed login', () => withHome(async kernel => {
|
|
60
|
+
const stale = 'HF_TOKEN_PATH=./cache/HF_AUTH/token\n'
|
|
61
|
+
const app = kernel.path('api', 'minijam.git')
|
|
62
|
+
const scriptPath = path.join(app, 'install.js')
|
|
63
|
+
await fs.mkdir(app, { recursive: true })
|
|
64
|
+
await fs.writeFile(kernel.path('ENVIRONMENT'), stale)
|
|
65
|
+
await fs.writeFile(path.join(app, 'ENVIRONMENT'), `HF_HOME=./cache/HF_HOME\n${stale}`)
|
|
66
|
+
await fs.writeFile(scriptPath, '')
|
|
67
|
+
assert.equal(dotenv.parse(await Environment.ENV('app', kernel.homedir, kernel)).HF_TOKEN_PATH, undefined)
|
|
68
|
+
const provider = new Huggingface(kernel, {})
|
|
69
|
+
const tokenPath = provider.defaultTokenPath()
|
|
70
|
+
const anonymousPath = kernel.path('cache', 'HF_AUTH', 'anonymous')
|
|
71
|
+
// A connection check succeeds after login; the existing fallback is then cleared.
|
|
72
|
+
kernel.envs.HF_TOKEN_PATH = anonymousPath
|
|
73
|
+
const existingShell = new Shell(kernel)
|
|
74
|
+
await existingShell.init_env({ $parent: { path: scriptPath }, '-env': ['HF_TOKEN_PATH'] })
|
|
75
|
+
assert.equal(existingShell.env.HF_TOKEN_PATH, anonymousPath)
|
|
76
|
+
|
|
77
|
+
provider.hfPath = () => process.execPath
|
|
78
|
+
const context = {
|
|
79
|
+
parentPath: scriptPath,
|
|
80
|
+
cwd: app,
|
|
81
|
+
env: { HF_TOKEN_PATH: './command-token', Hf_Token_Path: './mixed-case-token' },
|
|
82
|
+
}
|
|
83
|
+
await provider.runHf(['-e', 'require("node:fs").writeFileSync(process.env.HF_TOKEN_PATH, "synthetic-login-token")'], {}, context)
|
|
84
|
+
assert.equal(await fs.readFile(tokenPath, 'utf8'), 'synthetic-login-token')
|
|
85
|
+
assert.equal(await kernel.exists(path.join(app, 'command-token')), false)
|
|
86
|
+
assert.equal(await kernel.exists(path.join(app, 'mixed-case-token')), false)
|
|
87
|
+
|
|
88
|
+
for (const platform of ['win32', 'linux', 'darwin']) {
|
|
89
|
+
kernel.platform = platform
|
|
90
|
+
// Simulate a successful whoami command without contacting Hugging Face.
|
|
91
|
+
const originalRunHf = provider.runHf
|
|
92
|
+
provider.runHf = async () => ({ env: await provider.authEnv(context) })
|
|
93
|
+
assert.equal(await provider.connected(), true)
|
|
94
|
+
provider.runHf = originalRunHf
|
|
95
|
+
assert.equal(kernel.envs.HF_TOKEN_PATH, tokenPath)
|
|
96
|
+
|
|
97
|
+
const shell = new Shell(kernel)
|
|
98
|
+
shell.platform = platform
|
|
99
|
+
await shell.init_env({ $parent: { path: scriptPath }, env: context.env, '-env': ['HF_TOKEN_PATH'] })
|
|
100
|
+
assert.equal(shell.env.HF_TOKEN_PATH, tokenPath)
|
|
101
|
+
assert.equal(shell.env.Hf_Token_Path, undefined)
|
|
102
|
+
assert.equal(shell.env.HF_HOME, path.join(app, 'cache', 'HF_HOME'))
|
|
103
|
+
const authEnv = await provider.authEnv(context)
|
|
104
|
+
assert.equal(authEnv.HF_TOKEN_PATH, tokenPath)
|
|
105
|
+
assert.equal(authEnv.Hf_Token_Path, undefined)
|
|
106
|
+
}
|
|
107
|
+
assert.equal(existingShell.env.HF_TOKEN_PATH, anonymousPath)
|
|
108
|
+
assert.equal(await fs.readFile(kernel.path('ENVIRONMENT'), 'utf8'), stale)
|
|
109
|
+
assert.equal(await fs.readFile(path.join(app, 'ENVIRONMENT'), 'utf8'), `HF_HOME=./cache/HF_HOME\n${stale}`)
|
|
110
|
+
assert.equal(await kernel.exists(path.join(app, 'cache', 'HF_AUTH', 'token')), false)
|
|
111
|
+
}))
|
|
112
|
+
|
|
113
|
+
test('exec children receive the managed path despite command overrides', () => withHome(async kernel => {
|
|
114
|
+
for (const state of ['token', 'anonymous']) {
|
|
115
|
+
const tokenPath = kernel.path('cache', 'HF_AUTH', state)
|
|
116
|
+
kernel.envs.HF_TOKEN_PATH = tokenPath
|
|
117
|
+
const result = await Exec({ params: {
|
|
118
|
+
message: `"${process.execPath}" -p "process.env.HF_TOKEN_PATH"`,
|
|
119
|
+
env: { HF_TOKEN_PATH: '/wrong/token', Hf_Token_Path: '/another/wrong/token' },
|
|
120
|
+
} }, () => {}, kernel)
|
|
121
|
+
assert.equal(result.error, null)
|
|
122
|
+
assert.equal(result.stdout.trim(), tokenPath)
|
|
123
|
+
}
|
|
124
|
+
}))
|
|
@@ -213,7 +213,7 @@ test('a broken CLI executable path does not disable saved credentials at startup
|
|
|
213
213
|
}
|
|
214
214
|
})
|
|
215
215
|
|
|
216
|
-
test('provider login
|
|
216
|
+
test('provider login always uses the managed token path, including after anonymous fallback', async () => {
|
|
217
217
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-hf-context-'))
|
|
218
218
|
const appDir = path.join(root, 'api', 'demo')
|
|
219
219
|
const scriptPath = path.join(appDir, 'start.js')
|
|
@@ -230,14 +230,14 @@ test('provider login ignores only Pinokio\'s inherited anonymous path', async ()
|
|
|
230
230
|
assert.equal((await provider.authEnv(context)).HF_TOKEN_PATH, provider.defaultTokenPath())
|
|
231
231
|
assert.equal(
|
|
232
232
|
(await provider.authEnv({ ...context, env: { HF_TOKEN_PATH: './custom-token' } })).HF_TOKEN_PATH,
|
|
233
|
-
|
|
233
|
+
provider.defaultTokenPath()
|
|
234
234
|
)
|
|
235
235
|
} finally {
|
|
236
236
|
await fs.rm(root, { recursive: true, force: true })
|
|
237
237
|
}
|
|
238
238
|
})
|
|
239
239
|
|
|
240
|
-
test('invalid managed tokens
|
|
240
|
+
test('invalid managed tokens select anonymous credentials even with app overrides', async () => {
|
|
241
241
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-hf-shells-'))
|
|
242
242
|
await fs.writeFile(path.join(root, 'ENVIRONMENT'), '')
|
|
243
243
|
let checks = 0
|
|
@@ -268,7 +268,7 @@ test('invalid managed tokens are replaced once without overriding apps', async (
|
|
|
268
268
|
|
|
269
269
|
const appShell = new Shell(kernel)
|
|
270
270
|
await appShell.init_env({ env: { HF_TOKEN_PATH: '/tmp/app-hf-token' } })
|
|
271
|
-
assert.equal(appShell.env.HF_TOKEN_PATH,
|
|
271
|
+
assert.equal(appShell.env.HF_TOKEN_PATH, anonymousPath)
|
|
272
272
|
} finally {
|
|
273
273
|
await fs.rm(root, { recursive: true, force: true })
|
|
274
274
|
}
|
|
@@ -280,10 +280,10 @@ test('Shell.init_env disables Hugging Face hub update checks by default without
|
|
|
280
280
|
}
|
|
281
281
|
})
|
|
282
282
|
assert.equal(overrideShell.env.HF_HUB_DISABLE_UPDATE_CHECK, '0')
|
|
283
|
-
assert.equal(overrideShell.env.HF_TOKEN_PATH, path.join(root, '
|
|
283
|
+
assert.equal(overrideShell.env.HF_TOKEN_PATH, path.join(root, 'cache', 'HF_AUTH', 'token'))
|
|
284
284
|
})
|
|
285
285
|
|
|
286
|
-
test('Shell.init_env preserves
|
|
286
|
+
test('Shell.init_env preserves app HF_HOME while ignoring app token paths', async () => {
|
|
287
287
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-shell-hf-app-env-'))
|
|
288
288
|
const appDir = path.join(root, 'api', 'demo')
|
|
289
289
|
const scriptPath = path.join(appDir, 'start.js')
|
|
@@ -313,7 +313,7 @@ test('Shell.init_env preserves an app HF_HOME without changing the shared token
|
|
|
313
313
|
})
|
|
314
314
|
|
|
315
315
|
assert.equal(overrideShell.env.HF_HOME, path.join(appDir, 'cache', 'huggingface'))
|
|
316
|
-
assert.equal(overrideShell.env.HF_TOKEN_PATH, path.join(
|
|
316
|
+
assert.equal(overrideShell.env.HF_TOKEN_PATH, path.join(root, 'cache', 'HF_AUTH', 'token'))
|
|
317
317
|
})
|
|
318
318
|
|
|
319
319
|
test('Shell.init_env keeps Windows Hugging Face symlink defaults scoped to win32', async () => {
|