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.
@@ -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
- if (!env.HF_TOKEN_PATH) {
64
- env.HF_TOKEN_PATH = this.defaultTokenPath()
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
  }
@@ -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
- if (irrelevant_keys.includes(key)) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "8.2.24",
3
+ "version": "8.2.26",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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 configured HF paths and ignores ambient HF_TOKEN', async () => {
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, 'custom', 'hf-token'))
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 uses the app token path for its managed CLI', async () => {
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(appDir, 'auth', 'token')
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, commandTokenPath)
168
- assert.equal(await fs.readFile(commandTokenPath, 'utf8'), 'hf_command_token')
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 ignores only Pinokio\'s inherited anonymous path', async () => {
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
- path.join(appDir, 'custom-token')
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 are replaced once without overriding apps', async () => {
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, '/tmp/app-hf-token')
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, 'custom', 'hf-token'))
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 an app HF_HOME without changing the shared token default', async () => {
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(appDir, 'auth', 'token'))
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 () => {