pinokiod 8.0.3 → 8.0.5
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/bin/caddy.js +30 -0
- package/kernel/bin/conda.js +19 -1
- package/kernel/connect/index.js +6 -0
- package/kernel/connect/providers/huggingface/index.js +17 -1
- package/kernel/environment.js +0 -15
- package/package.json +1 -1
- package/server/index.js +15 -72
- package/server/public/task-launcher.css +4 -0
- package/server/routes/connect.js +49 -0
- package/server/views/connect.ejs +66 -46
- package/server/views/install.ejs +3 -0
- package/test/caddy-install.test.js +65 -0
- package/test/conda-bin.test.js +54 -0
- package/test/connect-page-route.test.js +143 -0
- package/test/github-connection.test.js +18 -3
- package/test/huggingface-connect.test.js +72 -0
package/kernel/bin/caddy.js
CHANGED
|
@@ -8,6 +8,7 @@ const Util = require('../util')
|
|
|
8
8
|
const ADMIN_READY_TIMEOUT_MS = 30000
|
|
9
9
|
const ADMIN_START_TIMEOUT_MS = 5000
|
|
10
10
|
const ADMIN_POLL_INTERVAL_MS = 250
|
|
11
|
+
const ADMIN_STOP_TIMEOUT_MS = 5000
|
|
11
12
|
|
|
12
13
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
13
14
|
|
|
@@ -29,6 +30,35 @@ class Caddy {
|
|
|
29
30
|
return false
|
|
30
31
|
}
|
|
31
32
|
}
|
|
33
|
+
async waitForStopped(timeout = ADMIN_STOP_TIMEOUT_MS, interval = ADMIN_POLL_INTERVAL_MS) {
|
|
34
|
+
const started = Date.now()
|
|
35
|
+
while ((Date.now() - started) < timeout) {
|
|
36
|
+
if (!(await this.running())) {
|
|
37
|
+
return true
|
|
38
|
+
}
|
|
39
|
+
await sleep(interval)
|
|
40
|
+
}
|
|
41
|
+
return !(await this.running())
|
|
42
|
+
}
|
|
43
|
+
async stop() {
|
|
44
|
+
if (!(await this.running())) {
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
await axios.post('http://127.0.0.1:2019/stop', null, {
|
|
49
|
+
timeout: ADMIN_STOP_TIMEOUT_MS
|
|
50
|
+
})
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (await this.running()) {
|
|
53
|
+
throw error
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const stopped = await this.waitForStopped()
|
|
57
|
+
if (!stopped) {
|
|
58
|
+
throw new Error("Pinokio could not stop Caddy before replacing the managed Conda runtime.")
|
|
59
|
+
}
|
|
60
|
+
return true
|
|
61
|
+
}
|
|
32
62
|
async start() {
|
|
33
63
|
// if peer.https_active is true,
|
|
34
64
|
// 1. kill existing caddy
|
package/kernel/bin/conda.js
CHANGED
|
@@ -170,6 +170,16 @@ report_errors: false`)
|
|
|
170
170
|
ondata({ raw: `downloading installer: ${installer_url}...\r\n` })
|
|
171
171
|
await this.kernel.bin.download(installer_url, installer, ondata)
|
|
172
172
|
|
|
173
|
+
const caddy = this.kernel.bin.mod && this.kernel.bin.mod.caddy
|
|
174
|
+
let caddyWasRunning = false
|
|
175
|
+
if (caddy && typeof caddy.running === "function") {
|
|
176
|
+
caddyWasRunning = await caddy.running()
|
|
177
|
+
}
|
|
178
|
+
if (caddyWasRunning) {
|
|
179
|
+
ondata({ raw: `stopping Home Server before replacing Miniforge...\r\n` })
|
|
180
|
+
await caddy.stop()
|
|
181
|
+
}
|
|
182
|
+
|
|
173
183
|
legacy_path_exists = await this.kernel.exists(`bin/${LEGACY_CONDA_ROOT_DIR}`)
|
|
174
184
|
if (legacy_path_exists) {
|
|
175
185
|
console.log("Removing legacy install path...", legacy_path)
|
|
@@ -202,8 +212,12 @@ report_errors: false`)
|
|
|
202
212
|
await fs.promises.writeFile(this.kernel.path(`bin/${CONDA_ROOT_DIR}/conda-meta/pinned`), this.pinnedPackages())
|
|
203
213
|
}
|
|
204
214
|
|
|
215
|
+
const dependencies = new Set(Array.isArray(req.dependencies) ? req.dependencies : [])
|
|
216
|
+
if (caddyWasRunning) {
|
|
217
|
+
dependencies.add("caddy")
|
|
218
|
+
}
|
|
205
219
|
let mods = this.kernel.bin.mods.filter((m) => {
|
|
206
|
-
return
|
|
220
|
+
return dependencies.has(m.name)
|
|
207
221
|
}).map((m) => {
|
|
208
222
|
if (m.mod.cmd) {
|
|
209
223
|
return m.mod.cmd()
|
|
@@ -241,6 +255,10 @@ report_errors: false`)
|
|
|
241
255
|
await this.ensureCompatibilityAlias()
|
|
242
256
|
ondata({ raw: `Install finished\r\n` })
|
|
243
257
|
await this.kernel.bin.rm(installer, ondata)
|
|
258
|
+
if (caddyWasRunning) {
|
|
259
|
+
ondata({ raw: `restarting Home Server after replacing Miniforge...\r\n` })
|
|
260
|
+
await caddy.start()
|
|
261
|
+
}
|
|
244
262
|
}
|
|
245
263
|
async removeInstallPath(target) {
|
|
246
264
|
try {
|
package/kernel/connect/index.js
CHANGED
|
@@ -42,5 +42,11 @@ class Connect {
|
|
|
42
42
|
let res = await this.clients[provider].keys()
|
|
43
43
|
return res
|
|
44
44
|
}
|
|
45
|
+
async connected(provider, options) {
|
|
46
|
+
if (this.clients[provider] && this.clients[provider].connected) {
|
|
47
|
+
return await this.clients[provider].connected(options)
|
|
48
|
+
}
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
45
51
|
}
|
|
46
52
|
module.exports = Connect
|
|
@@ -34,6 +34,13 @@ class Huggingface {
|
|
|
34
34
|
}
|
|
35
35
|
delete env.HF_TOKEN
|
|
36
36
|
delete env.HUGGING_FACE_HUB_TOKEN
|
|
37
|
+
try {
|
|
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
|
+
}
|
|
37
44
|
await fs.promises.mkdir(path.dirname(env.HF_TOKEN_PATH), { recursive: true }).catch(() => {})
|
|
38
45
|
return env
|
|
39
46
|
}
|
|
@@ -52,12 +59,13 @@ class Huggingface {
|
|
|
52
59
|
}
|
|
53
60
|
return candidates[0]
|
|
54
61
|
}
|
|
55
|
-
async runHf(args) {
|
|
62
|
+
async runHf(args, options = {}) {
|
|
56
63
|
const env = await this.authEnv()
|
|
57
64
|
return new Promise((resolve, reject) => {
|
|
58
65
|
execFile(this.hfPath(), args, {
|
|
59
66
|
cwd: this.kernel.homedir,
|
|
60
67
|
env,
|
|
68
|
+
timeout: Number.isFinite(options.timeout) ? options.timeout : undefined,
|
|
61
69
|
maxBuffer: 1024 * 1024,
|
|
62
70
|
}, (error, stdout, stderr) => {
|
|
63
71
|
if (error) {
|
|
@@ -199,6 +207,14 @@ class Huggingface {
|
|
|
199
207
|
return null
|
|
200
208
|
}
|
|
201
209
|
}
|
|
210
|
+
async connected(options = {}) {
|
|
211
|
+
try {
|
|
212
|
+
await this.runHf(["auth", "whoami", "--format", "quiet"], options)
|
|
213
|
+
return true
|
|
214
|
+
} catch (_) {
|
|
215
|
+
return false
|
|
216
|
+
}
|
|
217
|
+
}
|
|
202
218
|
async profile() {
|
|
203
219
|
const keys = await this.keys()
|
|
204
220
|
if (!keys || !keys.access_token) {
|
package/kernel/environment.js
CHANGED
|
@@ -489,21 +489,6 @@ const ENVS = async () => {
|
|
|
489
489
|
"#",
|
|
490
490
|
"##########################################################################",
|
|
491
491
|
],
|
|
492
|
-
}, {
|
|
493
|
-
type: ["system"],
|
|
494
|
-
key: "HF_TOKEN_PATH",
|
|
495
|
-
val: "./cache/HF_AUTH/token",
|
|
496
|
-
comment: [
|
|
497
|
-
"##########################################################################",
|
|
498
|
-
"#",
|
|
499
|
-
"# HF_TOKEN_PATH",
|
|
500
|
-
"#",
|
|
501
|
-
"# Hugging Face authentication token file",
|
|
502
|
-
"# Pinokio keeps this shared across apps so each app can customize HF_HOME",
|
|
503
|
-
"# for model/cache files without changing the logged-in Hugging Face account.",
|
|
504
|
-
"#",
|
|
505
|
-
"##########################################################################",
|
|
506
|
-
],
|
|
507
492
|
}, {
|
|
508
493
|
type: ["system", "app"],
|
|
509
494
|
key: "TORCH_HOME",
|
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -28,6 +28,7 @@ const system = require('systeminformation')
|
|
|
28
28
|
const serveIndex = require('./serveIndex')
|
|
29
29
|
const registerFileRoutes = require('./routes/files')
|
|
30
30
|
const registerAppRoutes = require('./routes/apps')
|
|
31
|
+
const registerConnectRoutes = require('./routes/connect')
|
|
31
32
|
const ServerAutolaunch = require('./autolaunch')
|
|
32
33
|
const Git = require("../kernel/git")
|
|
33
34
|
const TerminalApi = require('../kernel/api/terminal')
|
|
@@ -1500,7 +1501,7 @@ class Server {
|
|
|
1500
1501
|
)
|
|
1501
1502
|
})
|
|
1502
1503
|
}
|
|
1503
|
-
async get_github_connection() {
|
|
1504
|
+
async get_github_connection(options = {}) {
|
|
1504
1505
|
if (this._githubConnectionPromise) {
|
|
1505
1506
|
return this._githubConnectionPromise
|
|
1506
1507
|
}
|
|
@@ -1511,7 +1512,9 @@ class Server {
|
|
|
1511
1512
|
let gcmError = null
|
|
1512
1513
|
|
|
1513
1514
|
try {
|
|
1514
|
-
const stdout = await this.github_gcm(['list'], {
|
|
1515
|
+
const stdout = await this.github_gcm(['list'], {
|
|
1516
|
+
timeout: Number.isFinite(options.timeout) ? options.timeout : 10000
|
|
1517
|
+
})
|
|
1515
1518
|
accounts = stdout
|
|
1516
1519
|
.split(/\r?\n/)
|
|
1517
1520
|
.map((line) => line.trim())
|
|
@@ -1550,7 +1553,7 @@ class Server {
|
|
|
1550
1553
|
? "echo P^INOKIO_GITHUB_LOGIN_DONE"
|
|
1551
1554
|
: "(GCM_DONE=INOKIO_GITHUB_LOGIN_DONE; printf 'P%s\\n' \"$GCM_DONE\")"
|
|
1552
1555
|
const loginCommand = [
|
|
1553
|
-
"git credential-manager github login --
|
|
1556
|
+
"git credential-manager github login --device --force",
|
|
1554
1557
|
verifyCommand,
|
|
1555
1558
|
doneCommand
|
|
1556
1559
|
].join(" && ")
|
|
@@ -12668,80 +12671,19 @@ class Server {
|
|
|
12668
12671
|
|
|
12669
12672
|
/*
|
|
12670
12673
|
GET /connect => display connection options
|
|
12674
|
+
- huggingface
|
|
12671
12675
|
- github
|
|
12672
|
-
- x
|
|
12673
12676
|
*/
|
|
12674
|
-
this.app
|
|
12675
|
-
|
|
12676
|
-
let current_urls = await this.current_urls(req.originalUrl.slice(1))
|
|
12677
|
-
let items = [{
|
|
12678
|
-
// image: "/pinokio-black.png",
|
|
12679
|
-
// name: "pinokio",
|
|
12680
|
-
// title: "pinokio.co",
|
|
12681
|
-
// description: "Connect with pinokio.co",
|
|
12682
|
-
// url: "/connect/pinokio"
|
|
12683
|
-
// }, {
|
|
12684
|
-
icon: "fa-brands fa-hugging-face",
|
|
12685
|
-
name: "huggingface",
|
|
12686
|
-
title: "huggingface.co",
|
|
12687
|
-
description: "Connect with huggingface.co",
|
|
12688
|
-
url: "/connect/huggingface"
|
|
12689
|
-
}, {
|
|
12690
|
-
icon: "fa-brands fa-github",
|
|
12691
|
-
name: "github",
|
|
12692
|
-
title: "github.com",
|
|
12693
|
-
description: "Connect with GitHub.com",
|
|
12694
|
-
url: "/github"
|
|
12695
|
-
}, {
|
|
12696
|
-
icon: "fa-brands fa-square-x-twitter",
|
|
12697
|
-
name: "x",
|
|
12698
|
-
title: "x.com",
|
|
12699
|
-
description: "Connect with X.com",
|
|
12700
|
-
url: "/connect/x",
|
|
12701
|
-
hidden: true
|
|
12702
|
-
}]
|
|
12703
|
-
let github_hosts = await this.get_github_hosts()
|
|
12704
|
-
for(let i=0; i<items.length; i++) {
|
|
12705
|
-
try {
|
|
12706
|
-
if (items[i].name === "github") {
|
|
12707
|
-
if (github_hosts.length > 0) {
|
|
12708
|
-
items[i].profile = {
|
|
12709
|
-
icon: "fa-brands fa-github",
|
|
12710
|
-
items: [{
|
|
12711
|
-
key: "config",
|
|
12712
|
-
val: github_hosts
|
|
12713
|
-
}]
|
|
12714
|
-
}
|
|
12715
|
-
items[i].description = `<i class="fa-solid fa-circle-check"></i> Connected with ${items[i].title}`
|
|
12716
|
-
items[i].connected = true
|
|
12717
|
-
}
|
|
12718
|
-
} else {
|
|
12719
|
-
const config = this.kernel.connect.config[items[i].name]
|
|
12720
|
-
if (config) {
|
|
12721
|
-
let profile = await this.kernel.connect.profile(items[i].name)
|
|
12722
|
-
if (profile) {
|
|
12723
|
-
items[i].profile = profile
|
|
12724
|
-
items[i].description = `<i class="fa-solid fa-circle-check"></i> Connected with ${items[i].title}`
|
|
12725
|
-
items[i].connected = true
|
|
12726
|
-
}
|
|
12727
|
-
}
|
|
12728
|
-
}
|
|
12729
|
-
} catch (e) {
|
|
12730
|
-
}
|
|
12731
|
-
}
|
|
12732
|
-
const peerAccess = await this.composePeerAccessPayload()
|
|
12733
|
-
res.render(`connect`, {
|
|
12734
|
-
current_urls,
|
|
12735
|
-
current_host: this.kernel.peer.host,
|
|
12736
|
-
...peerAccess,
|
|
12737
|
-
list,
|
|
12677
|
+
registerConnectRoutes(this.app, {
|
|
12678
|
+
getPageContext: (req) => ({
|
|
12738
12679
|
portal: this.portal,
|
|
12739
12680
|
logo: this.logo,
|
|
12740
12681
|
theme: this.theme,
|
|
12741
|
-
agent: req.agent
|
|
12742
|
-
|
|
12743
|
-
|
|
12744
|
-
|
|
12682
|
+
agent: req.agent
|
|
12683
|
+
}),
|
|
12684
|
+
getGithubConnection: (options) => this.get_github_connection(options),
|
|
12685
|
+
getProviderConnection: (provider, options) => this.kernel.connect.connected(provider, options)
|
|
12686
|
+
})
|
|
12745
12687
|
/*
|
|
12746
12688
|
* GET /connect/x
|
|
12747
12689
|
* GET /connect/discord
|
|
@@ -13514,6 +13456,7 @@ class Server {
|
|
|
13514
13456
|
}))
|
|
13515
13457
|
this.app.get("/github/login", ex(async (req, res) => {
|
|
13516
13458
|
let id = "gh_login"
|
|
13459
|
+
this.kernel.shell.kill({ id: `shell/${id}` })
|
|
13517
13460
|
let params = new URLSearchParams()
|
|
13518
13461
|
const { doneMarker, message } = this.github_login_params()
|
|
13519
13462
|
params.set("message", encodeURIComponent(message))
|
|
@@ -2618,6 +2618,10 @@ body.connect-page .connect-service-status {
|
|
|
2618
2618
|
font-weight: 700;
|
|
2619
2619
|
}
|
|
2620
2620
|
|
|
2621
|
+
body.connect-page .connect-service-status[hidden] {
|
|
2622
|
+
display: none;
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2621
2625
|
body.connect-page .connect-service-open {
|
|
2622
2626
|
color: var(--task-muted);
|
|
2623
2627
|
font-size: 15px;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const CONNECT_STATUS_TIMEOUT_MS = 3000
|
|
2
|
+
|
|
3
|
+
const CONNECT_ITEMS = [{
|
|
4
|
+
icon: "fa-brands fa-hugging-face",
|
|
5
|
+
name: "huggingface",
|
|
6
|
+
title: "huggingface.co",
|
|
7
|
+
description: "Connect with huggingface.co",
|
|
8
|
+
url: "/connect/huggingface"
|
|
9
|
+
}, {
|
|
10
|
+
icon: "fa-brands fa-github",
|
|
11
|
+
name: "github",
|
|
12
|
+
title: "github.com",
|
|
13
|
+
description: "Connect with GitHub.com",
|
|
14
|
+
url: "/github"
|
|
15
|
+
}]
|
|
16
|
+
|
|
17
|
+
module.exports = function registerConnectRoutes(app, options = {}) {
|
|
18
|
+
const getPageContext = options.getPageContext || (() => ({}))
|
|
19
|
+
const getGithubConnection = options.getGithubConnection
|
|
20
|
+
const getProviderConnection = options.getProviderConnection
|
|
21
|
+
const statusTimeoutMs = Number.isFinite(options.statusTimeoutMs)
|
|
22
|
+
? options.statusTimeoutMs
|
|
23
|
+
: CONNECT_STATUS_TIMEOUT_MS
|
|
24
|
+
|
|
25
|
+
app.get("/connect", (req, res) => {
|
|
26
|
+
res.render("connect", {
|
|
27
|
+
...getPageContext(req),
|
|
28
|
+
items: CONNECT_ITEMS
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
app.get("/connect/status/:provider", async (req, res) => {
|
|
33
|
+
const provider = String(req.params.provider || "")
|
|
34
|
+
let connected = false
|
|
35
|
+
try {
|
|
36
|
+
if (provider === "github" && typeof getGithubConnection === "function") {
|
|
37
|
+
const connection = await getGithubConnection({ timeout: statusTimeoutMs })
|
|
38
|
+
connected = Boolean(connection && connection.connected)
|
|
39
|
+
} else if (provider === "huggingface" && typeof getProviderConnection === "function") {
|
|
40
|
+
connected = await getProviderConnection(provider, { timeout: statusTimeoutMs })
|
|
41
|
+
} else {
|
|
42
|
+
res.status(404).json({ provider, error: "Unknown provider" })
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
} catch (_) {}
|
|
46
|
+
res.set("Cache-Control", "no-store")
|
|
47
|
+
res.json({ provider, connected: Boolean(connected) })
|
|
48
|
+
})
|
|
49
|
+
}
|
package/server/views/connect.ejs
CHANGED
|
@@ -818,56 +818,31 @@ body.dark .profile td {
|
|
|
818
818
|
<% } else { %>
|
|
819
819
|
<div class='connect-service-list'>
|
|
820
820
|
<% items.forEach((item) => { %>
|
|
821
|
-
|
|
822
|
-
<
|
|
823
|
-
<div class='connect-service-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
<% } %>
|
|
834
|
-
</div>
|
|
835
|
-
<div class='connect-service-copy'>
|
|
836
|
-
<div class='connect-service-title-row'>
|
|
837
|
-
<h2 class='connect-service-title'><%=item.title%></h2>
|
|
838
|
-
<span class='connect-service-status'>Connected</span>
|
|
839
|
-
</div>
|
|
840
|
-
<div class='connect-service-description'><%- item.description %></div>
|
|
841
|
-
</div>
|
|
842
|
-
<div class='connect-service-open' aria-hidden='true'>
|
|
843
|
-
<i class="fa-solid fa-chevron-right"></i>
|
|
844
|
-
</div>
|
|
821
|
+
<a href="<%=item.url%>" class="connect-service-card" data-connect-provider="<%=item.name%>">
|
|
822
|
+
<div class='connect-service-head'>
|
|
823
|
+
<div class='connect-service-icon'>
|
|
824
|
+
<% if (item.image) { %>
|
|
825
|
+
<img class="connect-service-icon-image" src="<%=item.image%>" alt="">
|
|
826
|
+
<% } else if (item.icon) { %>
|
|
827
|
+
<i class="<%=item.icon%>"></i>
|
|
828
|
+
<% } else if (item.emoji) { %>
|
|
829
|
+
<span><%=item.emoji%></span>
|
|
830
|
+
<% } else { %>
|
|
831
|
+
<i class="fa-solid fa-plug"></i>
|
|
832
|
+
<% } %>
|
|
845
833
|
</div>
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
<a href="<%=item.url%>" class="connect-service-card">
|
|
849
|
-
<div class='connect-service-head'>
|
|
850
|
-
<div class='connect-service-icon'>
|
|
851
|
-
<% if (item.image) { %>
|
|
852
|
-
<img class="connect-service-icon-image" src="<%=item.image%>" alt="">
|
|
853
|
-
<% } else if (item.icon) { %>
|
|
854
|
-
<i class="<%=item.icon%>"></i>
|
|
855
|
-
<% } else if (item.emoji) { %>
|
|
856
|
-
<span><%=item.emoji%></span>
|
|
857
|
-
<% } else { %>
|
|
858
|
-
<i class="fa-solid fa-plug"></i>
|
|
859
|
-
<% } %>
|
|
860
|
-
</div>
|
|
861
|
-
<div class='connect-service-copy'>
|
|
834
|
+
<div class='connect-service-copy'>
|
|
835
|
+
<div class='connect-service-title-row'>
|
|
862
836
|
<h2 class='connect-service-title'><%=item.title%></h2>
|
|
863
|
-
<
|
|
864
|
-
</div>
|
|
865
|
-
<div class="connect-service-open" aria-hidden='true'>
|
|
866
|
-
<i class="fa-solid fa-chevron-right"></i>
|
|
837
|
+
<span class='connect-service-status' data-connect-status aria-live="polite">Checking...</span>
|
|
867
838
|
</div>
|
|
839
|
+
<div class="connect-service-description" data-connect-description><%=item.description%></div>
|
|
868
840
|
</div>
|
|
869
|
-
|
|
870
|
-
|
|
841
|
+
<div class="connect-service-open" aria-hidden='true'>
|
|
842
|
+
<i class="fa-solid fa-chevron-right"></i>
|
|
843
|
+
</div>
|
|
844
|
+
</div>
|
|
845
|
+
</a>
|
|
871
846
|
<% }) %>
|
|
872
847
|
</div>
|
|
873
848
|
<% } %>
|
|
@@ -878,5 +853,50 @@ body.dark .profile td {
|
|
|
878
853
|
<%- include('partials/main_sidebar', { selected: 'connect' }) %>
|
|
879
854
|
</main>
|
|
880
855
|
<%- include('partials/app_common_scripts') %>
|
|
856
|
+
<script>
|
|
857
|
+
(() => {
|
|
858
|
+
const cards = document.querySelectorAll("[data-connect-provider]")
|
|
859
|
+
cards.forEach((card) => {
|
|
860
|
+
const provider = card.dataset.connectProvider
|
|
861
|
+
const status = card.querySelector("[data-connect-status]")
|
|
862
|
+
const description = card.querySelector("[data-connect-description]")
|
|
863
|
+
const title = card.querySelector(".connect-service-title")
|
|
864
|
+
const controller = new AbortController()
|
|
865
|
+
const timer = window.setTimeout(() => controller.abort(), 4000)
|
|
866
|
+
|
|
867
|
+
fetch(`/connect/status/${encodeURIComponent(provider)}`, {
|
|
868
|
+
cache: "no-store",
|
|
869
|
+
headers: { "Accept": "application/json" },
|
|
870
|
+
signal: controller.signal
|
|
871
|
+
}).then((response) => {
|
|
872
|
+
if (!response.ok) {
|
|
873
|
+
throw new Error(`Status request failed: ${response.status}`)
|
|
874
|
+
}
|
|
875
|
+
return response.json()
|
|
876
|
+
}).then((result) => {
|
|
877
|
+
const connected = Boolean(result && result.connected)
|
|
878
|
+
if (!connected) {
|
|
879
|
+
if (status) {
|
|
880
|
+
status.hidden = true
|
|
881
|
+
}
|
|
882
|
+
return
|
|
883
|
+
}
|
|
884
|
+
if (status) {
|
|
885
|
+
status.textContent = "Connected"
|
|
886
|
+
status.hidden = false
|
|
887
|
+
}
|
|
888
|
+
if (description && title) {
|
|
889
|
+
description.textContent = `Connected with ${title.textContent}`
|
|
890
|
+
}
|
|
891
|
+
}).catch(() => {
|
|
892
|
+
if (status) {
|
|
893
|
+
status.hidden = true
|
|
894
|
+
}
|
|
895
|
+
}).finally(() => {
|
|
896
|
+
window.clearTimeout(timer)
|
|
897
|
+
})
|
|
898
|
+
})
|
|
899
|
+
})()
|
|
900
|
+
</script>
|
|
881
901
|
</body>
|
|
882
902
|
</html>
|
package/server/views/install.ejs
CHANGED
|
@@ -51,3 +51,68 @@ test('caddy admin wait returns true when admin responds', async () => {
|
|
|
51
51
|
assert.equal(ready, true)
|
|
52
52
|
assert.equal(checks, 2)
|
|
53
53
|
})
|
|
54
|
+
|
|
55
|
+
test('caddy start uses the normal shell execution contract', async () => {
|
|
56
|
+
const caddy = new Caddy()
|
|
57
|
+
let running = false
|
|
58
|
+
let launch
|
|
59
|
+
caddy.kernel = {
|
|
60
|
+
homedir: '/tmp/pinokio',
|
|
61
|
+
peer: {
|
|
62
|
+
https_active: true,
|
|
63
|
+
announce() {}
|
|
64
|
+
},
|
|
65
|
+
processes: {},
|
|
66
|
+
exec(params, ondata) {
|
|
67
|
+
launch = params
|
|
68
|
+
running = true
|
|
69
|
+
ondata({ raw: '', cleaned: 'admin endpoint started' })
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
caddy.running = async () => running
|
|
73
|
+
caddy.installed = async () => true
|
|
74
|
+
|
|
75
|
+
await caddy.start()
|
|
76
|
+
|
|
77
|
+
assert.equal(launch.message, 'caddy run --watch')
|
|
78
|
+
assert.deepEqual(Object.keys(launch).sort(), ['message', 'path'])
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('caddy stop uses the native admin API only when Caddy is running', async () => {
|
|
82
|
+
const caddy = new Caddy()
|
|
83
|
+
let running = true
|
|
84
|
+
caddy.running = async () => running
|
|
85
|
+
const originalPost = require('axios').post
|
|
86
|
+
const calls = []
|
|
87
|
+
require('axios').post = async (...args) => {
|
|
88
|
+
calls.push(args)
|
|
89
|
+
running = false
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
assert.equal(await caddy.stop(), true)
|
|
94
|
+
assert.equal(await caddy.stop(), false)
|
|
95
|
+
} finally {
|
|
96
|
+
require('axios').post = originalPost
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
assert.equal(calls.length, 1)
|
|
100
|
+
assert.equal(calls[0][0], 'http://127.0.0.1:2019/stop')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('caddy stop refuses to continue when native shutdown is not confirmed', async () => {
|
|
104
|
+
const caddy = new Caddy()
|
|
105
|
+
caddy.running = async () => true
|
|
106
|
+
caddy.waitForStopped = async () => false
|
|
107
|
+
const originalPost = require('axios').post
|
|
108
|
+
require('axios').post = async () => {}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
await assert.rejects(
|
|
112
|
+
caddy.stop(),
|
|
113
|
+
/could not stop Caddy/
|
|
114
|
+
)
|
|
115
|
+
} finally {
|
|
116
|
+
require('axios').post = originalPost
|
|
117
|
+
}
|
|
118
|
+
})
|
package/test/conda-bin.test.js
CHANGED
|
@@ -262,6 +262,60 @@ test('Conda install replaces the stale runtime and rebuilds declared Conda modul
|
|
|
262
262
|
)
|
|
263
263
|
})
|
|
264
264
|
|
|
265
|
+
test('Conda replacement stops, preserves, and restarts a running managed Caddy', async () => {
|
|
266
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-caddy-lifecycle-'))
|
|
267
|
+
await createMiniforge(root)
|
|
268
|
+
const kernel = createKernel(root, 'win32')
|
|
269
|
+
const events = []
|
|
270
|
+
let caddyRunning = true
|
|
271
|
+
let condaInstall
|
|
272
|
+
|
|
273
|
+
const caddy = {
|
|
274
|
+
cmd: () => 'caddy=2.9.1',
|
|
275
|
+
running: async () => caddyRunning,
|
|
276
|
+
stop: async () => {
|
|
277
|
+
events.push('caddy-stop')
|
|
278
|
+
caddyRunning = false
|
|
279
|
+
},
|
|
280
|
+
start: async () => {
|
|
281
|
+
events.push('caddy-start')
|
|
282
|
+
caddyRunning = true
|
|
283
|
+
},
|
|
284
|
+
}
|
|
285
|
+
kernel.bin.installed.conda = new Set(['caddy'])
|
|
286
|
+
kernel.bin.mod = { caddy }
|
|
287
|
+
kernel.bin.mods = [{ name: 'caddy', mod: caddy }]
|
|
288
|
+
kernel.bin.download = async () => events.push('download')
|
|
289
|
+
kernel.bin.rm = async () => events.push('installer-cleanup')
|
|
290
|
+
kernel.bin.exec = async (payload) => {
|
|
291
|
+
if (payload && payload.conda && payload.conda.skip) {
|
|
292
|
+
events.push('bootstrap')
|
|
293
|
+
const miniforge = path.join(root, 'bin', 'miniforge')
|
|
294
|
+
await fs.mkdir(path.join(miniforge, 'conda-meta'), { recursive: true })
|
|
295
|
+
await fs.writeFile(path.join(miniforge, 'python.exe'), 'fake python\n')
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
events.push('conda-install')
|
|
299
|
+
condaInstall = payload
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const conda = createConda(kernel)
|
|
303
|
+
const removeInstallPath = conda.removeInstallPath.bind(conda)
|
|
304
|
+
conda.removeInstallPath = async (target) => {
|
|
305
|
+
assert.equal(caddyRunning, false)
|
|
306
|
+
events.push('remove-miniforge')
|
|
307
|
+
await removeInstallPath(target)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
await conda._install({ dependencies: [] }, () => {})
|
|
311
|
+
|
|
312
|
+
assert.ok(events.indexOf('caddy-stop') < events.indexOf('remove-miniforge'))
|
|
313
|
+
assert.ok(events.indexOf('remove-miniforge') < events.indexOf('bootstrap'))
|
|
314
|
+
assert.ok(events.indexOf('conda-install') < events.indexOf('caddy-start'))
|
|
315
|
+
assert.match(condaInstall.message[1], / caddy=2\.9\.1$/)
|
|
316
|
+
assert.equal(caddyRunning, true)
|
|
317
|
+
})
|
|
318
|
+
|
|
265
319
|
test('Conda install replaces legacy miniconda with a compatibility alias to miniforge', async () => {
|
|
266
320
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-miniforge-alias-'))
|
|
267
321
|
const legacy = await createLegacyMiniconda(root)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
const assert = require('node:assert/strict')
|
|
2
|
+
const http = require('node:http')
|
|
3
|
+
const path = require('node:path')
|
|
4
|
+
const test = require('node:test')
|
|
5
|
+
const express = require('express')
|
|
6
|
+
const registerConnectRoutes = require('../server/routes/connect')
|
|
7
|
+
|
|
8
|
+
const viewsPath = path.resolve(__dirname, '..', 'server', 'views')
|
|
9
|
+
|
|
10
|
+
async function startConnectServer(options = {}) {
|
|
11
|
+
const app = express()
|
|
12
|
+
app.set('views', viewsPath)
|
|
13
|
+
app.set('view engine', 'ejs')
|
|
14
|
+
registerConnectRoutes(app, {
|
|
15
|
+
getPageContext: () => ({
|
|
16
|
+
portal: '',
|
|
17
|
+
logo: '',
|
|
18
|
+
theme: 'light',
|
|
19
|
+
agent: 'test'
|
|
20
|
+
}),
|
|
21
|
+
getGithubConnection: options.getGithubConnection,
|
|
22
|
+
getProviderConnection: options.getProviderConnection,
|
|
23
|
+
statusTimeoutMs: options.statusTimeoutMs
|
|
24
|
+
})
|
|
25
|
+
app.use((error, req, res, next) => {
|
|
26
|
+
res.status(500).send(error.message)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const server = http.createServer(app)
|
|
30
|
+
await new Promise((resolve, reject) => {
|
|
31
|
+
server.once('error', reject)
|
|
32
|
+
server.listen(0, '127.0.0.1', resolve)
|
|
33
|
+
})
|
|
34
|
+
const address = server.address()
|
|
35
|
+
return {
|
|
36
|
+
baseUrl: `http://127.0.0.1:${address.port}`,
|
|
37
|
+
close: () => new Promise((resolve, reject) => {
|
|
38
|
+
server.close((error) => error ? reject(error) : resolve())
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function fetchWithin(url, timeoutMs = 500) {
|
|
44
|
+
const controller = new AbortController()
|
|
45
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
46
|
+
try {
|
|
47
|
+
return await fetch(url, { signal: controller.signal })
|
|
48
|
+
} finally {
|
|
49
|
+
clearTimeout(timer)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
test('/connect renders without invoking provider status checks', async () => {
|
|
54
|
+
let githubCalls = 0
|
|
55
|
+
let huggingfaceCalls = 0
|
|
56
|
+
const fixture = await startConnectServer({
|
|
57
|
+
getGithubConnection: async () => {
|
|
58
|
+
githubCalls += 1
|
|
59
|
+
return await new Promise(() => {})
|
|
60
|
+
},
|
|
61
|
+
getProviderConnection: async () => {
|
|
62
|
+
huggingfaceCalls += 1
|
|
63
|
+
return await new Promise(() => {})
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const response = await fetchWithin(`${fixture.baseUrl}/connect`)
|
|
69
|
+
const html = await response.text()
|
|
70
|
+
|
|
71
|
+
assert.equal(response.status, 200)
|
|
72
|
+
assert.equal(githubCalls, 0)
|
|
73
|
+
assert.equal(huggingfaceCalls, 0)
|
|
74
|
+
assert.match(html, /data-connect-provider="huggingface"/)
|
|
75
|
+
assert.match(html, /data-connect-provider="github"/)
|
|
76
|
+
assert.match(html, /Checking\.\.\./)
|
|
77
|
+
} finally {
|
|
78
|
+
await fixture.close()
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('/connect status endpoints check providers independently with bounded options', async () => {
|
|
83
|
+
const calls = []
|
|
84
|
+
const fixture = await startConnectServer({
|
|
85
|
+
statusTimeoutMs: 75,
|
|
86
|
+
getGithubConnection: async (options) => {
|
|
87
|
+
calls.push({ provider: 'github', options })
|
|
88
|
+
return { connected: true }
|
|
89
|
+
},
|
|
90
|
+
getProviderConnection: async (provider, options) => {
|
|
91
|
+
calls.push({ provider, options })
|
|
92
|
+
return false
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const [githubResponse, huggingfaceResponse] = await Promise.all([
|
|
98
|
+
fetch(`${fixture.baseUrl}/connect/status/github`),
|
|
99
|
+
fetch(`${fixture.baseUrl}/connect/status/huggingface`)
|
|
100
|
+
])
|
|
101
|
+
const github = await githubResponse.json()
|
|
102
|
+
const huggingface = await huggingfaceResponse.json()
|
|
103
|
+
|
|
104
|
+
assert.deepEqual(github, { provider: 'github', connected: true })
|
|
105
|
+
assert.deepEqual(huggingface, { provider: 'huggingface', connected: false })
|
|
106
|
+
assert.deepEqual(calls, [{
|
|
107
|
+
provider: 'github',
|
|
108
|
+
options: { timeout: 75 }
|
|
109
|
+
}, {
|
|
110
|
+
provider: 'huggingface',
|
|
111
|
+
options: { timeout: 75 }
|
|
112
|
+
}])
|
|
113
|
+
assert.match(githubResponse.headers.get('cache-control'), /no-store/)
|
|
114
|
+
} finally {
|
|
115
|
+
await fixture.close()
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('/connect status endpoints degrade provider errors to disconnected', async () => {
|
|
120
|
+
const fixture = await startConnectServer({
|
|
121
|
+
statusTimeoutMs: 30,
|
|
122
|
+
getGithubConnection: async () => {
|
|
123
|
+
throw new Error('github unavailable')
|
|
124
|
+
},
|
|
125
|
+
getProviderConnection: async () => {
|
|
126
|
+
throw new Error('huggingface unavailable')
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const [githubResponse, huggingfaceResponse] = await Promise.all([
|
|
132
|
+
fetch(`${fixture.baseUrl}/connect/status/github`),
|
|
133
|
+
fetch(`${fixture.baseUrl}/connect/status/huggingface`)
|
|
134
|
+
])
|
|
135
|
+
|
|
136
|
+
assert.equal(githubResponse.status, 200)
|
|
137
|
+
assert.equal(huggingfaceResponse.status, 200)
|
|
138
|
+
assert.deepEqual(await githubResponse.json(), { provider: 'github', connected: false })
|
|
139
|
+
assert.deepEqual(await huggingfaceResponse.json(), { provider: 'huggingface', connected: false })
|
|
140
|
+
} finally {
|
|
141
|
+
await fixture.close()
|
|
142
|
+
}
|
|
143
|
+
})
|
|
@@ -36,6 +36,19 @@ test('GitHub connection reports GCM accounts as connected', async () => {
|
|
|
36
36
|
assert.equal(connection.display, 'github.com: octocat')
|
|
37
37
|
})
|
|
38
38
|
|
|
39
|
+
test('GitHub connection forwards a bounded timeout to GCM status checks', async () => {
|
|
40
|
+
let receivedOptions
|
|
41
|
+
await Server.prototype.get_github_connection.call({
|
|
42
|
+
get_legacy_github_hosts: async () => '',
|
|
43
|
+
github_gcm: async (args, options) => {
|
|
44
|
+
receivedOptions = options
|
|
45
|
+
return ''
|
|
46
|
+
},
|
|
47
|
+
}, { timeout: 2500 })
|
|
48
|
+
|
|
49
|
+
assert.deepEqual(receivedOptions, { timeout: 2500 })
|
|
50
|
+
})
|
|
51
|
+
|
|
39
52
|
test('GitHub connection coalesces only concurrent GCM checks', async () => {
|
|
40
53
|
let calls = 0
|
|
41
54
|
const context = {
|
|
@@ -66,8 +79,9 @@ test('GitHub login only emits completion marker after credential verification',
|
|
|
66
79
|
})
|
|
67
80
|
|
|
68
81
|
assert.equal(doneMarker, 'PINOKIO_GITHUB_LOGIN_DONE')
|
|
69
|
-
assert.match(message, /git credential-manager github login --
|
|
70
|
-
assert.doesNotMatch(message, /git credential-manager github login --
|
|
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/)
|
|
71
85
|
assert.doesNotMatch(message, /PINOKIO_GITHUB_LOGIN_DONE/)
|
|
72
86
|
})
|
|
73
87
|
|
|
@@ -76,7 +90,8 @@ test('GitHub login uses success-only credential verification on Windows', () =>
|
|
|
76
90
|
kernel: { platform: 'win32' }
|
|
77
91
|
})
|
|
78
92
|
|
|
79
|
-
assert.match(message, /git credential-manager github login --
|
|
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/)
|
|
94
|
+
assert.doesNotMatch(message, /--web/)
|
|
80
95
|
assert.doesNotMatch(message, /PINOKIO_GITHUB_LOGIN_DONE/)
|
|
81
96
|
})
|
|
82
97
|
|
|
@@ -5,6 +5,7 @@ const path = require('node:path')
|
|
|
5
5
|
const test = require('node:test')
|
|
6
6
|
|
|
7
7
|
const HuggingfaceConnect = require('../kernel/connect/providers/huggingface')
|
|
8
|
+
const Environment = require('../kernel/environment')
|
|
8
9
|
|
|
9
10
|
function createKernel(root) {
|
|
10
11
|
return {
|
|
@@ -23,6 +24,11 @@ function createKernel(root) {
|
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
test('system environment does not persist the default Hugging Face token path', async () => {
|
|
28
|
+
const content = await Environment.ENV('system', '/tmp/pinokio', {})
|
|
29
|
+
assert.doesNotMatch(content, /^HF_TOKEN_PATH=/m)
|
|
30
|
+
})
|
|
31
|
+
|
|
26
32
|
test('Hugging Face connect parses managed hf JSON device login events', () => {
|
|
27
33
|
const provider = new HuggingfaceConnect(createKernel('/tmp/pinokio'), {})
|
|
28
34
|
const parsed = provider.parseDeviceLogin(
|
|
@@ -72,6 +78,72 @@ test('Hugging Face connect uses shared HF_TOKEN_PATH and ignores ambient HF_TOKE
|
|
|
72
78
|
}
|
|
73
79
|
})
|
|
74
80
|
|
|
81
|
+
test('Hugging Face connect verifies status with bounded whoami', async () => {
|
|
82
|
+
const provider = new HuggingfaceConnect(createKernel('/tmp/pinokio'), {})
|
|
83
|
+
let receivedArgs
|
|
84
|
+
let receivedOptions
|
|
85
|
+
provider.runHf = async (args, options) => {
|
|
86
|
+
receivedArgs = args
|
|
87
|
+
receivedOptions = options
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const connected = await provider.connected({ timeout: 2500 })
|
|
91
|
+
|
|
92
|
+
assert.deepEqual(receivedArgs, ['auth', 'whoami', '--format', 'quiet'])
|
|
93
|
+
assert.deepEqual(receivedOptions, { timeout: 2500 })
|
|
94
|
+
assert.equal(connected, true)
|
|
95
|
+
|
|
96
|
+
provider.runHf = async () => {
|
|
97
|
+
throw new Error('not logged in')
|
|
98
|
+
}
|
|
99
|
+
assert.equal(await provider.connected({ timeout: 2500 }), false)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('Hugging Face connect supplies the shared token path at runtime when it is not persisted', async () => {
|
|
103
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-hf-connect-default-'))
|
|
104
|
+
await fs.writeFile(path.join(root, 'ENVIRONMENT'), 'OTHER=value\n')
|
|
105
|
+
try {
|
|
106
|
+
const provider = new HuggingfaceConnect(createKernel(root), {})
|
|
107
|
+
const env = await provider.authEnv()
|
|
108
|
+
|
|
109
|
+
assert.equal(env.HF_TOKEN_PATH, path.join(root, 'cache', 'HF_AUTH', 'token'))
|
|
110
|
+
await assert.doesNotReject(fs.access(path.join(root, 'cache', 'HF_AUTH')))
|
|
111
|
+
await assert.rejects(fs.access(env.HF_TOKEN_PATH), { code: 'ENOENT' })
|
|
112
|
+
} finally {
|
|
113
|
+
await fs.rm(root, { recursive: true, force: true })
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('Hugging Face connect repairs an empty directory at the token file path', async () => {
|
|
118
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-hf-connect-repair-'))
|
|
119
|
+
const tokenPath = path.join(root, 'cache', 'HF_AUTH', 'token')
|
|
120
|
+
await fs.mkdir(tokenPath, { recursive: true })
|
|
121
|
+
try {
|
|
122
|
+
const provider = new HuggingfaceConnect(createKernel(root), {})
|
|
123
|
+
const env = await provider.authEnv()
|
|
124
|
+
|
|
125
|
+
assert.equal(env.HF_TOKEN_PATH, tokenPath)
|
|
126
|
+
await assert.rejects(fs.access(tokenPath), { code: 'ENOENT' })
|
|
127
|
+
} finally {
|
|
128
|
+
await fs.rm(root, { recursive: true, force: true })
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test('Hugging Face connect preserves and rejects a non-empty token directory', async () => {
|
|
133
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-hf-connect-invalid-'))
|
|
134
|
+
const tokenPath = path.join(root, 'cache', 'HF_AUTH', 'token')
|
|
135
|
+
await fs.mkdir(tokenPath, { recursive: true })
|
|
136
|
+
await fs.writeFile(path.join(tokenPath, 'keep.txt'), 'keep')
|
|
137
|
+
try {
|
|
138
|
+
const provider = new HuggingfaceConnect(createKernel(root), {})
|
|
139
|
+
|
|
140
|
+
await assert.rejects(provider.authEnv(), /Hugging Face token path must be a file/)
|
|
141
|
+
assert.equal(await fs.readFile(path.join(tokenPath, 'keep.txt'), 'utf8'), 'keep')
|
|
142
|
+
} finally {
|
|
143
|
+
await fs.rm(root, { recursive: true, force: true })
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
|
|
75
147
|
test('Hugging Face connect cancelLogin only stops a pending login session', async () => {
|
|
76
148
|
const provider = new HuggingfaceConnect(createKernel('/tmp/pinokio'), {})
|
|
77
149
|
let killed = false
|