chatup 0.2.9__tar.gz → 0.2.11__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.9/src/chatup.egg-info → chatup-0.2.11}/PKG-INFO +1 -1
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/__init__.py +1 -1
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/elements.py +103 -0
- chatup-0.2.11/src/chatup/setup/twikoo.py +403 -0
- {chatup-0.2.9 → chatup-0.2.11/src/chatup.egg-info}/PKG-INFO +1 -1
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup.egg-info/SOURCES.txt +2 -0
- {chatup-0.2.9 → chatup-0.2.11}/tests/test_cli_tree_docs.py +9 -0
- {chatup-0.2.9 → chatup-0.2.11}/tests/test_publish_workflow.py +3 -0
- chatup-0.2.11/tests/test_twikoo_setup.py +136 -0
- {chatup-0.2.9 → chatup-0.2.11}/tests/test_version.py +2 -2
- {chatup-0.2.9 → chatup-0.2.11}/LICENSE +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/README.md +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/pyproject.toml +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/setup.cfg +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/chrome_for_testing.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/chromedriver.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/cli.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/commands/__init__.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/commands/chrome_for_testing.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/commands/chromedriver.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/commands/playwright.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/config.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/const.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/interaction/__init__.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/interaction/command_schema.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/interaction/policy.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/interaction/warnings.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/playwright.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/runtime/__init__.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/runtime/_managed_artifact.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/runtime/chrome_for_testing.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/runtime/chromedriver.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/runtime/playwright.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/__init__.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/assets/hermes/install.sh +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/assets/nvm.sh +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/cc_connect.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/claude.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/cli.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/codex.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/crs.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/cursor_agent.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/discourse.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/docker.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/frp.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/gitea.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/hermes.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/lark_cli.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/mode_prompt.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/mysql.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/nginx.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/nodejs.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/opencode.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/service_credentials.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/shell_rc.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/uv.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/workspace/__init__.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/workspace/cli.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/workspace/core.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/workspace/options.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/workspace/render.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/workspace/templates/default/en/AGENTS.md +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/workspace/templates/default/en/discussion/README.md +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/workspace/templates/default/en/projects/README.md +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/workspace/templates/default/zh/AGENTS.md +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/workspace/templates/default/zh/discussion/README.md +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/workspace/templates/default/zh/projects/README.md +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/workspace.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/zsh.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/zulip.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/utils/__init__.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/utils/custom_logger.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup/utils/pathing.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup.egg-info/dependency_links.txt +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup.egg-info/entry_points.txt +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup.egg-info/requires.txt +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/src/chatup.egg-info/top_level.txt +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/tests/test_browser_backend_cli.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/tests/test_chrome_for_testing.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/tests/test_chromedriver.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/tests/test_crs_setup.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/tests/test_cursor_agent_setup.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/tests/test_playwright.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/tests/test_playwright_cli.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/tests/test_pyproject_metadata.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/tests/test_service_installers.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/tests/test_setup_cli.py +0 -0
- {chatup-0.2.9 → chatup-0.2.11}/tests/test_workspace_setup.py +0 -0
|
@@ -54,6 +54,14 @@ from chatup.setup.nginx import (
|
|
|
54
54
|
)
|
|
55
55
|
from chatup.setup.opencode import setup_opencode
|
|
56
56
|
from chatup.setup.nodejs import setup_nodejs
|
|
57
|
+
from chatup.setup.twikoo import (
|
|
58
|
+
DEFAULT_TWIKOO_BIND_ADDRESS,
|
|
59
|
+
DEFAULT_TWIKOO_INSTANCE,
|
|
60
|
+
DEFAULT_TWIKOO_PORT,
|
|
61
|
+
DEFAULT_TWIKOO_REPO,
|
|
62
|
+
DEFAULT_TWIKOO_VERSION,
|
|
63
|
+
setup_twikoo,
|
|
64
|
+
)
|
|
57
65
|
from chatup.setup.uv import DEFAULT_PYTHON_VERSION, DEFAULT_VENV_PATH, setup_uv
|
|
58
66
|
from chatup.setup.workspace import setup_workspace
|
|
59
67
|
from chatup.setup.zsh import setup_zsh
|
|
@@ -304,6 +312,41 @@ def mysql_setup(
|
|
|
304
312
|
)
|
|
305
313
|
|
|
306
314
|
|
|
315
|
+
def twikoo_setup(
|
|
316
|
+
version,
|
|
317
|
+
repo,
|
|
318
|
+
home,
|
|
319
|
+
name,
|
|
320
|
+
port,
|
|
321
|
+
bind_address,
|
|
322
|
+
install,
|
|
323
|
+
init,
|
|
324
|
+
service,
|
|
325
|
+
start,
|
|
326
|
+
smoke,
|
|
327
|
+
force,
|
|
328
|
+
log_level,
|
|
329
|
+
):
|
|
330
|
+
result = setup_twikoo(
|
|
331
|
+
name=name,
|
|
332
|
+
version=version,
|
|
333
|
+
repo=repo,
|
|
334
|
+
home=home,
|
|
335
|
+
port=port,
|
|
336
|
+
bind_address=bind_address,
|
|
337
|
+
install=install,
|
|
338
|
+
init=init,
|
|
339
|
+
service=service,
|
|
340
|
+
start=start,
|
|
341
|
+
smoke=smoke,
|
|
342
|
+
force=force,
|
|
343
|
+
log_level=log_level,
|
|
344
|
+
)
|
|
345
|
+
service_info = result.get("service") if isinstance(result, dict) else None
|
|
346
|
+
if isinstance(service_info, dict) and service_info.get("unit"):
|
|
347
|
+
click.echo(service_info["unit"])
|
|
348
|
+
|
|
349
|
+
|
|
307
350
|
def nginx_setup(
|
|
308
351
|
template,
|
|
309
352
|
output_file,
|
|
@@ -870,6 +913,66 @@ SETUP_COMMAND_ELEMENTS = (
|
|
|
870
913
|
),
|
|
871
914
|
),
|
|
872
915
|
),
|
|
916
|
+
SetupCommandElement(
|
|
917
|
+
name="twikoo",
|
|
918
|
+
help="Install and prepare a multi-instance Twikoo comment service runtime.",
|
|
919
|
+
callback=twikoo_setup,
|
|
920
|
+
options=(
|
|
921
|
+
LOG_LEVEL_OPTION,
|
|
922
|
+
SetupOptionElement(
|
|
923
|
+
param_decls=("--version",),
|
|
924
|
+
kwargs={"default": DEFAULT_TWIKOO_VERSION, "show_default": True},
|
|
925
|
+
),
|
|
926
|
+
SetupOptionElement(
|
|
927
|
+
param_decls=("--repo",),
|
|
928
|
+
kwargs={"default": DEFAULT_TWIKOO_REPO, "show_default": True},
|
|
929
|
+
),
|
|
930
|
+
SetupOptionElement(
|
|
931
|
+
param_decls=("--home",),
|
|
932
|
+
kwargs={
|
|
933
|
+
"default": None,
|
|
934
|
+
"type": click.Path(path_type=Path),
|
|
935
|
+
"help": "Twikoo home. Defaults to ~/.chatarch/twikoo.",
|
|
936
|
+
},
|
|
937
|
+
),
|
|
938
|
+
SetupOptionElement(
|
|
939
|
+
param_decls=("--name",),
|
|
940
|
+
kwargs={"default": DEFAULT_TWIKOO_INSTANCE, "show_default": True},
|
|
941
|
+
),
|
|
942
|
+
SetupOptionElement(
|
|
943
|
+
param_decls=("--port",),
|
|
944
|
+
kwargs={"default": DEFAULT_TWIKOO_PORT, "show_default": True, "type": int},
|
|
945
|
+
),
|
|
946
|
+
SetupOptionElement(
|
|
947
|
+
param_decls=("--bind-address",),
|
|
948
|
+
kwargs={"default": DEFAULT_TWIKOO_BIND_ADDRESS, "show_default": True},
|
|
949
|
+
),
|
|
950
|
+
SetupOptionElement(
|
|
951
|
+
param_decls=("--install/--no-install",),
|
|
952
|
+
kwargs={"default": True, "show_default": True, "help": "Download the Twikoo release binary."},
|
|
953
|
+
),
|
|
954
|
+
SetupOptionElement(
|
|
955
|
+
param_decls=("--init/--no-init",),
|
|
956
|
+
kwargs={"default": True, "show_default": True, "help": "Create instance directories, env file, and instance-local binary link."},
|
|
957
|
+
),
|
|
958
|
+
SetupOptionElement(
|
|
959
|
+
param_decls=("--service/--no-service",),
|
|
960
|
+
kwargs={"default": True, "show_default": True, "help": "Install a user-level systemd service."},
|
|
961
|
+
),
|
|
962
|
+
SetupOptionElement(
|
|
963
|
+
param_decls=("--start/--no-start",),
|
|
964
|
+
kwargs={"default": False, "show_default": True, "help": "Start the user-level Twikoo service."},
|
|
965
|
+
),
|
|
966
|
+
SetupOptionElement(
|
|
967
|
+
param_decls=("--smoke/--no-smoke",),
|
|
968
|
+
kwargs={"default": False, "show_default": True, "help": "Run a local HTTP smoke test after starting."},
|
|
969
|
+
),
|
|
970
|
+
SetupOptionElement(
|
|
971
|
+
param_decls=("--force", "-f"),
|
|
972
|
+
kwargs={"is_flag": True, "help": "Replace existing runtime/config/service where supported."},
|
|
973
|
+
),
|
|
974
|
+
),
|
|
975
|
+
),
|
|
873
976
|
SetupCommandElement(
|
|
874
977
|
name="nginx",
|
|
875
978
|
help="Prepare user-level NGINX under ChatArch home and render config templates.",
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import os
|
|
5
|
+
import platform
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import tempfile
|
|
9
|
+
import urllib.request
|
|
10
|
+
from dataclasses import asdict, dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
from chatenv import get_paths
|
|
16
|
+
|
|
17
|
+
from chatup.utils.custom_logger import setup_logger
|
|
18
|
+
|
|
19
|
+
logger = setup_logger("setup_twikoo")
|
|
20
|
+
|
|
21
|
+
DEFAULT_TWIKOO_REPO = "twikoojs/twikoo"
|
|
22
|
+
DEFAULT_TWIKOO_VERSION = "1.7.15"
|
|
23
|
+
DEFAULT_TWIKOO_PORT = 8892
|
|
24
|
+
DEFAULT_TWIKOO_BIND_ADDRESS = "127.0.0.1"
|
|
25
|
+
DEFAULT_TWIKOO_INSTANCE = "chatblog"
|
|
26
|
+
TWIKOO_RUNTIME_SUPPORT_FILES = ("ip2region.db", "xhr-sync-worker.js")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class TwikooService:
|
|
31
|
+
name: str
|
|
32
|
+
path: Path
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class TwikooLayout:
|
|
37
|
+
home: Path
|
|
38
|
+
downloads: Path
|
|
39
|
+
runtime: Path
|
|
40
|
+
runtime_binary: Path
|
|
41
|
+
instance: Path
|
|
42
|
+
instance_bin: Path
|
|
43
|
+
instance_env_link: Path
|
|
44
|
+
binary: Path
|
|
45
|
+
env: Path
|
|
46
|
+
data: Path
|
|
47
|
+
logs: Path
|
|
48
|
+
run: Path
|
|
49
|
+
tmp: Path
|
|
50
|
+
service: TwikooService
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _configure_logger(log_level="INFO"):
|
|
54
|
+
global logger
|
|
55
|
+
logger = setup_logger("setup_twikoo", log_level=str(log_level).upper())
|
|
56
|
+
return logger
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def validate_safe_name(value: str, *, field: str) -> str:
|
|
60
|
+
if not value or any(ch in value for ch in ("/", "\\", "\x00")) or value in {".", ".."}:
|
|
61
|
+
raise click.ClickException(
|
|
62
|
+
f"Invalid {field}: {value!r}. Use an instance-safe name without path separators."
|
|
63
|
+
)
|
|
64
|
+
allowed = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.-")
|
|
65
|
+
if any(ch not in allowed for ch in value):
|
|
66
|
+
raise click.ClickException(
|
|
67
|
+
f"Invalid {field}: {value!r}. Use letters, numbers, dot, underscore, or dash."
|
|
68
|
+
)
|
|
69
|
+
return value
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def default_twikoo_home() -> Path:
|
|
73
|
+
return get_paths().home_dir / "twikoo"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def service_name(name: str = DEFAULT_TWIKOO_INSTANCE) -> str:
|
|
77
|
+
validate_safe_name(name, field="instance name")
|
|
78
|
+
return f"chatarch-twikoo-{name}.service"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def service_file_path(name: str = DEFAULT_TWIKOO_INSTANCE) -> Path:
|
|
82
|
+
return Path("~/.config/systemd/user").expanduser() / service_name(name)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def twikoo_layout(
|
|
86
|
+
name: str = DEFAULT_TWIKOO_INSTANCE,
|
|
87
|
+
version: str = DEFAULT_TWIKOO_VERSION,
|
|
88
|
+
home: Path | None = None,
|
|
89
|
+
) -> TwikooLayout:
|
|
90
|
+
validate_safe_name(name, field="instance name")
|
|
91
|
+
validate_safe_name(version, field="version")
|
|
92
|
+
root = (home or default_twikoo_home()).expanduser()
|
|
93
|
+
runtime = root / "runtimes" / version
|
|
94
|
+
instance = root / "instances" / name
|
|
95
|
+
instance_bin = instance / "bin" / "twikoo"
|
|
96
|
+
return TwikooLayout(
|
|
97
|
+
home=root,
|
|
98
|
+
downloads=root / "downloads",
|
|
99
|
+
runtime=runtime,
|
|
100
|
+
runtime_binary=runtime / "twikoo",
|
|
101
|
+
instance=instance,
|
|
102
|
+
instance_bin=instance_bin,
|
|
103
|
+
instance_env_link=instance / "bin" / ".env",
|
|
104
|
+
binary=instance_bin,
|
|
105
|
+
env=instance / "env" / "twikoo.env",
|
|
106
|
+
data=instance / "data",
|
|
107
|
+
logs=instance / "logs",
|
|
108
|
+
run=instance / "run",
|
|
109
|
+
tmp=instance / "tmp",
|
|
110
|
+
service=TwikooService(name=service_name(name), path=service_file_path(name)),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _machine_arch() -> str:
|
|
115
|
+
machine = platform.machine().lower()
|
|
116
|
+
if machine in {"x86_64", "amd64"}:
|
|
117
|
+
return "x64"
|
|
118
|
+
if machine in {"aarch64", "arm64"}:
|
|
119
|
+
return "arm64"
|
|
120
|
+
raise click.ClickException(f"Unsupported Twikoo release architecture: {machine}")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def select_twikoo_asset_name() -> str:
|
|
124
|
+
system = platform.system().lower()
|
|
125
|
+
arch = _machine_arch()
|
|
126
|
+
if system == "linux" and arch == "x64":
|
|
127
|
+
return "twikoo-linux-x64"
|
|
128
|
+
if system == "darwin" and arch in {"x64", "arm64"}:
|
|
129
|
+
return f"twikoo-darwin-{arch}"
|
|
130
|
+
if system == "windows" and arch == "x64":
|
|
131
|
+
return "twikoo-win-x64.exe"
|
|
132
|
+
raise click.ClickException(f"Unsupported Twikoo release platform: {platform.system()} {platform.machine()}")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def twikoo_release_asset_url(repo: str, version: str, asset_name: str) -> str:
|
|
136
|
+
version = version.removeprefix("v")
|
|
137
|
+
return f"https://github.com/{repo}/releases/download/{version}/{asset_name}"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _download_url(url: str, target: Path) -> None:
|
|
141
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
142
|
+
logger.info("Downloading %s", url)
|
|
143
|
+
with urllib.request.urlopen(url, timeout=180) as response, target.open("wb") as handle:
|
|
144
|
+
if response.status != 200:
|
|
145
|
+
raise click.ClickException(f"Failed to download Twikoo release asset: HTTP {response.status}")
|
|
146
|
+
shutil.copyfileobj(response, handle)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _sha256(path: Path) -> str:
|
|
150
|
+
digest = hashlib.sha256()
|
|
151
|
+
with path.open("rb") as handle:
|
|
152
|
+
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
153
|
+
digest.update(chunk)
|
|
154
|
+
return digest.hexdigest()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def install_twikoo_binary(
|
|
158
|
+
*,
|
|
159
|
+
repo: str = DEFAULT_TWIKOO_REPO,
|
|
160
|
+
version: str = DEFAULT_TWIKOO_VERSION,
|
|
161
|
+
home: Path | None = None,
|
|
162
|
+
force: bool = False,
|
|
163
|
+
asset_name: str | None = None,
|
|
164
|
+
) -> dict[str, Any]:
|
|
165
|
+
layout = twikoo_layout(version=version, home=home)
|
|
166
|
+
if layout.runtime_binary.exists() and not force:
|
|
167
|
+
return {
|
|
168
|
+
"version": version,
|
|
169
|
+
"binary": str(layout.runtime_binary),
|
|
170
|
+
"sha256": _sha256(layout.runtime_binary),
|
|
171
|
+
"reused": True,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
selected_asset = asset_name or select_twikoo_asset_name()
|
|
175
|
+
url = twikoo_release_asset_url(repo, version, selected_asset)
|
|
176
|
+
layout.runtime.mkdir(parents=True, exist_ok=True)
|
|
177
|
+
with tempfile.TemporaryDirectory(prefix=".chatup-twikoo-", dir=layout.runtime) as tmp:
|
|
178
|
+
tmp_path = Path(tmp) / selected_asset
|
|
179
|
+
_download_url(url, tmp_path)
|
|
180
|
+
tmp_path.chmod(0o755)
|
|
181
|
+
tmp_path.replace(layout.runtime_binary)
|
|
182
|
+
layout.runtime_binary.chmod(0o755)
|
|
183
|
+
return {
|
|
184
|
+
"version": version,
|
|
185
|
+
"binary": str(layout.runtime_binary),
|
|
186
|
+
"asset": selected_asset,
|
|
187
|
+
"sha256": _sha256(layout.runtime_binary),
|
|
188
|
+
"reused": False,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def render_env(
|
|
193
|
+
layout: TwikooLayout,
|
|
194
|
+
*,
|
|
195
|
+
port: int = DEFAULT_TWIKOO_PORT,
|
|
196
|
+
bind_address: str = DEFAULT_TWIKOO_BIND_ADDRESS,
|
|
197
|
+
) -> str:
|
|
198
|
+
return f"""# Managed by ChatUp. Do not store secrets in this generated default env file.
|
|
199
|
+
TWIKOO_HOST={bind_address}
|
|
200
|
+
TWIKOO_PORT={port}
|
|
201
|
+
TWIKOO_DATA={layout.data}
|
|
202
|
+
TWIKOO_THROTTLE=250
|
|
203
|
+
TWIKOO_LOG_LEVEL=info
|
|
204
|
+
TWIKOO_LOCALHOST_ONLY=true
|
|
205
|
+
TWIKOO_SHUTDOWN_TIMEOUT=5000
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def render_service(layout: TwikooLayout, *, name: str = DEFAULT_TWIKOO_INSTANCE) -> str:
|
|
210
|
+
return f"""[Unit]
|
|
211
|
+
Description=ChatUp Twikoo comment service ({name})
|
|
212
|
+
After=network-online.target
|
|
213
|
+
Wants=network-online.target
|
|
214
|
+
|
|
215
|
+
[Service]
|
|
216
|
+
Type=simple
|
|
217
|
+
WorkingDirectory={layout.instance}
|
|
218
|
+
EnvironmentFile={layout.env}
|
|
219
|
+
ExecStart={layout.binary}
|
|
220
|
+
Restart=always
|
|
221
|
+
RestartSec=5
|
|
222
|
+
StandardOutput=append:{layout.logs}/twikoo.stdout.log
|
|
223
|
+
StandardError=append:{layout.logs}/twikoo.stderr.log
|
|
224
|
+
|
|
225
|
+
[Install]
|
|
226
|
+
WantedBy=default.target
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _link_or_copy_binary(source: Path, target: Path, *, force: bool = False) -> None:
|
|
231
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
232
|
+
if target.exists() or target.is_symlink():
|
|
233
|
+
if not force:
|
|
234
|
+
return
|
|
235
|
+
target.unlink()
|
|
236
|
+
try:
|
|
237
|
+
os.link(source, target)
|
|
238
|
+
except OSError:
|
|
239
|
+
shutil.copy2(source, target)
|
|
240
|
+
target.chmod(0o755)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def ensure_runtime_binary(layout: TwikooLayout) -> Path:
|
|
244
|
+
"""Return the canonical runtime binary, migrating an existing asset-named file if present."""
|
|
245
|
+
|
|
246
|
+
if layout.runtime_binary.exists():
|
|
247
|
+
return layout.runtime_binary
|
|
248
|
+
try:
|
|
249
|
+
asset_name = select_twikoo_asset_name()
|
|
250
|
+
except click.ClickException:
|
|
251
|
+
asset_name = ""
|
|
252
|
+
if asset_name:
|
|
253
|
+
candidate = layout.runtime / asset_name
|
|
254
|
+
if candidate.exists():
|
|
255
|
+
_link_or_copy_binary(candidate, layout.runtime_binary, force=False)
|
|
256
|
+
return layout.runtime_binary
|
|
257
|
+
raise click.ClickException(
|
|
258
|
+
f"Twikoo runtime binary is missing at {layout.runtime_binary}; run without --no-install first."
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _copy_support_files(runtime: Path, target_dir: Path, *, force: bool = False) -> list[str]:
|
|
263
|
+
copied: list[str] = []
|
|
264
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
265
|
+
for filename in TWIKOO_RUNTIME_SUPPORT_FILES:
|
|
266
|
+
source = runtime / filename
|
|
267
|
+
if not source.exists():
|
|
268
|
+
continue
|
|
269
|
+
target = target_dir / filename
|
|
270
|
+
if target.exists() and not force:
|
|
271
|
+
copied.append(filename)
|
|
272
|
+
continue
|
|
273
|
+
shutil.copy2(source, target)
|
|
274
|
+
copied.append(filename)
|
|
275
|
+
return copied
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def init_instance(
|
|
279
|
+
*,
|
|
280
|
+
name: str = DEFAULT_TWIKOO_INSTANCE,
|
|
281
|
+
version: str = DEFAULT_TWIKOO_VERSION,
|
|
282
|
+
home: Path | None = None,
|
|
283
|
+
port: int = DEFAULT_TWIKOO_PORT,
|
|
284
|
+
bind_address: str = DEFAULT_TWIKOO_BIND_ADDRESS,
|
|
285
|
+
force: bool = False,
|
|
286
|
+
) -> dict[str, Any]:
|
|
287
|
+
layout = twikoo_layout(name=name, version=version, home=home)
|
|
288
|
+
for directory in [layout.data, layout.logs, layout.run, layout.tmp, layout.env.parent, layout.instance_bin.parent]:
|
|
289
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
290
|
+
runtime_binary = ensure_runtime_binary(layout)
|
|
291
|
+
_link_or_copy_binary(runtime_binary, layout.instance_bin, force=force)
|
|
292
|
+
support_files = _copy_support_files(layout.runtime, layout.instance_bin.parent, force=force)
|
|
293
|
+
if not layout.env.exists() or force:
|
|
294
|
+
layout.env.write_text(render_env(layout, port=port, bind_address=bind_address), encoding="utf-8")
|
|
295
|
+
layout.env.chmod(0o600)
|
|
296
|
+
if layout.instance_env_link.exists() or layout.instance_env_link.is_symlink():
|
|
297
|
+
if force:
|
|
298
|
+
layout.instance_env_link.unlink()
|
|
299
|
+
if not layout.instance_env_link.exists() and not layout.instance_env_link.is_symlink():
|
|
300
|
+
os.symlink(Path("../env/twikoo.env"), layout.instance_env_link)
|
|
301
|
+
return {
|
|
302
|
+
"instance": str(layout.instance),
|
|
303
|
+
"binary": str(layout.instance_bin),
|
|
304
|
+
"env": str(layout.env),
|
|
305
|
+
"data": str(layout.data),
|
|
306
|
+
"support_files": support_files,
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def install_service(
|
|
311
|
+
*,
|
|
312
|
+
name: str = DEFAULT_TWIKOO_INSTANCE,
|
|
313
|
+
version: str = DEFAULT_TWIKOO_VERSION,
|
|
314
|
+
home: Path | None = None,
|
|
315
|
+
force: bool = False,
|
|
316
|
+
) -> dict[str, Any]:
|
|
317
|
+
layout = twikoo_layout(name=name, version=version, home=home)
|
|
318
|
+
layout.service.path.parent.mkdir(parents=True, exist_ok=True)
|
|
319
|
+
if not layout.service.path.exists() or force:
|
|
320
|
+
layout.service.path.write_text(render_service(layout, name=name), encoding="utf-8")
|
|
321
|
+
return {"unit": layout.service.name, "path": str(layout.service.path)}
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def export_layout(
|
|
325
|
+
*,
|
|
326
|
+
name: str = DEFAULT_TWIKOO_INSTANCE,
|
|
327
|
+
version: str = DEFAULT_TWIKOO_VERSION,
|
|
328
|
+
home: Path | None = None,
|
|
329
|
+
) -> dict[str, Any]:
|
|
330
|
+
layout = twikoo_layout(name=name, version=version, home=home)
|
|
331
|
+
data = asdict(layout)
|
|
332
|
+
data["service"] = {"name": layout.service.name, "path": str(layout.service.path)}
|
|
333
|
+
for key, value in list(data.items()):
|
|
334
|
+
if isinstance(value, Path):
|
|
335
|
+
data[key] = str(value)
|
|
336
|
+
return data
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _systemctl_user(*args: str) -> None:
|
|
340
|
+
subprocess.run(["systemctl", "--user", *args], check=True)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def start_service(name: str = DEFAULT_TWIKOO_INSTANCE) -> None:
|
|
344
|
+
unit = service_name(name)
|
|
345
|
+
_systemctl_user("daemon-reload")
|
|
346
|
+
_systemctl_user("enable", "--now", unit)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def smoke_twikoo(*, port: int = DEFAULT_TWIKOO_PORT, bind_address: str = DEFAULT_TWIKOO_BIND_ADDRESS) -> str:
|
|
350
|
+
import json
|
|
351
|
+
|
|
352
|
+
url = f"http://{bind_address}:{port}/"
|
|
353
|
+
with urllib.request.urlopen(url, timeout=15) as response:
|
|
354
|
+
body = response.read().decode("utf-8")
|
|
355
|
+
try:
|
|
356
|
+
payload = json.loads(body)
|
|
357
|
+
except json.JSONDecodeError as exc:
|
|
358
|
+
raise click.ClickException(f"Twikoo smoke did not return JSON: {body[:200]}") from exc
|
|
359
|
+
if payload.get("code") != 100:
|
|
360
|
+
raise click.ClickException(f"Twikoo smoke failed: {payload}")
|
|
361
|
+
return body
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def setup_twikoo(
|
|
365
|
+
*,
|
|
366
|
+
name: str = DEFAULT_TWIKOO_INSTANCE,
|
|
367
|
+
version: str = DEFAULT_TWIKOO_VERSION,
|
|
368
|
+
repo: str = DEFAULT_TWIKOO_REPO,
|
|
369
|
+
home: Path | None = None,
|
|
370
|
+
port: int = DEFAULT_TWIKOO_PORT,
|
|
371
|
+
bind_address: str = DEFAULT_TWIKOO_BIND_ADDRESS,
|
|
372
|
+
install: bool = True,
|
|
373
|
+
init: bool = True,
|
|
374
|
+
service: bool = True,
|
|
375
|
+
start: bool = False,
|
|
376
|
+
smoke: bool = False,
|
|
377
|
+
force: bool = False,
|
|
378
|
+
log_level: str = "INFO",
|
|
379
|
+
) -> dict[str, Any]:
|
|
380
|
+
_configure_logger(log_level)
|
|
381
|
+
validate_safe_name(name, field="instance name")
|
|
382
|
+
if smoke and not start:
|
|
383
|
+
raise click.ClickException("--smoke requires --start")
|
|
384
|
+
result: dict[str, Any] = {"layout": export_layout(name=name, version=version, home=home)}
|
|
385
|
+
if install:
|
|
386
|
+
result["install"] = install_twikoo_binary(repo=repo, version=version, home=home, force=force)
|
|
387
|
+
if init:
|
|
388
|
+
result["init"] = init_instance(
|
|
389
|
+
name=name,
|
|
390
|
+
version=version,
|
|
391
|
+
home=home,
|
|
392
|
+
port=port,
|
|
393
|
+
bind_address=bind_address,
|
|
394
|
+
force=force,
|
|
395
|
+
)
|
|
396
|
+
if service:
|
|
397
|
+
result["service"] = install_service(name=name, version=version, home=home, force=force)
|
|
398
|
+
if start:
|
|
399
|
+
start_service(name)
|
|
400
|
+
result["started"] = service_name(name)
|
|
401
|
+
if smoke:
|
|
402
|
+
result["smoke"] = smoke_twikoo(port=port, bind_address=bind_address)
|
|
403
|
+
return result
|
|
@@ -48,6 +48,7 @@ src/chatup/setup/nodejs.py
|
|
|
48
48
|
src/chatup/setup/opencode.py
|
|
49
49
|
src/chatup/setup/service_credentials.py
|
|
50
50
|
src/chatup/setup/shell_rc.py
|
|
51
|
+
src/chatup/setup/twikoo.py
|
|
51
52
|
src/chatup/setup/uv.py
|
|
52
53
|
src/chatup/setup/workspace.py
|
|
53
54
|
src/chatup/setup/zsh.py
|
|
@@ -80,5 +81,6 @@ tests/test_publish_workflow.py
|
|
|
80
81
|
tests/test_pyproject_metadata.py
|
|
81
82
|
tests/test_service_installers.py
|
|
82
83
|
tests/test_setup_cli.py
|
|
84
|
+
tests/test_twikoo_setup.py
|
|
83
85
|
tests/test_version.py
|
|
84
86
|
tests/test_workspace_setup.py
|
|
@@ -33,6 +33,15 @@ def test_bilingual_cli_tree_matches_registered_click_commands():
|
|
|
33
33
|
assert _documented_reference_commands(ROOT / "docs" / "commands.en.md") == actual
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
def test_mkdocs_material_theme_has_material_icon_renderer():
|
|
37
|
+
mkdocs = (ROOT / "mkdocs.yml").read_text(encoding="utf-8")
|
|
38
|
+
|
|
39
|
+
assert "name: material" in mkdocs
|
|
40
|
+
assert "pymdownx.emoji" in mkdocs
|
|
41
|
+
assert "material.extensions.emoji.twemoji" in mkdocs
|
|
42
|
+
assert "material.extensions.emoji.to_svg" in mkdocs
|
|
43
|
+
|
|
44
|
+
|
|
36
45
|
def test_cli_tree_is_in_mkdocs_nav_and_backend_contracts_are_complete():
|
|
37
46
|
mkdocs = (ROOT / "mkdocs.yml").read_text(encoding="utf-8")
|
|
38
47
|
assert "CLI 树: cli-tree.md" in mkdocs
|
|
@@ -11,6 +11,9 @@ def test_publish_workflow_matches_active_any_environment_publisher():
|
|
|
11
11
|
assert "id-token: write" in workflow
|
|
12
12
|
assert "pypa/gh-action-pypi-publish@release/v1" in workflow
|
|
13
13
|
assert "environment: pypi" not in workflow
|
|
14
|
+
assert "Release workflow must run from a tag ref" in workflow
|
|
15
|
+
assert "GITHUB_REF_TYPE" in workflow
|
|
16
|
+
assert "GITHUB_REF_NAME" in workflow
|
|
14
17
|
for secret_name in (
|
|
15
18
|
"PYPI_API_TOKEN",
|
|
16
19
|
"PYPI_TOKEN",
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from click.testing import CliRunner
|
|
4
|
+
|
|
5
|
+
from chatup.cli import main
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_twikoo_layout_uses_chatarch_home_and_instance_scoped_paths(tmp_path):
|
|
9
|
+
from chatup.setup.twikoo import twikoo_layout
|
|
10
|
+
|
|
11
|
+
layout = twikoo_layout(name="chatblog", version="1.7.15", home=tmp_path / "twikoo")
|
|
12
|
+
|
|
13
|
+
assert layout.home == tmp_path / "twikoo"
|
|
14
|
+
assert layout.runtime == tmp_path / "twikoo" / "runtimes" / "1.7.15"
|
|
15
|
+
assert layout.runtime_binary == tmp_path / "twikoo" / "runtimes" / "1.7.15" / "twikoo"
|
|
16
|
+
assert layout.instance == tmp_path / "twikoo" / "instances" / "chatblog"
|
|
17
|
+
assert layout.instance_bin == tmp_path / "twikoo" / "instances" / "chatblog" / "bin" / "twikoo"
|
|
18
|
+
assert layout.instance_env_link == tmp_path / "twikoo" / "instances" / "chatblog" / "bin" / ".env"
|
|
19
|
+
assert layout.binary == layout.instance_bin
|
|
20
|
+
assert layout.env == tmp_path / "twikoo" / "instances" / "chatblog" / "env" / "twikoo.env"
|
|
21
|
+
assert layout.data == tmp_path / "twikoo" / "instances" / "chatblog" / "data"
|
|
22
|
+
assert layout.logs == tmp_path / "twikoo" / "instances" / "chatblog" / "logs"
|
|
23
|
+
assert layout.service.name == "chatarch-twikoo-chatblog.service"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_twikoo_render_env_and_service_are_instance_scoped(tmp_path):
|
|
27
|
+
from chatup.setup.twikoo import render_env, render_service, service_name, twikoo_layout
|
|
28
|
+
|
|
29
|
+
layout = twikoo_layout(name="chatblog", version="1.7.15", home=tmp_path / "twikoo")
|
|
30
|
+
|
|
31
|
+
env_text = render_env(layout, port=8892, bind_address="127.0.0.1")
|
|
32
|
+
assert "TWIKOO_HOST=127.0.0.1" in env_text
|
|
33
|
+
assert "TWIKOO_PORT=8892" in env_text
|
|
34
|
+
assert f"TWIKOO_DATA={layout.data}" in env_text
|
|
35
|
+
assert "MONGODB" not in env_text
|
|
36
|
+
|
|
37
|
+
service_text = render_service(layout, name="chatblog")
|
|
38
|
+
assert service_name("chatblog") == "chatarch-twikoo-chatblog.service"
|
|
39
|
+
assert f"WorkingDirectory={layout.instance}" in service_text
|
|
40
|
+
assert f"EnvironmentFile={layout.env}" in service_text
|
|
41
|
+
assert f"ExecStart={layout.binary}" in service_text
|
|
42
|
+
assert "WantedBy=default.target" in service_text
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_twikoo_help_exposes_multi_instance_binary_installer_options():
|
|
46
|
+
result = CliRunner().invoke(main, ["twikoo", "--help"])
|
|
47
|
+
|
|
48
|
+
assert result.exit_code == 0, result.output
|
|
49
|
+
assert "--version" in result.output
|
|
50
|
+
assert "1.7.15" in result.output
|
|
51
|
+
assert "--repo" in result.output
|
|
52
|
+
assert "twikoojs/twikoo" in result.output
|
|
53
|
+
assert "--home" in result.output
|
|
54
|
+
assert "--name" in result.output
|
|
55
|
+
assert "--port" in result.output
|
|
56
|
+
assert "--bind-address" in result.output
|
|
57
|
+
assert "--start / --no-start" in result.output
|
|
58
|
+
assert "--smoke / --no-smoke" in result.output
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_twikoo_cli_prepares_named_instance_without_starting(monkeypatch, tmp_path):
|
|
62
|
+
import chatup.setup.twikoo as twikoo_setup
|
|
63
|
+
|
|
64
|
+
calls = []
|
|
65
|
+
monkeypatch.setattr(
|
|
66
|
+
twikoo_setup,
|
|
67
|
+
"export_layout",
|
|
68
|
+
lambda **kwargs: {
|
|
69
|
+
"instance": str(tmp_path / "twikoo" / "instances" / kwargs["name"]),
|
|
70
|
+
"service": str(tmp_path / f"chatarch-twikoo-{kwargs['name']}.service"),
|
|
71
|
+
},
|
|
72
|
+
)
|
|
73
|
+
monkeypatch.setattr(
|
|
74
|
+
twikoo_setup,
|
|
75
|
+
"install_twikoo_binary",
|
|
76
|
+
lambda **kwargs: calls.append(("install", kwargs))
|
|
77
|
+
or {"binary": str(tmp_path / "twikoo" / "runtimes" / kwargs["version"] / "twikoo"), "reused": False},
|
|
78
|
+
)
|
|
79
|
+
monkeypatch.setattr(
|
|
80
|
+
twikoo_setup,
|
|
81
|
+
"init_instance",
|
|
82
|
+
lambda **kwargs: calls.append(("init", kwargs))
|
|
83
|
+
or {"env": str(tmp_path / "twikoo" / "instances" / kwargs["name"] / "env" / "twikoo.env")},
|
|
84
|
+
)
|
|
85
|
+
monkeypatch.setattr(
|
|
86
|
+
twikoo_setup,
|
|
87
|
+
"install_service",
|
|
88
|
+
lambda **kwargs: calls.append(("service", kwargs))
|
|
89
|
+
or {"unit": f"chatarch-twikoo-{kwargs['name']}.service"},
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
result = CliRunner().invoke(
|
|
93
|
+
main,
|
|
94
|
+
[
|
|
95
|
+
"twikoo",
|
|
96
|
+
"--home",
|
|
97
|
+
str(tmp_path / "twikoo"),
|
|
98
|
+
"--name",
|
|
99
|
+
"chatblog",
|
|
100
|
+
"--port",
|
|
101
|
+
"8892",
|
|
102
|
+
"--no-start",
|
|
103
|
+
"--no-smoke",
|
|
104
|
+
],
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
assert result.exit_code == 0, result.output
|
|
108
|
+
assert [name for name, _ in calls] == ["install", "init", "service"]
|
|
109
|
+
assert calls[0][1]["repo"] == "twikoojs/twikoo"
|
|
110
|
+
assert calls[1][1]["name"] == "chatblog"
|
|
111
|
+
assert calls[1][1]["port"] == 8892
|
|
112
|
+
assert calls[1][1]["bind_address"] == "127.0.0.1"
|
|
113
|
+
assert "chatarch-twikoo-chatblog.service" in result.output
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_twikoo_smoke_requires_start():
|
|
117
|
+
result = CliRunner().invoke(main, ["twikoo", "--no-install", "--no-init", "--no-service", "--smoke"])
|
|
118
|
+
|
|
119
|
+
assert result.exit_code != 0
|
|
120
|
+
assert "--smoke requires --start" in result.output
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_twikoo_no_install_can_adopt_asset_named_runtime(monkeypatch, tmp_path):
|
|
124
|
+
from chatup.setup.twikoo import ensure_runtime_binary, twikoo_layout
|
|
125
|
+
|
|
126
|
+
monkeypatch.setattr("chatup.setup.twikoo.select_twikoo_asset_name", lambda: "twikoo-linux-x64")
|
|
127
|
+
layout = twikoo_layout(name="chatblog", version="1.7.15", home=tmp_path / "twikoo")
|
|
128
|
+
layout.runtime.mkdir(parents=True)
|
|
129
|
+
asset = layout.runtime / "twikoo-linux-x64"
|
|
130
|
+
asset.write_bytes(b"fake")
|
|
131
|
+
asset.chmod(0o755)
|
|
132
|
+
|
|
133
|
+
adopted = ensure_runtime_binary(layout)
|
|
134
|
+
|
|
135
|
+
assert adopted == layout.runtime_binary
|
|
136
|
+
assert layout.runtime_binary.read_bytes() == b"fake"
|
|
@@ -5,7 +5,7 @@ from chatup.cli import main
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
def test_version_present():
|
|
8
|
-
assert __version__ == "0.2.
|
|
8
|
+
assert __version__ == "0.2.11"
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def test_cli_help():
|
|
@@ -42,4 +42,4 @@ def test_cli_doctor():
|
|
|
42
42
|
result = CliRunner().invoke(main, ["doctor"])
|
|
43
43
|
|
|
44
44
|
assert result.exit_code == 0
|
|
45
|
-
assert "chatup 0.2.
|
|
45
|
+
assert "chatup 0.2.11 ok" in result.output
|
|
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.9 → chatup-0.2.11}/src/chatup/setup/workspace/templates/default/en/discussion/README.md
RENAMED
|
File without changes
|
{chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/workspace/templates/default/en/projects/README.md
RENAMED
|
File without changes
|
|
File without changes
|
{chatup-0.2.9 → chatup-0.2.11}/src/chatup/setup/workspace/templates/default/zh/discussion/README.md
RENAMED
|
File without changes
|
{chatup-0.2.9 → chatup-0.2.11}/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
|