chatup 0.2.6__tar.gz → 0.2.7__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.6/src/chatup.egg-info → chatup-0.2.7}/PKG-INFO +6 -4
- {chatup-0.2.6 → chatup-0.2.7}/README.md +5 -3
- {chatup-0.2.6 → chatup-0.2.7}/pyproject.toml +2 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/__init__.py +1 -1
- chatup-0.2.7/src/chatup/config.py +122 -0
- chatup-0.2.7/src/chatup/setup/discourse.py +187 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/elements.py +205 -1
- chatup-0.2.7/src/chatup/setup/service_credentials.py +94 -0
- chatup-0.2.7/src/chatup/setup/zulip.py +295 -0
- {chatup-0.2.6 → chatup-0.2.7/src/chatup.egg-info}/PKG-INFO +6 -4
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup.egg-info/SOURCES.txt +4 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup.egg-info/entry_points.txt +2 -0
- {chatup-0.2.6 → chatup-0.2.7}/tests/test_pyproject_metadata.py +16 -1
- chatup-0.2.7/tests/test_service_installers.py +105 -0
- {chatup-0.2.6 → chatup-0.2.7}/tests/test_setup_cli.py +4 -0
- {chatup-0.2.6 → chatup-0.2.7}/tests/test_version.py +2 -2
- chatup-0.2.6/src/chatup/config.py +0 -45
- {chatup-0.2.6 → chatup-0.2.7}/LICENSE +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/setup.cfg +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/chrome_for_testing.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/chromedriver.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/cli.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/commands/__init__.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/commands/chrome_for_testing.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/commands/chromedriver.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/commands/playwright.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/const.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/interaction/__init__.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/interaction/command_schema.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/interaction/policy.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/interaction/warnings.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/playwright.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/runtime/__init__.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/runtime/_managed_artifact.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/runtime/chrome_for_testing.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/runtime/chromedriver.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/runtime/playwright.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/__init__.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/assets/hermes/install.sh +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/assets/nvm.sh +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/cc_connect.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/claude.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/cli.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/codex.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/crs.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/cursor_agent.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/docker.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/frp.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/gitea.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/hermes.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/lark_cli.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/mode_prompt.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/mysql.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/nginx.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/nodejs.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/opencode.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/shell_rc.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/uv.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/workspace/__init__.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/workspace/cli.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/workspace/core.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/workspace/options.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/workspace/render.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/workspace/templates/default/en/AGENTS.md +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/workspace/templates/default/en/discussion/README.md +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/workspace/templates/default/en/projects/README.md +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/workspace/templates/default/zh/AGENTS.md +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/workspace/templates/default/zh/discussion/README.md +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/workspace/templates/default/zh/projects/README.md +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/workspace.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/zsh.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/utils/__init__.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/utils/custom_logger.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup/utils/pathing.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup.egg-info/dependency_links.txt +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup.egg-info/requires.txt +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/src/chatup.egg-info/top_level.txt +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/tests/test_browser_backend_cli.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/tests/test_chrome_for_testing.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/tests/test_chromedriver.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/tests/test_cli_tree_docs.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/tests/test_crs_setup.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/tests/test_cursor_agent_setup.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/tests/test_playwright.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/tests/test_playwright_cli.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/tests/test_publish_workflow.py +0 -0
- {chatup-0.2.6 → chatup-0.2.7}/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.7
|
|
4
4
|
Summary: ChatArch setup CLI extracted from ChatTool
|
|
5
5
|
Author-email: rexwzh <1073853456@qq.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -41,7 +41,7 @@ ChatUp 是 ChatArch 的独立环境与工具安装 CLI。它承接原来 `chatto
|
|
|
41
41
|
| 查看命令分组、参数边界和服务默认路径 | `docs/commands.md` |
|
|
42
42
|
| 校对哪些 ChatArch setup 流程已经有一等命令 | `docs/capability-map.md` |
|
|
43
43
|
| 理解 `chatup workspace` 创建的目录和项目记录约定 | `docs/workspace.md` |
|
|
44
|
-
| 安装 ChatTea-compatible Gitea、ChatData-compatible MySQL、user-level NGINX 和 CRS | `docs/commands.md` |
|
|
44
|
+
| 安装 ChatTea-compatible Gitea、Discourse/Zulip 社区服务配置、ChatData-compatible MySQL、user-level NGINX 和 CRS | `docs/commands.md` |
|
|
45
45
|
|
|
46
46
|
## 快速开始
|
|
47
47
|
|
|
@@ -58,6 +58,8 @@ chatup workspace default ~/Playground
|
|
|
58
58
|
|
|
59
59
|
```bash
|
|
60
60
|
chatup gitea --force
|
|
61
|
+
chatup discourse -e discourse-prod --hostname discourse.public.wzhecnu.cn
|
|
62
|
+
chatup zulip -e zulip-prod --external-host zulip.public.wzhecnu.cn --port 3095
|
|
61
63
|
chatup mysql
|
|
62
64
|
chatup nginx
|
|
63
65
|
chatup nginx proxy-pass ./gitea-local.conf --set SERVER_NAME=gitea.local.example.invalid --set PROXY_PASS=http://127.0.0.1:3000
|
|
@@ -70,10 +72,10 @@ chatup crs --install-dir ~/.chatarch/crs/local --port 12392 --redis-port 6379
|
|
|
70
72
|
| --- | --- |
|
|
71
73
|
| 基础运行环境 | `doctor`、`uv`、`nodejs`、`docker`、`zsh`、`chrome-for-testing`、`chromedriver`、`playwright`、`frp` |
|
|
72
74
|
| 工作区脚手架 | `workspace` |
|
|
73
|
-
| 本地服务安装 | `gitea`、`mysql`、`nginx`、`crs` |
|
|
75
|
+
| 本地服务安装 | `gitea`、`discourse`、`zulip`、`mysql`、`nginx`、`crs` |
|
|
74
76
|
| Agent 工具链 | `cc-connect`、`claude`、`codex`、`cursor-agent`、`opencode`、`hermes`、`lark-cli` |
|
|
75
77
|
|
|
76
|
-
所有新增安装项默认目录都收敛到 `~/.chatarch/...`,例如 `~/.chatarch/chrome-for-testing`、`~/.chatarch/chromedriver`、`~/.chatarch/playwright`、`~/.chatarch/chattea`、`~/.chatarch/chatdata`、`~/.chatarch/nginx` 和 `~/.chatarch/crs/local`。`cursor-agent` 额外注册 ChatEnv `CursorAgent` profile,并支持 `-e/--env` 从 env 文件或 profile
|
|
78
|
+
所有新增安装项默认目录都收敛到 `~/.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 快速配置。更完整的能力边界见 `docs/capability-map.md`。
|
|
77
79
|
|
|
78
80
|
## 开发
|
|
79
81
|
|
|
@@ -15,7 +15,7 @@ ChatUp 是 ChatArch 的独立环境与工具安装 CLI。它承接原来 `chatto
|
|
|
15
15
|
| 查看命令分组、参数边界和服务默认路径 | `docs/commands.md` |
|
|
16
16
|
| 校对哪些 ChatArch setup 流程已经有一等命令 | `docs/capability-map.md` |
|
|
17
17
|
| 理解 `chatup workspace` 创建的目录和项目记录约定 | `docs/workspace.md` |
|
|
18
|
-
| 安装 ChatTea-compatible Gitea、ChatData-compatible MySQL、user-level NGINX 和 CRS | `docs/commands.md` |
|
|
18
|
+
| 安装 ChatTea-compatible Gitea、Discourse/Zulip 社区服务配置、ChatData-compatible MySQL、user-level NGINX 和 CRS | `docs/commands.md` |
|
|
19
19
|
|
|
20
20
|
## 快速开始
|
|
21
21
|
|
|
@@ -32,6 +32,8 @@ chatup workspace default ~/Playground
|
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
34
|
chatup gitea --force
|
|
35
|
+
chatup discourse -e discourse-prod --hostname discourse.public.wzhecnu.cn
|
|
36
|
+
chatup zulip -e zulip-prod --external-host zulip.public.wzhecnu.cn --port 3095
|
|
35
37
|
chatup mysql
|
|
36
38
|
chatup nginx
|
|
37
39
|
chatup nginx proxy-pass ./gitea-local.conf --set SERVER_NAME=gitea.local.example.invalid --set PROXY_PASS=http://127.0.0.1:3000
|
|
@@ -44,10 +46,10 @@ chatup crs --install-dir ~/.chatarch/crs/local --port 12392 --redis-port 6379
|
|
|
44
46
|
| --- | --- |
|
|
45
47
|
| 基础运行环境 | `doctor`、`uv`、`nodejs`、`docker`、`zsh`、`chrome-for-testing`、`chromedriver`、`playwright`、`frp` |
|
|
46
48
|
| 工作区脚手架 | `workspace` |
|
|
47
|
-
| 本地服务安装 | `gitea`、`mysql`、`nginx`、`crs` |
|
|
49
|
+
| 本地服务安装 | `gitea`、`discourse`、`zulip`、`mysql`、`nginx`、`crs` |
|
|
48
50
|
| Agent 工具链 | `cc-connect`、`claude`、`codex`、`cursor-agent`、`opencode`、`hermes`、`lark-cli` |
|
|
49
51
|
|
|
50
|
-
所有新增安装项默认目录都收敛到 `~/.chatarch/...`,例如 `~/.chatarch/chrome-for-testing`、`~/.chatarch/chromedriver`、`~/.chatarch/playwright`、`~/.chatarch/chattea`、`~/.chatarch/chatdata`、`~/.chatarch/nginx` 和 `~/.chatarch/crs/local`。`cursor-agent` 额外注册 ChatEnv `CursorAgent` profile,并支持 `-e/--env` 从 env 文件或 profile
|
|
52
|
+
所有新增安装项默认目录都收敛到 `~/.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 快速配置。更完整的能力边界见 `docs/capability-map.md`。
|
|
51
53
|
|
|
52
54
|
## 开发
|
|
53
55
|
|
|
@@ -40,6 +40,8 @@ chatup = "chatup.cli:main"
|
|
|
40
40
|
|
|
41
41
|
[project.entry-points."chatenv.configs"]
|
|
42
42
|
cursor-agent = "chatup.config:CursorAgentConfig"
|
|
43
|
+
discourse = "chatup.config:DiscourseAdminConfig"
|
|
44
|
+
zulip = "chatup.config:ZulipAdminConfig"
|
|
43
45
|
|
|
44
46
|
[tool.setuptools.dynamic]
|
|
45
47
|
version = {attr = "chatup.__version__"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from chatenv.fields import BaseEnvConfig, EnvField
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CursorAgentConfig(BaseEnvConfig):
|
|
7
|
+
"""ChatEnv schema for Cursor Agent credentials used by ChatUp setup."""
|
|
8
|
+
|
|
9
|
+
_title = "Cursor Agent"
|
|
10
|
+
_aliases = ["cursor-agent", "cursor_agent", "cursor"]
|
|
11
|
+
_storage_dir = "CursorAgent"
|
|
12
|
+
|
|
13
|
+
CURSOR_ACCESS_TOKEN = EnvField(
|
|
14
|
+
"CURSOR_ACCESS_TOKEN",
|
|
15
|
+
desc="Cursor Agent access token copied into Cursor auth.json by chatup cursor-agent.",
|
|
16
|
+
is_sensitive=True,
|
|
17
|
+
)
|
|
18
|
+
CURSOR_REFRESH_TOKEN = EnvField(
|
|
19
|
+
"CURSOR_REFRESH_TOKEN",
|
|
20
|
+
desc="Cursor Agent refresh token copied into Cursor auth.json by chatup cursor-agent.",
|
|
21
|
+
is_sensitive=True,
|
|
22
|
+
)
|
|
23
|
+
CURSOR_CREDENTIAL_STORE = EnvField(
|
|
24
|
+
"CURSOR_CREDENTIAL_STORE",
|
|
25
|
+
default="native",
|
|
26
|
+
desc="Cursor Agent credential store mode: native or file-wrapper.",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def test(cls) -> None:
|
|
31
|
+
"""Run a non-network schema smoke for chatenv test."""
|
|
32
|
+
|
|
33
|
+
print(f"Testing {cls._title}...")
|
|
34
|
+
fields = cls.get_fields()
|
|
35
|
+
expected = {
|
|
36
|
+
"CURSOR_ACCESS_TOKEN",
|
|
37
|
+
"CURSOR_REFRESH_TOKEN",
|
|
38
|
+
"CURSOR_CREDENTIAL_STORE",
|
|
39
|
+
}
|
|
40
|
+
missing = expected.difference(fields)
|
|
41
|
+
if missing:
|
|
42
|
+
raise RuntimeError(f"Missing Cursor Agent config field(s): {', '.join(sorted(missing))}")
|
|
43
|
+
if not fields["CURSOR_ACCESS_TOKEN"].is_sensitive or not fields["CURSOR_REFRESH_TOKEN"].is_sensitive:
|
|
44
|
+
raise RuntimeError("Cursor Agent token fields must be marked sensitive")
|
|
45
|
+
print("✓ Cursor Agent ChatEnv schema is registered.")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class DiscourseAdminConfig(BaseEnvConfig):
|
|
49
|
+
"""ChatEnv schema for ChatUp-managed Discourse bootstrap admin credentials."""
|
|
50
|
+
|
|
51
|
+
_title = "Discourse Admin"
|
|
52
|
+
_aliases = ["discourse", "discourse-admin", "discourse_admin"]
|
|
53
|
+
_storage_dir = "Discourse"
|
|
54
|
+
|
|
55
|
+
DISCOURSE_ADMIN_USERNAME = EnvField(
|
|
56
|
+
"DISCOURSE_ADMIN_USERNAME",
|
|
57
|
+
desc="Initial Discourse administrator username used by ChatUp service setup.",
|
|
58
|
+
)
|
|
59
|
+
DISCOURSE_ADMIN_EMAIL = EnvField(
|
|
60
|
+
"DISCOURSE_ADMIN_EMAIL",
|
|
61
|
+
desc="Initial Discourse administrator email used by ChatUp service setup.",
|
|
62
|
+
)
|
|
63
|
+
DISCOURSE_ADMIN_PASSWORD = EnvField(
|
|
64
|
+
"DISCOURSE_ADMIN_PASSWORD",
|
|
65
|
+
desc="Initial Discourse administrator password used by ChatUp service setup.",
|
|
66
|
+
is_sensitive=True,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def test(cls) -> None:
|
|
71
|
+
fields = cls.get_fields()
|
|
72
|
+
expected = {
|
|
73
|
+
"DISCOURSE_ADMIN_USERNAME",
|
|
74
|
+
"DISCOURSE_ADMIN_EMAIL",
|
|
75
|
+
"DISCOURSE_ADMIN_PASSWORD",
|
|
76
|
+
}
|
|
77
|
+
missing = expected.difference(fields)
|
|
78
|
+
if missing:
|
|
79
|
+
raise RuntimeError(f"Missing Discourse admin config field(s): {', '.join(sorted(missing))}")
|
|
80
|
+
if not fields["DISCOURSE_ADMIN_PASSWORD"].is_sensitive:
|
|
81
|
+
raise RuntimeError("Discourse admin password field must be marked sensitive")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ZulipAdminConfig(BaseEnvConfig):
|
|
85
|
+
"""ChatEnv schema for ChatUp-managed Zulip bootstrap admin credentials."""
|
|
86
|
+
|
|
87
|
+
_title = "Zulip Admin"
|
|
88
|
+
_aliases = ["zulip", "zulip-admin", "zulip_admin"]
|
|
89
|
+
_storage_dir = "Zulip"
|
|
90
|
+
|
|
91
|
+
ZULIP_ADMIN_USERNAME = EnvField(
|
|
92
|
+
"ZULIP_ADMIN_USERNAME",
|
|
93
|
+
desc="Initial Zulip administrator username recorded by ChatUp service setup.",
|
|
94
|
+
)
|
|
95
|
+
ZULIP_ADMIN_EMAIL = EnvField(
|
|
96
|
+
"ZULIP_ADMIN_EMAIL",
|
|
97
|
+
desc="Initial Zulip administrator email used for ZULIP_ADMINISTRATOR.",
|
|
98
|
+
)
|
|
99
|
+
ZULIP_ADMIN_MAIL = EnvField(
|
|
100
|
+
"ZULIP_ADMIN_MAIL",
|
|
101
|
+
desc="Compatibility alias for the initial Zulip administrator email.",
|
|
102
|
+
)
|
|
103
|
+
ZULIP_ADMIN_PASSWORD = EnvField(
|
|
104
|
+
"ZULIP_ADMIN_PASSWORD",
|
|
105
|
+
desc="Initial Zulip administrator password recorded by ChatUp service setup.",
|
|
106
|
+
is_sensitive=True,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def test(cls) -> None:
|
|
111
|
+
fields = cls.get_fields()
|
|
112
|
+
expected = {
|
|
113
|
+
"ZULIP_ADMIN_USERNAME",
|
|
114
|
+
"ZULIP_ADMIN_EMAIL",
|
|
115
|
+
"ZULIP_ADMIN_MAIL",
|
|
116
|
+
"ZULIP_ADMIN_PASSWORD",
|
|
117
|
+
}
|
|
118
|
+
missing = expected.difference(fields)
|
|
119
|
+
if missing:
|
|
120
|
+
raise RuntimeError(f"Missing Zulip admin config field(s): {', '.join(sorted(missing))}")
|
|
121
|
+
if not fields["ZULIP_ADMIN_PASSWORD"].is_sensitive:
|
|
122
|
+
raise RuntimeError("Zulip admin password field must be marked sensitive")
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from chatenv.paths import get_paths
|
|
8
|
+
|
|
9
|
+
from chatup.config import DiscourseAdminConfig
|
|
10
|
+
from chatup.interaction import abort_if_force_without_tty, resolve_interactive_mode
|
|
11
|
+
from chatup.setup.service_credentials import (
|
|
12
|
+
load_chatenv_values,
|
|
13
|
+
require_values,
|
|
14
|
+
write_private_env_file,
|
|
15
|
+
)
|
|
16
|
+
from chatup.utils.custom_logger import setup_logger
|
|
17
|
+
|
|
18
|
+
logger = setup_logger("setup_discourse")
|
|
19
|
+
|
|
20
|
+
DEFAULT_HOME = get_paths().home_dir / "discourse"
|
|
21
|
+
DEFAULT_REPO = "https://github.com/discourse/discourse_docker.git"
|
|
22
|
+
DEFAULT_HOSTNAME = "discourse.public.wzhecnu.cn"
|
|
23
|
+
DEFAULT_PORT = 3088
|
|
24
|
+
|
|
25
|
+
ADMIN_KEYS = (
|
|
26
|
+
"DISCOURSE_ADMIN_USERNAME",
|
|
27
|
+
"DISCOURSE_ADMIN_EMAIL",
|
|
28
|
+
"DISCOURSE_ADMIN_PASSWORD",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _configure_logger(log_level="INFO"):
|
|
33
|
+
global logger
|
|
34
|
+
logger = setup_logger("setup_discourse", log_level=str(log_level).upper())
|
|
35
|
+
return logger
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def run_command(args: list[str], *, cwd: Path | None = None, timeout: int = 600) -> None:
|
|
39
|
+
subprocess.run([str(arg) for arg in args], cwd=str(cwd) if cwd else None, check=True, timeout=timeout)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def render_app_yml(*, hostname: str, port: int, shared_dir: Path, with_ai: bool) -> str:
|
|
43
|
+
plugins = ["https://github.com/discourse/docker_manager.git"]
|
|
44
|
+
if with_ai:
|
|
45
|
+
plugins.append("https://github.com/discourse/discourse-ai.git")
|
|
46
|
+
plugin_hooks = "\n".join(
|
|
47
|
+
"\n".join(
|
|
48
|
+
[
|
|
49
|
+
" - exec:",
|
|
50
|
+
" cd: $home/plugins",
|
|
51
|
+
" cmd:",
|
|
52
|
+
f" - git clone {url}",
|
|
53
|
+
]
|
|
54
|
+
)
|
|
55
|
+
for url in plugins
|
|
56
|
+
)
|
|
57
|
+
return f"""templates:
|
|
58
|
+
- templates/postgres.template.yml
|
|
59
|
+
- templates/redis.template.yml
|
|
60
|
+
- templates/web.template.yml
|
|
61
|
+
- templates/web.ratelimited.template.yml
|
|
62
|
+
|
|
63
|
+
expose:
|
|
64
|
+
- \"127.0.0.1:{port}:80\"
|
|
65
|
+
|
|
66
|
+
params:
|
|
67
|
+
db_default_text_search_config: \"pg_catalog.english\"
|
|
68
|
+
|
|
69
|
+
# Credentials and API/model secrets should be managed outside this file.
|
|
70
|
+
# Admin bootstrap values are written by ChatUp to ../secrets/admin.env.
|
|
71
|
+
env:
|
|
72
|
+
LANG: en_US.UTF-8
|
|
73
|
+
DISCOURSE_HOSTNAME: {hostname}
|
|
74
|
+
DISCOURSE_DEVELOPER_EMAILS: ''
|
|
75
|
+
DISCOURSE_SKIP_EMAIL_SETUP: 1
|
|
76
|
+
|
|
77
|
+
volumes:
|
|
78
|
+
- volume:
|
|
79
|
+
host: {shared_dir}
|
|
80
|
+
guest: /shared
|
|
81
|
+
- volume:
|
|
82
|
+
host: {shared_dir}/log/var-log
|
|
83
|
+
guest: /var/log
|
|
84
|
+
|
|
85
|
+
hooks:
|
|
86
|
+
after_code:
|
|
87
|
+
{plugin_hooks}
|
|
88
|
+
|
|
89
|
+
run:
|
|
90
|
+
- exec: echo \"Beginning of custom commands\"
|
|
91
|
+
- exec: echo \"End of custom commands\"
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def ensure_directories(home: Path) -> dict[str, Path]:
|
|
96
|
+
paths = {
|
|
97
|
+
"home": home,
|
|
98
|
+
"docker": home / "docker",
|
|
99
|
+
"containers": home / "docker" / "containers",
|
|
100
|
+
"secrets": home / "secrets",
|
|
101
|
+
"logs": home / "logs",
|
|
102
|
+
"backups": home / "backups",
|
|
103
|
+
"shared": home / "shared" / "standalone",
|
|
104
|
+
"shared_log": home / "shared" / "standalone" / "log" / "var-log",
|
|
105
|
+
}
|
|
106
|
+
for path in paths.values():
|
|
107
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
108
|
+
paths["secrets"].chmod(0o700)
|
|
109
|
+
return paths
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def clone_or_update_discourse_docker(docker_dir: Path, repo: str) -> None:
|
|
113
|
+
if (docker_dir / ".git").exists():
|
|
114
|
+
run_command(["git", "fetch", "--all", "--prune"], cwd=docker_dir)
|
|
115
|
+
run_command(["git", "pull", "--ff-only"], cwd=docker_dir)
|
|
116
|
+
return
|
|
117
|
+
if any(docker_dir.iterdir()):
|
|
118
|
+
raise click.ClickException(f"Discourse docker dir is not empty and not a git repo: {docker_dir}")
|
|
119
|
+
run_command(["git", "clone", repo, str(docker_dir)], timeout=900)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def setup_discourse(
|
|
123
|
+
*,
|
|
124
|
+
home: str | Path = DEFAULT_HOME,
|
|
125
|
+
hostname: str = DEFAULT_HOSTNAME,
|
|
126
|
+
port: int = DEFAULT_PORT,
|
|
127
|
+
env_ref: str | Path | None = None,
|
|
128
|
+
env_profile: str | None = None,
|
|
129
|
+
write_admin_env: bool = True,
|
|
130
|
+
write_app_yml: bool = True,
|
|
131
|
+
clone: bool = False,
|
|
132
|
+
repo: str = DEFAULT_REPO,
|
|
133
|
+
with_ai: bool = True,
|
|
134
|
+
force: bool = False,
|
|
135
|
+
interactive=None,
|
|
136
|
+
log_level="INFO",
|
|
137
|
+
):
|
|
138
|
+
_configure_logger(log_level)
|
|
139
|
+
usage = "Usage: chatup discourse [--home PATH] [-e ENV|--env-profile PROFILE]"
|
|
140
|
+
interactive, can_prompt, force_interactive, _, _ = resolve_interactive_mode(
|
|
141
|
+
interactive=interactive,
|
|
142
|
+
auto_prompt_condition=False,
|
|
143
|
+
)
|
|
144
|
+
abort_if_force_without_tty(force_interactive, can_prompt, usage)
|
|
145
|
+
|
|
146
|
+
home_path = Path(home).expanduser().resolve()
|
|
147
|
+
paths = ensure_directories(home_path)
|
|
148
|
+
|
|
149
|
+
values, source = load_chatenv_values(DiscourseAdminConfig, env_ref=env_ref, env_profile=env_profile)
|
|
150
|
+
if write_admin_env:
|
|
151
|
+
require_values(values, ADMIN_KEYS, label="Discourse admin")
|
|
152
|
+
admin_env = write_private_env_file(paths["secrets"] / "admin.env", values, ADMIN_KEYS)
|
|
153
|
+
else:
|
|
154
|
+
admin_env = None
|
|
155
|
+
|
|
156
|
+
app_yml = paths["containers"] / "app.yml"
|
|
157
|
+
if write_app_yml:
|
|
158
|
+
if app_yml.exists() and not force:
|
|
159
|
+
logger.info("Keeping existing Discourse app.yml at %s", app_yml)
|
|
160
|
+
else:
|
|
161
|
+
app_yml.write_text(
|
|
162
|
+
render_app_yml(hostname=hostname, port=port, shared_dir=paths["shared"], with_ai=with_ai),
|
|
163
|
+
encoding="utf-8",
|
|
164
|
+
)
|
|
165
|
+
app_yml.chmod(0o640)
|
|
166
|
+
|
|
167
|
+
if clone:
|
|
168
|
+
clone_or_update_discourse_docker(paths["docker"], repo)
|
|
169
|
+
|
|
170
|
+
result = {
|
|
171
|
+
"home": str(home_path),
|
|
172
|
+
"docker_dir": str(paths["docker"]),
|
|
173
|
+
"app_yml": str(app_yml),
|
|
174
|
+
"admin_env": str(admin_env) if admin_env else None,
|
|
175
|
+
"admin_source": source if write_admin_env else None,
|
|
176
|
+
"hostname": hostname,
|
|
177
|
+
"port": port,
|
|
178
|
+
"cloned": clone,
|
|
179
|
+
}
|
|
180
|
+
click.echo(f"Discourse home: {home_path}")
|
|
181
|
+
if write_admin_env:
|
|
182
|
+
click.echo(f"Discourse admin env: {admin_env} (managed from ChatEnv; values hidden)")
|
|
183
|
+
if write_app_yml:
|
|
184
|
+
click.echo(f"Discourse app.yml: {app_yml}")
|
|
185
|
+
if clone:
|
|
186
|
+
click.echo(f"Discourse docker repo: {paths['docker']}")
|
|
187
|
+
return result
|
|
@@ -17,6 +17,13 @@ from chatup.setup.crs import (
|
|
|
17
17
|
DEFAULT_REDIS_PORT,
|
|
18
18
|
)
|
|
19
19
|
from chatup.setup.docker import setup_docker
|
|
20
|
+
from chatup.setup.discourse import (
|
|
21
|
+
DEFAULT_HOME as DEFAULT_DISCOURSE_HOME,
|
|
22
|
+
DEFAULT_HOSTNAME as DEFAULT_DISCOURSE_HOSTNAME,
|
|
23
|
+
DEFAULT_PORT as DEFAULT_DISCOURSE_PORT,
|
|
24
|
+
DEFAULT_REPO as DEFAULT_DISCOURSE_REPO,
|
|
25
|
+
setup_discourse,
|
|
26
|
+
)
|
|
20
27
|
from chatup.setup.frp import setup_frp
|
|
21
28
|
from chatup.setup.gitea import (
|
|
22
29
|
DEFAULT_BASE_URL as DEFAULT_GITEA_BASE_URL,
|
|
@@ -46,10 +53,19 @@ from chatup.setup.nginx import (
|
|
|
46
53
|
setup_nginx,
|
|
47
54
|
)
|
|
48
55
|
from chatup.setup.opencode import setup_opencode
|
|
49
|
-
from chatup.setup.zsh import setup_zsh
|
|
50
56
|
from chatup.setup.nodejs import setup_nodejs
|
|
51
57
|
from chatup.setup.uv import DEFAULT_PYTHON_VERSION, DEFAULT_VENV_PATH, setup_uv
|
|
52
58
|
from chatup.setup.workspace import setup_workspace
|
|
59
|
+
from chatup.setup.zsh import setup_zsh
|
|
60
|
+
from chatup.setup.zulip import (
|
|
61
|
+
DEFAULT_BIND_ADDRESS as DEFAULT_ZULIP_BIND_ADDRESS,
|
|
62
|
+
DEFAULT_EXTERNAL_HOST as DEFAULT_ZULIP_EXTERNAL_HOST,
|
|
63
|
+
DEFAULT_HOME as DEFAULT_ZULIP_HOME,
|
|
64
|
+
DEFAULT_IMAGE as DEFAULT_ZULIP_IMAGE,
|
|
65
|
+
DEFAULT_PORT as DEFAULT_ZULIP_PORT,
|
|
66
|
+
DEFAULT_POSTGRES_IMAGE as DEFAULT_ZULIP_POSTGRES_IMAGE,
|
|
67
|
+
setup_zulip,
|
|
68
|
+
)
|
|
53
69
|
|
|
54
70
|
|
|
55
71
|
@dataclass(frozen=True)
|
|
@@ -186,6 +202,74 @@ def gitea_setup(
|
|
|
186
202
|
)
|
|
187
203
|
|
|
188
204
|
|
|
205
|
+
def discourse_setup(
|
|
206
|
+
home,
|
|
207
|
+
hostname,
|
|
208
|
+
port,
|
|
209
|
+
env,
|
|
210
|
+
env_profile,
|
|
211
|
+
write_admin_env,
|
|
212
|
+
write_app_yml,
|
|
213
|
+
clone,
|
|
214
|
+
repo,
|
|
215
|
+
with_ai,
|
|
216
|
+
force,
|
|
217
|
+
interactive,
|
|
218
|
+
log_level,
|
|
219
|
+
):
|
|
220
|
+
setup_discourse(
|
|
221
|
+
home=home,
|
|
222
|
+
hostname=hostname,
|
|
223
|
+
port=port,
|
|
224
|
+
env_ref=env,
|
|
225
|
+
env_profile=env_profile,
|
|
226
|
+
write_admin_env=write_admin_env,
|
|
227
|
+
write_app_yml=write_app_yml,
|
|
228
|
+
clone=clone,
|
|
229
|
+
repo=repo,
|
|
230
|
+
with_ai=with_ai,
|
|
231
|
+
force=force,
|
|
232
|
+
interactive=interactive,
|
|
233
|
+
log_level=log_level,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def zulip_setup(
|
|
238
|
+
home,
|
|
239
|
+
image,
|
|
240
|
+
postgres_image,
|
|
241
|
+
external_host,
|
|
242
|
+
bind_address,
|
|
243
|
+
port,
|
|
244
|
+
env,
|
|
245
|
+
env_profile,
|
|
246
|
+
write_admin_env,
|
|
247
|
+
write_compose,
|
|
248
|
+
pull,
|
|
249
|
+
start,
|
|
250
|
+
force,
|
|
251
|
+
interactive,
|
|
252
|
+
log_level,
|
|
253
|
+
):
|
|
254
|
+
setup_zulip(
|
|
255
|
+
home=home,
|
|
256
|
+
image=image,
|
|
257
|
+
postgres_image=postgres_image,
|
|
258
|
+
external_host=external_host,
|
|
259
|
+
bind_address=bind_address,
|
|
260
|
+
port=port,
|
|
261
|
+
env_ref=env,
|
|
262
|
+
env_profile=env_profile,
|
|
263
|
+
write_admin_env=write_admin_env,
|
|
264
|
+
write_compose=write_compose,
|
|
265
|
+
pull=pull,
|
|
266
|
+
start=start,
|
|
267
|
+
force=force,
|
|
268
|
+
interactive=interactive,
|
|
269
|
+
log_level=log_level,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
189
273
|
def mysql_setup(
|
|
190
274
|
version,
|
|
191
275
|
home,
|
|
@@ -602,6 +686,126 @@ SETUP_COMMAND_ELEMENTS = (
|
|
|
602
686
|
),
|
|
603
687
|
),
|
|
604
688
|
),
|
|
689
|
+
SetupCommandElement(
|
|
690
|
+
name="discourse",
|
|
691
|
+
help="Prepare ChatArch-contained Discourse docker config and ChatEnv-managed admin credentials.",
|
|
692
|
+
callback=discourse_setup,
|
|
693
|
+
options=(
|
|
694
|
+
LOG_LEVEL_OPTION,
|
|
695
|
+
SetupOptionElement(
|
|
696
|
+
param_decls=("--home",),
|
|
697
|
+
kwargs={"default": str(DEFAULT_DISCOURSE_HOME), "show_default": True, "type": click.Path(path_type=Path)},
|
|
698
|
+
),
|
|
699
|
+
SetupOptionElement(
|
|
700
|
+
param_decls=("--hostname",),
|
|
701
|
+
kwargs={"default": DEFAULT_DISCOURSE_HOSTNAME, "show_default": True},
|
|
702
|
+
),
|
|
703
|
+
SetupOptionElement(
|
|
704
|
+
param_decls=("--port",),
|
|
705
|
+
kwargs={"default": DEFAULT_DISCOURSE_PORT, "show_default": True, "type": int},
|
|
706
|
+
),
|
|
707
|
+
SetupOptionElement(
|
|
708
|
+
param_decls=("-e", "--env"),
|
|
709
|
+
kwargs={"default": None, "help": "ChatEnv profile name or env file with DISCOURSE_ADMIN_* values."},
|
|
710
|
+
),
|
|
711
|
+
SetupOptionElement(
|
|
712
|
+
param_decls=("--env-profile",),
|
|
713
|
+
kwargs={"default": None, "help": "Explicit ChatEnv profile for Discourse admin values."},
|
|
714
|
+
),
|
|
715
|
+
SetupOptionElement(
|
|
716
|
+
param_decls=("--write-admin-env/--no-write-admin-env",),
|
|
717
|
+
kwargs={"default": True, "show_default": True, "help": "Write secrets/admin.env from ChatEnv values."},
|
|
718
|
+
),
|
|
719
|
+
SetupOptionElement(
|
|
720
|
+
param_decls=("--write-app-yml/--no-write-app-yml",),
|
|
721
|
+
kwargs={"default": True, "show_default": True, "help": "Write containers/app.yml using ChatArch-local paths."},
|
|
722
|
+
),
|
|
723
|
+
SetupOptionElement(
|
|
724
|
+
param_decls=("--clone/--no-clone",),
|
|
725
|
+
kwargs={"default": False, "show_default": True, "help": "Clone/update discourse_docker into HOME/docker."},
|
|
726
|
+
),
|
|
727
|
+
SetupOptionElement(
|
|
728
|
+
param_decls=("--repo",),
|
|
729
|
+
kwargs={"default": DEFAULT_DISCOURSE_REPO, "show_default": True, "help": "discourse_docker repository URL."},
|
|
730
|
+
),
|
|
731
|
+
SetupOptionElement(
|
|
732
|
+
param_decls=("--with-ai/--without-ai",),
|
|
733
|
+
kwargs={"default": True, "show_default": True, "help": "Include discourse-ai plugin hook in generated app.yml."},
|
|
734
|
+
),
|
|
735
|
+
SetupOptionElement(
|
|
736
|
+
param_decls=("--force", "-f"),
|
|
737
|
+
kwargs={"is_flag": True, "help": "Overwrite generated app.yml when it already exists."},
|
|
738
|
+
),
|
|
739
|
+
SetupOptionElement(
|
|
740
|
+
param_decls=("--interactive/--no-interactive", "-i/-I"),
|
|
741
|
+
kwargs={"default": None, "help": INTERACTIVE_OPTION_HELP},
|
|
742
|
+
),
|
|
743
|
+
),
|
|
744
|
+
),
|
|
745
|
+
SetupCommandElement(
|
|
746
|
+
name="zulip",
|
|
747
|
+
help="Prepare ChatArch-contained Zulip Docker Compose config and ChatEnv-managed admin credentials.",
|
|
748
|
+
callback=zulip_setup,
|
|
749
|
+
options=(
|
|
750
|
+
LOG_LEVEL_OPTION,
|
|
751
|
+
SetupOptionElement(
|
|
752
|
+
param_decls=("--home",),
|
|
753
|
+
kwargs={"default": str(DEFAULT_ZULIP_HOME), "show_default": True, "type": click.Path(path_type=Path)},
|
|
754
|
+
),
|
|
755
|
+
SetupOptionElement(
|
|
756
|
+
param_decls=("--image",),
|
|
757
|
+
kwargs={"default": DEFAULT_ZULIP_IMAGE, "show_default": True},
|
|
758
|
+
),
|
|
759
|
+
SetupOptionElement(
|
|
760
|
+
param_decls=("--postgres-image",),
|
|
761
|
+
kwargs={"default": DEFAULT_ZULIP_POSTGRES_IMAGE, "show_default": True},
|
|
762
|
+
),
|
|
763
|
+
SetupOptionElement(
|
|
764
|
+
param_decls=("--external-host",),
|
|
765
|
+
kwargs={"default": DEFAULT_ZULIP_EXTERNAL_HOST, "show_default": True},
|
|
766
|
+
),
|
|
767
|
+
SetupOptionElement(
|
|
768
|
+
param_decls=("--bind-address",),
|
|
769
|
+
kwargs={"default": DEFAULT_ZULIP_BIND_ADDRESS, "show_default": True},
|
|
770
|
+
),
|
|
771
|
+
SetupOptionElement(
|
|
772
|
+
param_decls=("--port",),
|
|
773
|
+
kwargs={"default": DEFAULT_ZULIP_PORT, "show_default": True, "type": int},
|
|
774
|
+
),
|
|
775
|
+
SetupOptionElement(
|
|
776
|
+
param_decls=("-e", "--env"),
|
|
777
|
+
kwargs={"default": None, "help": "ChatEnv profile name or env file with ZULIP_ADMIN_* values."},
|
|
778
|
+
),
|
|
779
|
+
SetupOptionElement(
|
|
780
|
+
param_decls=("--env-profile",),
|
|
781
|
+
kwargs={"default": None, "help": "Explicit ChatEnv profile for Zulip admin values."},
|
|
782
|
+
),
|
|
783
|
+
SetupOptionElement(
|
|
784
|
+
param_decls=("--write-admin-env/--no-write-admin-env",),
|
|
785
|
+
kwargs={"default": True, "show_default": True, "help": "Write secrets/admin.env from ChatEnv values."},
|
|
786
|
+
),
|
|
787
|
+
SetupOptionElement(
|
|
788
|
+
param_decls=("--write-compose/--no-write-compose",),
|
|
789
|
+
kwargs={"default": True, "show_default": True, "help": "Write ChatArch-contained compose.yaml."},
|
|
790
|
+
),
|
|
791
|
+
SetupOptionElement(
|
|
792
|
+
param_decls=("--pull/--no-pull",),
|
|
793
|
+
kwargs={"default": False, "show_default": True, "help": "Run Docker Compose pull after writing config."},
|
|
794
|
+
),
|
|
795
|
+
SetupOptionElement(
|
|
796
|
+
param_decls=("--start/--no-start",),
|
|
797
|
+
kwargs={"default": False, "show_default": True, "help": "Run Docker Compose up -d after writing config."},
|
|
798
|
+
),
|
|
799
|
+
SetupOptionElement(
|
|
800
|
+
param_decls=("--force", "-f"),
|
|
801
|
+
kwargs={"is_flag": True, "help": "Overwrite generated compose.yaml when it already exists."},
|
|
802
|
+
),
|
|
803
|
+
SetupOptionElement(
|
|
804
|
+
param_decls=("--interactive/--no-interactive", "-i/-I"),
|
|
805
|
+
kwargs={"default": None, "help": INTERACTIVE_OPTION_HELP},
|
|
806
|
+
),
|
|
807
|
+
),
|
|
808
|
+
),
|
|
605
809
|
SetupCommandElement(
|
|
606
810
|
name="mysql",
|
|
607
811
|
help="Install and prepare a ChatData-compatible user-level MySQL runtime.",
|