chatup 0.2.21__tar.gz → 0.2.22__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.
Files changed (108) hide show
  1. {chatup-0.2.21/src/chatup.egg-info → chatup-0.2.22}/PKG-INFO +7 -2
  2. {chatup-0.2.21 → chatup-0.2.22}/README.md +5 -1
  3. {chatup-0.2.21 → chatup-0.2.22}/pyproject.toml +1 -0
  4. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/__init__.py +1 -1
  5. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/elements.py +41 -0
  6. chatup-0.2.22/src/chatup/setup/glance.py +240 -0
  7. {chatup-0.2.21 → chatup-0.2.22/src/chatup.egg-info}/PKG-INFO +7 -2
  8. {chatup-0.2.21 → chatup-0.2.22}/src/chatup.egg-info/SOURCES.txt +3 -0
  9. {chatup-0.2.21 → chatup-0.2.22}/src/chatup.egg-info/requires.txt +1 -0
  10. chatup-0.2.22/tests/test_glance_bridge.py +6 -0
  11. chatup-0.2.22/tests/test_glance_setup.py +190 -0
  12. {chatup-0.2.21 → chatup-0.2.22}/tests/test_pyproject_metadata.py +1 -0
  13. {chatup-0.2.21 → chatup-0.2.22}/tests/test_setup_cli.py +2 -0
  14. {chatup-0.2.21 → chatup-0.2.22}/tests/test_version.py +1 -1
  15. {chatup-0.2.21 → chatup-0.2.22}/LICENSE +0 -0
  16. {chatup-0.2.21 → chatup-0.2.22}/setup.cfg +0 -0
  17. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/chrome_for_testing.py +0 -0
  18. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/chromedriver.py +0 -0
  19. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/cli.py +0 -0
  20. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/commands/__init__.py +0 -0
  21. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/commands/chrome_for_testing.py +0 -0
  22. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/commands/chromedriver.py +0 -0
  23. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/commands/desktop.py +0 -0
  24. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/commands/macos.py +0 -0
  25. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/commands/playwright.py +0 -0
  26. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/commands/remotion.py +0 -0
  27. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/config.py +0 -0
  28. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/const.py +0 -0
  29. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/interaction/__init__.py +0 -0
  30. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/interaction/command_schema.py +0 -0
  31. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/interaction/policy.py +0 -0
  32. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/interaction/warnings.py +0 -0
  33. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/playwright.py +0 -0
  34. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/runtime/__init__.py +0 -0
  35. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/runtime/_managed_artifact.py +0 -0
  36. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/runtime/chrome_for_testing.py +0 -0
  37. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/runtime/chromedriver.py +0 -0
  38. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/runtime/playwright.py +0 -0
  39. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/__init__.py +0 -0
  40. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/assets/hermes/install.sh +0 -0
  41. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/assets/nvm.sh +0 -0
  42. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/assets/remotion/.gitignore +0 -0
  43. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/assets/remotion/package-lock.json +0 -0
  44. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/assets/remotion/package.json +0 -0
  45. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/assets/remotion/src/index.jsx +0 -0
  46. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/cc_connect.py +0 -0
  47. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/chatgpt.py +0 -0
  48. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/claude.py +0 -0
  49. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/cli.py +0 -0
  50. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/codex.py +0 -0
  51. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/crs.py +0 -0
  52. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/cursor_agent.py +0 -0
  53. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/desktop.py +0 -0
  54. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/discourse.py +0 -0
  55. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/docker.py +0 -0
  56. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/frp.py +0 -0
  57. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/gitea.py +0 -0
  58. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/hermes.py +0 -0
  59. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/lark_cli.py +0 -0
  60. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/mode_prompt.py +0 -0
  61. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/mysql.py +0 -0
  62. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/nginx.py +0 -0
  63. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/nodejs.py +0 -0
  64. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/opencode.py +0 -0
  65. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/remotion.py +0 -0
  66. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/service_credentials.py +0 -0
  67. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/shell_rc.py +0 -0
  68. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/twikoo.py +0 -0
  69. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/uv.py +0 -0
  70. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/workspace/__init__.py +0 -0
  71. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/workspace/cli.py +0 -0
  72. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/workspace/core.py +0 -0
  73. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/workspace/options.py +0 -0
  74. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/workspace/render.py +0 -0
  75. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/workspace/templates/default/en/AGENTS.md +0 -0
  76. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/workspace/templates/default/en/discussion/README.md +0 -0
  77. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/workspace/templates/default/en/projects/README.md +0 -0
  78. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/workspace/templates/default/zh/AGENTS.md +0 -0
  79. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/workspace/templates/default/zh/discussion/README.md +0 -0
  80. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/workspace/templates/default/zh/projects/README.md +0 -0
  81. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/workspace.py +0 -0
  82. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/zsh.py +0 -0
  83. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/zulip.py +0 -0
  84. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/utils/__init__.py +0 -0
  85. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/utils/custom_logger.py +0 -0
  86. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/utils/pathing.py +0 -0
  87. {chatup-0.2.21 → chatup-0.2.22}/src/chatup/utils/platforming.py +0 -0
  88. {chatup-0.2.21 → chatup-0.2.22}/src/chatup.egg-info/dependency_links.txt +0 -0
  89. {chatup-0.2.21 → chatup-0.2.22}/src/chatup.egg-info/entry_points.txt +0 -0
  90. {chatup-0.2.21 → chatup-0.2.22}/src/chatup.egg-info/top_level.txt +0 -0
  91. {chatup-0.2.21 → chatup-0.2.22}/tests/test_browser_backend_cli.py +0 -0
  92. {chatup-0.2.21 → chatup-0.2.22}/tests/test_chatgpt_setup.py +0 -0
  93. {chatup-0.2.21 → chatup-0.2.22}/tests/test_chrome_for_testing.py +0 -0
  94. {chatup-0.2.21 → chatup-0.2.22}/tests/test_chromedriver.py +0 -0
  95. {chatup-0.2.21 → chatup-0.2.22}/tests/test_cli_tree_docs.py +0 -0
  96. {chatup-0.2.21 → chatup-0.2.22}/tests/test_codex_setup.py +0 -0
  97. {chatup-0.2.21 → chatup-0.2.22}/tests/test_crs_setup.py +0 -0
  98. {chatup-0.2.21 → chatup-0.2.22}/tests/test_cursor_agent_setup.py +0 -0
  99. {chatup-0.2.21 → chatup-0.2.22}/tests/test_hermes_model_defaults.py +0 -0
  100. {chatup-0.2.21 → chatup-0.2.22}/tests/test_playwright.py +0 -0
  101. {chatup-0.2.21 → chatup-0.2.22}/tests/test_playwright_cli.py +0 -0
  102. {chatup-0.2.21 → chatup-0.2.22}/tests/test_publish_workflow.py +0 -0
  103. {chatup-0.2.21 → chatup-0.2.22}/tests/test_service_installers.py +0 -0
  104. {chatup-0.2.21 → chatup-0.2.22}/tests/test_twikoo_setup.py +0 -0
  105. {chatup-0.2.21 → chatup-0.2.22}/tests/test_uv_activation.py +0 -0
  106. {chatup-0.2.21 → chatup-0.2.22}/tests/test_windows_compat.py +0 -0
  107. {chatup-0.2.21 → chatup-0.2.22}/tests/test_workspace_setup.py +0 -0
  108. {chatup-0.2.21 → chatup-0.2.22}/tests/test_zsh_alias_names.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chatup
