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.
- {chatup-0.2.21/src/chatup.egg-info → chatup-0.2.22}/PKG-INFO +7 -2
- {chatup-0.2.21 → chatup-0.2.22}/README.md +5 -1
- {chatup-0.2.21 → chatup-0.2.22}/pyproject.toml +1 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/__init__.py +1 -1
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/elements.py +41 -0
- chatup-0.2.22/src/chatup/setup/glance.py +240 -0
- {chatup-0.2.21 → chatup-0.2.22/src/chatup.egg-info}/PKG-INFO +7 -2
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup.egg-info/SOURCES.txt +3 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup.egg-info/requires.txt +1 -0
- chatup-0.2.22/tests/test_glance_bridge.py +6 -0
- chatup-0.2.22/tests/test_glance_setup.py +190 -0
- {chatup-0.2.21 → chatup-0.2.22}/tests/test_pyproject_metadata.py +1 -0
- {chatup-0.2.21 → chatup-0.2.22}/tests/test_setup_cli.py +2 -0
- {chatup-0.2.21 → chatup-0.2.22}/tests/test_version.py +1 -1
- {chatup-0.2.21 → chatup-0.2.22}/LICENSE +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/setup.cfg +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/chrome_for_testing.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/chromedriver.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/cli.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/commands/__init__.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/commands/chrome_for_testing.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/commands/chromedriver.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/commands/desktop.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/commands/macos.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/commands/playwright.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/commands/remotion.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/config.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/const.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/interaction/__init__.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/interaction/command_schema.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/interaction/policy.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/interaction/warnings.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/playwright.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/runtime/__init__.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/runtime/_managed_artifact.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/runtime/chrome_for_testing.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/runtime/chromedriver.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/runtime/playwright.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/__init__.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/assets/hermes/install.sh +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/assets/nvm.sh +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/assets/remotion/.gitignore +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/assets/remotion/package-lock.json +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/assets/remotion/package.json +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/assets/remotion/src/index.jsx +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/cc_connect.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/chatgpt.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/claude.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/cli.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/codex.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/crs.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/cursor_agent.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/desktop.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/discourse.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/docker.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/frp.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/gitea.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/hermes.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/lark_cli.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/mode_prompt.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/mysql.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/nginx.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/nodejs.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/opencode.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/remotion.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/service_credentials.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/shell_rc.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/twikoo.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/uv.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/workspace/__init__.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/workspace/cli.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/workspace/core.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/workspace/options.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/workspace/render.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/workspace/templates/default/en/AGENTS.md +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/workspace/templates/default/en/discussion/README.md +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/workspace/templates/default/en/projects/README.md +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/workspace/templates/default/zh/AGENTS.md +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/workspace/templates/default/zh/discussion/README.md +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/workspace/templates/default/zh/projects/README.md +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/workspace.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/zsh.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/zulip.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/utils/__init__.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/utils/custom_logger.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/utils/pathing.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup/utils/platforming.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup.egg-info/dependency_links.txt +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup.egg-info/entry_points.txt +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/src/chatup.egg-info/top_level.txt +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/tests/test_browser_backend_cli.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/tests/test_chatgpt_setup.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/tests/test_chrome_for_testing.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/tests/test_chromedriver.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/tests/test_cli_tree_docs.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/tests/test_codex_setup.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/tests/test_crs_setup.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/tests/test_cursor_agent_setup.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/tests/test_hermes_model_defaults.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/tests/test_playwright.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/tests/test_playwright_cli.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/tests/test_publish_workflow.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/tests/test_service_installers.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/tests/test_twikoo_setup.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/tests/test_uv_activation.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/tests/test_windows_compat.py +0 -0
- {chatup-0.2.21 → chatup-0.2.22}/tests/test_workspace_setup.py +0 -0
- {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.
|
|
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`。
|
|
@@ -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.
|
|
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
|
|
@@ -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",
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/workspace/templates/default/en/discussion/README.md
RENAMED
|
File without changes
|
{chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/workspace/templates/default/en/projects/README.md
RENAMED
|
File without changes
|
|
File without changes
|
{chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/workspace/templates/default/zh/discussion/README.md
RENAMED
|
File without changes
|
{chatup-0.2.21 → chatup-0.2.22}/src/chatup/setup/workspace/templates/default/zh/projects/README.md
RENAMED
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|