pinokiod 8.0.5 → 8.0.7
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 +13 -4
- package/kernel/connect/index.js +6 -6
- package/kernel/connect/providers/huggingface/index.js +62 -27
- package/package.json +1 -1
- package/server/index.js +0 -4
- package/test/github-connection.test.js +17 -12
- package/test/hf-api.test.js +39 -10
- package/test/huggingface-connect.test.js +94 -9
- package/test/shell-run-template.test.js +33 -0
package/kernel/api/hf/index.js
CHANGED
|
@@ -168,6 +168,13 @@ class HF {
|
|
|
168
168
|
}
|
|
169
169
|
await this.htmlModal.close(this.modalRequest(req, this.modalId(req)), ondata, kernel)
|
|
170
170
|
}
|
|
171
|
+
authContext(req = {}, params = {}) {
|
|
172
|
+
return {
|
|
173
|
+
parentPath: req.parent && req.parent.path,
|
|
174
|
+
cwd: req.cwd,
|
|
175
|
+
env: params.env,
|
|
176
|
+
}
|
|
177
|
+
}
|
|
171
178
|
async cancelLogin(connect, params) {
|
|
172
179
|
if (connect && typeof connect.cancelLogin === "function") {
|
|
173
180
|
await connect.cancelLogin("huggingface", params || {})
|
|
@@ -201,9 +208,10 @@ class HF {
|
|
|
201
208
|
const shouldWait = params.wait !== false
|
|
202
209
|
const timeout = positiveNumber(params.timeout, 120000)
|
|
203
210
|
const interval = positiveNumber(params.interval, 2000)
|
|
211
|
+
const authContext = this.authContext(req, params)
|
|
204
212
|
|
|
205
213
|
if (!force) {
|
|
206
|
-
const existing = await connect.keys("huggingface")
|
|
214
|
+
const existing = await connect.keys("huggingface", authContext)
|
|
207
215
|
if (existing && existing.access_token) {
|
|
208
216
|
return {
|
|
209
217
|
status: "success",
|
|
@@ -216,7 +224,7 @@ class HF {
|
|
|
216
224
|
if (typeof connect.login !== "function") {
|
|
217
225
|
throw new Error("Hugging Face connect login is not available")
|
|
218
226
|
}
|
|
219
|
-
const response = await connect.login("huggingface", params)
|
|
227
|
+
const response = await connect.login("huggingface", params, authContext)
|
|
220
228
|
if (!response || response.status === "error") {
|
|
221
229
|
return response
|
|
222
230
|
}
|
|
@@ -262,7 +270,7 @@ class HF {
|
|
|
262
270
|
|
|
263
271
|
const startedAt = Date.now()
|
|
264
272
|
while (Date.now() - startedAt < timeout) {
|
|
265
|
-
const keys = await connect.keys("huggingface")
|
|
273
|
+
const keys = await connect.keys("huggingface", authContext)
|
|
266
274
|
if (keys && keys.access_token) {
|
|
267
275
|
if (useModal) {
|
|
268
276
|
await this.closeLoginModal(req, ondata, kernel)
|
|
@@ -293,7 +301,8 @@ class HF {
|
|
|
293
301
|
*/
|
|
294
302
|
async logout(req, ondata, kernel) {
|
|
295
303
|
const connect = this.connect(kernel, "logout")
|
|
296
|
-
|
|
304
|
+
const params = req.params || {}
|
|
305
|
+
await connect.logout("huggingface", params, this.authContext(req, params))
|
|
297
306
|
return { status: "success" }
|
|
298
307
|
}
|
|
299
308
|
/*
|
package/kernel/connect/index.js
CHANGED
|
@@ -24,12 +24,12 @@ class Connect {
|
|
|
24
24
|
return null
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
-
async login(provider,
|
|
28
|
-
let res = await this.clients[provider].login(
|
|
27
|
+
async login(provider, params, context) {
|
|
28
|
+
let res = await this.clients[provider].login(params, context)
|
|
29
29
|
return res
|
|
30
30
|
}
|
|
31
|
-
async logout(provider,
|
|
32
|
-
let res = await this.clients[provider].logout(
|
|
31
|
+
async logout(provider, params, context) {
|
|
32
|
+
let res = await this.clients[provider].logout(params, context)
|
|
33
33
|
return res
|
|
34
34
|
}
|
|
35
35
|
async cancelLogin(provider, req) {
|
|
@@ -38,8 +38,8 @@ class Connect {
|
|
|
38
38
|
}
|
|
39
39
|
return null
|
|
40
40
|
}
|
|
41
|
-
async keys(provider) {
|
|
42
|
-
let res = await this.clients[provider].keys()
|
|
41
|
+
async keys(provider, context) {
|
|
42
|
+
let res = await this.clients[provider].keys(context)
|
|
43
43
|
return res
|
|
44
44
|
}
|
|
45
45
|
async connected(provider, options) {
|
|
@@ -18,30 +18,55 @@ class Huggingface {
|
|
|
18
18
|
defaultTokenPath() {
|
|
19
19
|
return this.kernel.path("cache", "HF_AUTH", "token")
|
|
20
20
|
}
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
requestCwd(context = {}) {
|
|
22
|
+
if (context.cwd) {
|
|
23
|
+
return path.resolve(context.cwd)
|
|
24
|
+
}
|
|
25
|
+
if (context.parentPath) {
|
|
26
|
+
return path.dirname(path.resolve(context.parentPath))
|
|
27
|
+
}
|
|
28
|
+
return this.kernel.homedir
|
|
29
|
+
}
|
|
30
|
+
async repairTokenPath(tokenPath) {
|
|
23
31
|
try {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
const stat = await fs.promises.lstat(tokenPath)
|
|
33
|
+
if (stat.isDirectory()) {
|
|
34
|
+
await fs.promises.rm(tokenPath, { recursive: true, force: true })
|
|
35
|
+
}
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (!error || error.code !== "ENOENT") {
|
|
38
|
+
throw error
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
await fs.promises.mkdir(path.dirname(tokenPath), { recursive: true })
|
|
42
|
+
}
|
|
43
|
+
async authEnv(context = {}) {
|
|
44
|
+
let env
|
|
45
|
+
if (context.parentPath) {
|
|
46
|
+
env = await Environment.get2(context.parentPath, this.kernel)
|
|
47
|
+
} else {
|
|
48
|
+
let systemEnv = {}
|
|
49
|
+
try {
|
|
50
|
+
systemEnv = await Environment.get(this.kernel.homedir, this.kernel)
|
|
51
|
+
} catch (_) {
|
|
52
|
+
systemEnv = {}
|
|
53
|
+
}
|
|
54
|
+
env = Object.assign({}, process.env, systemEnv)
|
|
55
|
+
}
|
|
56
|
+
if (context.env && typeof context.env === "object") {
|
|
57
|
+
env = Object.assign(env, context.env)
|
|
27
58
|
}
|
|
28
|
-
const env = Object.assign({}, process.env, systemEnv)
|
|
29
59
|
if (!env.HF_TOKEN_PATH) {
|
|
30
60
|
env.HF_TOKEN_PATH = this.defaultTokenPath()
|
|
61
|
+
} else if (!path.isAbsolute(env.HF_TOKEN_PATH)) {
|
|
62
|
+
env.HF_TOKEN_PATH = path.resolve(this.requestCwd(context), env.HF_TOKEN_PATH)
|
|
31
63
|
}
|
|
32
64
|
if (!env.HF_HUB_DISABLE_UPDATE_CHECK) {
|
|
33
65
|
env.HF_HUB_DISABLE_UPDATE_CHECK = "1"
|
|
34
66
|
}
|
|
35
67
|
delete env.HF_TOKEN
|
|
36
68
|
delete env.HUGGING_FACE_HUB_TOKEN
|
|
37
|
-
|
|
38
|
-
await fs.promises.rmdir(env.HF_TOKEN_PATH)
|
|
39
|
-
} catch (error) {
|
|
40
|
-
if (!error || !["ENOENT", "ENOTDIR"].includes(error.code)) {
|
|
41
|
-
throw new Error(`Hugging Face token path must be a file: ${env.HF_TOKEN_PATH}`)
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
await fs.promises.mkdir(path.dirname(env.HF_TOKEN_PATH), { recursive: true }).catch(() => {})
|
|
69
|
+
await this.repairTokenPath(env.HF_TOKEN_PATH)
|
|
45
70
|
return env
|
|
46
71
|
}
|
|
47
72
|
hfPath() {
|
|
@@ -59,11 +84,11 @@ class Huggingface {
|
|
|
59
84
|
}
|
|
60
85
|
return candidates[0]
|
|
61
86
|
}
|
|
62
|
-
async runHf(args, options = {}) {
|
|
63
|
-
const env = await this.authEnv()
|
|
87
|
+
async runHf(args, options = {}, context = {}) {
|
|
88
|
+
const env = await this.authEnv(context)
|
|
64
89
|
return new Promise((resolve, reject) => {
|
|
65
90
|
execFile(this.hfPath(), args, {
|
|
66
|
-
cwd: this.
|
|
91
|
+
cwd: this.requestCwd(context),
|
|
67
92
|
env,
|
|
68
93
|
timeout: Number.isFinite(options.timeout) ? options.timeout : undefined,
|
|
69
94
|
maxBuffer: 1024 * 1024,
|
|
@@ -122,11 +147,18 @@ class Huggingface {
|
|
|
122
147
|
expires_in: expiresMatch ? Number(expiresMatch[1]) : null,
|
|
123
148
|
}
|
|
124
149
|
}
|
|
125
|
-
async login() {
|
|
150
|
+
async login(_params = {}, context = {}) {
|
|
151
|
+
const env = await this.authEnv(context)
|
|
126
152
|
if (this.loginSession && this.loginSession.status === "pending") {
|
|
153
|
+
if (this.loginSession.tokenPath !== env.HF_TOKEN_PATH) {
|
|
154
|
+
return {
|
|
155
|
+
status: "error",
|
|
156
|
+
error: `A Hugging Face login is already pending for ${this.loginSession.tokenPath}`,
|
|
157
|
+
token_path: env.HF_TOKEN_PATH,
|
|
158
|
+
}
|
|
159
|
+
}
|
|
127
160
|
return this.serializeLoginSession(this.loginSession)
|
|
128
161
|
}
|
|
129
|
-
const env = await this.authEnv()
|
|
130
162
|
const session = {
|
|
131
163
|
status: "pending",
|
|
132
164
|
login: null,
|
|
@@ -156,7 +188,7 @@ class Huggingface {
|
|
|
156
188
|
}
|
|
157
189
|
}
|
|
158
190
|
const child = spawn(this.hfPath(), ["auth", "login", "--format", "agent", "--force"], {
|
|
159
|
-
cwd: this.
|
|
191
|
+
cwd: this.requestCwd(context),
|
|
160
192
|
env,
|
|
161
193
|
windowsHide: false,
|
|
162
194
|
})
|
|
@@ -192,12 +224,15 @@ class Huggingface {
|
|
|
192
224
|
}
|
|
193
225
|
return this.serializeLoginSession(session)
|
|
194
226
|
}
|
|
195
|
-
async keys() {
|
|
227
|
+
async keys(context = {}) {
|
|
196
228
|
if (this.loginSession && this.loginSession.status === "pending") {
|
|
197
|
-
|
|
229
|
+
const env = await this.authEnv(context)
|
|
230
|
+
if (this.loginSession.tokenPath === env.HF_TOKEN_PATH) {
|
|
231
|
+
return this.serializeLoginSession(this.loginSession)
|
|
232
|
+
}
|
|
198
233
|
}
|
|
199
234
|
try {
|
|
200
|
-
const { stdout, env } = await this.runHf(["auth", "token", "--format", "quiet"])
|
|
235
|
+
const { stdout, env } = await this.runHf(["auth", "token", "--format", "quiet"], {}, context)
|
|
201
236
|
const access_token = stripAnsi(stdout).trim().split(/\r?\n/).find(Boolean)
|
|
202
237
|
if (!access_token) {
|
|
203
238
|
return null
|
|
@@ -235,12 +270,12 @@ class Huggingface {
|
|
|
235
270
|
await this.config.profile.cache(response, connectPath).catch(() => {})
|
|
236
271
|
return this.config.profile.render(response)
|
|
237
272
|
}
|
|
238
|
-
async destroy() {
|
|
273
|
+
async destroy(context = {}) {
|
|
239
274
|
if (this.loginSession && this.loginSession.child && this.loginSession.status === "pending") {
|
|
240
275
|
this.loginSession.child.kill()
|
|
241
276
|
}
|
|
242
277
|
this.loginSession = null
|
|
243
|
-
await this.runHf(["auth", "logout"]).catch(() => {})
|
|
278
|
+
await this.runHf(["auth", "logout"], {}, context).catch(() => {})
|
|
244
279
|
await fs.promises.rm(this.kernel.path("connect", "huggingface"), { recursive: true, force: true }).catch(() => {})
|
|
245
280
|
}
|
|
246
281
|
async cancelLogin() {
|
|
@@ -257,8 +292,8 @@ class Huggingface {
|
|
|
257
292
|
this.loginSession = null
|
|
258
293
|
}
|
|
259
294
|
}
|
|
260
|
-
async logout() {
|
|
261
|
-
await this.destroy()
|
|
295
|
+
async logout(_params = {}, context = {}) {
|
|
296
|
+
await this.destroy(context)
|
|
262
297
|
}
|
|
263
298
|
}
|
|
264
299
|
|
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
|
|
|
@@ -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
|
@@ -17,12 +17,14 @@ test('hf.login shows a modal, waits for the user to open Hugging Face, then clos
|
|
|
17
17
|
const originalOpenURI = Util.openURI
|
|
18
18
|
const originalClipboard = Util.clipboard
|
|
19
19
|
let keyReads = 0
|
|
20
|
+
const keyContexts = []
|
|
20
21
|
const loginCalls = []
|
|
21
22
|
const waitCalls = []
|
|
22
23
|
const kernel = {
|
|
23
24
|
connect: {
|
|
24
|
-
async keys(provider) {
|
|
25
|
+
async keys(provider, context) {
|
|
25
26
|
assert.equal(provider, 'huggingface')
|
|
27
|
+
keyContexts.push(context)
|
|
26
28
|
keyReads += 1
|
|
27
29
|
if (keyReads < 3) {
|
|
28
30
|
return null
|
|
@@ -32,8 +34,8 @@ test('hf.login shows a modal, waits for the user to open Hugging Face, then clos
|
|
|
32
34
|
token_path: '/tmp/pinokio/hf-token'
|
|
33
35
|
}
|
|
34
36
|
},
|
|
35
|
-
async login(provider, params) {
|
|
36
|
-
loginCalls.push({ provider, params })
|
|
37
|
+
async login(provider, params, context) {
|
|
38
|
+
loginCalls.push({ provider, params, context })
|
|
37
39
|
return {
|
|
38
40
|
status: 'pending',
|
|
39
41
|
login,
|
|
@@ -58,15 +60,28 @@ test('hf.login shows a modal, waits for the user to open Hugging Face, then clos
|
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
try {
|
|
61
|
-
const
|
|
63
|
+
const request = {
|
|
62
64
|
parent: { id: 'test-script', path: '/pinokio/api/test/hf-login.js' },
|
|
63
65
|
params: {
|
|
64
66
|
timeout: 100,
|
|
65
67
|
interval: 1
|
|
66
68
|
}
|
|
67
|
-
}
|
|
69
|
+
}
|
|
70
|
+
const result = await hf.login(request, (stream, type) => packets.push({ type, stream }), kernel)
|
|
68
71
|
|
|
69
|
-
|
|
72
|
+
const authContext = {
|
|
73
|
+
parentPath: '/pinokio/api/test/hf-login.js',
|
|
74
|
+
cwd: undefined,
|
|
75
|
+
env: undefined
|
|
76
|
+
}
|
|
77
|
+
assert.deepEqual(loginCalls, [{
|
|
78
|
+
provider: 'huggingface',
|
|
79
|
+
params: request.params,
|
|
80
|
+
context: authContext
|
|
81
|
+
}])
|
|
82
|
+
assert.equal(loginCalls[0].params, request.params)
|
|
83
|
+
assert.equal(keyContexts.length, 3)
|
|
84
|
+
assert.equal(keyContexts.every((context) => context === loginCalls[0].context), true)
|
|
70
85
|
assert.deepEqual(waitCalls, ['/pinokio/api/test/hf-login.js'])
|
|
71
86
|
assert.deepEqual(actions, [{
|
|
72
87
|
type: 'clipboard',
|
|
@@ -185,9 +200,14 @@ test('hf.login can use the non-modal browser fallback when modal is disabled', a
|
|
|
185
200
|
assert.equal(provider, 'huggingface')
|
|
186
201
|
return null
|
|
187
202
|
},
|
|
188
|
-
async login(provider, params) {
|
|
203
|
+
async login(provider, params, context) {
|
|
189
204
|
assert.equal(provider, 'huggingface')
|
|
190
205
|
assert.deepEqual(params, { modal: false, wait: false })
|
|
206
|
+
assert.deepEqual(context, {
|
|
207
|
+
parentPath: undefined,
|
|
208
|
+
cwd: undefined,
|
|
209
|
+
env: undefined
|
|
210
|
+
})
|
|
191
211
|
return {
|
|
192
212
|
status: 'pending',
|
|
193
213
|
login,
|
|
@@ -396,8 +416,8 @@ test('hf.logout delegates to the Hugging Face connect provider and returns succe
|
|
|
396
416
|
const calls = []
|
|
397
417
|
const kernel = {
|
|
398
418
|
connect: {
|
|
399
|
-
async logout(provider, params) {
|
|
400
|
-
calls.push({ method: 'logout', provider, params })
|
|
419
|
+
async logout(provider, params, context) {
|
|
420
|
+
calls.push({ method: 'logout', provider, params, context })
|
|
401
421
|
}
|
|
402
422
|
}
|
|
403
423
|
}
|
|
@@ -405,7 +425,16 @@ test('hf.logout delegates to the Hugging Face connect provider and returns succe
|
|
|
405
425
|
const result = await hf.logout(req, () => {}, kernel)
|
|
406
426
|
|
|
407
427
|
assert.deepEqual(result, { status: 'success' })
|
|
408
|
-
assert.deepEqual(calls, [{
|
|
428
|
+
assert.deepEqual(calls, [{
|
|
429
|
+
method: 'logout',
|
|
430
|
+
provider: 'huggingface',
|
|
431
|
+
params: req.params,
|
|
432
|
+
context: {
|
|
433
|
+
parentPath: undefined,
|
|
434
|
+
cwd: undefined,
|
|
435
|
+
env: undefined
|
|
436
|
+
}
|
|
437
|
+
}])
|
|
409
438
|
})
|
|
410
439
|
|
|
411
440
|
test('hf.upload runs hf upload through shell.run with Hugging Face CLI args', async () => {
|
|
@@ -56,15 +56,16 @@ test('Hugging Face connect parses managed hf agent login instructions', () => {
|
|
|
56
56
|
})
|
|
57
57
|
})
|
|
58
58
|
|
|
59
|
-
test('Hugging Face connect
|
|
59
|
+
test('Hugging Face connect preserves configured HF paths and ignores ambient HF_TOKEN', async () => {
|
|
60
60
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-hf-connect-'))
|
|
61
|
-
await fs.writeFile(path.join(root, 'ENVIRONMENT'), '
|
|
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
|
|
63
63
|
process.env.HF_TOKEN = 'hf_should_not_win'
|
|
64
64
|
try {
|
|
65
65
|
const provider = new HuggingfaceConnect(createKernel(root), {})
|
|
66
66
|
const env = await provider.authEnv()
|
|
67
67
|
|
|
68
|
+
assert.equal(env.HF_HOME, path.join(root, 'custom', 'hf-home'))
|
|
68
69
|
assert.equal(env.HF_TOKEN_PATH, path.join(root, 'custom', 'hf-token'))
|
|
69
70
|
assert.equal(env.HF_TOKEN, undefined)
|
|
70
71
|
assert.equal(env.HF_HUB_DISABLE_UPDATE_CHECK, '1')
|
|
@@ -78,6 +79,63 @@ test('Hugging Face connect uses shared HF_TOKEN_PATH and ignores ambient HF_TOKE
|
|
|
78
79
|
}
|
|
79
80
|
})
|
|
80
81
|
|
|
82
|
+
test('Hugging Face connect uses the app token path for its managed CLI', async () => {
|
|
83
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-hf-connect-app-env-'))
|
|
84
|
+
const appDir = path.join(root, 'api', 'demo')
|
|
85
|
+
const scriptPath = path.join(appDir, 'start.js')
|
|
86
|
+
const tokenPath = path.join(appDir, 'auth', 'token')
|
|
87
|
+
await fs.mkdir(appDir, { recursive: true })
|
|
88
|
+
await fs.writeFile(scriptPath, '')
|
|
89
|
+
await fs.writeFile(
|
|
90
|
+
path.join(appDir, 'ENVIRONMENT'),
|
|
91
|
+
'HF_HOME=./cache/huggingface\n'
|
|
92
|
+
)
|
|
93
|
+
try {
|
|
94
|
+
const provider = new HuggingfaceConnect(createKernel(root), {})
|
|
95
|
+
const context = {
|
|
96
|
+
parentPath: scriptPath,
|
|
97
|
+
cwd: appDir,
|
|
98
|
+
}
|
|
99
|
+
const sharedEnv = await provider.authEnv(context)
|
|
100
|
+
assert.equal(sharedEnv.HF_HOME, path.join(appDir, 'cache', 'huggingface'))
|
|
101
|
+
assert.equal(sharedEnv.HF_TOKEN_PATH, path.join(root, 'cache', 'HF_AUTH', 'token'))
|
|
102
|
+
|
|
103
|
+
await fs.writeFile(
|
|
104
|
+
path.join(appDir, 'ENVIRONMENT'),
|
|
105
|
+
'HF_HOME=./cache/huggingface\nHF_TOKEN_PATH=./auth/token\n'
|
|
106
|
+
)
|
|
107
|
+
provider.hfPath = () => process.execPath
|
|
108
|
+
const { env } = await provider.runHf([
|
|
109
|
+
'-e',
|
|
110
|
+
'require("node:fs").writeFileSync(process.env.HF_TOKEN_PATH, "hf_app_token")'
|
|
111
|
+
], {}, context)
|
|
112
|
+
|
|
113
|
+
assert.equal(env.HF_HOME, path.join(appDir, 'cache', 'huggingface'))
|
|
114
|
+
assert.equal(env.HF_TOKEN_PATH, tokenPath)
|
|
115
|
+
assert.equal(await fs.readFile(tokenPath, 'utf8'), 'hf_app_token')
|
|
116
|
+
await assert.rejects(
|
|
117
|
+
fs.access(path.join(root, 'cache', 'HF_AUTH', 'token')),
|
|
118
|
+
{ code: 'ENOENT' }
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
const commandTokenPath = path.join(appDir, 'command-auth', 'token')
|
|
122
|
+
const commandContext = {
|
|
123
|
+
parentPath: scriptPath,
|
|
124
|
+
cwd: appDir,
|
|
125
|
+
env: { HF_TOKEN_PATH: './command-auth/token' }
|
|
126
|
+
}
|
|
127
|
+
const commandResult = await provider.runHf([
|
|
128
|
+
'-e',
|
|
129
|
+
'require("node:fs").writeFileSync(process.env.HF_TOKEN_PATH, "hf_command_token")'
|
|
130
|
+
], {}, commandContext)
|
|
131
|
+
|
|
132
|
+
assert.equal(commandResult.env.HF_TOKEN_PATH, commandTokenPath)
|
|
133
|
+
assert.equal(await fs.readFile(commandTokenPath, 'utf8'), 'hf_command_token')
|
|
134
|
+
} finally {
|
|
135
|
+
await fs.rm(root, { recursive: true, force: true })
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
|
|
81
139
|
test('Hugging Face connect verifies status with bounded whoami', async () => {
|
|
82
140
|
const provider = new HuggingfaceConnect(createKernel('/tmp/pinokio'), {})
|
|
83
141
|
let receivedArgs
|
|
@@ -99,7 +157,7 @@ test('Hugging Face connect verifies status with bounded whoami', async () => {
|
|
|
99
157
|
assert.equal(await provider.connected({ timeout: 2500 }), false)
|
|
100
158
|
})
|
|
101
159
|
|
|
102
|
-
test('Hugging Face connect supplies the shared token path
|
|
160
|
+
test('Hugging Face connect supplies the shared token path when none is configured', async () => {
|
|
103
161
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-hf-connect-default-'))
|
|
104
162
|
await fs.writeFile(path.join(root, 'ENVIRONMENT'), 'OTHER=value\n')
|
|
105
163
|
try {
|
|
@@ -129,16 +187,43 @@ test('Hugging Face connect repairs an empty directory at the token file path', a
|
|
|
129
187
|
}
|
|
130
188
|
})
|
|
131
189
|
|
|
132
|
-
test('Hugging Face connect
|
|
133
|
-
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-hf-connect-
|
|
134
|
-
const
|
|
190
|
+
test('Hugging Face connect leaves existing token and refresh files unchanged', async () => {
|
|
191
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-hf-connect-existing-login-'))
|
|
192
|
+
const authDir = path.join(root, 'cache', 'HF_AUTH')
|
|
193
|
+
await fs.mkdir(authDir, { recursive: true })
|
|
194
|
+
await fs.writeFile(path.join(authDir, 'token'), 'hf_existing_token')
|
|
195
|
+
await fs.writeFile(path.join(authDir, 'stored_tokens'), 'existing stored tokens')
|
|
196
|
+
try {
|
|
197
|
+
const provider = new HuggingfaceConnect(createKernel(root), {})
|
|
198
|
+
const env = await provider.authEnv()
|
|
199
|
+
|
|
200
|
+
assert.equal(env.HF_TOKEN_PATH, path.join(authDir, 'token'))
|
|
201
|
+
assert.equal(await fs.readFile(path.join(authDir, 'token'), 'utf8'), 'hf_existing_token')
|
|
202
|
+
assert.equal(await fs.readFile(path.join(authDir, 'stored_tokens'), 'utf8'), 'existing stored tokens')
|
|
203
|
+
} finally {
|
|
204
|
+
await fs.rm(root, { recursive: true, force: true })
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test('Hugging Face connect removes a non-empty token directory', async () => {
|
|
209
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-hf-connect-invalid-dir-'))
|
|
210
|
+
const authDir = path.join(root, 'cache', 'HF_AUTH')
|
|
211
|
+
const tokenPath = path.join(authDir, 'token')
|
|
135
212
|
await fs.mkdir(tokenPath, { recursive: true })
|
|
136
|
-
await fs.writeFile(path.join(tokenPath, '
|
|
213
|
+
await fs.writeFile(path.join(tokenPath, 'leftover.txt'), 'leftover')
|
|
214
|
+
await fs.writeFile(path.join(authDir, 'stored_tokens'), 'refresh state')
|
|
137
215
|
try {
|
|
138
216
|
const provider = new HuggingfaceConnect(createKernel(root), {})
|
|
217
|
+
await Promise.all([provider.authEnv(), provider.authEnv()])
|
|
218
|
+
provider.hfPath = () => process.execPath
|
|
219
|
+
const { env } = await provider.runHf([
|
|
220
|
+
'-e',
|
|
221
|
+
'require("node:fs").writeFileSync(process.env.HF_TOKEN_PATH, "hf_replacement_token")'
|
|
222
|
+
])
|
|
139
223
|
|
|
140
|
-
|
|
141
|
-
assert.equal(await fs.readFile(
|
|
224
|
+
assert.equal(env.HF_TOKEN_PATH, tokenPath)
|
|
225
|
+
assert.equal(await fs.readFile(tokenPath, 'utf8'), 'hf_replacement_token')
|
|
226
|
+
assert.equal(await fs.readFile(path.join(authDir, 'stored_tokens'), 'utf8'), 'refresh state')
|
|
142
227
|
} finally {
|
|
143
228
|
await fs.rm(root, { recursive: true, force: true })
|
|
144
229
|
}
|
|
@@ -172,6 +172,39 @@ test('Shell.init_env disables Hugging Face hub update checks by default without
|
|
|
172
172
|
assert.equal(overrideShell.env.HF_TOKEN_PATH, path.join(root, 'custom', 'hf-token'))
|
|
173
173
|
})
|
|
174
174
|
|
|
175
|
+
test('Shell.init_env preserves an app HF_HOME without changing the shared token default', async () => {
|
|
176
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-shell-hf-app-env-'))
|
|
177
|
+
const appDir = path.join(root, 'api', 'demo')
|
|
178
|
+
const scriptPath = path.join(appDir, 'start.js')
|
|
179
|
+
await fs.mkdir(appDir, { recursive: true })
|
|
180
|
+
await fs.writeFile(scriptPath, '')
|
|
181
|
+
await fs.writeFile(path.join(appDir, 'ENVIRONMENT'), 'HF_HOME=./cache/huggingface\n')
|
|
182
|
+
|
|
183
|
+
const shell = createShell(createKernel(root))
|
|
184
|
+
await shell.init_env({
|
|
185
|
+
path: appDir,
|
|
186
|
+
$parent: { path: scriptPath },
|
|
187
|
+
env: {}
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
assert.equal(shell.env.HF_HOME, path.join(appDir, 'cache', 'huggingface'))
|
|
191
|
+
assert.equal(shell.env.HF_TOKEN_PATH, path.join(root, 'cache', 'HF_AUTH', 'token'))
|
|
192
|
+
|
|
193
|
+
await fs.writeFile(
|
|
194
|
+
path.join(appDir, 'ENVIRONMENT'),
|
|
195
|
+
'HF_HOME=./cache/huggingface\nHF_TOKEN_PATH=./auth/token\n'
|
|
196
|
+
)
|
|
197
|
+
const overrideShell = createShell(createKernel(root))
|
|
198
|
+
await overrideShell.init_env({
|
|
199
|
+
path: appDir,
|
|
200
|
+
$parent: { path: scriptPath },
|
|
201
|
+
env: {}
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
assert.equal(overrideShell.env.HF_HOME, path.join(appDir, 'cache', 'huggingface'))
|
|
205
|
+
assert.equal(overrideShell.env.HF_TOKEN_PATH, path.join(appDir, 'auth', 'token'))
|
|
206
|
+
})
|
|
207
|
+
|
|
175
208
|
test('Shell.init_env keeps Windows Hugging Face symlink defaults scoped to win32', async () => {
|
|
176
209
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-shell-hf-win-'))
|
|
177
210
|
await fs.mkdir(path.join(root, 'api'), { recursive: true })
|