feat: a development loop in one command

`docsgpt up --native` installs services meant to outlive the shell. Development
wants the opposite, and until now it meant three terminals from the guide:
uvicorn, celery, and vite.

`docsgpt dev` runs this checkout's API and worker as children of one terminal,
both restarting when a file is saved, their output interleaved and labelled, and
Ctrl-C stopping them together. `--ui` adds the Vite dev server, `--mock-llm`
runs the bundled mock model so no API key is needed, and `--no-worker` leaves
the worker to your editor's debugger. Celery has no reloader of its own, so the
worker is wrapped in watchfiles when it is installed, and runs plain when it is
not.

Alongside it, the commands a dev loop keeps reaching for:

- `docsgpt doctor` checks what usually breaks a new setup: PostgreSQL answering
  and its schema matching this version, Redis answering, a model provider being
  configured, and the port being free.
- `docsgpt restart [api|worker]` bounces services without rewriting settings or
  rerunning migrations, which `down` plus `up` did.
- `docsgpt logs -f` follows a native install instead of telling you to run
  `tail -f` yourself.
- `docsgpt env set` applies itself to a running native install rather than
  asking you to run `docsgpt up` again to change one value.

Two bugs found on the way, both older than this change:

- `docsgpt api --reload` watched the working directory, which in a checkout is
  178,425 files: .venv, node_modules, and the indexes/ and inputs/ the app
  writes to while ingesting, so the server restarted itself mid-request. It
  watches the package now — 1,217 files.
- The VS Code "Flask Debugger" ran `flask run`, which serves only the WSGI app:
  /mcp, the SSE streams and artifact downloads 404 under it. The guide warned
  about this in prose while the debug config did it anyway. It runs uvicorn on
  the ASGI app now, like production.
This commit is contained in:
Alex committed 2026-09-17 11:32:50 +01:00
1 parent 06f233925b
commit a20c83f468
9 files changed
+932 -36

No files matched your search

+23 -1
View File
@@ -10,6 +10,7 @@ import click
import pytest
from docsgpt import cli
from docsgpt.core.paths import package_dir
from docsgpt.version import __version__
@@ -126,7 +127,28 @@ class TestApi:
uvicorn = types.SimpleNamespace(run=MagicMock())
monkeypatch.setitem(sys.modules, "uvicorn", uvicorn)
assert cli.main(["api", "--reload", "--host", "127.0.0.1"]) == 0
uvicorn.run.assert_called_once_with("docsgpt.asgi:asgi_app", host="127.0.0.1", port=7091, reload=True)
uvicorn.run.assert_called_once_with(
"docsgpt.asgi:asgi_app", host="127.0.0.1", port=7091, reload=True,
reload_dirs=[str(package_dir())],
)
def test_reload_watches_the_package_not_the_working_directory(self, monkeypatch, tmp_path):
"""A checkout also holds .venv, node_modules and the indexes and inputs the app writes to,
so watching the working directory restarts the server mid-ingest."""
uvicorn = types.SimpleNamespace(run=MagicMock())
monkeypatch.setitem(sys.modules, "uvicorn", uvicorn)
monkeypatch.chdir(tmp_path)
assert cli.main(["api", "--reload"]) == 0
watched = uvicorn.run.call_args.kwargs["reload_dirs"]
assert watched == [str(package_dir())]
assert str(tmp_path) not in watched
def test_without_reload_nothing_is_watched(self, monkeypatch):
uvicorn = types.SimpleNamespace(run=MagicMock())
monkeypatch.setitem(sys.modules, "uvicorn", uvicorn)
monkeypatch.setattr(sys, "platform", "win32")
assert cli.main(["api"]) == 0
assert uvicorn.run.call_args.kwargs["reload_dirs"] is None
class TestWorker: