chatup 0.2.16__tar.gz → 0.2.18__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.16/src/chatup.egg-info → chatup-0.2.18}/PKG-INFO +14 -3
- {chatup-0.2.16 → chatup-0.2.18}/README.md +13 -2
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/__init__.py +1 -1
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/cli.py +3 -0
- chatup-0.2.18/src/chatup/commands/desktop.py +50 -0
- chatup-0.2.18/src/chatup/setup/desktop.py +279 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/zsh.py +1 -1
- {chatup-0.2.16 → chatup-0.2.18/src/chatup.egg-info}/PKG-INFO +14 -3
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup.egg-info/SOURCES.txt +4 -1
- {chatup-0.2.16 → chatup-0.2.18}/tests/test_browser_backend_cli.py +3 -1
- {chatup-0.2.16 → chatup-0.2.18}/tests/test_cli_tree_docs.py +2 -1
- {chatup-0.2.16 → chatup-0.2.18}/tests/test_version.py +1 -1
- chatup-0.2.18/tests/test_zsh_alias_names.py +8 -0
- {chatup-0.2.16 → chatup-0.2.18}/LICENSE +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/pyproject.toml +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/setup.cfg +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/chrome_for_testing.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/chromedriver.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/commands/__init__.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/commands/chrome_for_testing.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/commands/chromedriver.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/commands/playwright.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/config.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/const.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/interaction/__init__.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/interaction/command_schema.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/interaction/policy.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/interaction/warnings.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/playwright.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/runtime/__init__.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/runtime/_managed_artifact.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/runtime/chrome_for_testing.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/runtime/chromedriver.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/runtime/playwright.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/__init__.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/assets/hermes/install.sh +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/assets/nvm.sh +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/cc_connect.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/chatgpt.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/claude.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/cli.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/codex.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/crs.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/cursor_agent.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/discourse.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/docker.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/elements.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/frp.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/gitea.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/hermes.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/lark_cli.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/mode_prompt.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/mysql.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/nginx.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/nodejs.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/opencode.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/service_credentials.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/shell_rc.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/twikoo.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/uv.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/workspace/__init__.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/workspace/cli.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/workspace/core.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/workspace/options.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/workspace/render.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/workspace/templates/default/en/AGENTS.md +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/workspace/templates/default/en/discussion/README.md +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/workspace/templates/default/en/projects/README.md +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/workspace/templates/default/zh/AGENTS.md +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/workspace/templates/default/zh/discussion/README.md +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/workspace/templates/default/zh/projects/README.md +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/workspace.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/zulip.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/utils/__init__.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/utils/custom_logger.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/utils/pathing.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup/utils/platforming.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup.egg-info/dependency_links.txt +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup.egg-info/entry_points.txt +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup.egg-info/requires.txt +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/src/chatup.egg-info/top_level.txt +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/tests/test_chatgpt_setup.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/tests/test_chrome_for_testing.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/tests/test_chromedriver.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/tests/test_codex_setup.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/tests/test_crs_setup.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/tests/test_cursor_agent_setup.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/tests/test_hermes_model_defaults.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/tests/test_playwright.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/tests/test_playwright_cli.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/tests/test_publish_workflow.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/tests/test_pyproject_metadata.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/tests/test_service_installers.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/tests/test_setup_cli.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/tests/test_twikoo_setup.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/tests/test_uv_activation.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/tests/test_windows_compat.py +0 -0
- {chatup-0.2.16 → chatup-0.2.18}/tests/test_workspace_setup.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.18
|
|
4
4
|
Summary: ChatArch setup CLI extracted from ChatTool
|
|
5
5
|
Author-email: rexwzh <1073853456@qq.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -76,6 +76,17 @@ chatup nginx proxy-pass ./gitea-local.conf --set SERVER_NAME=gitea.local.example
|
|
|
76
76
|
chatup crs --install-dir ~/.chatarch/crs/local --port 12392 --redis-port 6379
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
+
## Chrome 与 iTerm2
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
chatup chrome --dry-run
|
|
83
|
+
chatup chrome # 按系统安装普通 Google Chrome
|
|
84
|
+
chatup iterm # 安装 iTerm2,仅限 macOS
|
|
85
|
+
chatup chrome --sudo --yes # Linux:允许提权并确认包安装
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
macOS 直接下载官方 DMG/ZIP 并校验开发者签名,无需 Homebrew;默认装到 `/Applications`,不可写时使用 `~/Applications`。Windows 使用 WinGet `Google.Chrome`;Linux x86_64 使用官方 DEB/RPM 与 apt/dnf/yum/zypper。已安装时验证并复用,不改默认浏览器、终端或用户配置。自动化浏览器仍使用 `chatup chrome-for-testing`。详见[安装约定](https://arch.gh.wzhecnu.cn/ChatUp/commands/#chrome-iterm)。
|
|
89
|
+
|
|
79
90
|
## ChatGPT / Codex 桌面应用
|
|
80
91
|
|
|
81
92
|
```bash
|
|
@@ -94,11 +105,11 @@ chatup chatgpt --yes # Windows 首次安装时接受 Store 协议
|
|
|
94
105
|
| --- | --- |
|
|
95
106
|
| 基础运行环境 | `doctor`、`uv`、`nodejs`、`docker`、`zsh`、`chrome-for-testing`、`chromedriver`、`playwright`、`frp` |
|
|
96
107
|
| 工作区脚手架 | `workspace` |
|
|
97
|
-
|
|
|
108
|
+
| 桌面应用 | `chrome`、`iterm`(仅 macOS)、`chatgpt`(含 Codex) |
|
|
98
109
|
| 本地服务安装 | `gitea`、`discourse`、`zulip`、`mysql`、`nginx`、`crs` |
|
|
99
110
|
| Agent 工具链 | `cc-connect`、`claude`、`codex`、`cursor-agent`、`opencode`、`hermes`、`lark-cli` |
|
|
100
111
|
|
|
101
|
-
|
|
112
|
+
桌面应用使用系统原生应用目录;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`。
|
|
102
113
|
|
|
103
114
|
## 开发
|
|
104
115
|
|
|
@@ -45,6 +45,17 @@ chatup nginx proxy-pass ./gitea-local.conf --set SERVER_NAME=gitea.local.example
|
|
|
45
45
|
chatup crs --install-dir ~/.chatarch/crs/local --port 12392 --redis-port 6379
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
+
## Chrome 与 iTerm2
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
chatup chrome --dry-run
|
|
52
|
+
chatup chrome # 按系统安装普通 Google Chrome
|
|
53
|
+
chatup iterm # 安装 iTerm2,仅限 macOS
|
|
54
|
+
chatup chrome --sudo --yes # Linux:允许提权并确认包安装
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
macOS 直接下载官方 DMG/ZIP 并校验开发者签名,无需 Homebrew;默认装到 `/Applications`,不可写时使用 `~/Applications`。Windows 使用 WinGet `Google.Chrome`;Linux x86_64 使用官方 DEB/RPM 与 apt/dnf/yum/zypper。已安装时验证并复用,不改默认浏览器、终端或用户配置。自动化浏览器仍使用 `chatup chrome-for-testing`。详见[安装约定](https://arch.gh.wzhecnu.cn/ChatUp/commands/#chrome-iterm)。
|
|
58
|
+
|
|
48
59
|
## ChatGPT / Codex 桌面应用
|
|
49
60
|
|
|
50
61
|
```bash
|
|
@@ -63,11 +74,11 @@ chatup chatgpt --yes # Windows 首次安装时接受 Store 协议
|
|
|
63
74
|
| --- | --- |
|
|
64
75
|
| 基础运行环境 | `doctor`、`uv`、`nodejs`、`docker`、`zsh`、`chrome-for-testing`、`chromedriver`、`playwright`、`frp` |
|
|
65
76
|
| 工作区脚手架 | `workspace` |
|
|
66
|
-
|
|
|
77
|
+
| 桌面应用 | `chrome`、`iterm`(仅 macOS)、`chatgpt`(含 Codex) |
|
|
67
78
|
| 本地服务安装 | `gitea`、`discourse`、`zulip`、`mysql`、`nginx`、`crs` |
|
|
68
79
|
| Agent 工具链 | `cc-connect`、`claude`、`codex`、`cursor-agent`、`opencode`、`hermes`、`lark-cli` |
|
|
69
80
|
|
|
70
|
-
|
|
81
|
+
桌面应用使用系统原生应用目录;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`。
|
|
71
82
|
|
|
72
83
|
## 开发
|
|
73
84
|
|
|
@@ -6,6 +6,7 @@ from chatstyle import add_tree_option
|
|
|
6
6
|
from chatup import __version__
|
|
7
7
|
from chatup.commands.chrome_for_testing import cli as chrome_for_testing_cli
|
|
8
8
|
from chatup.commands.chromedriver import cli as chromedriver_cli
|
|
9
|
+
from chatup.commands.desktop import chrome_cli, iterm_cli
|
|
9
10
|
from chatup.commands.playwright import cli as playwright_cli
|
|
10
11
|
from chatup.setup.cli import register_setup_commands
|
|
11
12
|
|
|
@@ -32,6 +33,8 @@ register_setup_commands(main)
|
|
|
32
33
|
main.add_command(chrome_for_testing_cli)
|
|
33
34
|
main.add_command(chromedriver_cli)
|
|
34
35
|
main.add_command(playwright_cli)
|
|
36
|
+
main.add_command(chrome_cli)
|
|
37
|
+
main.add_command(iterm_cli)
|
|
35
38
|
|
|
36
39
|
|
|
37
40
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""CLI entry points for native desktop app installation."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import shlex
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _install(app: str, **kwargs) -> None:
|
|
10
|
+
from chatup.setup.desktop import setup_desktop_app
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
result = setup_desktop_app(app, **kwargs)
|
|
14
|
+
except RuntimeError as exc:
|
|
15
|
+
raise click.ClickException(str(exc)) from exc
|
|
16
|
+
if result["status"] == "planned":
|
|
17
|
+
click.echo(f"{result['app']} install plan ({result['platform']}):")
|
|
18
|
+
if "url" in result:
|
|
19
|
+
click.echo(f"Download: {result['url']}")
|
|
20
|
+
if "path" in result:
|
|
21
|
+
click.echo(f"Install to: {result['path']} (verify publisher signature)")
|
|
22
|
+
if "command" in result:
|
|
23
|
+
click.echo(shlex.join(result["command"]))
|
|
24
|
+
click.echo("Dry run: no download or installation performed.")
|
|
25
|
+
return
|
|
26
|
+
status = "Already installed" if result["status"] == "already_installed" else "Installed"
|
|
27
|
+
click.echo(f"{status}: {result['app']} {result.get('version', '')}".strip())
|
|
28
|
+
if "path" in result:
|
|
29
|
+
click.echo(result["path"])
|
|
30
|
+
elif "binary" in result:
|
|
31
|
+
click.echo(result["binary"])
|
|
32
|
+
click.echo("Installation verified.")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@click.command(name="chrome")
|
|
36
|
+
@click.option("--dry-run", is_flag=True, help="Show the installation plan without downloading or installing.")
|
|
37
|
+
@click.option("--sudo", is_flag=True, help="Allow sudo for Linux system package installation.")
|
|
38
|
+
@click.option("--yes", "-y", is_flag=True, help="Accept Windows agreements or Linux package-manager prompts.")
|
|
39
|
+
@click.option("--log-level", type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR"], case_sensitive=False), default="INFO")
|
|
40
|
+
def chrome_cli(**kwargs) -> None:
|
|
41
|
+
"""Install regular Google Chrome for macOS, Windows or Linux."""
|
|
42
|
+
_install("chrome", **kwargs)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@click.command(name="iterm")
|
|
46
|
+
@click.option("--dry-run", is_flag=True, help="Show the installation plan without downloading or installing.")
|
|
47
|
+
@click.option("--log-level", type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR"], case_sensitive=False), default="INFO")
|
|
48
|
+
def iterm_cli(**kwargs) -> None:
|
|
49
|
+
"""Install iTerm2 from the official download (macOS only)."""
|
|
50
|
+
_install("iterm", **kwargs)
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Native installers for regular Google Chrome and the macOS iTerm2 app."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import platform
|
|
8
|
+
import plistlib
|
|
9
|
+
import re
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
import tempfile
|
|
13
|
+
from typing import Any
|
|
14
|
+
import urllib.request
|
|
15
|
+
|
|
16
|
+
from chatup.runtime._managed_artifact import safe_extract_zip
|
|
17
|
+
from chatup.utils.custom_logger import setup_logger
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
MAC_APPS = {
|
|
22
|
+
"chrome": {
|
|
23
|
+
"app": "Google Chrome",
|
|
24
|
+
"bundle": "Google Chrome.app",
|
|
25
|
+
"bundle_id": "com.google.Chrome",
|
|
26
|
+
"team_id": "EQHXZ8M8AV",
|
|
27
|
+
"url": "https://dl.google.com/chrome/mac/universal/stable/GGRO/googlechrome.dmg",
|
|
28
|
+
"format": "dmg",
|
|
29
|
+
},
|
|
30
|
+
"iterm": {
|
|
31
|
+
"app": "iTerm2",
|
|
32
|
+
"bundle": "iTerm.app",
|
|
33
|
+
"bundle_id": "com.googlecode.iterm2",
|
|
34
|
+
"team_id": "H7V7XYVQ7D",
|
|
35
|
+
"url": "https://iterm2.com/downloads/stable/latest",
|
|
36
|
+
"format": "zip",
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
LINUX_PACKAGES = {
|
|
40
|
+
"apt-get": ("deb", ["install"]),
|
|
41
|
+
"dnf": ("rpm", ["install"]),
|
|
42
|
+
"yum": ("rpm", ["install"]),
|
|
43
|
+
"zypper": ("rpm", ["install"]),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _mac_app_dirs() -> tuple[Path, Path]:
|
|
48
|
+
return Path("/Applications"), Path.home() / "Applications"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _is_root() -> bool:
|
|
52
|
+
return hasattr(os, "geteuid") and os.geteuid() == 0
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def plan_desktop_install(
|
|
56
|
+
app: str, *, sudo: bool = False, yes: bool = False,
|
|
57
|
+
) -> dict[str, Any]:
|
|
58
|
+
"""Describe the current platform's installation without writes or processes."""
|
|
59
|
+
spec = MAC_APPS[app]
|
|
60
|
+
system = platform.system()
|
|
61
|
+
plan = {"app": spec["app"], "platform": system}
|
|
62
|
+
if app == "iterm" and system != "Darwin":
|
|
63
|
+
raise RuntimeError(f"iTerm2 is supported on macOS only; current platform: {system}.")
|
|
64
|
+
if system == "Darwin":
|
|
65
|
+
if platform.machine().lower() not in {"arm64", "aarch64", "x86_64", "amd64"}:
|
|
66
|
+
raise RuntimeError(f"Unsupported macOS architecture: {platform.machine()}.")
|
|
67
|
+
directories = _mac_app_dirs()
|
|
68
|
+
existing = next((d / spec["bundle"] for d in directories if (d / spec["bundle"]).exists()), None)
|
|
69
|
+
directory = directories[0] if os.access(directories[0], os.W_OK) else directories[1]
|
|
70
|
+
return {**plan, **spec, "method": "macos", "path": str(existing or directory / spec["bundle"])}
|
|
71
|
+
if system == "Windows":
|
|
72
|
+
selector = ["--id", "Google.Chrome", "--exact", "--source", "winget", "--disable-interactivity"]
|
|
73
|
+
command = ["winget", "install", *selector, "--silent", "--no-upgrade"]
|
|
74
|
+
verify = ["winget", "list", *selector]
|
|
75
|
+
if yes:
|
|
76
|
+
command += ["--accept-package-agreements", "--accept-source-agreements"]
|
|
77
|
+
verify += ["--accept-source-agreements"]
|
|
78
|
+
return {**plan, "method": "winget", "command": command, "verify_command": verify}
|
|
79
|
+
if system == "Linux":
|
|
80
|
+
if platform.machine().lower() not in {"x86_64", "amd64"}:
|
|
81
|
+
raise RuntimeError("Google Chrome for Linux requires x86_64; Google does not provide a Linux ARM package.")
|
|
82
|
+
manager = next((name for name in LINUX_PACKAGES if shutil.which(name)), None)
|
|
83
|
+
if not manager:
|
|
84
|
+
raise RuntimeError("Google Chrome requires apt-get, dnf, yum or zypper on Linux. See https://www.google.com/chrome/.")
|
|
85
|
+
extension, action = LINUX_PACKAGES[manager]
|
|
86
|
+
filename = f"google-chrome-stable_current_{'amd64' if extension == 'deb' else 'x86_64'}.{extension}"
|
|
87
|
+
command = [manager]
|
|
88
|
+
if manager == "zypper" and yes:
|
|
89
|
+
command += ["--non-interactive"]
|
|
90
|
+
command += action
|
|
91
|
+
if manager != "zypper" and yes:
|
|
92
|
+
command += ["-y"]
|
|
93
|
+
command += ["{package}"]
|
|
94
|
+
if sudo and not _is_root():
|
|
95
|
+
command.insert(0, "sudo")
|
|
96
|
+
return {**plan, "method": "linux", "manager": manager, "command": command,
|
|
97
|
+
"url": f"https://dl.google.com/linux/direct/{filename}", "filename": filename}
|
|
98
|
+
raise RuntimeError(f"Google Chrome installation is not supported on {system}.")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _run(command: list[str], *, capture: bool = True, timeout: int = 1800, check: bool = True):
|
|
102
|
+
logger.debug("Running: %s", command)
|
|
103
|
+
result = subprocess.run(
|
|
104
|
+
command, capture_output=capture, text=True, encoding="utf-8", errors="replace",
|
|
105
|
+
timeout=timeout, check=False,
|
|
106
|
+
)
|
|
107
|
+
if check and result.returncode:
|
|
108
|
+
detail = (result.stderr or result.stdout or "See installer output.").strip()
|
|
109
|
+
raise RuntimeError(f"{Path(command[0]).name} failed (exit {result.returncode}): {detail[-2000:]}")
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _download(url: str, destination: Path) -> None:
|
|
114
|
+
logger.info("Downloading %s", url)
|
|
115
|
+
request = urllib.request.Request(url, headers={"User-Agent": "ChatUp desktop installer"})
|
|
116
|
+
with urllib.request.urlopen(request, timeout=120) as response, destination.open("wb") as output:
|
|
117
|
+
if not response.url.startswith("https://"):
|
|
118
|
+
raise RuntimeError("Refusing a download redirected away from HTTPS.")
|
|
119
|
+
total = 0
|
|
120
|
+
expected = int(response.headers.get("Content-Length", "0"))
|
|
121
|
+
limit = 1024 * 1024 * 1024
|
|
122
|
+
if expected > limit:
|
|
123
|
+
raise RuntimeError("Desktop installer exceeds the 1 GiB download limit.")
|
|
124
|
+
for chunk in iter(lambda: response.read(1024 * 1024), b""):
|
|
125
|
+
total += len(chunk)
|
|
126
|
+
if total > limit:
|
|
127
|
+
raise RuntimeError("Desktop installer exceeds the 1 GiB download limit.")
|
|
128
|
+
output.write(chunk)
|
|
129
|
+
if not total or (expected and total != expected):
|
|
130
|
+
raise RuntimeError("Desktop installer download is incomplete; retry the command.")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _cache_dir() -> Path:
|
|
134
|
+
home = Path(os.environ.get("CHATARCH_HOME", str(Path.home() / ".chatarch"))).expanduser()
|
|
135
|
+
cache = home / "cache" / "desktop"
|
|
136
|
+
cache.mkdir(parents=True, exist_ok=True)
|
|
137
|
+
return cache
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _mac_bundle(plan: dict[str, Any], path: Path) -> dict[str, str]:
|
|
141
|
+
try:
|
|
142
|
+
with (path / "Contents" / "Info.plist").open("rb") as stream:
|
|
143
|
+
info = plistlib.load(stream)
|
|
144
|
+
executable_name = info.get("CFBundleExecutable", "")
|
|
145
|
+
binary = path / "Contents" / "MacOS" / executable_name
|
|
146
|
+
version = info.get("CFBundleShortVersionString", "")
|
|
147
|
+
if (info.get("CFBundleIdentifier") != plan["bundle_id"] or not version
|
|
148
|
+
or not executable_name or Path(executable_name).name != executable_name
|
|
149
|
+
or not binary.is_file() or not os.access(binary, os.X_OK)):
|
|
150
|
+
raise ValueError("bundle identity, version or executable is invalid")
|
|
151
|
+
minimum = info.get("LSMinimumSystemVersion", "")
|
|
152
|
+
current = platform.mac_ver()[0]
|
|
153
|
+
if minimum and current:
|
|
154
|
+
def version_tuple(value):
|
|
155
|
+
return tuple(int(part) for part in value.split(".")) + (0,) * (3 - len(value.split(".")))
|
|
156
|
+
if version_tuple(current) < version_tuple(minimum):
|
|
157
|
+
raise ValueError(f"requires macOS {minimum} or later; current: {current}")
|
|
158
|
+
except (OSError, ValueError, plistlib.InvalidFileException) as exc:
|
|
159
|
+
raise RuntimeError(f"Cannot verify {plan['app']} at {path}: {exc}. Existing apps are never overwritten.") from exc
|
|
160
|
+
requirement = f'=anchor apple generic and certificate leaf[subject.OU] = "{plan["team_id"]}"'
|
|
161
|
+
_run(["/usr/bin/codesign", "--verify", "--deep", "--strict", "-R", requirement, str(path)], timeout=120)
|
|
162
|
+
return {"path": str(path), "binary": str(binary), "version": str(version)}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _install_macos(plan: dict[str, Any]) -> dict[str, Any]:
|
|
166
|
+
target = Path(plan["path"])
|
|
167
|
+
if target.exists() or target.is_symlink():
|
|
168
|
+
logger.info("Checking existing %s", target)
|
|
169
|
+
return {**plan, **_mac_bundle(plan, target), "status": "already_installed", "verified": True}
|
|
170
|
+
work = Path(tempfile.mkdtemp(prefix="install-", dir=_cache_dir()))
|
|
171
|
+
mount = work / "volume"
|
|
172
|
+
mounted = False
|
|
173
|
+
try:
|
|
174
|
+
archive = work / f"download.{plan['format']}"
|
|
175
|
+
_download(plan["url"], archive)
|
|
176
|
+
if plan["format"] == "dmg":
|
|
177
|
+
mount.mkdir()
|
|
178
|
+
_run(["/usr/bin/hdiutil", "attach", str(archive), "-mountpoint", str(mount),
|
|
179
|
+
"-nobrowse", "-readonly", "-noautoopen"], timeout=120)
|
|
180
|
+
mounted = True
|
|
181
|
+
source = mount / plan["bundle"]
|
|
182
|
+
else:
|
|
183
|
+
source = work / "unpacked" / plan["bundle"]
|
|
184
|
+
safe_extract_zip(archive, source.parent, label=plan["app"])
|
|
185
|
+
logger.info("Verifying %s publisher signature and app bundle", plan["app"])
|
|
186
|
+
_mac_bundle(plan, source)
|
|
187
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
188
|
+
# Stage on the target filesystem so the final app appears in one rename.
|
|
189
|
+
with tempfile.TemporaryDirectory(prefix=".chatup-", dir=target.parent) as staging:
|
|
190
|
+
staged = Path(staging) / plan["bundle"]
|
|
191
|
+
logger.info("Installing %s", target)
|
|
192
|
+
_run(["/usr/bin/ditto", str(source), str(staged)])
|
|
193
|
+
_mac_bundle(plan, staged)
|
|
194
|
+
if target.exists() or target.is_symlink():
|
|
195
|
+
raise RuntimeError(f"App appeared during installation; refusing to overwrite {target}.")
|
|
196
|
+
staged.rename(target)
|
|
197
|
+
details = _mac_bundle(plan, target)
|
|
198
|
+
return {**plan, **details, "status": "installed", "verified": True}
|
|
199
|
+
finally:
|
|
200
|
+
if mounted:
|
|
201
|
+
detached = _run(["/usr/bin/hdiutil", "detach", str(mount)], timeout=120, check=False)
|
|
202
|
+
if detached.returncode:
|
|
203
|
+
_run(["/usr/bin/hdiutil", "detach", "-force", str(mount)], timeout=120, check=False)
|
|
204
|
+
# Never recursively clean up a still-mounted disk image.
|
|
205
|
+
if not os.path.ismount(mount):
|
|
206
|
+
shutil.rmtree(work)
|
|
207
|
+
else:
|
|
208
|
+
logger.warning("Disk image still mounted at %s; installer cache retained at %s", mount, work)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _linux_chrome() -> dict[str, str] | None:
|
|
212
|
+
binary = shutil.which("google-chrome-stable") or shutil.which("google-chrome")
|
|
213
|
+
if not binary:
|
|
214
|
+
return None
|
|
215
|
+
result = _run([binary, "--version"], timeout=30, check=False)
|
|
216
|
+
match = re.match(r"Google Chrome (\d+(?:\.\d+)+)", result.stdout.strip())
|
|
217
|
+
if result.returncode or not match:
|
|
218
|
+
raise RuntimeError(f"Cannot verify regular Google Chrome at {binary}.")
|
|
219
|
+
return {"binary": binary, "version": match.group(1)}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _install_linux(plan: dict[str, Any], *, sudo: bool) -> dict[str, Any]:
|
|
223
|
+
existing = _linux_chrome()
|
|
224
|
+
if existing:
|
|
225
|
+
return {**plan, **existing, "status": "already_installed", "verified": True}
|
|
226
|
+
if not _is_root() and not sudo:
|
|
227
|
+
raise RuntimeError("Installing Google Chrome on Linux requires root. Re-run: chatup chrome --sudo (optionally --yes).")
|
|
228
|
+
if not _is_root() and not shutil.which("sudo"):
|
|
229
|
+
raise RuntimeError("sudo is unavailable; run chatup chrome as root.")
|
|
230
|
+
with tempfile.TemporaryDirectory(prefix="chrome-", dir=_cache_dir()) as staging:
|
|
231
|
+
package = Path(staging) / plan["filename"]
|
|
232
|
+
_download(plan["url"], package)
|
|
233
|
+
command = [str(package) if arg == "{package}" else arg for arg in plan["command"]]
|
|
234
|
+
logger.info("Installing Google Chrome with %s", plan["manager"])
|
|
235
|
+
_run(command, capture=False)
|
|
236
|
+
installed = _linux_chrome()
|
|
237
|
+
if not installed:
|
|
238
|
+
raise RuntimeError("Google Chrome installation verification failed: browser executable not found.")
|
|
239
|
+
return {**plan, **installed, "status": "installed", "verified": True}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _install_windows(plan: dict[str, Any]) -> dict[str, Any]:
|
|
243
|
+
executable = shutil.which("winget")
|
|
244
|
+
if not executable:
|
|
245
|
+
raise RuntimeError("WinGet is required to install Google Chrome: https://aka.ms/getwinget")
|
|
246
|
+
|
|
247
|
+
def installed():
|
|
248
|
+
result = _run([executable, *plan["verify_command"][1:]], timeout=120, check=False)
|
|
249
|
+
return result.returncode == 0 and re.search(r"\bGoogle\.Chrome\b", result.stdout) is not None
|
|
250
|
+
|
|
251
|
+
if installed():
|
|
252
|
+
return {**plan, "status": "already_installed", "verified": True}
|
|
253
|
+
logger.info("Installing Google Chrome with WinGet; use --yes to accept source/package agreements")
|
|
254
|
+
_run([executable, *plan["command"][1:]], capture=False)
|
|
255
|
+
if not installed():
|
|
256
|
+
raise RuntimeError("Google Chrome installation verification failed: no matching WinGet package record.")
|
|
257
|
+
return {**plan, "status": "installed", "verified": True}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def setup_desktop_app(
|
|
261
|
+
app: str, *, dry_run: bool = False, sudo: bool = False, yes: bool = False,
|
|
262
|
+
log_level: str = "INFO",
|
|
263
|
+
) -> dict[str, Any]:
|
|
264
|
+
"""Install a native app if absent and verify it; never launch or upgrade it."""
|
|
265
|
+
global logger
|
|
266
|
+
logger = setup_logger(__name__, log_level=log_level)
|
|
267
|
+
try:
|
|
268
|
+
plan = plan_desktop_install(app, sudo=sudo, yes=yes)
|
|
269
|
+
if dry_run:
|
|
270
|
+
return {**plan, "status": "planned", "verified": False}
|
|
271
|
+
logger.info("Checking %s installation on %s", plan["app"], plan["platform"])
|
|
272
|
+
if plan["method"] == "macos":
|
|
273
|
+
return _install_macos(plan)
|
|
274
|
+
if plan["method"] == "linux":
|
|
275
|
+
return _install_linux(plan, sudo=sudo)
|
|
276
|
+
return _install_windows(plan)
|
|
277
|
+
except (OSError, ValueError, RuntimeError, subprocess.SubprocessError) as exc:
|
|
278
|
+
logger.error("%s installation failed: %s", MAC_APPS[app]["app"], exc)
|
|
279
|
+
raise RuntimeError(f"{MAC_APPS[app]['app']} installation failed: {exc}") from exc
|
|
@@ -120,7 +120,7 @@ addkernel (){
|
|
|
120
120
|
|
|
121
121
|
# pip install
|
|
122
122
|
alias tspip="pip install -i https://pypi.tuna.tsinghua.edu.cn/simple"
|
|
123
|
-
alias
|
|
123
|
+
alias pypip='pip install -i https://pypi.python.org/simple'
|
|
124
124
|
|
|
125
125
|
# kill process
|
|
126
126
|
kport (){
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chatup
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.18
|
|
4
4
|
Summary: ChatArch setup CLI extracted from ChatTool
|
|
5
5
|
Author-email: rexwzh <1073853456@qq.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -76,6 +76,17 @@ chatup nginx proxy-pass ./gitea-local.conf --set SERVER_NAME=gitea.local.example
|
|
|
76
76
|
chatup crs --install-dir ~/.chatarch/crs/local --port 12392 --redis-port 6379
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
+
## Chrome 与 iTerm2
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
chatup chrome --dry-run
|
|
83
|
+
chatup chrome # 按系统安装普通 Google Chrome
|
|
84
|
+
chatup iterm # 安装 iTerm2,仅限 macOS
|
|
85
|
+
chatup chrome --sudo --yes # Linux:允许提权并确认包安装
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
macOS 直接下载官方 DMG/ZIP 并校验开发者签名,无需 Homebrew;默认装到 `/Applications`,不可写时使用 `~/Applications`。Windows 使用 WinGet `Google.Chrome`;Linux x86_64 使用官方 DEB/RPM 与 apt/dnf/yum/zypper。已安装时验证并复用,不改默认浏览器、终端或用户配置。自动化浏览器仍使用 `chatup chrome-for-testing`。详见[安装约定](https://arch.gh.wzhecnu.cn/ChatUp/commands/#chrome-iterm)。
|
|
89
|
+
|
|
79
90
|
## ChatGPT / Codex 桌面应用
|
|
80
91
|
|
|
81
92
|
```bash
|
|
@@ -94,11 +105,11 @@ chatup chatgpt --yes # Windows 首次安装时接受 Store 协议
|
|
|
94
105
|
| --- | --- |
|
|
95
106
|
| 基础运行环境 | `doctor`、`uv`、`nodejs`、`docker`、`zsh`、`chrome-for-testing`、`chromedriver`、`playwright`、`frp` |
|
|
96
107
|
| 工作区脚手架 | `workspace` |
|
|
97
|
-
|
|
|
108
|
+
| 桌面应用 | `chrome`、`iterm`(仅 macOS)、`chatgpt`(含 Codex) |
|
|
98
109
|
| 本地服务安装 | `gitea`、`discourse`、`zulip`、`mysql`、`nginx`、`crs` |
|
|
99
110
|
| Agent 工具链 | `cc-connect`、`claude`、`codex`、`cursor-agent`、`opencode`、`hermes`、`lark-cli` |
|
|
100
111
|
|
|
101
|
-
|
|
112
|
+
桌面应用使用系统原生应用目录;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`。
|
|
102
113
|
|
|
103
114
|
## 开发
|
|
104
115
|
|
|
@@ -17,6 +17,7 @@ src/chatup.egg-info/top_level.txt
|
|
|
17
17
|
src/chatup/commands/__init__.py
|
|
18
18
|
src/chatup/commands/chrome_for_testing.py
|
|
19
19
|
src/chatup/commands/chromedriver.py
|
|
20
|
+
src/chatup/commands/desktop.py
|
|
20
21
|
src/chatup/commands/playwright.py
|
|
21
22
|
src/chatup/interaction/__init__.py
|
|
22
23
|
src/chatup/interaction/command_schema.py
|
|
@@ -35,6 +36,7 @@ src/chatup/setup/cli.py
|
|
|
35
36
|
src/chatup/setup/codex.py
|
|
36
37
|
src/chatup/setup/crs.py
|
|
37
38
|
src/chatup/setup/cursor_agent.py
|
|
39
|
+
src/chatup/setup/desktop.py
|
|
38
40
|
src/chatup/setup/discourse.py
|
|
39
41
|
src/chatup/setup/docker.py
|
|
40
42
|
src/chatup/setup/elements.py
|
|
@@ -90,4 +92,5 @@ tests/test_twikoo_setup.py
|
|
|
90
92
|
tests/test_uv_activation.py
|
|
91
93
|
tests/test_version.py
|
|
92
94
|
tests/test_windows_compat.py
|
|
93
|
-
tests/test_workspace_setup.py
|
|
95
|
+
tests/test_workspace_setup.py
|
|
96
|
+
tests/test_zsh_alias_names.py
|
|
@@ -14,7 +14,9 @@ def test_browser_artifacts_have_independent_top_level_backends():
|
|
|
14
14
|
assert "chrome-for-testing" in commands
|
|
15
15
|
assert "chromedriver" in commands
|
|
16
16
|
assert "playwright" in commands
|
|
17
|
-
assert "chrome"
|
|
17
|
+
assert "chrome" in commands
|
|
18
|
+
assert not isinstance(main.commands["chrome"], click.Group)
|
|
19
|
+
assert "regular Google Chrome" in main.commands["chrome"].help
|
|
18
20
|
assert "browser" not in commands
|
|
19
21
|
assert "chromium" not in commands
|
|
20
22
|
|
|
@@ -80,5 +80,6 @@ def test_cli_tree_is_in_mkdocs_nav_and_backend_contracts_are_complete():
|
|
|
80
80
|
text = (ROOT / "docs" / name).read_text(encoding="utf-8")
|
|
81
81
|
missing = sorted(item for item in required if item not in text)
|
|
82
82
|
assert not missing, f"{name} is missing backend contract terms: {missing}"
|
|
83
|
-
assert "
|
|
83
|
+
assert "chatup chrome" in text
|
|
84
|
+
assert "chatup iterm" in text
|
|
84
85
|
assert "from chatup.chrome import" not in text
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from chatup.setup.zsh import render_zsh_aliases
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_pip_install_alias_rename_preserves_both_urls():
|
|
5
|
+
lines = render_zsh_aliases().splitlines()
|
|
6
|
+
assert 'alias tspip="pip install -i https://pypi.tuna.tsinghua.edu.cn/simple"' in lines
|
|
7
|
+
assert "alias pypip='pip install -i https://pypi.python.org/simple'" in lines
|
|
8
|
+
assert not any(line.startswith("alias pypi=") for line in lines)
|
|
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.16 → chatup-0.2.18}/src/chatup/setup/workspace/templates/default/en/discussion/README.md
RENAMED
|
File without changes
|
{chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/workspace/templates/default/en/projects/README.md
RENAMED
|
File without changes
|
|
File without changes
|
{chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/workspace/templates/default/zh/discussion/README.md
RENAMED
|
File without changes
|
{chatup-0.2.16 → chatup-0.2.18}/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
|