opencode-llmstack 0.6.0__py3-none-any.whl

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.
@@ -0,0 +1,119 @@
1
+ """Snapshot the currently-configured GGUFs and what's recommended.
2
+
3
+ For every tier in ``models.ini``, prints a row per file (current + upgrade
4
+ target if defined) with:
5
+
6
+ - filename
7
+ - HuggingFace size + last-modified
8
+ - direct URL to the GGUF on HF
9
+ - DRIFT marker when ``models.ini`` and ``llama-swap.yaml`` disagree about
10
+ the currently-configured file
11
+
12
+ Read-only -- no side effects. Invoked by ``llmstack check``.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import re
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ import yaml
22
+ from huggingface_hub import HfApi
23
+
24
+ from llmstack.paths import resolve
25
+ from llmstack.tiers import load_tiers
26
+
27
+ HF_RE = re.compile(r"-hf\s+(\S+/\S+)")
28
+ HFF_RE = re.compile(r"-hff\s+(\S+\.gguf)")
29
+
30
+
31
+ def parse_yaml(yaml_path: Path) -> dict[str, tuple[str, str]]:
32
+ """Map tier-name -> (repo, file) by reading ``llama-swap.yaml``.
33
+
34
+ Strips comment lines from each ``cmd:`` block before regex-matching so
35
+ commented-out ``-hf`` examples don't pollute the result.
36
+ """
37
+ if not yaml_path.exists():
38
+ return {}
39
+ cfg = yaml.safe_load(yaml_path.read_text())
40
+ out: dict[str, tuple[str, str]] = {}
41
+ for tier, m in (cfg.get("models") or {}).items():
42
+ cmd_lines = [
43
+ line for line in (m.get("cmd") or "").splitlines()
44
+ if not line.strip().startswith("#")
45
+ ]
46
+ cmd = "\n".join(cmd_lines)
47
+ repo_m = HF_RE.search(cmd)
48
+ file_m = HFF_RE.search(cmd)
49
+ if repo_m and file_m:
50
+ out[tier] = (repo_m.group(1), file_m.group(1))
51
+ return out
52
+
53
+
54
+ def hf_meta(api: HfApi, repo: str, fname: str) -> tuple[str, str]:
55
+ """Fetch (size_human, last_modified_iso) from HF for a single file."""
56
+ try:
57
+ info = api.model_info(repo, files_metadata=True)
58
+ size = next((s.size for s in info.siblings if s.rfilename == fname), None)
59
+ size_s = f"{size / 1024 / 1024 / 1024:.1f} GB" if size else "?"
60
+ mod = info.last_modified.strftime("%Y-%m-%d") if info.last_modified else "?"
61
+ return size_s, mod
62
+ except Exception as e: # network / 404 / auth - keep going for the next row
63
+ return "ERR", str(e)[:24]
64
+
65
+
66
+ def main(argv: list[str] | None = None) -> int:
67
+ api = HfApi()
68
+ tiers = load_tiers()
69
+ yaml_cfg = parse_yaml(resolve().llama_swap_yaml)
70
+
71
+ fmt = "{:<18} {:<8} {:<70} {:>10} {:>12} {}"
72
+ print(fmt.format("tier", "label", "file / model-id", "size", "updated", "url / region"))
73
+ print("-" * 165)
74
+
75
+ drift = []
76
+ for tier in tiers.values():
77
+ if tier.is_bedrock and tier.bedrock is not None:
78
+ b = tier.bedrock
79
+ scope_parts = [p for p in (b.region, b.profile) if p]
80
+ scope = " / ".join(scope_parts) if scope_parts else "(default chain)"
81
+ print(fmt.format(tier.name, "bedrock", b.model_id, "-", "-", scope))
82
+ if b.has_next:
83
+ next_scope_parts = [p for p in (b.region_next or b.region, b.profile) if p]
84
+ next_scope = " / ".join(next_scope_parts) if next_scope_parts else "(default chain)"
85
+ print(fmt.format(tier.name, "next", b.model_id_next or "", "-", "-", next_scope))
86
+ continue
87
+
88
+ for tf in tier.files():
89
+ size_s, mod = hf_meta(api, tf.repo, tf.file)
90
+ url = f"https://huggingface.co/{tf.repo}/blob/main/{tf.file}"
91
+ label = tf.label
92
+ if tf.label == "current":
93
+ actual = yaml_cfg.get(tier.name)
94
+ if actual and actual != (tf.repo, tf.file):
95
+ label = "DRIFT!"
96
+ drift.append((tier.name, (tf.repo, tf.file), actual))
97
+ print(fmt.format(tier.name, label, tf.file, size_s, mod, url))
98
+
99
+ if drift:
100
+ print()
101
+ print("[!] DRIFT detected between models.ini (recommended) and llama-swap.yaml (active):")
102
+ for tier, want, got in drift:
103
+ print(f" [{tier}] ini wants {want[0]} / {want[1]}")
104
+ print(f" yaml has {got[0]} / {got[1]}")
105
+ print(" Reconcile by editing one of the two so they match.")
106
+
107
+ print()
108
+ print("To look for upgrades, browse:")
109
+ print(" https://huggingface.co/models?library=gguf&sort=trending")
110
+ print(" https://huggingface.co/bartowski (general GGUF maintainer)")
111
+ print(" https://huggingface.co/unsloth (Qwen + UD dynamic quants)")
112
+ print(" https://huggingface.co/mradermacher (i1 + abliterated/heretic)")
113
+ print()
114
+ print("Then: see UPGRADING.md for the workflow.")
115
+ return 0
116
+
117
+
118
+ if __name__ == "__main__":
119
+ sys.exit(main(sys.argv[1:]))
llmstack/cli.py ADDED
@@ -0,0 +1,264 @@
1
+ """``llmstack`` console-script entry point.
2
+
3
+ This is the Python replacement for ``llmstack.sh``. It does only one
4
+ thing: parse the action word, look up the matching ``commands.<action>``
5
+ module, and call its ``run(args)`` function. Every action implements its
6
+ own flag parsing so help text and error messages stay close to the
7
+ behaviour they describe.
8
+
9
+ llmstack <action> [args...]
10
+
11
+ For machine readers / shell completions, ``llmstack help`` prints the
12
+ full action table; ``llmstack <action> -h`` prints per-action help.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import sys
18
+ from collections.abc import Callable
19
+
20
+ from llmstack import __version__
21
+
22
+ USAGE = """\
23
+ llmstack - multi-tier local LLM stack (llama-swap + auto-router + opencode wiring)
24
+
25
+ Does NOT touch ~/.config/opencode/opencode.json. Instead, the generated
26
+ opencode config lives at <work-dir>/.llmstack/opencode.json, and the
27
+ activate hook (`llmstack activate <shell>`) auto-exports OPENCODE_CONFIG
28
+ whenever you cd into a project that has a `.llmstack/`. Inside that
29
+ hooked shell, `opencode` picks up our config; in any other terminal,
30
+ opencode keeps using your global setup unchanged.
31
+
32
+ Usage:
33
+ llmstack <action> [options]
34
+
35
+ Actions:
36
+ setup [--skip-download] [--skip-wait]
37
+ First-time walkthrough: kick off GGUF downloads, wait for them, install
38
+ the llama-swap binary, print the shell activation hook, check opencode.
39
+
40
+ install [--print] [--current | --next | --external [URL]]
41
+ Regenerate .llmstack/opencode.json (+ AGENTS.md copy) and pin
42
+ the default channel for the next `start`. The source of tier
43
+ config depends on channel:
44
+
45
+ --current / --next (local)
46
+ Read <work-dir>/.llmstack/models.ini, seeding it from the
47
+ bundled template on first run. llama-swap.yaml is NOT
48
+ touched -- `start` owns that and regenerates it for the
49
+ chosen channel on each launch.
50
+
51
+ --external [URL] (thin client)
52
+ Fetch models.ini live from the router (`GET URL/models.ini`)
53
+ and render opencode.json against that -- no local
54
+ models.ini is created or kept. Re-run `install` any time to
55
+ pick up router-side edits. URL precedence:
56
+ flag arg > $LLMSTACK_REMOTE_URL > the local router
57
+ (http://127.0.0.1:10101).
58
+ The localhost default is what makes "two projects, one
59
+ host" work zero-config: install one project local and the
60
+ others --external.
61
+
62
+ $LLMSTACK_REMOTE_URL set without --external still implies
63
+ --external (back-compat). The activate hook re-exports this
64
+ var when you `cd` into an external project, so re-running
65
+ `install` from inside an active shell never needs the URL or
66
+ the flag again.
67
+
68
+ `--print` writes the rendered opencode.json to stdout instead
69
+ of files (still fetches the remote in external mode).
70
+
71
+ install-llama-swap [--force]
72
+ (Re-)download the llama-swap Go binary into $LLMSTACK_BIN_DIR (default
73
+ $XDG_DATA_HOME/llmstack/bin/). Setup runs this for you.
74
+
75
+ download
76
+ Download every GGUF named in models.ini (current + queued next) to
77
+ the standard llama.cpp cache, in parallel, in the background.
78
+
79
+ start [--current | --next] [--detach]
80
+ Generate .llmstack/llama-swap.yaml for the chosen channel, bring up
81
+ llama-swap (:10102) + auto-router (:10101). Default channel =
82
+ whatever `install` pinned, else `current`. `--next` swaps any tier
83
+ with hf_file_next. The yaml is regenerated on each fresh launch
84
+ so it always matches the live models.ini; if the daemons are
85
+ already up the running yaml is left alone.
86
+
87
+ Subshell behaviour: if LLMSTACK_ACTIVE is already set (i.e. the
88
+ activate hook has wired this shell up) `start` just brings up
89
+ daemons and returns. Only when the env is not set does `start`
90
+ drop you into a subshell with OPENCODE_CONFIG exported -- as a
91
+ fallback for users who haven't run the activate hook yet.
92
+ `--detach` skips the subshell unconditionally.
93
+
94
+ When the project is installed with channel=external (see
95
+ `install --external`), no daemons are launched: this just
96
+ verifies the pinned remote `GET /models.ini` (which doubles as
97
+ the router's health check -- there's no separate /health route).
98
+
99
+ activate <zsh|bash|powershell>
100
+ Write the auto-activation hook to ~/.<shell>_llmstack_hook and
101
+ print a `source` line to stdout, so
102
+
103
+ eval "$(llmstack activate zsh)"
104
+
105
+ both regenerates the file and turns the hook on in the current
106
+ shell. Paste the same line into your shell rc to make it stick:
107
+ # ~/.zshrc
108
+ eval "$(llmstack activate zsh)"
109
+ The hook walks up from $PWD on every prompt, finds the nearest
110
+ .llmstack/opencode.json, and exports OPENCODE_CONFIG +
111
+ LLMSTACK_WORK_DIR + LLMSTACK_CHANNEL accordingly. Walks back out
112
+ when you cd away. There is no separate `shell` action -- this is
113
+ the shell action.
114
+
115
+ stop
116
+ Stop the router + llama-swap (and any orphaned llama-server children).
117
+
118
+ restart [--current | --next] [--detach]
119
+ stop + start. Convenient for cycling channels.
120
+
121
+ reload
122
+ Emit shell commands that re-export LLMSTACK_CHANNEL +
123
+ OPENCODE_CONFIG and re-render the [llmstack:<project>] prompt
124
+ prefix for the current channel marker. Pipe through eval to
125
+ apply in-place (no nested subshell):
126
+ eval "$(llmstack reload)"
127
+ Useful after `start --next` switches channels in an
128
+ already-active shell -- the activate hook only refreshes on
129
+ chpwd, so without this the prompt would lag until your next cd.
130
+
131
+ status
132
+ Show channel, pids, /v1/models, loaded llama-server processes.
133
+
134
+ check [args]
135
+ Snapshot configured GGUFs + flag drift between models.ini and
136
+ llama-swap.yaml.
137
+
138
+ help | -h | --help
139
+ This message.
140
+
141
+ version | --version
142
+ Print the package version and exit.
143
+
144
+ Environment overrides:
145
+ LLMSTACK_REMOTE_URL base URL of a *remote* llmstack router (e.g.
146
+ `http://10.0.0.5:10101`). Picked up by
147
+ `install` as an alternative to passing
148
+ `--external <url>`; once `install` runs, the
149
+ channel + URL are persisted in
150
+ .llmstack/default-channel and that file is
151
+ the source of truth (the env var is only
152
+ re-exported by the activate hook for
153
+ downstream callers).
154
+ LLMSTACK_MODELS_INI path to models.ini (default:
155
+ <work-dir>/.llmstack/models.ini).
156
+ LLMSTACK_WORK_DIR where .llmstack/ + logs/ live (default: $PWD
157
+ when invoked). Auto-exported by the activate
158
+ hook (`llmstack activate <shell>`) and by the
159
+ subshell `start` spawns, set to the project
160
+ root -- so commands work from any subdirectory
161
+ of an installed project. Without the hook,
162
+ run from the project root (or set this var).
163
+ Local daemons are singleton (ports 10101/10102);
164
+ to consume them from a second project on the
165
+ same host, install that project --external.
166
+ LLMSTACK_DATA_DIR persistent user-data root (default:
167
+ $XDG_DATA_HOME/llmstack). Where the binary lives.
168
+ LLMSTACK_BIN_DIR override just the binary location.
169
+ OPENCODE_CONFIG_DIR where to write opencode.json (default: .llmstack/).
170
+ LLAMA_SWAP_VERSION pin a specific llama-swap release (e.g. v211).
171
+ HF_TOKEN authenticate model downloads (faster rate limits).
172
+ LLMSTACK_SHELL shell to spawn from `start` when no active env
173
+ is detected (default: $SHELL).
174
+
175
+ Channel labels (LLMSTACK_CHANNEL):
176
+ current local stack, canonical channel (steel-blue prompt prefix)
177
+ next local stack, queued-upgrade channel (orange prompt prefix)
178
+ external thin client of an llmstack router (medium-purple prompt
179
+ prefix; the URL is shown alongside the project name in the
180
+ prompt: `[llmstack:<project> <url>]`). The URL is pinned at
181
+ install time -- typically a remote host, but defaults to
182
+ the local router so two projects on one host can share a
183
+ single set of daemons cleanly.
184
+
185
+ Channel markers on disk (.llmstack/active-channel, .llmstack/default-channel):
186
+ one line, format `<channel>[ <url>]`. The URL is only present for
187
+ channel=external; the activate hook re-exports it as
188
+ LLMSTACK_REMOTE_URL when you cd into the project, so you don't have to
189
+ put the URL in your shell rc.
190
+
191
+ Variables exported by the activate hook (and the start fallback subshell):
192
+ OPENCODE_CONFIG path to the generated .llmstack/opencode.json
193
+ LLMSTACK_WORK_DIR absolute path to the project root (auto-detected
194
+ by walking up from $PWD looking for .llmstack/)
195
+ LLMSTACK_CHANNEL current | next | external
196
+ LLMSTACK_ACTIVE "1" while the env is wired up
197
+ LLMSTACK_REMOTE_URL set when channel == external, from the marker file
198
+ LLMSTACK_ROOT absolute path to the llmstack package (start only)
199
+ """
200
+
201
+
202
+ def _print_help() -> None:
203
+ sys.stdout.write(USAGE)
204
+
205
+
206
+ def _print_version() -> None:
207
+ print(f"llmstack {__version__}")
208
+
209
+
210
+ def _load_action(action: str) -> Callable[[list[str]], int]:
211
+ """Resolve ``action`` to a ``run(args)`` callable, lazy-importing the module."""
212
+ aliases = {
213
+ "download-models": "download",
214
+ "check-models": "check",
215
+ }
216
+ name = aliases.get(action, action)
217
+ name = name.replace("-", "_")
218
+ target = f"llmstack.commands.{name}"
219
+
220
+ from importlib import import_module
221
+
222
+ try:
223
+ module = import_module(target)
224
+ except ModuleNotFoundError as e:
225
+ # Only swallow the error when the *action module itself* is missing.
226
+ # Transitive ImportErrors (e.g. an uninstalled third-party dep) must
227
+ # surface, otherwise we mislead the user with "unknown action".
228
+ if e.name == target:
229
+ raise SystemExit(f"[!] unknown action: {action}\n\nrun: llmstack help") from None
230
+ raise SystemExit(
231
+ f"[!] action '{action}' failed to load: missing dependency '{e.name}'\n"
232
+ f" hint: pip install -e . (or pipx install .) to install llmstack's deps"
233
+ ) from e
234
+
235
+ run = getattr(module, "run", None)
236
+ if not callable(run):
237
+ raise SystemExit(f"[!] action '{action}' is missing run() -- bug in llmstack")
238
+ return run
239
+
240
+
241
+ def main(argv: list[str] | None = None) -> int:
242
+ args = sys.argv[1:] if argv is None else list(argv)
243
+
244
+ if not args or args[0] in ("help", "-h", "--help"):
245
+ _print_help()
246
+ return 0
247
+ if args[0] in ("version", "-V", "--version"):
248
+ _print_version()
249
+ return 0
250
+
251
+ action, rest = args[0], args[1:]
252
+ run = _load_action(action)
253
+ try:
254
+ rc = run(rest)
255
+ except SystemExit:
256
+ raise
257
+ except KeyboardInterrupt:
258
+ print("\n[!] interrupted", file=sys.stderr)
259
+ return 130
260
+ return rc if isinstance(rc, int) else 0
261
+
262
+
263
+ if __name__ == "__main__":
264
+ sys.exit(main())
@@ -0,0 +1,10 @@
1
+ """One module per CLI action.
2
+
3
+ Each module exports a single ``run(args: list[str]) -> int`` callable
4
+ that the dispatcher in :mod:`llmstack.cli` invokes after stripping the
5
+ action name. ``args`` is the rest of ``sys.argv``; modules do their own
6
+ argparse / manual parsing -- the commands are small enough that one
7
+ shared parser would be more friction than it's worth.
8
+ """
9
+
10
+ from __future__ import annotations
@@ -0,0 +1,91 @@
1
+ """Shared helpers for the start/stop/status commands.
2
+
3
+ Just process lifecycle plumbing -- pid files, port probes, daemon
4
+ spawning, kill-by-pattern. Kept separate from the command modules so the
5
+ control-flow per command stays readable. The actual platform-specific
6
+ process bits (POSIX signals vs Windows ``taskkill`` etc.) live in
7
+ :mod:`llmstack._platform` so this module stays portable.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import urllib.error
13
+ import urllib.request
14
+ from pathlib import Path
15
+
16
+ from llmstack._platform import (
17
+ describe_matching,
18
+ detached_popen,
19
+ find_pids,
20
+ kill_matching,
21
+ pid_alive,
22
+ terminate_pid,
23
+ )
24
+
25
+
26
+ def is_running(pid_file: Path) -> bool:
27
+ """``True`` iff ``pid_file`` exists and points at a live process."""
28
+ if not pid_file.is_file():
29
+ return False
30
+ try:
31
+ pid = int(pid_file.read_text().strip())
32
+ except (ValueError, OSError):
33
+ return False
34
+ if pid <= 0:
35
+ return False
36
+ return pid_alive(pid)
37
+
38
+
39
+ def read_pid(pid_file: Path) -> int | None:
40
+ if not pid_file.is_file():
41
+ return None
42
+ try:
43
+ return int(pid_file.read_text().strip())
44
+ except (ValueError, OSError):
45
+ return None
46
+
47
+
48
+ def port_responds(url: str, *, timeout: float = 2.0) -> bool:
49
+ """Probe ``url`` for any 2xx response. Used to detect external daemons."""
50
+ try:
51
+ with urllib.request.urlopen(url, timeout=timeout) as resp:
52
+ return 200 <= resp.status < 300
53
+ except (urllib.error.URLError, ConnectionError, TimeoutError, OSError):
54
+ return False
55
+
56
+
57
+ def spawn_daemon(
58
+ argv: list[str],
59
+ *,
60
+ log: Path,
61
+ pid_file: Path,
62
+ env: dict[str, str] | None = None,
63
+ ) -> int:
64
+ """Spawn ``argv`` detached, redirect stdio to ``log``, write the pid."""
65
+ log.parent.mkdir(parents=True, exist_ok=True)
66
+ pid_file.parent.mkdir(parents=True, exist_ok=True)
67
+ fp = log.open("ab")
68
+ proc = detached_popen(argv, stdout=fp, stderr=fp, env=env)
69
+ fp.close()
70
+ pid_file.write_text(f"{proc.pid}\n")
71
+ return proc.pid
72
+
73
+
74
+ def kill_pid(pid: int, *, grace: float = 5.0) -> None:
75
+ """SIGTERM (or taskkill), wait up to ``grace`` seconds, then hard-kill."""
76
+ terminate_pid(pid, grace=grace)
77
+
78
+
79
+ def pgrep(pattern: str) -> list[int]:
80
+ """Return PIDs whose full command-line matches ``pattern``."""
81
+ return find_pids(pattern)
82
+
83
+
84
+ def pkill(pattern: str, *, grace: float = 5.0) -> int:
85
+ """Terminate every process matching ``pattern``."""
86
+ return kill_matching(pattern, grace=grace)
87
+
88
+
89
+ def pgrep_describe(pattern: str) -> str:
90
+ """``pgrep -af``-style multi-line summary (empty when nothing matches)."""
91
+ return describe_matching(pattern)
@@ -0,0 +1,71 @@
1
+ """``llmstack activate <shell>`` -- install + source the auto-activation hook.
2
+
3
+ Writes the hook to ``~/.<shell>_llmstack_hook`` and prints the matching
4
+ ``source`` line to **stdout** so a one-shot
5
+
6
+ eval "$(llmstack activate zsh)"
7
+
8
+ both regenerates the file and turns on the hook in the current shell.
9
+ Pasting the same line into your shell rc keeps it on for every new
10
+ shell. All informational output goes to stderr so it doesn't get
11
+ captured by ``eval``.
12
+
13
+ Once the hook is installed, ``cd`` into any project with ``.llmstack/``
14
+ and the env (``OPENCODE_CONFIG``, ``LLMSTACK_WORK_DIR``,
15
+ ``LLMSTACK_CHANNEL``) is set up automatically -- there is no separate
16
+ ``llmstack shell`` action.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ from llmstack.shell_env import activate_hook
25
+
26
+
27
+ def _print_help() -> None:
28
+ print("usage: llmstack activate <zsh|bash|powershell>", file=sys.stderr)
29
+
30
+
31
+ def _hook_path(shell: str) -> Path:
32
+ """``~/.<shell>_llmstack_hook`` -- ``pwsh`` is normalised to ``powershell``
33
+ so the user doesn't end up with two redundant files."""
34
+ name = "powershell" if shell in ("powershell", "pwsh") else shell
35
+ return Path.home() / f".{name}_llmstack_hook"
36
+
37
+
38
+ def _source_line(shell: str, path: Path) -> str:
39
+ """Shell-specific incantation to load the hook file."""
40
+ if shell in ("powershell", "pwsh"):
41
+ return f". '{path}'"
42
+ return f'source "{path}"'
43
+
44
+
45
+ def write_hook(shell: str) -> tuple[Path, str]:
46
+ """Render the hook for ``shell``, write it to disk, return ``(path, source_line)``.
47
+
48
+ Shared by ``llmstack activate`` (CLI surface) and ``llmstack setup``
49
+ (first-run walkthrough) so they install the hook the same way.
50
+ """
51
+ body = activate_hook(shell) # raises SystemExit on unknown shell
52
+ path = _hook_path(shell)
53
+ path.write_text(body)
54
+ return path, _source_line(shell, path)
55
+
56
+
57
+ def run(args: list[str]) -> int:
58
+ if not args or args[0] in ("-h", "--help"):
59
+ _print_help()
60
+ return 0
61
+ shell = args[0]
62
+
63
+ path, src = write_hook(shell)
64
+
65
+ eval_line = f'eval "$(llmstack activate {shell})"'
66
+ print(f"[OK] hook written: {path}", file=sys.stderr)
67
+ print( " activate in this shell now (and for every new shell:", file=sys.stderr)
68
+ print(f" paste into your rc): {eval_line}", file=sys.stderr)
69
+
70
+ print(src)
71
+ return 0
@@ -0,0 +1,13 @@
1
+ """``llmstack check`` -- snapshot configured GGUFs + flag drift.
2
+
3
+ Thin wrapper around :mod:`llmstack.check_models` so the action stays in
4
+ the standard commands/ tree rather than special-cased in the dispatcher.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from llmstack import check_models
10
+
11
+
12
+ def run(args: list[str]) -> int:
13
+ return check_models.main(args)
@@ -0,0 +1,27 @@
1
+ """``llmstack download`` -- queue every GGUF in models.ini in the background."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from llmstack.download.ggufs import download_all
6
+ from llmstack.paths import is_remote, remote_url
7
+
8
+
9
+ def _print_help() -> None:
10
+ print("usage: llmstack download")
11
+
12
+
13
+ def run(args: list[str]) -> int:
14
+ for arg in args:
15
+ if arg in ("-h", "--help"):
16
+ _print_help()
17
+ return 0
18
+ print(f"[!] unknown arg to download: {arg}")
19
+ return 2
20
+
21
+ if is_remote():
22
+ print(f"[!] this project is wired as a thin client of {remote_url()} (channel: external);")
23
+ print(" GGUFs live on the remote. `llmstack download` is a local-only command.")
24
+ return 1
25
+
26
+ download_all()
27
+ return 0