3
- Version: 0.2.21
3
+ Version: 0.2.22
4
4
  Summary: ChatArch setup CLI extracted from ChatTool
5
5
  Author-email: rexwzh <1073853456@qq.com>
6
6
  License-Expression: MIT
@@ -16,6 +16,7 @@ License-File: LICENSE
16
16
  Requires-Dist: click<9.0,>=8.0
17
17
  Requires-Dist: chatstyle[tui]<0.3.0,>=0.2.0
18
18
  Requires-Dist: chatenv<0.3.0,>=0.2.11
19
+ Requires-Dist: ChatGlance<0.4.0,>=0.3.0
19
20
  Requires-Dist: python-dotenv<2.0,>=1.0
20
21
  Requires-Dist: colorama
21
22
  Provides-Extra: dev
@@ -68,6 +69,8 @@ chatup workspace ~/Playground
68
69
 
69
70
  ```bash
70
71
  chatup gitea --force
72
+ chatup glance --dry-run
73
+ chatup glance
71
74
  chatup discourse -e discourse-prod --hostname discourse.public.wzhecnu.cn
72
75
  chatup zulip -e zulip-prod --external-host zulip.public.wzhecnu.cn --port 3095
73
76
  chatup mysql
@@ -131,7 +134,9 @@ chatup chatgpt --yes # Windows 首次安装时接受 Store 协议
131
134
  | 工作区脚手架 | `workspace` |
132
135
  | 桌面应用 | `macos`(Snipaste/iTerm2/Chrome/Blender 多选)、`chrome`、`snipaste`(macOS/Windows)、`iterm`(仅 macOS)、`chatgpt`(含 Codex) |
133
136
  | 视频项目 | `remotion` |
134
- | 本地服务安装 | `gitea`、`discourse`、`zulip`、`mysql`、`nginx`、`crs` |
137
+ | 本地服务安装 | `gitea`、`glance`、`discourse`、`zulip`、`mysql`、`nginx`、`crs` |
138
+
139
+ `chatup glance` 裸命令安装 `ChatArch/glance` 最新稳定维护版到有效 ChatArch home 下的 `glance`。当前仅支持 Linux amd64;严格验证归档、`SHA256SUMS`、`BUILDINFO.txt`、source SHA 和原始二进制版本,复用 ChatGlance portable API 初始化 loopback 配置。它不启动服务、不建立公网入口、不写密码;重复运行保留配置和数据,版本变更请使用 `chatglance runtime update`。
135
140
  | Agent 工具链 | `cc-connect`、`claude`、`codex`、`cursor-agent`、`opencode`、`hermes`、`lark-cli` |
136
141
 
137
142
  桌面应用使用系统原生应用目录;ChatArch 自管安装项默认目录收敛到 `~/.chatarch/...`,例如 `~/.chatarch/chrome-for-testing`、`~/.chatarch/chromedriver`、`~/.chatarch/playwright`、`~/.chatarch/chattea`、`~/.chatarch/discourse`、`~/.chatarch/zulip`、`~/.chatarch/chatdata`、`~/.chatarch/nginx` 和 `~/.chatarch/crs/local`。`cursor-agent` 额外注册 ChatEnv `CursorAgent` profile,`discourse`/`zulip` 注册管理员凭据 profile,并支持 `-e/--env` 从 env 文件或 profile 快速配置;显式选择 profile 时不会混入进程环境中的其他账号凭据。CLI 树由 ChatStyle 的注册表渲染。更完整的能力边界见 `docs/capability-map.md`。
@@ -37,6 +37,8 @@ chatup workspace ~/Playground
37
37
 
38
38
  ```bash
39
39
  chatup gitea --force
40
+ chatup glance --dry-run
41
+ chatup glance
40
42
  chatup discourse -e discourse-prod --hostname discourse.public.wzhecnu.cn
41
43
  chatup zulip -e zulip-prod --external-host zulip.public.wzhecnu.cn --port 3095
