ChatHFD 0.1.2__tar.gz → 0.1.3__tar.gz

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.
chathfd-0.1.3/PKG-INFO ADDED
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: ChatHFD
3
+ Version: 0.1.3
4
+ Summary: ChatHFD: ChatArch Hugging Face Download tooling package
5
+ Author-email: ChatArch <1073853456@qq.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/ChatArch/ChatHFD
8
+ Project-URL: Repository, https://github.com/ChatArch/ChatHFD
9
+ Project-URL: Documentation, https://arch.gh.wzhecnu.cn/ChatHFD/
10
+ Keywords: chathfd,chatarch,cli
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Operating System :: OS Independent
13
+ Requires-Python: >=3.10
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: click>=8.0
17
+ Requires-Dist: chatstyle<0.3.0,>=0.2.0
18
+ Requires-Dist: chatenv<0.3.0,>=0.2.10
19
+ Provides-Extra: dev
20
+ Requires-Dist: build; extra == "dev"
21
+ Requires-Dist: pytest; extra == "dev"
22
+ Requires-Dist: twine; extra == "dev"
23
+ Provides-Extra: docs
24
+ Requires-Dist: mkdocs<2.0,>=1.6; extra == "docs"
25
+ Requires-Dist: mkdocs-material<10.0,>=9.5; extra == "docs"
26
+ Requires-Dist: mkdocs-static-i18n<2.0,>=1.2; extra == "docs"
27
+ Requires-Dist: mike<3.0,>=2.0; extra == "docs"
28
+ Dynamic: license-file
29
+
30
+ <div align="center">
31
+ <a href="https://pypi.python.org/pypi/ChatHFD"><img src="https://img.shields.io/pypi/v/ChatHFD.svg" alt="PyPI version" /></a>
32
+ <a href="https://github.com/ChatArch/ChatHFD/actions/workflows/ci.yml"><img src="https://github.com/ChatArch/ChatHFD/actions/workflows/ci.yml/badge.svg" alt="Tests" /></a>
33
+ </div>
34
+
35
+ <div align="center">[英文版](README.en.md) | [简体中文](README.md)</div>
36
+
37
+ # ChatHFD
38
+
39
+ ChatHFD 是 ChatArch 的 Hugging Face 模型与数据集下载工具。要求 Python 3.10+ 和 `aria2c`;只支持公开仓库,不在命令行传递 Token。
40
+
41
+ ## 下载示例
42
+
43
+ ```bash
44
+ pip install ChatHFD==0.1.3
45
+ chathfd --tree
46
+ chathfd setup # 检查 aria2c;未安装时提示命令
47
+ # 如确需安装:chathfd setup --install(还需确认)
48
+ # 先预检最新 Qwen 通用开放权重系列(此仓库约 360 GB,不要直接全量下载)
49
+ chathfd download Qwen/Qwen3.8-Flash-Next --include config.json --dry-run
50
+ chathfd download Qwen/Qwen3.8-Flash-Next --include config.json --local-dir ./models/qwen-sample
51
+ # 数学数据集:约 246 KB,包含 README、元数据和一个 Parquet 分片
52
+ chathfd download MathArena/arxivmath-0926 --repo-type dataset --dry-run
53
+ chathfd download MathArena/arxivmath-0926 --repo-type dataset --local-dir ./datasets/arxivmath-0926
54
+ ```
55
+
56
+ 默认端点为 `https://hf-mirror.com`;`HF_ENDPOINT` 环境变量或 `--endpoint` 可覆盖为其它 Hugging Face 兼容 HTTPS 端点(如 `https://huggingface.co`)。使用 `--include` / `--exclude`(可重复)筛选仓库路径,`--revision` 固定版本,`--jobs`(默认 5)控制并发文件数,`--connections`(默认 4)控制每文件连接数。下载通过 aria2 续传,完成后只核对文件大小,不做加密哈希校验。若已有文件大小不匹配且无 aria2 续传状态,命令会报错而不会删除它。
57
+
58
+ 默认最多选择 10 GiB 文件;如确需更大的全量权重,先看 `--dry-run` 的大小与磁盘空间,再明确使用 `--allow-large`。元数据响应的 commit SHA 会固定到下载 URL,目标目录也记录仓库、commit 和选中文件集合;再次运行同一集合可续传,分支更新、选中文件集合改变或目标目录已有未知文件时应另选目录。10 GiB 是元数据预检,不是服务器返回字节数的硬上限。`--dry-run` 不创建目标文件,即使超过 10 GiB 仍可预览。默认目录为 `./models/<org>/<repo>` 或 `./datasets/<org>/<repo>`。
59
+
60
+ ModelScope 是独立仓库平台,不是 HF-Mirror 的 `HF_ENDPOINT` 替代地址;仅当该模型/数据集也在 ModelScope 发布时,另用其 SDK,例如 `modelscope download --model Qwen/Qwen2.5-0.5B-Instruct --local_dir ./model`。不能假定 HF 仓库 ID 在 ModelScope 同步存在,也不能把其 SDK 当作本命令的自动回退。
61
+
62
+ 参考:[HF-Mirror 下载说明](https://hf-mirror.com/) · [ModelScope CLI](https://www.modelscope.cn/docs/sdk/cli) · [Qwen 模型](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) · [MathArena 数据集](https://huggingface.co/datasets/MathArena/arxivmath-0926)。
63
+
64
+ ## 当前 CLI 树(`--tree`)
65
+
66
+ ```text
67
+ chathfd
68
+ ├── --help # Show this message and exit.
69
+ ├── --version # Show the version and exit.
70
+ ├── --tree # Print the registered CLI tree and exit.
71
+ ├── --tree-brief # Print the registered CLI tree without parameter signatures and exit.
72
+ ├── download <REPO-ID> [--repo-type REPO-TYPE] [--revision REVISION] [--endpoint ENDPOINT] [--local-dir LOCAL-DIR] [--include INCLUDE] [--exclude EXCLUDE] [--connections CONNECTIONS] [--jobs JOBS] [--dry-run] [--allow-large] # Download a public model or dataset with aria2c.
73
+ └── setup [--install] # Check the aria2c download engine and offer installation.
74
+ ```
75
+
76
+ `--tree-brief` 省略子命令参数签名。`src/` 是源码;`tests/cli-tests/` 是真实 CLI 测试;`tests/mock-cli-tests/` 是模拟测试;`docs/` 是长期文档。扩展前请阅读 `DEVELOP.md` 和 `AGENTS.md`。
@@ -0,0 +1,47 @@
1
+ <div align="center">
2
+ <a href="https://pypi.python.org/pypi/ChatHFD"><img src="https://img.shields.io/pypi/v/ChatHFD.svg" alt="PyPI version" /></a>
3
+ <a href="https://github.com/ChatArch/ChatHFD/actions/workflows/ci.yml"><img src="https://github.com/ChatArch/ChatHFD/actions/workflows/ci.yml/badge.svg" alt="Tests" /></a>
4
+ </div>
5
+
6
+ <div align="center">[英文版](README.en.md) | [简体中文](README.md)</div>
7
+
8
+ # ChatHFD
9
+
10
+ ChatHFD 是 ChatArch 的 Hugging Face 模型与数据集下载工具。要求 Python 3.10+ 和 `aria2c`;只支持公开仓库,不在命令行传递 Token。
11
+
12
+ ## 下载示例
13
+
14
+ ```bash
15
+ pip install ChatHFD==0.1.3
16
+ chathfd --tree
17
+ chathfd setup # 检查 aria2c;未安装时提示命令
18
+ # 如确需安装:chathfd setup --install(还需确认)
19
+ # 先预检最新 Qwen 通用开放权重系列(此仓库约 360 GB,不要直接全量下载)
20
+ chathfd download Qwen/Qwen3.8-Flash-Next --include config.json --dry-run
21
+ chathfd download Qwen/Qwen3.8-Flash-Next --include config.json --local-dir ./models/qwen-sample
22
+ # 数学数据集:约 246 KB,包含 README、元数据和一个 Parquet 分片
23
+ chathfd download MathArena/arxivmath-0926 --repo-type dataset --dry-run
24
+ chathfd download MathArena/arxivmath-0926 --repo-type dataset --local-dir ./datasets/arxivmath-0926
25
+ ```
26
+
27
+ 默认端点为 `https://hf-mirror.com`;`HF_ENDPOINT` 环境变量或 `--endpoint` 可覆盖为其它 Hugging Face 兼容 HTTPS 端点(如 `https://huggingface.co`)。使用 `--include` / `--exclude`(可重复)筛选仓库路径,`--revision` 固定版本,`--jobs`(默认 5)控制并发文件数,`--connections`(默认 4)控制每文件连接数。下载通过 aria2 续传,完成后只核对文件大小,不做加密哈希校验。若已有文件大小不匹配且无 aria2 续传状态,命令会报错而不会删除它。
28
+
29
+ 默认最多选择 10 GiB 文件;如确需更大的全量权重,先看 `--dry-run` 的大小与磁盘空间,再明确使用 `--allow-large`。元数据响应的 commit SHA 会固定到下载 URL,目标目录也记录仓库、commit 和选中文件集合;再次运行同一集合可续传,分支更新、选中文件集合改变或目标目录已有未知文件时应另选目录。10 GiB 是元数据预检,不是服务器返回字节数的硬上限。`--dry-run` 不创建目标文件,即使超过 10 GiB 仍可预览。默认目录为 `./models/<org>/<repo>` 或 `./datasets/<org>/<repo>`。
30
+
31
+ ModelScope 是独立仓库平台,不是 HF-Mirror 的 `HF_ENDPOINT` 替代地址;仅当该模型/数据集也在 ModelScope 发布时,另用其 SDK,例如 `modelscope download --model Qwen/Qwen2.5-0.5B-Instruct --local_dir ./model`。不能假定 HF 仓库 ID 在 ModelScope 同步存在,也不能把其 SDK 当作本命令的自动回退。
32
+
33
+ 参考:[HF-Mirror 下载说明](https://hf-mirror.com/) · [ModelScope CLI](https://www.modelscope.cn/docs/sdk/cli) · [Qwen 模型](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) · [MathArena 数据集](https://huggingface.co/datasets/MathArena/arxivmath-0926)。
34
+
35
+ ## 当前 CLI 树(`--tree`)
36
+
37
+ ```text
38
+ chathfd
39
+ ├── --help # Show this message and exit.
40
+ ├── --version # Show the version and exit.
41
+ ├── --tree # Print the registered CLI tree and exit.
42
+ ├── --tree-brief # Print the registered CLI tree without parameter signatures and exit.
43
+ ├── download <REPO-ID> [--repo-type REPO-TYPE] [--revision REVISION] [--endpoint ENDPOINT] [--local-dir LOCAL-DIR] [--include INCLUDE] [--exclude EXCLUDE] [--connections CONNECTIONS] [--jobs JOBS] [--dry-run] [--allow-large] # Download a public model or dataset with aria2c.
44
+ └── setup [--install] # Check the aria2c download engine and offer installation.
45
+ ```
46
+
47
+ `--tree-brief` 省略子命令参数签名。`src/` 是源码;`tests/cli-tests/` 是真实 CLI 测试;`tests/mock-cli-tests/` 是模拟测试;`docs/` 是长期文档。扩展前请阅读 `DEVELOP.md` 和 `AGENTS.md`。
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: ChatHFD
3
+ Version: 0.1.3
4
+ Summary: ChatHFD: ChatArch Hugging Face Download tooling package
5
+ Author-email: ChatArch <1073853456@qq.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/ChatArch/ChatHFD
8
+ Project-URL: Repository, https://github.com/ChatArch/ChatHFD
9
+ Project-URL: Documentation, https://arch.gh.wzhecnu.cn/ChatHFD/
10
+ Keywords: chathfd,chatarch,cli
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Operating System :: OS Independent
13
+ Requires-Python: >=3.10
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: click>=8.0
17
+ Requires-Dist: chatstyle<0.3.0,>=0.2.0
18
+ Requires-Dist: chatenv<0.3.0,>=0.2.10
19
+ Provides-Extra: dev
20
+ Requires-Dist: build; extra == "dev"
21
+ Requires-Dist: pytest; extra == "dev"
22
+ Requires-Dist: twine; extra == "dev"
23
+ Provides-Extra: docs
24
+ Requires-Dist: mkdocs<2.0,>=1.6; extra == "docs"
25
+ Requires-Dist: mkdocs-material<10.0,>=9.5; extra == "docs"
26
+ Requires-Dist: mkdocs-static-i18n<2.0,>=1.2; extra == "docs"
27
+ Requires-Dist: mike<3.0,>=2.0; extra == "docs"
28
+ Dynamic: license-file
29
+
30
+ <div align="center">
31
+ <a href="https://pypi.python.org/pypi/ChatHFD"><img src="https://img.shields.io/pypi/v/ChatHFD.svg" alt="PyPI version" /></a>
32
+ <a href="https://github.com/ChatArch/ChatHFD/actions/workflows/ci.yml"><img src="https://github.com/ChatArch/ChatHFD/actions/workflows/ci.yml/badge.svg" alt="Tests" /></a>
33
+ </div>
34
+
35
+ <div align="center">[英文版](README.en.md) | [简体中文](README.md)</div>
36
+
37
+ # ChatHFD
38
+
39
+ ChatHFD 是 ChatArch 的 Hugging Face 模型与数据集下载工具。要求 Python 3.10+ 和 `aria2c`;只支持公开仓库,不在命令行传递 Token。
40
+
41
+ ## 下载示例
42
+
43
+ ```bash
44
+ pip install ChatHFD==0.1.3
45
+ chathfd --tree
46
+ chathfd setup # 检查 aria2c;未安装时提示命令
47
+ # 如确需安装:chathfd setup --install(还需确认)
48
+ # 先预检最新 Qwen 通用开放权重系列(此仓库约 360 GB,不要直接全量下载)
49
+ chathfd download Qwen/Qwen3.8-Flash-Next --include config.json --dry-run
50
+ chathfd download Qwen/Qwen3.8-Flash-Next --include config.json --local-dir ./models/qwen-sample
51
+ # 数学数据集:约 246 KB,包含 README、元数据和一个 Parquet 分片
52
+ chathfd download MathArena/arxivmath-0926 --repo-type dataset --dry-run
53
+ chathfd download MathArena/arxivmath-0926 --repo-type dataset --local-dir ./datasets/arxivmath-0926
54
+ ```
55
+
56
+ 默认端点为 `https://hf-mirror.com`;`HF_ENDPOINT` 环境变量或 `--endpoint` 可覆盖为其它 Hugging Face 兼容 HTTPS 端点(如 `https://huggingface.co`)。使用 `--include` / `--exclude`(可重复)筛选仓库路径,`--revision` 固定版本,`--jobs`(默认 5)控制并发文件数,`--connections`(默认 4)控制每文件连接数。下载通过 aria2 续传,完成后只核对文件大小,不做加密哈希校验。若已有文件大小不匹配且无 aria2 续传状态,命令会报错而不会删除它。
57
+
58
+ 默认最多选择 10 GiB 文件;如确需更大的全量权重,先看 `--dry-run` 的大小与磁盘空间,再明确使用 `--allow-large`。元数据响应的 commit SHA 会固定到下载 URL,目标目录也记录仓库、commit 和选中文件集合;再次运行同一集合可续传,分支更新、选中文件集合改变或目标目录已有未知文件时应另选目录。10 GiB 是元数据预检,不是服务器返回字节数的硬上限。`--dry-run` 不创建目标文件,即使超过 10 GiB 仍可预览。默认目录为 `./models/<org>/<repo>` 或 `./datasets/<org>/<repo>`。
59
+
60
+ ModelScope 是独立仓库平台,不是 HF-Mirror 的 `HF_ENDPOINT` 替代地址;仅当该模型/数据集也在 ModelScope 发布时,另用其 SDK,例如 `modelscope download --model Qwen/Qwen2.5-0.5B-Instruct --local_dir ./model`。不能假定 HF 仓库 ID 在 ModelScope 同步存在,也不能把其 SDK 当作本命令的自动回退。
61
+
62
+ 参考:[HF-Mirror 下载说明](https://hf-mirror.com/) · [ModelScope CLI](https://www.modelscope.cn/docs/sdk/cli) · [Qwen 模型](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) · [MathArena 数据集](https://huggingface.co/datasets/MathArena/arxivmath-0926)。
63
+
64
+ ## 当前 CLI 树(`--tree`)
65
+
66
+ ```text
67
+ chathfd
68
+ ├── --help # Show this message and exit.
69
+ ├── --version # Show the version and exit.
70
+ ├── --tree # Print the registered CLI tree and exit.
71
+ ├── --tree-brief # Print the registered CLI tree without parameter signatures and exit.
72
+ ├── download <REPO-ID> [--repo-type REPO-TYPE] [--revision REVISION] [--endpoint ENDPOINT] [--local-dir LOCAL-DIR] [--include INCLUDE] [--exclude EXCLUDE] [--connections CONNECTIONS] [--jobs JOBS] [--dry-run] [--allow-large] # Download a public model or dataset with aria2c.
73
+ └── setup [--install] # Check the aria2c download engine and offer installation.
74
+ ```
75
+
76
+ `--tree-brief` 省略子命令参数签名。`src/` 是源码;`tests/cli-tests/` 是真实 CLI 测试;`tests/mock-cli-tests/` 是模拟测试;`docs/` 是长期文档。扩展前请阅读 `DEVELOP.md` 和 `AGENTS.md`。
@@ -10,6 +10,8 @@ src/ChatHFD.egg-info/top_level.txt
10
10
  src/chathfd/__init__.py
11
11
  src/chathfd/cli.py
12
12
  src/chathfd/config.py
13
+ src/chathfd/download.py
14
+ src/chathfd/setup.py
13
15
  tests/test_cli.py
14
16
  tests/test_version.py
15
17
  tests/test_workflow_contract.py
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.1.2"
5
+ __version__ = "0.1.3"
@@ -0,0 +1,58 @@
1
+ """CLI entrypoint for chathfd."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+ import click
9
+ from chatstyle import add_tree_option
10
+
11
+ from chathfd import __version__
12
+
13
+
14
+ @click.group(invoke_without_command=True)
15
+ @click.version_option(__version__, prog_name="chathfd")
16
+ @add_tree_option(renderer_options={"root_name": "chathfd"})
17
+ @click.pass_context
18
+ def main(ctx: click.Context) -> None:
19
+ """chathfd command line interface."""
20
+ if ctx.invoked_subcommand is None:
21
+ # Show help on bare invocation while registered commands stay discoverable.
22
+ click.echo(ctx.get_help())
23
+
24
+
25
+ @main.command()
26
+ @click.argument("repo_id")
27
+ @click.option("--repo-type", type=click.Choice(["model", "dataset"]), default="model", show_default=True)
28
+ @click.option("--revision", default="main", show_default=True, help="Branch, tag or commit.")
29
+ @click.option("--endpoint", default=lambda: os.environ.get("HF_ENDPOINT") or "https://hf-mirror.com", show_default="HF_ENDPOINT or https://hf-mirror.com", help="Hugging Face-compatible HTTPS origin.")
30
+ @click.option("--local-dir", type=click.Path(path_type=Path, file_okay=False), help="Destination (default: ./models or ./datasets / repo ID).")
31
+ @click.option("--include", multiple=True, help="Glob pattern to include (repeatable).")
32
+ @click.option("--exclude", multiple=True, help="Glob pattern to exclude (repeatable).")
33
+ @click.option("--connections", type=click.IntRange(1, 10), default=4, show_default=True, help="Connections per file for aria2c.")
34
+ @click.option("--jobs", type=click.IntRange(1, 10), default=5, show_default=True, help="Concurrent files for aria2c.")
35
+ @click.option("--dry-run", is_flag=True, help="List files and sizes without downloading.")
36
+ @click.option("--allow-large", is_flag=True, help="Allow a selection above 10 GiB.")
37
+ def download(
38
+ repo_id: str, repo_type: str, revision: str, endpoint: str, local_dir: Path | None,
39
+ include: tuple[str, ...], exclude: tuple[str, ...], connections: int, jobs: int,
40
+ dry_run: bool, allow_large: bool,
41
+ ) -> None:
42
+ """Download a public model or dataset with aria2c."""
43
+ from chathfd.download import download_repo
44
+
45
+ download_repo(repo_id, repo_type, revision, endpoint, local_dir, include, exclude, connections, jobs, dry_run, allow_large)
46
+
47
+
48
+ @main.command()
49
+ @click.option("--install", is_flag=True, help="Install missing aria2c after confirmation.")
50
+ def setup(install: bool) -> None:
51
+ """Check the aria2c download engine and offer installation."""
52
+ from chathfd.setup import setup_aria2
53
+
54
+ setup_aria2(install)
55
+
56
+
57
+ if __name__ == "__main__":
58
+ main()
@@ -0,0 +1,199 @@
1
+ """Bounded, resumable public Hub downloads through aria2c."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import fnmatch
6
+ import json
7
+ import re
8
+ import shutil
9
+ import subprocess
10
+ from pathlib import Path, PurePosixPath
11
+ from urllib.error import HTTPError, URLError
12
+ from urllib.parse import quote, urlsplit
13
+ from urllib.request import urlopen
14
+
15
+ import click
16
+
17
+ DEFAULT_ENDPOINT = "https://hf-mirror.com"
18
+ DEFAULT_MAX_BYTES = 10 * 1024**3
19
+ _ID_PART = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
20
+ _REV_PART = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
21
+
22
+
23
+ def _parts(value: str, pattern: re.Pattern[str], label: str) -> list[str]:
24
+ parts = value.split("/")
25
+ if not all(part not in (".", "..") and pattern.fullmatch(part) for part in parts):
26
+ raise click.ClickException(f"Invalid {label}: {value!r}")
27
+ return parts
28
+
29
+
30
+ def _endpoint(value: str) -> str:
31
+ parsed = urlsplit(value)
32
+ if parsed.scheme != "https" or not parsed.hostname or parsed.username or parsed.password or parsed.query or parsed.fragment or parsed.path not in ("", "/"):
33
+ raise click.ClickException("Endpoint must be an HTTPS origin without credentials, path or query")
34
+ return value.rstrip("/")
35
+
36
+
37
+ def fetch_metadata(endpoint: str, repo_id: str, repo_type: str, revision: str) -> dict:
38
+ prefix = "models" if repo_type == "model" else "datasets"
39
+ segments = ["api", prefix, *_parts(repo_id, _ID_PART, "repository ID")]
40
+ if revision != "main":
41
+ segments += ["revision", *_parts(revision, _REV_PART, "revision")]
42
+ url = endpoint + "/" + "/".join(quote(part, safe="") for part in segments) + "?blobs=true"
43
+ try:
44
+ with urlopen(url, timeout=30) as response:
45
+ payload = response.read(4 * 1024 * 1024 + 1)
46
+ except (HTTPError, URLError, TimeoutError) as exc:
47
+ # Do not include the exception's URL or headers in output.
48
+ raise click.ClickException(f"Failed to fetch {repo_type} metadata from {endpoint}: {type(exc).__name__}") from exc
49
+ if len(payload) > 4 * 1024 * 1024:
50
+ raise click.ClickException("Repository metadata exceeds 4 MiB; refusing incomplete preflight")
51
+ try:
52
+ data = json.loads(payload)
53
+ except (ValueError, UnicodeDecodeError) as exc:
54
+ raise click.ClickException("Invalid repository metadata response") from exc
55
+ if not isinstance(data, dict):
56
+ raise click.ClickException("Unexpected repository metadata response")
57
+ return data
58
+
59
+
60
+ def _selected_files(metadata: dict, includes: tuple[str, ...], excludes: tuple[str, ...]) -> list[tuple[str, int]]:
61
+ if metadata.get("gated") or metadata.get("private"):
62
+ raise click.ClickException("Gated/private repositories are not supported; no token is forwarded")
63
+ siblings = metadata.get("siblings")
64
+ if not isinstance(siblings, list):
65
+ raise click.ClickException("Repository metadata has no file list")
66
+ files = []
67
+ for entry in siblings:
68
+ name = entry.get("rfilename") if isinstance(entry, dict) else None
69
+ if not isinstance(name, str):
70
+ raise click.ClickException("Repository metadata contains an invalid filename")
71
+ path = PurePosixPath(name)
72
+ if path.is_absolute() or "\\" in name or any(ord(char) < 32 or ord(char) == 127 for char in name) or any(part in ("", ".", "..") for part in name.split("/")):
73
+ raise click.ClickException(f"Unsafe repository path: {name!r}")
74
+ if includes and not any(fnmatch.fnmatchcase(name, p) for p in includes):
75
+ continue
76
+ if any(fnmatch.fnmatchcase(name, p) for p in excludes):
77
+ continue
78
+ if path.parts[0] == ".chathfd":
79
+ raise click.ClickException("Repository file conflicts with the reserved .chathfd directory")
80
+ size = entry.get("size")
81
+ if not isinstance(size, int) or isinstance(size, bool) or size < 0:
82
+ raise click.ClickException(f"Missing file size for {name!r}; cannot preflight")
83
+ files.append((name, size))
84
+ if not files:
85
+ raise click.ClickException("No files match the include/exclude filters")
86
+ names = {name for name, _ in files}
87
+ for name in names:
88
+ if name + ".aria2" in names or any(str(parent) in names for parent in PurePosixPath(name).parents if str(parent) != "."):
89
+ raise click.ClickException(f"Repository file paths conflict with each other: {name!r}")
90
+ return files
91
+
92
+
93
+ def _check_paths(root: Path, files: list[tuple[str, int]]) -> None:
94
+ base = root.resolve()
95
+ if any(ord(char) < 32 or ord(char) == 127 for char in str(base)):
96
+ raise click.ClickException("Destination contains control characters unsafe for aria2c")
97
+ for name, size in files:
98
+ ancestor = root
99
+ for part in name.split("/"):
100
+ ancestor = ancestor / part
101
+ if ancestor.is_symlink():
102
+ raise click.ClickException(f"Destination contains a symlink: {name!r}")
103
+ target = root.joinpath(*name.split("/"))
104
+ if Path(str(target) + ".aria2").is_symlink():
105
+ raise click.ClickException(f"Destination contains a symlinked aria2 state file: {name!r}")
106
+ if not target.resolve().is_relative_to(base):
107
+ raise click.ClickException(f"File path escapes destination: {name!r}")
108
+ if target.exists() and target.is_file() and target.stat().st_size != size and not Path(str(target) + ".aria2").exists():
109
+ raise click.ClickException(f"Existing file has wrong size: {name!r}; move it aside before retrying")
110
+ if target.exists() and not target.is_file():
111
+ raise click.ClickException(f"Destination is not a regular file: {name!r}")
112
+ control = root / ".chathfd"
113
+ if not control.resolve().is_relative_to(base) or control.is_symlink() or (control / "urls.txt").is_symlink() or (control / "selection.json").is_symlink():
114
+ raise click.ClickException("Unsafe downloader control path")
115
+
116
+
117
+ def _check_selection(root: Path, files: list[tuple[str, int]], identity: dict) -> None:
118
+ control = root / ".chathfd"
119
+ receipt = control / "selection.json"
120
+ if receipt.is_file():
121
+ try:
122
+ previous = json.loads(receipt.read_text(encoding="utf-8"))
123
+ except (OSError, ValueError, UnicodeDecodeError) as exc:
124
+ raise click.ClickException("Unreadable download selection; choose a fresh destination") from exc
125
+ if previous != identity:
126
+ raise click.ClickException("Destination belongs to another repository, endpoint, commit or file selection; choose a fresh destination")
127
+ elif control.exists() or any(
128
+ target.exists() or Path(str(target) + ".aria2").exists()
129
+ for name, _ in files
130
+ for target in (root.joinpath(*name.split("/")),)
131
+ ):
132
+ raise click.ClickException("Existing files have no matching download selection; choose a fresh destination")
133
+
134
+
135
+ def download_repo(
136
+ repo_id: str, repo_type: str, revision: str, endpoint: str, local_dir: Path | None,
137
+ includes: tuple[str, ...], excludes: tuple[str, ...], connections: int, jobs: int,
138
+ dry_run: bool, allow_large: bool,
139
+ ) -> None:
140
+ repo_parts = _parts(repo_id, _ID_PART, "repository ID")
141
+ _parts(revision, _REV_PART, "revision")
142
+ endpoint = _endpoint(endpoint)
143
+ root = local_dir or Path.cwd().joinpath(repo_type + "s", *repo_parts)
144
+ metadata = fetch_metadata(endpoint, repo_id, repo_type, revision)
145
+ files = _selected_files(metadata, includes, excludes)
146
+ commit = metadata.get("sha")
147
+ if not isinstance(commit, str) or not re.fullmatch(r"[0-9a-f]{40}", commit):
148
+ raise click.ClickException("Repository metadata has no valid commit SHA; refusing unpinned download")
149
+ _check_paths(root, files)
150
+ total = sum(size for _, size in files)
151
+ click.echo(f"{repo_type} {repo_id}@{revision} ({commit}): {len(files)} files, {total} bytes -> {root} (endpoint {endpoint})")
152
+ for name, size in files:
153
+ click.echo(f" {name}: {size} bytes")
154
+ if dry_run:
155
+ if total > DEFAULT_MAX_BYTES and not allow_large:
156
+ click.echo("Selection exceeds 10 GiB; a real download requires --allow-large or narrower --include")
157
+ click.echo("Dry run: no files downloaded")
158
+ return
159
+ if total > DEFAULT_MAX_BYTES and not allow_large:
160
+ raise click.ClickException("Selected files exceed 10 GiB; narrow --include or pass --allow-large explicitly")
161
+ identity = {
162
+ "version": 1, "endpoint": endpoint, "repo_id": repo_id,
163
+ "repo_type": repo_type, "commit": commit,
164
+ "files": sorted([name, size] for name, size in files),
165
+ }
166
+ _check_selection(root, files, identity)
167
+ if not shutil.which("aria2c"):
168
+ raise click.ClickException("aria2c is required; run chathfd setup for instructions or chathfd setup --install")
169
+ control = root / ".chathfd"
170
+ control.mkdir(parents=True, exist_ok=True)
171
+ (control / "selection.json").write_text(json.dumps(identity, sort_keys=True) + "\n", encoding="utf-8")
172
+ urls = control / "urls.txt"
173
+ prefix = ["datasets"] if repo_type == "dataset" else []
174
+ url_prefix = endpoint + "/" + "/".join(quote(part, safe="") for part in [*prefix, *repo_parts, "resolve", commit])
175
+ pending = []
176
+ for name, size in files:
177
+ target = root.joinpath(*name.split("/"))
178
+ if target.is_file() and target.stat().st_size == size and not Path(str(target) + ".aria2").exists():
179
+ continue
180
+ target.parent.mkdir(parents=True, exist_ok=True)
181
+ url = url_prefix + "/" + "/".join(quote(part, safe="") for part in name.split("/"))
182
+ pending.extend([url, f" dir={target.parent.resolve()}", f" out={target.name}"])
183
+ urls.write_text("\n".join(pending) + ("\n" if pending else ""), encoding="utf-8")
184
+ if pending:
185
+ command = [
186
+ "aria2c", "--no-conf=true", "--no-netrc=true", f"--input-file={urls}",
187
+ "--continue=true", "--auto-file-renaming=false",
188
+ f"--max-connection-per-server={connections}", f"--split={connections}",
189
+ f"--max-concurrent-downloads={jobs}", "--file-allocation=none",
190
+ ]
191
+ try:
192
+ subprocess.run(command, check=True)
193
+ except (OSError, subprocess.CalledProcessError) as exc:
194
+ raise click.ClickException(f"aria2c failed ({type(exc).__name__}); re-run to resume") from exc
195
+ for name, size in files:
196
+ target = root.joinpath(*name.split("/"))
197
+ if not target.is_file() or target.stat().st_size != size or Path(str(target) + ".aria2").exists():
198
+ raise click.ClickException(f"Download incomplete or size mismatch: {name!r}")
199
+ click.echo(f"Verified {len(files)} files by size (not cryptographic hashes)")
@@ -0,0 +1,50 @@
1
+ """Guided installation for the external aria2 download engine."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+
9
+ import click
10
+ from chatstyle import ask_confirm
11
+
12
+
13
+ def _install_command() -> list[str] | None:
14
+ if shutil.which("brew"):
15
+ return ["brew", "install", "aria2"]
16
+ for manager, args in (
17
+ ("apt-get", ["install", "-y", "aria2"]),
18
+ ("dnf", ["install", "-y", "aria2"]),
19
+ ("pacman", ["-S", "--noconfirm", "aria2"]),
20
+ ):
21
+ if shutil.which(manager):
22
+ command = [manager, *args]
23
+ return command if os.geteuid() == 0 else ["sudo", "-n", *command]
24
+ return None
25
+
26
+
27
+ def setup_aria2(install: bool) -> None:
28
+ path = shutil.which("aria2c")
29
+ if path:
30
+ click.echo(f"aria2c ready: {path}")
31
+ return
32
+ click.echo("aria2c is not installed; ChatHFD requires it for concurrent downloads.")
33
+ command = _install_command()
34
+ if not command:
35
+ raise click.ClickException("No supported package manager found; install aria2 via your OS package manager")
36
+ manual = " ".join(part for part in command if part != "-n")
37
+ if not install:
38
+ click.echo(f"Suggested command: {manual}")
39
+ click.echo("Run chathfd setup --install to attempt installation with confirmation.")
40
+ return
41
+ if not ask_confirm(f"Run {manual}? This changes system packages", default=False):
42
+ click.echo("Installation cancelled")
43
+ return
44
+ try:
45
+ subprocess.run(command, check=True)
46
+ except (OSError, subprocess.CalledProcessError) as exc:
47
+ raise click.ClickException(f"Installation failed ({type(exc).__name__}); run {manual} interactively if authorization is needed") from exc
48
+ if not shutil.which("aria2c"):
49
+ raise click.ClickException("Installer completed but aria2c is not on PATH; check your shell environment")
50
+ click.echo(f"aria2c ready: {shutil.which('aria2c')}")
@@ -5,13 +5,15 @@ from chathfd import __version__
5
5
  from chathfd.cli import main
6
6
 
7
7
 
8
- EXPECTED_ROOT_TREE = (
8
+ TREE_ROOT = (
9
9
  "chathfd\n"
10
10
  "├── --help # Show this message and exit.\n"
11
11
  "├── --version # Show the version and exit.\n"
12
12
  "├── --tree # Print the registered CLI tree and exit.\n"
13
- "└── --tree-brief # Print the registered CLI tree without parameter signatures and exit.\n"
13
+ "├── --tree-brief # Print the registered CLI tree without parameter signatures and exit.\n"
14
14
  )
15
+ DETAILED_TREE = TREE_ROOT + "├── download <REPO-ID> [--repo-type REPO-TYPE] [--revision REVISION] [--endpoint ENDPOINT] [--local-dir LOCAL-DIR] [--include INCLUDE] [--exclude EXCLUDE] [--connections CONNECTIONS] [--jobs JOBS] [--dry-run] [--allow-large] # Download a public model or dataset with aria2c.\n└── setup [--install] # Check the aria2c download engine and offer installation.\n"
16
+ BRIEF_TREE = TREE_ROOT + "├── download # Download a public model or dataset with aria2c.\n└── setup # Check the aria2c download engine and offer installation.\n"
15
17
 
16
18
 
17
19
  def test_help_mentions_tree_options():
@@ -29,37 +31,37 @@ def test_version_option_reports_package_version():
29
31
  assert f"chathfd, version {__version__}" in result.output
30
32
 
31
33
 
32
- def test_tree_reports_registered_root_only_surface():
34
+ def test_tree_reports_registered_download_surface():
33
35
  result = CliRunner().invoke(main, ["--tree"])
34
36
 
35
37
  assert result.exit_code == 0
36
- assert result.output == EXPECTED_ROOT_TREE
38
+ assert result.output == DETAILED_TREE
37
39
 
38
40
 
39
- def test_tree_brief_reports_registered_root_only_surface():
41
+ def test_tree_brief_reports_registered_download_surface():
40
42
  result = CliRunner().invoke(main, ["--tree-brief"])
41
43
 
42
44
  assert result.exit_code == 0
43
- assert result.output == EXPECTED_ROOT_TREE
45
+ assert result.output == BRIEF_TREE
44
46
 
45
47
 
46
48
  def test_tree_modes_include_and_omit_command_parameter_signatures():
47
- @click.command("download")
49
+ @click.command("example")
48
50
  @click.argument("repo_id")
49
51
  @click.option("--revision", help="Repository revision.")
50
- def download(repo_id: str, revision: str | None) -> None:
52
+ def example(repo_id: str, revision: str | None) -> None:
51
53
  """Download a repository."""
52
54
 
53
- main.add_command(download)
55
+ main.add_command(example)
54
56
  try:
55
57
  detailed = CliRunner().invoke(main, ["--tree"])
56
58
  brief = CliRunner().invoke(main, ["--tree-brief"])
57
59
  finally:
58
- main.commands.pop("download")
60
+ main.commands.pop("example")
59
61
 
60
62
  assert detailed.exit_code == 0
61
- assert "download <REPO-ID> [--revision REVISION] # Download a repository." in detailed.output
63
+ assert "example <REPO-ID> [--revision REVISION] # Download a repository." in detailed.output
62
64
  assert brief.exit_code == 0
63
- assert "download # Download a repository." in brief.output
65
+ assert "example # Download a repository." in brief.output
64
66
  assert "<REPO-ID>" not in brief.output
65
67
  assert "[--revision REVISION]" not in brief.output
@@ -2,4 +2,4 @@ from chathfd import __version__
2
2
 
3
3
 
4
4
  def test_version_present():
5
- assert __version__ == "0.1.2"
5
+ assert __version__ == "0.1.3"
chathfd-0.1.2/PKG-INFO DELETED
@@ -1,88 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: ChatHFD
3
- Version: 0.1.2
4
- Summary: ChatHFD: ChatArch Hugging Face Download tooling package
5
- Author-email: ChatArch <1073853456@qq.com>
6
- License-Expression: MIT
7
- Project-URL: Homepage, https://github.com/ChatArch/ChatHFD
8
- Project-URL: Repository, https://github.com/ChatArch/ChatHFD
9
- Project-URL: Documentation, https://arch.gh.wzhecnu.cn/ChatHFD/
10
- Keywords: chathfd,chatarch,cli
11
- Classifier: Programming Language :: Python :: 3
12
- Classifier: Operating System :: OS Independent
13
- Requires-Python: >=3.10
14
- Description-Content-Type: text/markdown
15
- License-File: LICENSE
16
- Requires-Dist: click>=8.0
17
- Requires-Dist: chatstyle<0.3.0,>=0.2.0
18
- Requires-Dist: chatenv<0.3.0,>=0.2.10
19
- Provides-Extra: dev
20
- Requires-Dist: build; extra == "dev"
21
- Requires-Dist: pytest; extra == "dev"
22
- Requires-Dist: twine; extra == "dev"
23
- Provides-Extra: docs
24
- Requires-Dist: mkdocs<2.0,>=1.6; extra == "docs"
25
- Requires-Dist: mkdocs-material<10.0,>=9.5; extra == "docs"
26
- Requires-Dist: mkdocs-static-i18n<2.0,>=1.2; extra == "docs"
27
- Requires-Dist: mike<3.0,>=2.0; extra == "docs"
28
- Dynamic: license-file
29
-
30
- <div align="center">
31
- <a href="https://pypi.python.org/pypi/ChatHFD">
32
- <img src="https://img.shields.io/pypi/v/ChatHFD.svg" alt="PyPI version" />
33
- </a>
34
- <a href="https://github.com/ChatArch/ChatHFD/actions/workflows/ci.yml">
35
- <img src="https://github.com/ChatArch/ChatHFD/actions/workflows/ci.yml/badge.svg" alt="Tests" />
36
- </a>
37
- <a href="https://arch.gh.wzhecnu.cn/ChatHFD/">
38
- <img src="https://img.shields.io/badge/docs-mkdocs-blue.svg" alt="Documentation" />
39
- </a>
40
- </div>
41
-
42
- <div align="center">
43
-
44
- [英文版](README.en.md) | [简体中文](README.md)
45
- </div>
46
-
47
- # ChatHFD
48
-
49
- ChatHFD 是 ChatArch 的 Hugging Face Download(HFD)工具包入口。当前包保持最小 root-only CLI,用于保留可安装、可发现、可发布的工具壳;实际下载编排能力尚未暴露为子命令。
50
-
51
- ## 快速开始
52
-
53
- ```bash
54
- pip install ChatHFD
55
- chathfd --help
56
- chathfd --version
57
- chathfd --tree
58
- chathfd --tree-brief
59
- ```
60
-
61
- ## 当前 CLI 树(`--tree`)
62
-
63
- ```text
64
- chathfd
65
- ├── --help # Show this message and exit.
66
- ├── --version # Show the version and exit.
67
- ├── --tree # Print the registered CLI tree and exit.
68
- └── --tree-brief # Print the registered CLI tree without parameter signatures and exit.
69
- ```
70
-
71
- ## CLI 边界
72
-
73
- - 当前 CLI 只有根选项,没有业务子命令。
74
- - `--tree` 和 `--tree-brief` 由 ChatStyle 共享 Click tree runtime 从实际注册面生成,公开根名固定为 `chathfd`。
75
- - `--tree` 默认保留命令参数签名;`--tree-brief` 省略参数签名,但保留命令节点和描述。当前没有参数化子命令,因此两种模式显示同一组 root-only 节点。
76
- - 后续新增真实 HFD 下载、镜像、缓存或校验命令时,必须先更新 Click 注册面,再用真实 `chathfd --tree` 和 `chathfd --tree-brief` 同步文档。
77
-
78
- ## 目录结构
79
-
80
- - `src/`:包源码
81
- - `tests/code-tests/`:代码测试和历史测试迁移
82
- - `tests/cli-tests/`:真实 CLI 测试,doc-first
83
- - `tests/mock-cli-tests/`:mock/fake CLI 测试,doc-first
84
- - `docs/`:长期维护文档,由 MkDocs 构建
85
-
86
- ## 开发说明
87
-
88
- 扩展脚手架前,先阅读 `DEVELOP.md` 和 `AGENTS.md`。
chathfd-0.1.2/README.md DELETED
@@ -1,59 +0,0 @@
1
- <div align="center">
2
- <a href="https://pypi.python.org/pypi/ChatHFD">
3
- <img src="https://img.shields.io/pypi/v/ChatHFD.svg" alt="PyPI version" />
4
- </a>
5
- <a href="https://github.com/ChatArch/ChatHFD/actions/workflows/ci.yml">
6
- <img src="https://github.com/ChatArch/ChatHFD/actions/workflows/ci.yml/badge.svg" alt="Tests" />
7
- </a>
8
- <a href="https://arch.gh.wzhecnu.cn/ChatHFD/">
9
- <img src="https://img.shields.io/badge/docs-mkdocs-blue.svg" alt="Documentation" />
10
- </a>
11
- </div>
12
-
13
- <div align="center">
14
-
15
- [英文版](README.en.md) | [简体中文](README.md)
16
- </div>
17
-
18
- # ChatHFD
19
-
20
- ChatHFD 是 ChatArch 的 Hugging Face Download(HFD)工具包入口。当前包保持最小 root-only CLI,用于保留可安装、可发现、可发布的工具壳;实际下载编排能力尚未暴露为子命令。
21
-
22
- ## 快速开始
23
-
24
- ```bash
25
- pip install ChatHFD
26
- chathfd --help
27
- chathfd --version
28
- chathfd --tree
29
- chathfd --tree-brief
30
- ```
31
-
32
- ## 当前 CLI 树(`--tree`)
33
-
34
- ```text
35
- chathfd
36
- ├── --help # Show this message and exit.
37
- ├── --version # Show the version and exit.
38
- ├── --tree # Print the registered CLI tree and exit.
39
- └── --tree-brief # Print the registered CLI tree without parameter signatures and exit.
40
- ```
41
-
42
- ## CLI 边界
43
-
44
- - 当前 CLI 只有根选项,没有业务子命令。
45
- - `--tree` 和 `--tree-brief` 由 ChatStyle 共享 Click tree runtime 从实际注册面生成,公开根名固定为 `chathfd`。
46
- - `--tree` 默认保留命令参数签名;`--tree-brief` 省略参数签名,但保留命令节点和描述。当前没有参数化子命令,因此两种模式显示同一组 root-only 节点。
47
- - 后续新增真实 HFD 下载、镜像、缓存或校验命令时,必须先更新 Click 注册面,再用真实 `chathfd --tree` 和 `chathfd --tree-brief` 同步文档。
48
-
49
- ## 目录结构
50
-
51
- - `src/`:包源码
52
- - `tests/code-tests/`:代码测试和历史测试迁移
53
- - `tests/cli-tests/`:真实 CLI 测试,doc-first
54
- - `tests/mock-cli-tests/`:mock/fake CLI 测试,doc-first
55
- - `docs/`:长期维护文档,由 MkDocs 构建
56
-
57
- ## 开发说明
58
-
59
- 扩展脚手架前,先阅读 `DEVELOP.md` 和 `AGENTS.md`。
@@ -1,88 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: ChatHFD
3
- Version: 0.1.2
4
- Summary: ChatHFD: ChatArch Hugging Face Download tooling package
5
- Author-email: ChatArch <1073853456@qq.com>
6
- License-Expression: MIT
7
- Project-URL: Homepage, https://github.com/ChatArch/ChatHFD
8
- Project-URL: Repository, https://github.com/ChatArch/ChatHFD
9
- Project-URL: Documentation, https://arch.gh.wzhecnu.cn/ChatHFD/
10
- Keywords: chathfd,chatarch,cli
11
- Classifier: Programming Language :: Python :: 3
12
- Classifier: Operating System :: OS Independent
13
- Requires-Python: >=3.10
14
- Description-Content-Type: text/markdown
15
- License-File: LICENSE
16
- Requires-Dist: click>=8.0
17
- Requires-Dist: chatstyle<0.3.0,>=0.2.0
18
- Requires-Dist: chatenv<0.3.0,>=0.2.10
19
- Provides-Extra: dev
20
- Requires-Dist: build; extra == "dev"
21
- Requires-Dist: pytest; extra == "dev"
22
- Requires-Dist: twine; extra == "dev"
23
- Provides-Extra: docs
24
- Requires-Dist: mkdocs<2.0,>=1.6; extra == "docs"
25
- Requires-Dist: mkdocs-material<10.0,>=9.5; extra == "docs"
26
- Requires-Dist: mkdocs-static-i18n<2.0,>=1.2; extra == "docs"
27
- Requires-Dist: mike<3.0,>=2.0; extra == "docs"
28
- Dynamic: license-file
29
-
30
- <div align="center">
31
- <a href="https://pypi.python.org/pypi/ChatHFD">
32
- <img src="https://img.shields.io/pypi/v/ChatHFD.svg" alt="PyPI version" />
33
- </a>
34
- <a href="https://github.com/ChatArch/ChatHFD/actions/workflows/ci.yml">
35
- <img src="https://github.com/ChatArch/ChatHFD/actions/workflows/ci.yml/badge.svg" alt="Tests" />
36
- </a>
37
- <a href="https://arch.gh.wzhecnu.cn/ChatHFD/">
38
- <img src="https://img.shields.io/badge/docs-mkdocs-blue.svg" alt="Documentation" />
39
- </a>
40
- </div>
41
-
42
- <div align="center">
43
-
44
- [英文版](README.en.md) | [简体中文](README.md)
45
- </div>
46
-
47
- # ChatHFD
48
-
49
- ChatHFD 是 ChatArch 的 Hugging Face Download(HFD)工具包入口。当前包保持最小 root-only CLI,用于保留可安装、可发现、可发布的工具壳;实际下载编排能力尚未暴露为子命令。
50
-
51
- ## 快速开始
52
-
53
- ```bash
54
- pip install ChatHFD
55
- chathfd --help
56
- chathfd --version
57
- chathfd --tree
58
- chathfd --tree-brief
59
- ```
60
-
61
- ## 当前 CLI 树(`--tree`)
62
-
63
- ```text
64
- chathfd
65
- ├── --help # Show this message and exit.
66
- ├── --version # Show the version and exit.
67
- ├── --tree # Print the registered CLI tree and exit.
68
- └── --tree-brief # Print the registered CLI tree without parameter signatures and exit.
69
- ```
70
-
71
- ## CLI 边界
72
-
73
- - 当前 CLI 只有根选项,没有业务子命令。
74
- - `--tree` 和 `--tree-brief` 由 ChatStyle 共享 Click tree runtime 从实际注册面生成,公开根名固定为 `chathfd`。
75
- - `--tree` 默认保留命令参数签名;`--tree-brief` 省略参数签名,但保留命令节点和描述。当前没有参数化子命令,因此两种模式显示同一组 root-only 节点。
76
- - 后续新增真实 HFD 下载、镜像、缓存或校验命令时,必须先更新 Click 注册面,再用真实 `chathfd --tree` 和 `chathfd --tree-brief` 同步文档。
77
-
78
- ## 目录结构
79
-
80
- - `src/`:包源码
81
- - `tests/code-tests/`:代码测试和历史测试迁移
82
- - `tests/cli-tests/`:真实 CLI 测试,doc-first
83
- - `tests/mock-cli-tests/`:mock/fake CLI 测试,doc-first
84
- - `docs/`:长期维护文档,由 MkDocs 构建
85
-
86
- ## 开发说明
87
-
88
- 扩展脚手架前,先阅读 `DEVELOP.md` 和 `AGENTS.md`。
@@ -1,23 +0,0 @@
1
- """CLI entrypoint for chathfd."""
2
-
3
- from __future__ import annotations
4
-
5
- import click
6
- from chatstyle import add_tree_option
7
-
8
- from chathfd import __version__
9
-
10
-
11
- @click.group(invoke_without_command=True)
12
- @click.version_option(__version__, prog_name="chathfd")
13
- @add_tree_option(renderer_options={"root_name": "chathfd"})
14
- @click.pass_context
15
- def main(ctx: click.Context) -> None:
16
- """chathfd command line interface."""
17
- if ctx.invoked_subcommand is None:
18
- # Root-only package for now: keep normal Click help behavior when no command is supplied.
19
- click.echo(ctx.get_help())
20
-
21
-
22
- if __name__ == "__main__":
23
- main()
File without changes
File without changes
File without changes
File without changes