@turmind/halo 2.0.1 → 2.0.3
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/admin-out/404.html +1 -1
- package/admin-out/_next/static/chunks/241-75bac3bb8e7aca1b.js +1 -0
- package/admin-out/_next/static/chunks/989-5c90f88e8111b360.js +1 -0
- package/admin-out/_next/static/chunks/app/_not-found/{page-4271518f2746cf73.js → page-484fd244e4bc7b70.js} +1 -1
- package/admin-out/_next/static/chunks/app/layout-3e437a977abd0081.js +1 -0
- package/admin-out/_next/static/chunks/app/page-faee6fbd17dc7b80.js +1 -0
- package/admin-out/_next/static/chunks/{main-app-340a5b58ed229c4d.js → main-app-42f48cb3fd4811f4.js} +1 -1
- package/admin-out/_next/static/chunks/{webpack-74f577c1b094f942.js → webpack-b7907e0d16faa4ca.js} +1 -1
- package/admin-out/_next/static/css/{e890aa0d13e94290.css → 2e6b00182ac44ae1.css} +1 -1
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +5 -5
- package/bundled-docs/design/a2a.md +1 -1
- package/bundled-docs/design/agentcore.md +385 -34
- package/bundled-docs/design/canvas-extensions.md +76 -29
- package/bundled-docs/design/cron.md +1 -1
- package/bundled-docs/design/observability.md +4 -2
- package/bundled-docs/design/relay.md +2 -2
- package/bundled-docs/design/session.md +1 -1
- package/bundled-docs/design/storage.md +25 -3
- package/bundled-docs/design/stt-local.md +2 -2
- package/bundled-docs/design/ws.md +2 -1
- package/bundled-docs/dev/api.md +93 -13
- package/bundled-docs/dev/deploy.md +6 -0
- package/bundled-docs/dev/env.md +8 -4
- package/bundled-docs/dev/tools.md +1 -1
- package/bundled-docs/guide/cli.md +25 -11
- package/bundled-docs/guide/skills.md +2 -2
- package/bundled-docs/guide/workspace.md +2 -2
- package/bundled-docs/requirements/editor.md +4 -4
- package/bundled-docs/requirements/settings.md +16 -4
- package/dist/index.js +4011 -2135
- package/package.json +2 -1
- package/templates/skills/extension/SKILL.md +21 -16
- package/templates/skills/extension/config.yaml +6 -2
- package/templates/skills/extension/templates/ext.sh +137 -57
- package/admin-out/_next/static/chunks/87-6a2716c98a819c4e.js +0 -1
- package/admin-out/_next/static/chunks/989-cb287787a4aca288.js +0 -1
- package/admin-out/_next/static/chunks/app/layout-ea161287d934ba96.js +0 -1
- package/admin-out/_next/static/chunks/app/page-c9fb252d0cf34c20.js +0 -1
- /package/admin-out/_next/static/{52ayDC9phpvb-95S7qHHq → P7yMYf16Q3-XqGtzCJNnw}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{52ayDC9phpvb-95S7qHHq → P7yMYf16Q3-XqGtzCJNnw}/_ssgManifest.js +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@turmind/halo",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.3",
|
|
4
4
|
"description": "Halo — multi-agent collaborative workspace. CLI + TUI + HTTP/WS server in one package.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"qrcode": "^1.5.4",
|
|
56
56
|
"react": "^19.2.5",
|
|
57
57
|
"react-dom": "^19.2.5",
|
|
58
|
+
"semver": "^7.8.5",
|
|
58
59
|
"sherpa-onnx-node": "1.13.8",
|
|
59
60
|
"simple-git": "^3.27.0",
|
|
60
61
|
"ws": "^8.21.3",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: extension
|
|
3
|
-
description: Install / list / remove admin preview extensions (`~/.halo/global/extensions/<id>/`) — static viewers the admin editor loads for file types it has no built-in preview for (e.g. `.glb`) — and update the model provider list (new models, changed provider endpoints) from the hub. `/extension install <id|zip|url>`
|
|
3
|
+
description: Install / list / remove admin preview extensions (`~/.halo/global/extensions/<id>/`) — static viewers the admin editor loads for file types it has no built-in preview for (e.g. `.glb`) — and update the model provider list (new models, changed provider endpoints) from the hub. `/extension install <id|zip|url>` installs the version the configured hub's `<id>/update.json` names (default hub turmind/halo-hub); `/extension models` pulls the latest `models-v*` provider configs. Activate when the user wants to preview a file type the editor can't open, asks to install / update / remove an extension, or asks to update the model list / get a newly released model ("更新模型列表").
|
|
4
4
|
command: /extension
|
|
5
5
|
requiresAccess: full
|
|
6
6
|
verbs:
|
|
@@ -35,16 +35,17 @@ Pick EXT = the first that exists, then:
|
|
|
35
35
|
When the payload names a source, run the install right away — no confirmation,
|
|
36
36
|
no explanation first. Ask only when no source was given.
|
|
37
37
|
|
|
38
|
-
`HALO_HUB_REPO='{{params.hub_repo}}' bash EXT install <source>`
|
|
39
|
-
`{{params.hub_repo}}` exactly as-is (the system
|
|
40
|
-
`<source>` is one of:
|
|
41
|
-
- an extension id (e.g. `glb`) →
|
|
42
|
-
|
|
43
|
-
when the hub is any other git URL / local repo;
|
|
38
|
+
`HALO_HUB_BASE='{{params.hub_base}}' HALO_HUB_REPO='{{params.hub_repo}}' bash EXT install <source>`
|
|
39
|
+
— keep `{{params.hub_base}}` and `{{params.hub_repo}}` exactly as-is (the system
|
|
40
|
+
substitutes them at runtime), where `<source>` is one of:
|
|
41
|
+
- an extension id (e.g. `glb`) → reads `<hub_base>/glb/update.json` and installs
|
|
42
|
+
the zip it names (https only, checked against its sha256, id and version);
|
|
44
43
|
- a local `.zip` path;
|
|
45
44
|
- an `https://…zip` URL.
|
|
46
45
|
|
|
47
|
-
The hub is set in Settings → Skills → extension
|
|
46
|
+
The hub is set in Settings → Skills → extension: `hub_base` (where the
|
|
47
|
+
`<id>/update.json` files live — a mirror goes here); empty means the GitHub raw
|
|
48
|
+
`extensions/` directory of `hub_repo`, and empty `hub_repo` means
|
|
48
49
|
https://github.com/turmind/halo-hub.
|
|
49
50
|
|
|
50
51
|
The script unpacks to a temp dir, pre-checks `halo-extension.json` (id matches,
|
|
@@ -53,12 +54,11 @@ Reinstalling the same id upgrades (or downgrades) it. Relay the script's last
|
|
|
53
54
|
line to the user (`installed glb 1.0.0` / the error). Never edit files inside an
|
|
54
55
|
installed extension — reinstall instead.
|
|
55
56
|
|
|
56
|
-
On failure relay the script's hint. "
|
|
57
|
-
|
|
58
|
-
`
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
user can download the zip by hand and run `install <path>`.
|
|
57
|
+
On failure relay the script's hint. "no update.json for <id>" → the id isn't on
|
|
58
|
+
that hub, or `hub_base` is wrong. "no hub_base set" → `hub_repo` isn't on GitHub,
|
|
59
|
+
so `hub_base` must be set. "sha256 mismatch" / "does not match update.json" →
|
|
60
|
+
the hub's package doesn't match its update.json; tell the user, don't retry.
|
|
61
|
+
Either way the user can download the zip by hand and run `install <path>`.
|
|
62
62
|
|
|
63
63
|
## list
|
|
64
64
|
|
|
@@ -92,8 +92,10 @@ The script prints one line per provider (`install` / `up-to-date` / `skip` /
|
|
|
92
92
|
`--yes` on your own.
|
|
93
93
|
- anything else — relay the error. Model configs come only from an https
|
|
94
94
|
GitHub / Gitea / Forgejo / GitLab hub with releases; a local / plain-git /
|
|
95
|
-
http hub is refused. "release API failed"
|
|
96
|
-
|
|
95
|
+
http hub is refused. "release API failed" → rate-limited (GitHub: 60
|
|
96
|
+
anonymous requests/hour), private, or a wrong `hub_repo`; setting
|
|
97
|
+
`GITHUB_TOKEN` / `GITLAB_TOKEN` / `GITEA_TOKEN` in the server environment
|
|
98
|
+
helps. "no release for models" → that hub publishes no model configs.
|
|
97
99
|
|
|
98
100
|
`bash EXT models list` shows each provider, which copy is in effect (`bundled`
|
|
99
101
|
= shipped with halo, `hub` = installed by this command) and its revision.
|
|
@@ -104,6 +106,9 @@ The script prints one line per provider (`install` / `up-to-date` / `skip` /
|
|
|
104
106
|
asks "install it for this project".
|
|
105
107
|
- Mention the configured hub (its `extensions/` directory) only when the user
|
|
106
108
|
asks what extensions exist or an install fails; do not scrape it.
|
|
109
|
+
- The admin's Settings → Extensions checks for updates itself (same
|
|
110
|
+
update.json files) and offers a one-click update; point the user there for
|
|
111
|
+
"is there a newer version".
|
|
107
112
|
- The script only pre-checks the required fields; the server's scanner is the
|
|
108
113
|
authority. A package it rejects shows up in `list` (and the admin) as
|
|
109
114
|
`ERROR` with the reason.
|
|
@@ -4,6 +4,10 @@ description_zh: 安装 / 列出 / 删除管理后台的预览扩展。
|
|
|
4
4
|
|
|
5
5
|
params:
|
|
6
6
|
- key: hub_repo
|
|
7
|
-
description: "Hub that `
|
|
8
|
-
description_zh: "`
|
|
7
|
+
description: "Hub that `models` pulls from (newest `models-v*` release zip): an https GitHub / Gitea / Forgejo / GitLab repo URL, or `owner/repo` for GitHub. Also where the default `hub_base` comes from. Empty = https://github.com/turmind/halo-hub."
|
|
8
|
+
description_zh: "`models` 的来源仓库(取最新的 `models-v*` release zip):https 的 GitHub / Gitea / Forgejo / GitLab 仓库地址,GitHub 可简写为 `owner/repo`。`hub_base` 的默认值也由它推出。留空 = https://github.com/turmind/halo-hub。"
|
|
9
9
|
default: https://github.com/turmind/halo-hub
|
|
10
|
+
- key: hub_base
|
|
11
|
+
description: "https base URL holding `<id>/update.json` for every hub extension — where `install <id>` and the admin's update check (extensions without their own `source`) read from. Point it at a mirror (intranet server, S3, CDN). Empty = the GitHub raw `extensions/` directory on `main` of `hub_repo` (https://raw.githubusercontent.com/<owner>/<repo>/main/extensions); a non-GitHub `hub_repo` needs this set."
|
|
12
|
+
description_zh: "存放各个 hub 扩展 `<id>/update.json` 的 https 基础地址,`install <id>` 和后台的检查更新(没写 `source` 的扩展)都从这里读。用镜像(内网服务器、S3、CDN)时改成镜像地址。留空 = `hub_repo` 在 GitHub 上 `main` 分支的 `extensions/` 目录(https://raw.githubusercontent.com/<owner>/<repo>/main/extensions);`hub_repo` 不是 GitHub 时必须填写。"
|
|
13
|
+
default: ""
|
|
@@ -11,12 +11,15 @@
|
|
|
11
11
|
# ext.sh models update [--yes] # hub's newest models-v* zip → halo models install
|
|
12
12
|
# ext.sh models list # = halo models list
|
|
13
13
|
#
|
|
14
|
-
# `install <id>` reads
|
|
15
|
-
# empty →
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
14
|
+
# `install <id>` reads `<hub base>/<id>/update.json` — $HALO_HUB_BASE (the
|
|
15
|
+
# skill's hub_base param; empty → the GitHub raw `extensions/` dir of
|
|
16
|
+
# $HALO_HUB_REPO) — over https only, then the zip it names, checked against
|
|
17
|
+
# its sha256, id and version (same protocol as the admin's update check,
|
|
18
|
+
# server updates/update-json.ts). `models update` reads $HALO_HUB_REPO (the
|
|
19
|
+
# hub_repo param; empty → https://github.com/turmind/halo-hub) through its
|
|
20
|
+
# public release API (GITHUB_TOKEN / GITEA_TOKEN / GITLAB_TOKEN, if set, are
|
|
21
|
+
# sent — lifts GitHub's 60 req/h anonymous limit).
|
|
22
|
+
# Needs curl + unzip + node. No `gh`.
|
|
20
23
|
set -euo pipefail
|
|
21
24
|
|
|
22
25
|
ROOT="$HOME/.halo/global/extensions"
|
|
@@ -47,8 +50,7 @@ precheck() {
|
|
|
47
50
|
|
|
48
51
|
# Normalize a hub setting: empty / unsubstituted `{{…}}` → default,
|
|
49
52
|
# `owner/repo` → GitHub, an existing dir → absolute path, drop trailing `/`.
|
|
50
|
-
# A `.git` suffix stays
|
|
51
|
-
# below drop it.
|
|
53
|
+
# A `.git` suffix stays; the API helpers below drop it.
|
|
52
54
|
hub_url() {
|
|
53
55
|
local h=${1:-}
|
|
54
56
|
if [[ -z $h || $h == '{{'* ]]; then h=$DEFAULT_HUB; fi
|
|
@@ -93,7 +95,7 @@ hub_probe() {
|
|
|
93
95
|
[[ $ct == application/json* ]]
|
|
94
96
|
}
|
|
95
97
|
|
|
96
|
-
# github | gitlab | gitea | git (=
|
|
98
|
+
# github | gitlab | gitea | git (= no release API) for a normalized hub URL.
|
|
97
99
|
hub_platform() {
|
|
98
100
|
if [[ ! $1 =~ ^https?://([^/]+)/[^/]+/.+$ ]]; then echo git; return; fi
|
|
99
101
|
case ${BASH_REMATCH[1]} in
|
|
@@ -128,33 +130,6 @@ hub_pick_release() {
|
|
|
128
130
|
})' "$1" "$2"
|
|
129
131
|
}
|
|
130
132
|
|
|
131
|
-
# stdin = `git ls-remote --tags --refs`; print the highest `<id>-vX.Y.Z` tag
|
|
132
|
-
# (prerelease `-…` suffixes skipped), nothing if there is none.
|
|
133
|
-
hub_pick_tag() {
|
|
134
|
-
local v
|
|
135
|
-
v=$(sed -n "s|.*refs/tags/$1-v\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\)$|\1|p" | sort -t. -k1,1n -k2,2n -k3,3n | tail -1)
|
|
136
|
-
if [[ -n $v ]]; then echo "$1-v$v"; fi
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
# Tag-based hub: shallow-clone the newest `<id>-v<semver>` tag and echo
|
|
140
|
-
# `extensions/<id>/` minus the build inputs scripts/pack.mjs leaves out of a
|
|
141
|
-
# release zip. A package with a build step is refused — its output isn't in git.
|
|
142
|
-
hub_git_package() {
|
|
143
|
-
local hub=$1 id=$2 tmp=$3 url=$1 refs tag dir
|
|
144
|
-
if [[ $url == /* ]]; then url="file://$url"; fi # a plain path makes git ignore --depth
|
|
145
|
-
refs=$(git ls-remote --tags --refs "$url") || { echo "cannot list tags of $hub (git ls-remote failed)" >&2; exit 1; }
|
|
146
|
-
tag=$(printf '%s\n' "$refs" | hub_pick_tag "$id")
|
|
147
|
-
[[ -n $tag ]] || { echo "no release for $id in $hub (no $id-v<x.y.z> tag)" >&2; exit 1; }
|
|
148
|
-
git -c advice.detachedHead=false clone -q --depth 1 --branch "$tag" "$url" "$tmp/repo" || { echo "git clone of $tag from $hub failed" >&2; exit 1; }
|
|
149
|
-
dir="$tmp/repo/extensions/$id"
|
|
150
|
-
[[ -d $dir ]] || { echo "$hub tag $tag has no extensions/$id/" >&2; exit 1; }
|
|
151
|
-
if [[ -e $dir/fetch-deps.sh || -e $dir/build.sh ]]; then
|
|
152
|
-
echo "$id needs a build step — install it from a release zip (install <path/to.zip> or <https://…zip>)" >&2; exit 1
|
|
153
|
-
fi
|
|
154
|
-
rm -rf "$dir/node_modules" "$dir/src" "$dir/package.json" "$dir/package-lock.json"
|
|
155
|
-
echo "$dir"
|
|
156
|
-
}
|
|
157
|
-
|
|
158
133
|
# Download the newest `<id>-v*` release zip of <platform> hub $2 into $4 and
|
|
159
134
|
# echo its path. Extra args ($5…) go to every curl (models pass
|
|
160
135
|
# `--proto =https --proto-redir =https`).
|
|
@@ -174,14 +149,99 @@ hub_fetch_release() {
|
|
|
174
149
|
echo "$tmp/pkg.zip"
|
|
175
150
|
}
|
|
176
151
|
|
|
177
|
-
#
|
|
178
|
-
#
|
|
179
|
-
#
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if [[ $
|
|
184
|
-
|
|
152
|
+
# Hub base for `install <id>` from settings $1 = hub_base, $2 = hub_repo:
|
|
153
|
+
# hub_base as given (trailing `/` dropped); empty / unsubstituted `{{…}}` →
|
|
154
|
+
# the GitHub raw `extensions/` dir on `main` of a GitHub hub_repo (the
|
|
155
|
+
# server's updates/update-json.ts defaultHubBase); nothing for any other hub.
|
|
156
|
+
hub_base() {
|
|
157
|
+
local b=${1:-} h
|
|
158
|
+
if [[ -n $b && $b != '{{'* ]]; then echo "${b%/}"; return; fi
|
|
159
|
+
h=$(hub_url "${2:-}")
|
|
160
|
+
if [[ $h =~ ^https://github\.com/([A-Za-z0-9][A-Za-z0-9_.-]*)/([A-Za-z0-9_.-]+)$ ]]; then
|
|
161
|
+
echo "https://raw.githubusercontent.com/${BASH_REMATCH[1]}/${BASH_REMATCH[2]%.git}/main/extensions"
|
|
162
|
+
fi
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# update.json `minHalo` $3 for <id> $1 <version> $2 vs the installed Halo
|
|
166
|
+
# (`halo --version`, the build sha / `-dirty` dropped — the admin check's rule,
|
|
167
|
+
# server updates/update-json.ts haloBaseVersion). Below it → refuse; no
|
|
168
|
+
# readable version (no halo command, desktop, dev build) → warn and go on.
|
|
169
|
+
hub_check_min_halo() {
|
|
170
|
+
local hv
|
|
171
|
+
hv=$("${HALO_CLI:-halo}" --version 2>/dev/null) || hv=""
|
|
172
|
+
node -e '
|
|
173
|
+
const [id, ver, min, raw] = process.argv.slice(1)
|
|
174
|
+
const base = raw.trim().replace(/^halo\s+/, "").replace(/-dirty$/, "").replace(/-[0-9a-f]{7,40}$/, "")
|
|
175
|
+
const parse = (v) => {
|
|
176
|
+
const m = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(v)
|
|
177
|
+
return m && { core: m.slice(1, 4).map(Number), pre: m[4] ? m[4].split(".") : [] }
|
|
178
|
+
}
|
|
179
|
+
// semver precedence: core numbers, then a prerelease sorts before none.
|
|
180
|
+
const cmp = (a, b) => {
|
|
181
|
+
for (let i = 0; i < 3; i++) if (a.core[i] !== b.core[i]) return a.core[i] - b.core[i]
|
|
182
|
+
if (!a.pre.length || !b.pre.length) return b.pre.length - a.pre.length
|
|
183
|
+
for (let i = 0; i < Math.max(a.pre.length, b.pre.length); i++) {
|
|
184
|
+
const x = a.pre[i], y = b.pre[i]
|
|
185
|
+
if (x === undefined) return -1
|
|
186
|
+
if (y === undefined) return 1
|
|
187
|
+
if (x === y) continue
|
|
188
|
+
const nx = /^\d+$/.test(x), ny = /^\d+$/.test(y)
|
|
189
|
+
if (nx && ny) return Number(x) - Number(y)
|
|
190
|
+
if (nx !== ny) return nx ? -1 : 1
|
|
191
|
+
return x < y ? -1 : 1
|
|
192
|
+
}
|
|
193
|
+
return 0
|
|
194
|
+
}
|
|
195
|
+
const cur = parse(base)
|
|
196
|
+
if (!cur) {
|
|
197
|
+
console.error("warning: cannot read the Halo version (" + (raw.trim() || "no halo command") + ") — not checking that " + id + " " + ver + " needs Halo " + min)
|
|
198
|
+
process.exit(0)
|
|
199
|
+
}
|
|
200
|
+
if (cmp(cur, parse(min)) < 0) {
|
|
201
|
+
console.error(id + " " + ver + " needs Halo " + min + " (this is " + base + ") — upgrade Halo first: halo upgrade && halo server restart")
|
|
202
|
+
process.exit(1)
|
|
203
|
+
}
|
|
204
|
+
' "$1" "$2" "$3" "$hv"
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
# Fetch hub package <id> into $3 by `<hub base $1>/<id>/update.json`: https
|
|
208
|
+
# only (redirects too, ≤ 5), update.json ≤ 64 KB / 10 s, zip ≤ 100 MB (10 min,
|
|
209
|
+
# aborted after 30 s under 1 KB/s); a relative `url` resolves against
|
|
210
|
+
# update.json's final (redirected) URL; `minHalo` is checked against this
|
|
211
|
+
# Halo; the zip must hash to its `sha256`. Echo the zip path; the version it
|
|
212
|
+
# promises goes to $3/want-version for the manifest check after unpacking.
|
|
213
|
+
hub_fetch_update() {
|
|
214
|
+
local base=$1 id=$2 tmp=$3 json final line url sha ver min got
|
|
215
|
+
local https=(--proto =https --proto-redir =https --max-redirs 5)
|
|
216
|
+
[[ -n $base ]] || { echo "no hub_base set and hub_repo is not a GitHub repo to derive one from — set hub_base (Settings → Skills → extension), or install <path/to.zip>" >&2; exit 1; }
|
|
217
|
+
[[ $base == https://* ]] || { echo "hub_base must be an https URL: $base" >&2; exit 1; }
|
|
218
|
+
json="$base/$id/update.json"
|
|
219
|
+
final=$(curl -fsSL "${https[@]}" --max-time 10 --max-filesize 65536 -o "$tmp/update.json" -w '%{url_effective}' "$json") \
|
|
220
|
+
|| { echo "no update.json for $id at $json — check the id and hub_base, or install <path/to.zip>" >&2; exit 1; }
|
|
221
|
+
line=$(node -e '
|
|
222
|
+
const fs = require("fs"), [file, base, id] = process.argv.slice(1)
|
|
223
|
+
const fail = (msg) => { console.log(msg); process.exit(1) }
|
|
224
|
+
let u
|
|
225
|
+
try { u = JSON.parse(fs.readFileSync(file, "utf8")) } catch { fail("not valid JSON") }
|
|
226
|
+
if (!u || typeof u !== "object" || Array.isArray(u)) fail("not a JSON object")
|
|
227
|
+
if (u.id !== id) fail("id " + JSON.stringify(u.id) + " does not match \"" + id + "\"")
|
|
228
|
+
if (typeof u.version !== "string" || !/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(u.version)) fail("version must be semver (x.y.z[-pre])")
|
|
229
|
+
if (typeof u.url !== "string" || u.url === "") fail("url is required")
|
|
230
|
+
let url
|
|
231
|
+
try { url = new URL(u.url, base) } catch { fail("url is invalid: " + u.url) }
|
|
232
|
+
if (url.protocol !== "https:") fail("url must be https: " + url.href)
|
|
233
|
+
if (typeof u.sha256 !== "string" || !/^[0-9a-f]{64}$/i.test(u.sha256)) fail("sha256 must be 64 hex characters")
|
|
234
|
+
if (u.minHalo !== undefined && (typeof u.minHalo !== "string" || !/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(u.minHalo))) fail("minHalo must be semver (x.y.z[-pre])")
|
|
235
|
+
console.log(url.href + " " + u.sha256.toLowerCase() + " " + u.version + " " + (u.minHalo || ""))
|
|
236
|
+
' "$tmp/update.json" "$final" "$id") || { echo "invalid update.json at $final: $line" >&2; exit 1; }
|
|
237
|
+
read -r url sha ver min <<<"$line"
|
|
238
|
+
if [[ -n $min ]]; then hub_check_min_halo "$id" "$ver" "$min" || exit 1; fi
|
|
239
|
+
curl -fsSL "${https[@]}" --max-filesize 104857600 --connect-timeout 10 --max-time 600 --speed-limit 1024 --speed-time 30 "$url" -o "$tmp/pkg.zip" \
|
|
240
|
+
|| { echo "download failed: $url" >&2; exit 1; }
|
|
241
|
+
got=$(node -e 'console.log(require("crypto").createHash("sha256").update(require("fs").readFileSync(process.argv[1])).digest("hex"))' "$tmp/pkg.zip")
|
|
242
|
+
[[ $got == "$sha" ]] || { echo "sha256 mismatch for $url: update.json says $sha, the download is $got" >&2; exit 1; }
|
|
243
|
+
echo "$ver" >"$tmp/want-version"
|
|
244
|
+
echo "$tmp/pkg.zip"
|
|
185
245
|
}
|
|
186
246
|
|
|
187
247
|
# Unpack zip $1 into the new dir $2 with the same refusals as the server's zip
|
|
@@ -194,6 +254,23 @@ safe_unzip() {
|
|
|
194
254
|
unzip -q "$1" -d "$2"
|
|
195
255
|
}
|
|
196
256
|
|
|
257
|
+
# Drop extension $1's on / off state from `$ROOT/.state.json` (the server's
|
|
258
|
+
# extensions/state.ts owns that file; this is its forgetExtension), so the id
|
|
259
|
+
# starts on. Missing or unreadable file = nothing to drop. A failed write only
|
|
260
|
+
# warns: the directory change it follows already happened.
|
|
261
|
+
state_forget() {
|
|
262
|
+
node -e '
|
|
263
|
+
const fs = require("fs"), file = process.argv[1], id = process.argv[2]
|
|
264
|
+
let s
|
|
265
|
+
try { s = JSON.parse(fs.readFileSync(file, "utf8")) } catch { process.exit(0) }
|
|
266
|
+
if (!s || typeof s !== "object" || Array.isArray(s) || !Object.hasOwn(s, id)) process.exit(0)
|
|
267
|
+
delete s[id]
|
|
268
|
+
const tmp = file + "." + process.pid + ".tmp"
|
|
269
|
+
fs.writeFileSync(tmp, JSON.stringify(s, null, 2) + "\n")
|
|
270
|
+
fs.renameSync(tmp, file)
|
|
271
|
+
' "$ROOT/.state.json" "$1" || echo "warning: could not clear the on / off state of $1 in $ROOT/.state.json" >&2
|
|
272
|
+
}
|
|
273
|
+
|
|
197
274
|
# Model provider configs: newest `models-v*` release of hub $1, https +
|
|
198
275
|
# release API only (a provider yaml carries endpoints the server will call —
|
|
199
276
|
# no plain-git / local / http source). Validation, planning and the write into
|
|
@@ -220,13 +297,13 @@ models_update() {
|
|
|
220
297
|
"${HALO_CLI:-halo}" models install "$dir" "$@"
|
|
221
298
|
}
|
|
222
299
|
|
|
223
|
-
# Echo a local zip
|
|
300
|
+
# Echo a local zip path for source $1 (path, https URL, or hub id).
|
|
224
301
|
resolve_source() {
|
|
225
302
|
local src=$1
|
|
226
303
|
if [[ -f $src ]]; then echo "$src"; return; fi
|
|
227
304
|
if [[ $src =~ ^https?:// ]]; then curl -fsSL "$src" -o "$TMP/pkg.zip"; echo "$TMP/pkg.zip"; return; fi
|
|
228
305
|
[[ $src =~ $ID_RE ]] || { echo "invalid id or missing file: $src" >&2; exit 1; }
|
|
229
|
-
|
|
306
|
+
hub_fetch_update "$(hub_base "${HALO_HUB_BASE:-}" "${HALO_HUB_REPO:-}")" "$src" "$TMP"
|
|
230
307
|
}
|
|
231
308
|
|
|
232
309
|
# Sourced (tests, later reuse of the hub_* functions): definitions only.
|
|
@@ -240,26 +317,28 @@ case $cmd in
|
|
|
240
317
|
src=${1:?usage: ext.sh install <id|zip|url>}
|
|
241
318
|
TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
|
|
242
319
|
pkg=$(resolve_source "$src")
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if [[ ! -f $dir/halo-extension.json ]]; then
|
|
250
|
-
only=$(find "$dir" -mindepth 1 -maxdepth 1 | head -2)
|
|
251
|
-
if [[ $(printf '%s\n' "$only" | wc -l) -eq 1 && -f $only/halo-extension.json ]]; then dir=$only; fi
|
|
252
|
-
fi
|
|
320
|
+
safe_unzip "$pkg" "$TMP/x"
|
|
321
|
+
# `zip -r glb.zip glb/` wraps everything in one directory — descend into it.
|
|
322
|
+
dir="$TMP/x"
|
|
323
|
+
if [[ ! -f $dir/halo-extension.json ]]; then
|
|
324
|
+
only=$(find "$dir" -mindepth 1 -maxdepth 1 | head -2)
|
|
325
|
+
if [[ $(printf '%s\n' "$only" | wc -l) -eq 1 && -f $only/halo-extension.json ]]; then dir=$only; fi
|
|
253
326
|
fi
|
|
254
327
|
expect=""
|
|
255
328
|
if [[ $src =~ $ID_RE && ! -f $src ]]; then expect=$src; fi
|
|
256
329
|
if ! out=$(precheck "$dir" "$expect"); then echo "invalid extension: $out" >&2; exit 1; fi
|
|
257
330
|
read -r id ver _ <<<"$out"
|
|
331
|
+
if [[ -f $TMP/want-version ]]; then
|
|
332
|
+
want=$(<"$TMP/want-version")
|
|
333
|
+
[[ $ver == "$want" ]] || { echo "invalid extension: manifest version \"$ver\" does not match update.json \"$want\"" >&2; exit 1; }
|
|
334
|
+
fi
|
|
258
335
|
mkdir -p "$ROOT"
|
|
259
336
|
stage="$ROOT/.tmp-$id-$RANDOM$RANDOM"
|
|
260
337
|
mv "$dir" "$stage"
|
|
261
338
|
old=""
|
|
262
|
-
|
|
339
|
+
# A fresh install (no `<id>/` yet) starts on, whatever a manual `rm -rf`
|
|
340
|
+
# left in the state file; an update keeps the switch.
|
|
341
|
+
if [[ -d $ROOT/$id ]]; then old="$ROOT/.old-$id-$RANDOM$RANDOM"; mv "$ROOT/$id" "$old"; else state_forget "$id"; fi
|
|
263
342
|
mv "$stage" "$ROOT/$id" || { [[ -n $old ]] && mv "$old" "$ROOT/$id"; rm -rf "$stage"; exit 1; }
|
|
264
343
|
if [[ -n $old ]]; then rm -rf "$old"; fi
|
|
265
344
|
echo "installed $id $ver" ;;
|
|
@@ -280,6 +359,7 @@ case $cmd in
|
|
|
280
359
|
[[ $id =~ $ID_RE ]] || { echo "invalid id: $id" >&2; exit 1; }
|
|
281
360
|
[[ -d $ROOT/$id ]] || { echo "not installed: $id" >&2; exit 1; }
|
|
282
361
|
rm -rf "$ROOT/$id"
|
|
362
|
+
state_forget "$id"
|
|
283
363
|
echo "removed $id" ;;
|
|
284
364
|
models)
|
|
285
365
|
sub=${1:-update}
|