mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-14 02:15:06 +00:00
feat: docsgpt up runs and manages DocsGPT on Docker
`docsgpt up` copies the standalone Compose file shipped with this package version into the stack directory (~/.docsgpt/server by default), writes its .env and starts the stack on the images of the same version. A first run asks who should reach DocsGPT (this computer, the network with a token, or a domain with HTTPS) and which model provider to use; flags answer the same questions for scripts. Re-running keeps secrets and settings and moves the image tag, and the database password is only generated for a new database. Also: down, status, logs, token, open, env, upgrade (uv tool installs upgrade themselves and run `up` again) and uninstall (keeps settings and data unless --purge). The commands import no Flask, Celery or settings. The wheel carries deployment/docker-compose-standalone.yaml as docsgpt/deploy/docker-compose.yaml; the sdist includes the source file.
This commit is contained in:
1 parent
ac06527a8d
commit
aa0e4ea280
13 files changed
+1567
-3
No files matched your search
@@ -0,0 +1,74 @@
|
||||
"""Read and update a ``.env`` file in place, keeping the lines the user wrote."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
from collections.abc import Mapping
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
_ASSIGNMENT = re.compile(r"^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$")
|
||||
# Values written without quotes: nothing Compose would interpolate, strip or treat as a comment.
|
||||
_PLAIN = re.compile(r"^[A-Za-z0-9_./:@+,=-]*$")
|
||||
|
||||
|
||||
def _parse_value(raw: str) -> str:
|
||||
"""The value of one assignment, with Compose's quoting rules."""
|
||||
value = raw.strip()
|
||||
if len(value) >= 2 and value[0] == value[-1] == "'":
|
||||
return value[1:-1]
|
||||
if len(value) >= 2 and value[0] == value[-1] == '"':
|
||||
return re.sub(r'\\(["\\])', r"\1", value[1:-1])
|
||||
return value.split(" #", 1)[0].rstrip()
|
||||
|
||||
|
||||
def _format_value(value: str) -> str:
|
||||
"""``value`` quoted so it reads back unchanged."""
|
||||
if "\n" in value or "\r" in value:
|
||||
raise ValueError("a .env value cannot contain a newline")
|
||||
if _PLAIN.match(value):
|
||||
return value
|
||||
if "'" not in value:
|
||||
return f"'{value}'"
|
||||
return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
||||
|
||||
|
||||
def read(path: Path) -> dict[str, str]:
|
||||
"""The assignments in ``path`` (empty when it does not exist); a repeated key keeps its last value."""
|
||||
path = Path(path)
|
||||
if not path.is_file():
|
||||
return {}
|
||||
values: dict[str, str] = {}
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
match = _ASSIGNMENT.match(line)
|
||||
if match:
|
||||
values[match.group(1)] = _parse_value(match.group(2))
|
||||
return values
|
||||
|
||||
|
||||
def update(path: Path, values: Mapping[str, Optional[str]]) -> None:
|
||||
"""Set each key in place (``None`` removes it), append new keys, and leave every other line alone.
|
||||
|
||||
A new file is created readable by its owner only: it holds secrets.
|
||||
"""
|
||||
formatted = {key: None if value is None else _format_value(value) for key, value in values.items()}
|
||||
path = Path(path)
|
||||
lines = path.read_text(encoding="utf-8").splitlines() if path.is_file() else []
|
||||
written: set[str] = set()
|
||||
out: list[str] = []
|
||||
for line in lines:
|
||||
match = _ASSIGNMENT.match(line)
|
||||
key = match.group(1) if match else None
|
||||
if key not in formatted:
|
||||
out.append(line)
|
||||
continue
|
||||
if key not in written and formatted[key] is not None:
|
||||
out.append(f"{key}={formatted[key]}")
|
||||
written.add(key)
|
||||
out.extend(f"{key}={value}" for key, value in formatted.items() if key not in written and value is not None)
|
||||
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||||
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
||||
handle.write("\n".join(out) + "\n" if out else "")
|
||||
Reference in new issue
Block a user