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.
Files changed (87) hide show
  1. {chatup-0.2.6/src/chatup.egg-info → chatup-0.2.7}/PKG-INFO +6 -4
  2. {chatup-0.2.6 → chatup-0.2.7}/README.md +5 -3
  3. {chatup-0.2.6 → chatup-0.2.7}/pyproject.toml +2 -0
  4. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/__init__.py +1 -1
  5. chatup-0.2.7/src/chatup/config.py +122 -0
  6. chatup-0.2.7/src/chatup/setup/discourse.py +187 -0
  7. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/elements.py +205 -1
  8. chatup-0.2.7/src/chatup/setup/service_credentials.py +94 -0
  9. chatup-0.2.7/src/chatup/setup/zulip.py +295 -0
  10. {chatup-0.2.6 → chatup-0.2.7/src/chatup.egg-info}/PKG-INFO +6 -4
  11. {chatup-0.2.6 → chatup-0.2.7}/src/chatup.egg-info/SOURCES.txt +4 -0
  12. {chatup-0.2.6 → chatup-0.2.7}/src/chatup.egg-info/entry_points.txt +2 -0
  13. {chatup-0.2.6 → chatup-0.2.7}/tests/test_pyproject_metadata.py +16 -1
  14. chatup-0.2.7/tests/test_service_installers.py +105 -0
  15. {chatup-0.2.6 → chatup-0.2.7}/tests/test_setup_cli.py +4 -0
  16. {chatup-0.2.6 → chatup-0.2.7}/tests/test_version.py +2 -2
  17. chatup-0.2.6/src/chatup/config.py +0 -45
  18. {chatup-0.2.6 → chatup-0.2.7}/LICENSE +0 -0
  19. {chatup-0.2.6 → chatup-0.2.7}/setup.cfg +0 -0
  20. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/chrome_for_testing.py +0 -0
  21. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/chromedriver.py +0 -0
  22. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/cli.py +0 -0
  23. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/commands/__init__.py +0 -0
  24. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/commands/chrome_for_testing.py +0 -0
  25. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/commands/chromedriver.py +0 -0
  26. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/commands/playwright.py +0 -0
  27. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/const.py +0 -0
  28. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/interaction/__init__.py +0 -0
  29. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/interaction/command_schema.py +0 -0
  30. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/interaction/policy.py +0 -0
  31. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/interaction/warnings.py +0 -0
  32. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/playwright.py +0 -0
  33. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/runtime/__init__.py +0 -0
  34. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/runtime/_managed_artifact.py +0 -0
  35. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/runtime/chrome_for_testing.py +0 -0
  36. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/runtime/chromedriver.py +0 -0
  37. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/runtime/playwright.py +0 -0
  38. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/__init__.py +0 -0
  39. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/assets/hermes/install.sh +0 -0
  40. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/assets/nvm.sh +0 -0
  41. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/cc_connect.py +0 -0
  42. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/claude.py +0 -0
  43. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/cli.py +0 -0
  44. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/codex.py +0 -0
  45. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/crs.py +0 -0
  46. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/cursor_agent.py +0 -0
  47. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/docker.py +0 -0
  48. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/frp.py +0 -0
  49. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/gitea.py +0 -0
  50. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/hermes.py +0 -0
  51. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/lark_cli.py +0 -0
  52. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/mode_prompt.py +0 -0
  53. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/mysql.py +0 -0
  54. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/nginx.py +0 -0
  55. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/nodejs.py +0 -0
  56. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/opencode.py +0 -0
  57. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/shell_rc.py +0 -0
  58. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/uv.py +0 -0
  59. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/workspace/__init__.py +0 -0
  60. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/workspace/cli.py +0 -0
  61. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/workspace/core.py +0 -0
  62. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/workspace/options.py +0 -0
  63. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/workspace/render.py +0 -0
  64. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/workspace/templates/default/en/AGENTS.md +0 -0
  65. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/workspace/templates/default/en/discussion/README.md +0 -0
  66. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/workspace/templates/default/en/projects/README.md +0 -0
  67. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/workspace/templates/default/zh/AGENTS.md +0 -0
  68. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/workspace/templates/default/zh/discussion/README.md +0 -0
  69. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/workspace/templates/default/zh/projects/README.md +0 -0
  70. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/workspace.py +0 -0
  71. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/setup/zsh.py +0 -0
  72. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/utils/__init__.py +0 -0
  73. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/utils/custom_logger.py +0 -0
  74. {chatup-0.2.6 → chatup-0.2.7}/src/chatup/utils/pathing.py +0 -0
  75. {chatup-0.2.6 → chatup-0.2.7}/src/chatup.egg-info/dependency_links.txt +0 -0
  76. {chatup-0.2.6 → chatup-0.2.7}/src/chatup.egg-info/requires.txt +0 -0
  77. {chatup-0.2.6 → chatup-0.2.7}/src/chatup.egg-info/top_level.txt +0 -0
  78. {chatup-0.2.6 → chatup-0.2.7}/tests/test_browser_backend_cli.py +0 -0
  79. {chatup-0.2.6 → chatup-0.2.7}/tests/test_chrome_for_testing.py +0 -0
  80. {chatup-0.2.6 → chatup-0.2.7}/tests/test_chromedriver.py +0 -0
  81. {chatup-0.2.6 → chatup-0.2.7}/tests/test_cli_tree_docs.py +0 -0
  82. {chatup-0.2.6 → chatup-0.2.7}/tests/test_crs_setup.py +0 -0
  83. {chatup-0.2.6 → chatup-0.2.7}/tests/test_cursor_agent_setup.py +0 -0
  84. {chatup-0.2.6 → chatup-0.2.7}/tests/test_playwright.py +0 -0
  85. {chatup-0.2.6 → chatup-0.2.7}/tests/test_playwright_cli.py +0 -0
  86. {chatup-0.2.6 → chatup-0.2.7}/tests/test_publish_workflow.py +0 -0
  87. {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.6
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 快速配置迁移来的 Cursor token。更完整的能力边界见 `docs/capability-map.md`。
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 快速配置迁移来的 Cursor token。更完整的能力边界见 `docs/capability-map.md`。
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__"}
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.2.6"
5
+ __version__ = "0.2.7"
@@ -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.",