42
44
  chatup mysql
@@ -100,7 +102,9 @@ chatup chatgpt --yes # Windows 首次安装时接受 Store 协议
100
102
  | 工作区脚手架 | `workspace` |
101
103
  | 桌面应用 | `macos`(Snipaste/iTerm2/Chrome/Blender 多选)、`chrome`、`snipaste`(macOS/Windows)、`iterm`(仅 macOS)、`chatgpt`(含 Codex) |
102
104
  | 视频项目 | `remotion` |
103
- | 本地服务安装 | `gitea`、`discourse`、`zulip`、`mysql`、`nginx`、`crs` |
105
+ | 本地服务安装 | `gitea`、`glance`、`discourse`、`zulip`、`mysql`、`nginx`、`crs` |
106
+
107
+ `chatup glance` 裸命令安装 `ChatArch/glance` 最新稳定维护版到有效 ChatArch home 下的 `glance`。当前仅支持 Linux amd64;严格验证归档、`SHA256SUMS`、`BUILDINFO.txt`、source SHA 和原始二进制版本,复用 ChatGlance portable API 初始化 loopback 配置。它不启动服务、不建立公网入口、不写密码;重复运行保留配置和数据,版本变更请使用 `chatglance runtime update`。
104
108
  | Agent 工具链 | `cc-connect`、`claude`、`codex`、`cursor-agent`、`opencode`、`hermes`、`lark-cli` |
105
109
 
106
110
  桌面应用使用系统原生应用目录;ChatArch 自管安装项默认目录收敛到 `~/.chatarch/...`,例如 `~/.chatarch/chrome-for-testing`、`~/.chatarch/chromedriver`、`~/.chatarch/playwright`、`~/.chatarch/chattea`、`~/.chatarch/discourse`、`~/.chatarch/zulip`、`~/.chatarch/chatdata`、`~/.chatarch/nginx` 和 `~/.chatarch/crs/local`。`cursor-agent` 额外注册 ChatEnv `CursorAgent` profile,`discourse`/`zulip` 注册管理员凭据 profile,并支持 `-e/--env` 从 env 文件或 profile 快速配置;显式选择 profile 时不会混入进程环境中的其他账号凭据。CLI 树由 ChatStyle 的注册表渲染。更完整的能力边界见 `docs/capability-map.md`。
@@ -13,6 +13,7 @@ dependencies = [
13
13
  "click>=8.0,<9.0",
14
14
  "chatstyle[tui]>=0.2.0,<0.3.0",
15
15
  "chatenv>=0.2.11,<0.3.0",
16
+ "ChatGlance>=0.3.0,<0.4.0",
16
17
  "python-dotenv>=1.0,<2.0",
17
18
  "colorama",
18
19
  ]
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.2.21"
5
+ __version__ = "0.2.22"
@@ -26,6 +26,7 @@ from chatup.setup.discourse import (
26
26
  setup_discourse,
27
27
  )
28
28
  from chatup.setup.frp import setup_frp
29
+ from chatup.setup.glance import DEFAULT_GLANCE_VERSION, setup_glance
29
30
  from chatup.setup.gitea import (
30
31
  DEFAULT_BASE_URL as DEFAULT_GITEA_BASE_URL,
31
32
  DEFAULT_DATABASE_BACKEND as DEFAULT_GITEA_DATABASE_BACKEND,
@@ -231,6 +232,15 @@ def gitea_setup(
231
232
  )
232
233
 
233
234
 
235
+ def glance_setup(version, runtime_home, dry_run, interactive):
236
+ return setup_glance(
237
+ version=version,
238
+ runtime_home=runtime_home,
239
+ dry_run=dry_run,
240
+ interactive=interactive,
241
+ )
242
+
243
+
234
244
  def discourse_setup(
235
245
  home,
236
246
  hostname,
@@ -612,6 +622,37 @@ SETUP_COMMAND_ELEMENTS = (
612
622
  ),
613
623
  ),
614
624
 
625
+ SetupCommandElement(
626
+ name="glance",
627
+ help="Install the verified maintained ChatArch Glance runtime without starting it.",
628
+ callback=glance_setup,
629
+ options=(
630
+ SetupOptionElement(
631
+ param_decls=("--version",),
632
+ kwargs={
633
+ "default": DEFAULT_GLANCE_VERSION,
634
+ "show_default": True,
635
+ "help": "Maintained release: latest or exact chatarch-vMAJOR.MINOR.PATCH.",
636
+ },
637
+ ),
638
+ SetupOptionElement(
639
+ param_decls=("--runtime-home",),
640
+ kwargs={
641
+ "default": None,
642
+ "type": click.Path(path_type=Path),
643
+ "help": "Runtime root; defaults to the active ChatArch home/glance.",
644
+ },
645
+ ),
646
+ SetupOptionElement(
647
+ param_decls=("--dry-run",),
648
+ kwargs={"is_flag": True, "help": "Show paths without network access or writes."},
649
+ ),
650
+ SetupOptionElement(
651
+ param_decls=("--interactive/--no-interactive", "-i/-I"),
652
+ kwargs={"default": None, "help": INTERACTIVE_OPTION_HELP},
653
+ ),
654
+ ),
655
+ ),
615
656
  SetupCommandElement(
616
657
  name="gitea",
617
658
  help="Install ChatArch Gitea and optionally write a local app.ini/user service.",
@@ -0,0 +1,240 @@
1
+ """Install the maintained ChatArch Glance release into a portable runtime."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ import hashlib
7
+ import json
8
+ import os
9
+ from pathlib import Path
10
+ import platform
11
+ import re
12
+ import shlex
13
+ import subprocess
14
+ import tempfile
15
+ import urllib.request
16
+
17
+ import click
18
+ from chatenv import get_paths
19
+ from chatglance.portable import initialize, install_verified_binary, runtime_home
20
+
21
+
22
+ DEFAULT_GLANCE_REPO = "ChatArch/glance"
23
+ DEFAULT_GLANCE_VERSION = "latest"
24
+ TAG_PATTERN = re.compile(r"chatarch-v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)")
25
+ SOURCE_SHA_PATTERN = re.compile(r"[0-9a-f]{40}")
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class GlanceInstallation:
30
+ status: str
31
+ tag: str
32
+ version: str | None
33
+ binary: Path
34
+ config: Path
35
+ start_command: tuple[str, ...]
36
+
37
+
38
+ def normalize_release_tag(value: str) -> str:
39
+ tag = str(value).strip()
40
+ if not TAG_PATTERN.fullmatch(tag):
41
+ raise ValueError("Glance version must be an exact chatarch-vMAJOR.MINOR.PATCH tag")
42
+ return tag
43
+
44
+
45
+ def _require_supported_platform() -> None:
46
+ system = platform.system().lower()
47
+ machine = platform.machine().lower()
48
+ if system != "linux" or machine not in {"x86_64", "amd64"}:
49
+ raise ValueError("Maintained ChatArch/glance releases currently support Linux amd64 only")
50
+
51
+
52
+ def resolve_latest_release_tag(repo: str = DEFAULT_GLANCE_REPO) -> str:
53
+ url = f"https://api.github.com/repos/{repo}/releases/latest"
54
+ with urllib.request.urlopen(url, timeout=30) as response:
55
+ payload = json.loads(response.read().decode("utf-8"))
56
+ tag = payload.get("tag_name")
57
+ if not isinstance(tag, str):
58
+ raise ValueError(f"Latest {repo} release did not provide a tag")
59
+ return normalize_release_tag(tag)
60
+
61
+
62
+ def resolve_requested_tag(version: str, repo: str = DEFAULT_GLANCE_REPO) -> str:
63
+ value = str(version or DEFAULT_GLANCE_VERSION).strip()
64
+ return resolve_latest_release_tag(repo) if value == DEFAULT_GLANCE_VERSION else normalize_release_tag(value)
65
+
66
+
67
+ def release_asset_name(tag: str) -> str:
68
+ return f"glance-{normalize_release_tag(tag)}-linux-amd64.tar.gz"
69
+
70
+
71
+ def release_asset_url(repo: str, tag: str, name: str) -> str:
72
+ return f"https://github.com/{repo}/releases/download/{tag}/{name}"
73
+
74
+
75
+ def download_release_asset(url: str, destination: Path) -> None:
76
+ with urllib.request.urlopen(url, timeout=120) as response, destination.open("wb") as output:
77
+ status = getattr(response, "status", 200)
78
+ if status != 200:
79
+ raise ValueError(f"Glance release download failed with HTTP {status}")
80
+ while chunk := response.read(1024 * 1024):
81
+ output.write(chunk)
82
+
83
+
84
+ def _sha256(path: Path) -> str:
85
+ digest = hashlib.sha256()
86
+ with path.open("rb") as stream:
87
+ for chunk in iter(lambda: stream.read(1024 * 1024), b""):
88
+ digest.update(chunk)
89
+ return digest.hexdigest()
90
+
91
+
92
+ def _parse_checksums(path: Path, expected_names: set[str]) -> dict[str, str]:
93
+ checksums: dict[str, str] = {}
94
+ for line in path.read_text(encoding="utf-8").splitlines():
95
+ match = re.fullmatch(r"([0-9a-f]{64}) ([A-Za-z0-9._+-]+)", line)
96
+ if not match or match.group(2) in checksums:
97
+ raise ValueError("Invalid SHA256SUMS manifest")
98
+ checksums[match.group(2)] = match.group(1)
99
+ if set(checksums) != expected_names:
100
+ raise ValueError("SHA256SUMS does not describe the exact expected release files")
101
+ return checksums
102
+
103
+
104
+ def _parse_buildinfo(path: Path) -> dict[str, str]:
105
+ result: dict[str, str] = {}
106
+ for line in path.read_text(encoding="utf-8").splitlines():
107
+ key, separator, value = line.partition("=")
108
+ if not separator or not key or key in result:
109
+ raise ValueError("Invalid BUILDINFO.txt manifest")
110
+ result[key] = value
111
+ expected = {"tag", "source_sha", "binary_version", "archive", "goos", "goarch", "cgo_enabled"}
112
+ if set(result) != expected:
113
+ raise ValueError("BUILDINFO.txt has unexpected or missing fields")
114
+ return result
115
+
116
+
117
+ def _verify_release_files(archive: Path, checksums_path: Path, buildinfo_path: Path, tag: str) -> tuple[str, str]:
118
+ checksums = _parse_checksums(checksums_path, {archive.name, buildinfo_path.name})
119
+ for path in (archive, buildinfo_path):
120
+ if _sha256(path) != checksums[path.name]:
121
+ raise ValueError(f"SHA256 mismatch for {path.name}")
122
+ info = _parse_buildinfo(buildinfo_path)
123
+ if info["tag"] != tag:
124
+ raise ValueError("BUILDINFO tag does not match requested release")
125
+ if not SOURCE_SHA_PATTERN.fullmatch(info["source_sha"]):
126
+ raise ValueError("BUILDINFO source_sha must be exactly 40 lowercase hex characters")
127
+ version = f"{tag}+{info['source_sha']}"
128
+ expected = {
129
+ "binary_version": version,
130
+ "archive": archive.name,
131
+ "goos": "linux",
132
+ "goarch": "amd64",
133
+ "cgo_enabled": "0",
134
+ }
135
+ for key, value in expected.items():
136
+ if info[key] != value:
137
+ raise ValueError(f"BUILDINFO {key} does not match the requested release")
138
+ return checksums[archive.name], version
139
+
140
+
141
+ def _read_observed_version(binary: Path) -> str:
142
+ try:
143
+ result = subprocess.run(
144
+ [str(binary), "--version"], capture_output=True, text=True, timeout=5,
145
+ check=True, env={"PATH": "/usr/bin:/bin"},
146
+ )
147
+ except (OSError, subprocess.SubprocessError, UnicodeError) as exc:
148
+ raise ValueError(f"Installed Glance version check failed ({type(exc).__name__})") from None
149
+ return result.stdout.strip()
150
+
151
+
152
+ def _existing_binary_matches(home: Path, archive_sha256: str, version: str) -> bool:
153
+ binary = home / "bin/glance"
154
+ provenance_path = home / "bin/glance.provenance.json"
155
+ if not (binary.exists() or binary.is_symlink() or provenance_path.exists() or provenance_path.is_symlink()):
156
+ return False
157
+ refusal = "Existing Glance runtime is not this exact verified release; use native `chatglance runtime update` for explicit changes"
158
+ if binary.is_symlink() or provenance_path.is_symlink() or not binary.is_file() or not provenance_path.is_file():
159
+ raise ValueError(refusal)
160
+ try:
161
+ provenance = json.loads(provenance_path.read_text(encoding="utf-8"))
162
+ except (OSError, UnicodeError, json.JSONDecodeError):
163
+ raise ValueError(refusal) from None
164
+ tag, source_sha = version.split("+", 1)
165
+ expected = {
166
+ "source": "verified local archive",
167
+ "source_tag": tag,
168
+ "source_revision": source_sha,
169
+ "version": version,
170
+ "archive_sha256": archive_sha256,
171
+ "binary_sha256": _sha256(binary),
172
+ }
173
+ if provenance != expected or _read_observed_version(binary) != version:
174
+ raise ValueError(refusal)
175
+ return True
176
+
177
+
178
+ def install_glance(
179
+ *, version: str = DEFAULT_GLANCE_VERSION, home: str | Path | None = None,
180
+ repo: str = DEFAULT_GLANCE_REPO, dry_run: bool = False,
181
+ ) -> GlanceInstallation:
182
+ """Install or exactly reuse one verified maintained Go release, then initialize resources."""
183
+ _require_supported_platform()
184
+ root = runtime_home(home)
185
+ binary = root / "bin/glance"
186
+ config = root / "config/glance.yml"
187
+ requested = str(version or DEFAULT_GLANCE_VERSION).strip()
188
+ if dry_run:
189
+ tag = DEFAULT_GLANCE_VERSION if requested == DEFAULT_GLANCE_VERSION else normalize_release_tag(requested)
190
+ return GlanceInstallation("planned", tag, None, binary, config, ("chatglance", "runtime", "serve", "--runtime-home", str(root)))
191
+
192
+ tag = resolve_requested_tag(requested, repo)
193
+ archive_name = release_asset_name(tag)
194
+ names = (archive_name, "SHA256SUMS", "BUILDINFO.txt")
195
+ staging_parent = get_paths().home_dir / "chatup" / "staging"
196
+ staging_parent.mkdir(parents=True, mode=0o700, exist_ok=True)
197
+ if staging_parent.is_symlink() or not staging_parent.is_dir():
198
+ raise ValueError("Unsafe Glance download staging directory")
199
+ staging_parent.chmod(0o700)
200
+ with tempfile.TemporaryDirectory(prefix="glance-", dir=staging_parent) as temporary:
201
+ staging = Path(temporary)
202
+ for name in names:
203
+ download_release_asset(release_asset_url(repo, tag, name), staging / name)
204
+ archive = staging / archive_name
205
+ archive_sha256, raw_version = _verify_release_files(
206
+ archive, staging / "SHA256SUMS", staging / "BUILDINFO.txt", tag
207
+ )
208
+ reused = _existing_binary_matches(root, archive_sha256, raw_version)
209
+ if not reused:
210
+ install_verified_binary(archive, archive_sha256, root, version=raw_version)
211
+ initialize(root)
212
+ if not config.is_file() or config.is_symlink():
213
+ raise ValueError("ChatGlance initialization did not provide a safe runtime config")
214
+ return GlanceInstallation(
215
+ "reused" if reused else "installed", tag, raw_version, binary, config,
216
+ ("chatglance", "runtime", "serve", "--runtime-home", str(root)),
217
+ )
218
+
219
+
220
+ def setup_glance(
221
+ version: str = DEFAULT_GLANCE_VERSION,
222
+ runtime_home: str | Path | None = None,
223
+ dry_run: bool = False,
224
+ interactive=None,
225
+ ) -> GlanceInstallation:
226
+ """Click-facing setup wrapper; setup never starts or enables the server."""
227
+ del interactive
228
+ try:
229
+ result = install_glance(version=version, home=runtime_home, dry_run=dry_run)
230
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
231
+ raise click.ClickException(str(exc)) from exc
232
+ if result.status == "planned":
233
+ click.echo(f"Glance setup plan: {result.tag} -> {result.binary}")
234
+ else:
235
+ click.echo(f"Glance binary: {result.binary}")
236
+ click.echo(f"Glance config: {result.config}")
237
+ click.echo(f"Glance release: {result.version} ({result.status})")
238
+ click.echo("Native start command: " + shlex.join(result.start_command))
239
+ click.echo("The server was not started and no service or public endpoint was created.")
240
+ return result
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chatup
3
- Version: 0.2.21
3
+ Version: 0.2.22
4
4
  Summary: ChatArch setup CLI extracted from ChatTool
5
5
  Author-email: rexwzh <1073853456@qq.com>
6
6
  License-Expression: MIT
@@ -16,6 +16,7 @@ License-File: LICENSE
16
16
  Requires-Dist: click<9.0,>=8.0
17
17
  Requires-Dist: chatstyle[tui]<0.3.0,>=0.2.0
18
18
  Requires-Dist: chatenv<0.3.0,>=0.2.11
19
+ Requires-Dist: ChatGlance<0.4.0,>=0.3.0
19
20
  Requires-Dist: python-dotenv<2.0,>=1.0
20
21
  Requires-Dist: colorama
21
22
  Provides-Extra: dev
@@ -68,6 +69,8 @@ chatup workspace ~/Playground
68
69
 
69
70
  ```bash
70
71
  chatup gitea --force
72
+ chatup glance --dry-run
73
+ chatup glance
71
74
  chatup discourse -e discourse-prod --hostname discourse.public.wzhecnu.cn
72
75
  chatup zulip -e zulip-prod --external-host zulip.public.wzhecnu.cn --port 3095
73
76
  chatup mysql
@@ -131,7 +134,9 @@ chatup chatgpt --yes # Windows 首次安装时接受 Store 协议
131
134
  | 工作区脚手架 | `workspace` |
132
135
  | 桌面应用 | `macos`(Snipaste/iTerm2/Chrome/Blender 多选)、`chrome`、`snipaste`(macOS/Windows)、`iterm`(仅 macOS)、`chatgpt`(含 Codex) |
133
136
  | 视频项目 | `remotion` |
134
- | 本地服务安装 | `gitea`、`discourse`、`zulip`、`mysql`、`nginx`、`crs` |
137
+ | 本地服务安装 | `gitea`、`glance`、`discourse`、`zulip`、`mysql`、`nginx`、`crs` |
138
+
139
+ `chatup glance` 裸命令安装 `ChatArch/glance` 最新稳定维护版到有效 ChatArch home 下的 `glance`。当前仅支持 Linux amd64;严格验证归档、`SHA256SUMS`、`BUILDINFO.txt`、source SHA 和原始二进制版本,复用 ChatGlance portable API 初始化 loopback 配置。它不启动服务、不建立公网入口、不写密码;重复运行保留配置和数据,版本变更请使用 `chatglance runtime update`。
135
140
  | Agent 工具链 | `cc-connect`、`claude`、`codex`、`cursor-agent`、`opencode`、`hermes`、`lark-cli` |
136
141
 
137
142
  桌面应用使用系统原生应用目录;ChatArch 自管安装项默认目录收敛到 `~/.chatarch/...`,例如 `~/.chatarch/chrome-for-testing`、`~/.chatarch/chromedriver`、`~/.chatarch/playwright`、`~/.chatarch/chattea`、`~/.chatarch/discourse`、`~/.chatarch/zulip`、`~/.chatarch/chatdata`、`~/.chatarch/nginx` 和 `~/.chatarch/crs/local`。`cursor-agent` 额外注册 ChatEnv `CursorAgent` profile,`discourse`/`zulip` 注册管理员凭据 profile,并支持 `-e/--env` 从 env 文件或 profile 快速配置;显式选择 profile 时不会混入进程环境中的其他账号凭据。CLI 树由 ChatStyle 的注册表渲染。更完整的能力边界见 `docs/capability-map.md`。
@@ -44,6 +44,7 @@ src/chatup/setup/docker.py
44
44
  src/chatup/setup/elements.py
45
45
  src/chatup/setup/frp.py
46
46
  src/chatup/setup/gitea.py
47
+ src/chatup/setup/glance.py
47
48
  src/chatup/setup/hermes.py
48
49
  src/chatup/setup/lark_cli.py
49
50
  src/chatup/setup/mode_prompt.py
@@ -88,6 +89,8 @@ tests/test_cli_tree_docs.py
88
89
  tests/test_codex_setup.py
89
90
  tests/test_crs_setup.py
90
91
  tests/test_cursor_agent_setup.py
92
+ tests/test_glance_bridge.py
93
+ tests/test_glance_setup.py
91
94
  tests/test_hermes_model_defaults.py
92
95
  tests/test_playwright.py
93
96
  tests/test_playwright_cli.py
@@ -1,6 +1,7 @@
1
1
  click<9.0,>=8.0
2
2
  chatstyle[tui]<0.3.0,>=0.2.0
3
3
  chatenv<0.3.0,>=0.2.11
4
+ ChatGlance<0.4.0,>=0.3.0
4
5
  python-dotenv<2.0,>=1.0
5
6
  colorama
6
7
 
@@ -0,0 +1,6 @@
1
+ def test_glance_start_command_uses_typed_provider_bridge(monkeypatch, tmp_path):
2
+ from chatup.setup import glance
3
+ monkeypatch.setattr(glance.platform, "system", lambda: "Linux")
4
+ monkeypatch.setattr(glance.platform, "machine", lambda: "amd64")
5
+ result = glance.install_glance(home=tmp_path / "runtime", dry_run=True)
6
+ assert result.start_command == ("chatglance", "runtime", "serve", "--runtime-home", str(tmp_path / "runtime"))
@@ -0,0 +1,190 @@
1
+ import hashlib
2
+ import io
3
+ import json
4
+ import tarfile
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+ from click.testing import CliRunner
9
+
10
+ from chatup.cli import main
11
+
12
+
13
+ TAG = "chatarch-v0.2.1"
14
+ SOURCE_SHA = "a" * 40
15
+ VERSION = f"{TAG}+{SOURCE_SHA}"
16
+ ARCHIVE = f"glance-{TAG}-linux-amd64.tar.gz"
17
+
18
+
19
+ def _release_files(archive: bytes = b"archive", **overrides):
20
+ fields = {
21
+ "tag": TAG,
22
+ "source_sha": SOURCE_SHA,
23
+ "binary_version": VERSION,
24
+ "archive": ARCHIVE,
25
+ "goos": "linux",
26
+ "goarch": "amd64",
27
+ "cgo_enabled": "0",
28
+ }
29
+ fields.update(overrides)
30
+ buildinfo = "".join(f"{key}={value}\n" for key, value in fields.items()).encode()
31
+ sums = (
32
+ f"{hashlib.sha256(archive).hexdigest()} {ARCHIVE}\n"
33
+ f"{hashlib.sha256(buildinfo).hexdigest()} BUILDINFO.txt\n"
34
+ ).encode()
35
+ return {ARCHIVE: archive, "BUILDINFO.txt": buildinfo, "SHA256SUMS": sums}
36
+
37
+
38
+ def _fake_download(files, requested):
39
+ def download(url, destination):
40
+ name = url.rsplit("/", 1)[-1]
41
+ requested.append(name)
42
+ destination.write_bytes(files[name])
43
+
44
+ return download
45
+
46
+
47
+ def test_glance_command_exists_and_bare_invocation_dispatches(monkeypatch, tmp_path):
48
+ import chatup.setup.elements as elements
49
+
50
+ called = []
51
+ monkeypatch.setattr(elements, "setup_glance", lambda **kwargs: called.append(kwargs) or {})
52
+
53
+ result = CliRunner().invoke(main, ["glance", "--runtime-home", str(tmp_path), "-I"])
54
+
55
+ assert result.exit_code == 0, result.output
56
+ assert called == [{
57
+ "version": "latest", "runtime_home": tmp_path, "dry_run": False, "interactive": False,
58
+ }]
59
+
60
+
61
+ def test_glance_installer_has_importable_api_and_strict_version():
62
+ from chatup.setup.glance import install_glance, normalize_release_tag
63
+
64
+ assert callable(install_glance)
65
+ assert normalize_release_tag(TAG) == TAG
66
+ for invalid in ("v0.2.1", "0.2.1", "chatarch-v1.2", "chatarch-v01.2.3", "chatarch-v1.2.3+abc"):
67
+ with pytest.raises(ValueError, match="chatarch-vMAJOR.MINOR.PATCH"):
68
+ normalize_release_tag(invalid)
69
+
70
+
71
+ def test_glance_rejects_unsupported_platform_before_download_or_write(monkeypatch, tmp_path):
72
+ import chatup.setup.glance as glance
73
+
74
+ requested = []
75
+ monkeypatch.setattr(glance.platform, "system", lambda: "Darwin")
76
+ monkeypatch.setattr(glance.platform, "machine", lambda: "arm64")
77
+ monkeypatch.setattr(glance, "download_release_asset", lambda *args: requested.append(args))
78
+ with pytest.raises(ValueError, match="Linux amd64"):
79
+ glance.install_glance(version=TAG, home=tmp_path / "runtime")
80
+ assert requested == []
81
+ assert not (tmp_path / "runtime").exists()
82
+
83
+
84
+ @pytest.mark.parametrize(("files", "message"), [
85
+ ({**_release_files(), "SHA256SUMS": (f"{'0' * 64} {ARCHIVE}\n" f"{'0' * 64} BUILDINFO.txt\n").encode()}, "SHA256 mismatch"),
86
+ (_release_files(source_sha="b" * 39), "source_sha"),
87
+ (_release_files(tag="chatarch-v0.2.0"), "tag"),
88
+ (_release_files(binary_version="chatarch-v0.2.1+" + "b" * 40), "binary_version"),
89
+ ])
90
+ def test_glance_rejects_hash_or_buildinfo_mismatch_before_runtime_write(monkeypatch, tmp_path, files, message):
91
+ import chatup.setup.glance as glance
92
+
93
+ requested = []
94
+ monkeypatch.setattr(glance.platform, "system", lambda: "Linux")
95
+ monkeypatch.setattr(glance.platform, "machine", lambda: "x86_64")
96
+ monkeypatch.setattr(glance, "download_release_asset", _fake_download(files, requested))
97
+ with pytest.raises(ValueError, match=message):
98
+ glance.install_glance(version=TAG, home=tmp_path / "runtime")
99
+ assert requested == [ARCHIVE, "SHA256SUMS", "BUILDINFO.txt"]
100
+ assert not (tmp_path / "runtime").exists()
101
+
102
+
103
+ def test_hostile_archive_is_rejected_by_chatglance_portable_api(monkeypatch, tmp_path):
104
+ import chatup.setup.glance as glance
105
+
106
+ stream = io.BytesIO()
107
+ with tarfile.open(fileobj=stream, mode="w:gz") as archive:
108
+ payload = b"hostile"
109
+ member = tarfile.TarInfo("../outside")
110
+ member.size = len(payload)
111
+ archive.addfile(member, io.BytesIO(payload))
112
+ files = _release_files(stream.getvalue())
113
+ monkeypatch.setattr(glance.platform, "system", lambda: "Linux")
114
+ monkeypatch.setattr(glance.platform, "machine", lambda: "amd64")
115
+ monkeypatch.setattr(glance, "download_release_asset", _fake_download(files, []))
116
+ with pytest.raises(ValueError, match="only a regular glance executable"):
117
+ glance.install_glance(version=TAG, home=tmp_path / "runtime")
118
+ assert not (tmp_path / "outside").exists()
119
+ assert not (tmp_path / "runtime").exists()
120
+
121
+
122
+ def test_install_is_idempotent_and_preserves_existing_config(monkeypatch, tmp_path):
123
+ import chatup.setup.glance as glance
124
+
125
+ archive_bytes = b"reviewed archive"
126
+ files = _release_files(archive_bytes)
127
+ requested = []
128
+ home = tmp_path / "runtime"
129
+ monkeypatch.setattr(glance.platform, "system", lambda: "Linux")
130
+ monkeypatch.setattr(glance.platform, "machine", lambda: "amd64")
131
+ monkeypatch.setattr(glance, "download_release_asset", _fake_download(files, requested))
132
+ monkeypatch.setattr(glance, "_read_observed_version", lambda path: VERSION)
133
+
134
+ def install_verified_binary(archive, sha256, runtime_home, *, version):
135
+ binary = Path(runtime_home) / "bin/glance"
136
+ binary.parent.mkdir(parents=True)
137
+ binary.write_bytes(b"binary")
138
+ binary.chmod(0o755)
139
+ provenance = {
140
+ "source": "verified local archive", "source_tag": TAG, "source_revision": SOURCE_SHA,
141
+ "version": VERSION, "archive_sha256": hashlib.sha256(archive_bytes).hexdigest(),
142
+ "binary_sha256": hashlib.sha256(b"binary").hexdigest(),
143
+ }
144
+ binary.with_name("glance.provenance.json").write_text(json.dumps(provenance))
145
+ return binary
146
+
147
+ def initialize(runtime_home):
148
+ config = Path(runtime_home) / "config/glance.yml"
149
+ config.parent.mkdir(parents=True, exist_ok=True)
150
+ if not config.exists():
151
+ config.write_text("server:\n host: 127.0.0.1\n")
152
+ return [config]
153
+
154
+ monkeypatch.setattr(glance, "install_verified_binary", install_verified_binary)
155
+ monkeypatch.setattr(glance, "initialize", initialize)
156
+ first = glance.install_glance(version=TAG, home=home)
157
+ config = home / "config/glance.yml"
158
+ config.write_text(config.read_text() + "# user note\n")
159
+ second = glance.install_glance(version=TAG, home=home)
160
+ assert first.status == "installed"
161
+ assert second.status == "reused"
162
+ assert config.read_text().endswith("# user note\n")
163
+ assert second.start_command == ("chatglance", "runtime", "serve", "--runtime-home", str(home))
164
+ assert requested == [ARCHIVE, "SHA256SUMS", "BUILDINFO.txt"] * 2
165
+
166
+
167
+ def test_existing_unverified_runtime_is_never_replaced(monkeypatch, tmp_path):
168
+ import chatup.setup.glance as glance
169
+
170
+ home = tmp_path / "runtime"
171
+ (home / "bin").mkdir(parents=True)
172
+ (home / "bin/glance").write_text("user binary")
173
+ monkeypatch.setattr(glance.platform, "system", lambda: "Linux")
174
+ monkeypatch.setattr(glance.platform, "machine", lambda: "amd64")
175
+ monkeypatch.setattr(glance, "download_release_asset", _fake_download(_release_files(), []))
176
+ with pytest.raises(ValueError, match="chatglance runtime update"):
177
+ glance.install_glance(version=TAG, home=home)
178
+ assert (home / "bin/glance").read_text() == "user binary"
179
+
180
+
181
+ def test_dry_run_has_no_network_or_filesystem_side_effects(monkeypatch, tmp_path):
182
+ import chatup.setup.glance as glance
183
+
184
+ monkeypatch.setattr(glance.platform, "system", lambda: "Linux")
185
+ monkeypatch.setattr(glance.platform, "machine", lambda: "amd64")
186
+ monkeypatch.setattr(glance, "download_release_asset", lambda *args: pytest.fail("downloaded"))
187
+ result = glance.install_glance(version=TAG, home=tmp_path / "runtime", dry_run=True)
188
+ assert result.status == "planned"
189
+ assert result.tag == TAG
190
+ assert not (tmp_path / "runtime").exists()
@@ -24,6 +24,7 @@ def test_chatup_depends_on_chatenv_and_registers_cursor_agent_config_provider():
24
24
  assert "click>=8.0,<9.0" in data["project"]["dependencies"]
25
25
  assert "chatstyle[tui]>=0.2.0,<0.3.0" in data["project"]["dependencies"]
26
26
  assert "chatenv>=0.2.11,<0.3.0" in data["project"]["dependencies"]
27
+ assert "ChatGlance>=0.3.0,<0.4.0" in data["project"]["dependencies"]
27
28
  assert "python-dotenv>=1.0,<2.0" in data["project"]["dependencies"]
28
29
  entry_points = data["project"]["entry-points"]["chatenv.configs"]
29
30
  assert entry_points["cursor-agent"] == "chatup.config:CursorAgentConfig"
@@ -17,6 +17,7 @@ def test_chatup_root_help_lists_setup_commands_without_setup_group_or_alias():
17
17
  "nodejs",
18
18
  "uv",
19
19
  "gitea",
20
+ "glance",
20
21
  "discourse",
21
22
  "zulip",
22
23
  "mysql",
@@ -59,6 +60,7 @@ def test_top_level_setup_commands_expose_help():
59
60
  "docker",
60
61
  "frp",
61
62
  "gitea",
63
+ "glance",
62
64
  "discourse",
63
65
  "zulip",
64
66
  "mysql",
@@ -5,7 +5,7 @@ from chatup.cli import main
5
5
 
6
6
 
7
7
  def test_version_present():
8
- assert __version__ == "0.2.21"
8
+ assert __version__ == "0.2.22"
9
9
 
10
10
 
11
11
  def test_cli_help():
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes