Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .bandit.yaml

This file was deleted.

19 changes: 9 additions & 10 deletions .github/actions/codestyle/action.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: "codestyle"
description: "Runs python code style checklist (black, flake8, mypy and bandit)"
description: "Runs Python code style checks via ruff (format + lint) and ty (type-check)"
inputs:
workdir:
description: "working directory"
Expand All @@ -16,15 +16,14 @@ runs:
run: |
cd ${{ inputs.workdir }}
uv sync
- name: "Bandit"
- name: "Ruff format (check)"
shell: bash
run: uv run bandit -c .bandit.yaml -r ${{ inputs.workdir }}
- name: "Mypy"
run: uv run ruff format --check ${{ inputs.workdir }}
- name: "Ruff lint"
shell: bash
run: uv run mypy --config-file .mypy.ini ${{ inputs.workdir }}
- name: "Black"
run: uv run ruff check ${{ inputs.workdir }}
- name: "ty (type check)"
shell: bash
run: uv run black --exclude ".venv" --check ${{ inputs.workdir }}
- name: "Flake8"
shell: bash
run: uv run flake8 ${{ inputs.workdir }}
# ty does whole-project inference; the explicit roots match
# `[tool.ty.src]` in pyproject.toml.
run: uv run ty check rogue sdks/python hatch_build.py
24 changes: 24 additions & 0 deletions .github/actions/web-build/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: "web-build"
description: "Builds the Vite SPA into packages/web/dist for inclusion in the rogue-ai wheel"
runs:
using: "composite"
steps:
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "pnpm"
cache-dependency-path: packages/web/pnpm-lock.yaml
- name: Install dependencies
shell: bash
run: cd packages/web && pnpm install --frozen-lockfile
- name: Build SPA
shell: bash
run: cd packages/web && pnpm build
- name: Verify dist exists
shell: bash
run: test -f packages/web/dist/index.html
31 changes: 31 additions & 0 deletions .github/actions/web-codestyle/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: "web-codestyle"
description: "Runs prettier --check, eslint, and tsc --noEmit on packages/web"
runs:
using: "composite"
steps:
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "pnpm"
cache-dependency-path: packages/web/pnpm-lock.yaml
- name: Install dependencies
shell: bash
run: cd packages/web && pnpm install --frozen-lockfile
- name: Prettier
shell: bash
run: cd packages/web && pnpm exec prettier --check .
- name: ESLint
shell: bash
run: cd packages/web && pnpm exec eslint .
- name: TypeScript
shell: bash
# `pnpm typecheck` is `pnpm routes && tsc --noEmit`. The route-tree
# generator (`tsr generate`) is required because src/routeTree.gen.ts
# is gitignored — without it tsc can't resolve `./routeTree.gen` and
# every `createFileRoute(...)` call collapses to `(arg: undefined)`.
run: cd packages/web && pnpm typecheck
7 changes: 3 additions & 4 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,9 @@

- [ ] I have read the [CONTRIBUTING.md](../CONTRIBUTING.md) guide
- [ ] My code follows the code style of this project (PEP 8, type hints, docstrings)
- [ ] I have run `uv run black .` to format my code
- [ ] I have run `uv run flake8 .` and fixed all issues
- [ ] I have run `uv run mypy --config-file .mypy.ini .` and addressed type checking issues
- [ ] I have run `uv run bandit -c .bandit.yaml -r .` for security checks
- [ ] I have run `uv run ruff format .` to format my code
- [ ] I have run `uv run ruff check --fix .` and fixed all issues (replaces flake8 + isort + bandit)
- [ ] I have run `uv run ty check rogue sdks/python hatch_build.py` and addressed type-check errors
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] I have run `uv run pytest` and all tests pass
- [ ] I have manually tested my changes
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,13 @@ jobs:
with:
python-version-file: ".python-version"

- name: Build web SPA
uses: ./.github/actions/web-build

- name: Run uv build - main package
uses: ./.github/actions/uv-build
env:
ROGUE_SKIP_WEB_BUILD: "1"

- name: Run uv build - sdk package
uses: ./.github/actions/uv-build
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ jobs:

- name: Go tui codestyle
uses: ./.github/actions/tui-codestyle

- name: Web codestyle
uses: ./.github/actions/web-codestyle
5 changes: 5 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,15 @@ jobs:
with:
python-version-file: ".python-version"

- name: Build web SPA
uses: ./.github/actions/web-build

- name: Run uv build - main package
uses: ./.github/actions/uv-build
with:
dist_dir: "${{ runner.temp }}/dist/rogue-ai"
env:
ROGUE_SKIP_WEB_BUILD: "1"

- name: Run uv build - sdk package
uses: ./.github/actions/uv-build
Expand Down
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,12 @@ bin/

# Go TUI
packages/tui/cmd/rogue/__debug*

# Web UI (Vite SPA)
packages/web/node_modules/
packages/web/dist/
packages/web/.vite/
packages/web/src/api/schema.d.ts
packages/web/src/routeTree.gen.ts
rogue/web_dist/

3 changes: 0 additions & 3 deletions .mypy.ini

This file was deleted.

2 changes: 1 addition & 1 deletion examples/mcp/tshirt_store_langgraph_mcp/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

@click.command()
@click.option("--host", "host", default="127.0.0.1", help="Host to run the server on")
@click.option("--port", "port", default=10001, help="Port to run the server on")
@click.option("--port", "port", default=10002, help="Port to run the server on")
@click.option(
"--transport",
"transport",
Expand Down
2 changes: 1 addition & 1 deletion examples/tshirt_store_agent/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def print_json_response(response: Any, description: str) -> None:
if hasattr(response, "root"):
print(f"{response.root.model_dump_json(exclude_none=True)}\n")
else:
print(f'{response.model_dump(mode="json", exclude_none=True)}\n')
print(f"{response.model_dump(mode='json', exclude_none=True)}\n")


def build_message(
Expand Down
3 changes: 1 addition & 2 deletions examples/tshirt_store_langgraph_agent/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
from . import shirtify_langgraph_agent, shirtify_langgraph_agent_executor
from .shirtify_langgraph_agent import root_agent
from . import shirtify_langgraph_agent
from . import shirtify_langgraph_agent_executor
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Dict, Any, AsyncIterable, Literal
from typing import Any, AsyncIterable, Dict, Literal

from langchain_core.messages import AIMessage, ToolMessage
from langchain_core.runnables import RunnableConfig
Expand Down
94 changes: 94 additions & 0 deletions hatch_build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""Hatch build hook that ensures the web SPA is built before the wheel ships.

The hook runs ``pnpm install --frozen-lockfile && pnpm build`` inside
``packages/web`` only when the dist is missing or stale. If pnpm is not
installed, the hook degrades gracefully: it warns and continues, so the wheel
build still succeeds for contributors who don't need the web UI. CI is
expected to run ``pnpm --filter web build`` explicitly before ``uv build`` so
the published wheel always carries ``dist/``.
"""

from __future__ import annotations

import os
import shutil
import subprocess # noqa: S404
import sys
from pathlib import Path
from typing import Any

from hatchling.builders.hooks.plugin.interface import BuildHookInterface


class WebBuildHook(BuildHookInterface):
PLUGIN_NAME = "custom"

def initialize(self, version: str, build_data: dict[str, Any]) -> None:
repo_root = Path(self.root)
web_dir = repo_root / "packages" / "web"
dist_dir = web_dir / "dist"
dist_index = dist_dir / "index.html"
src_dir = web_dir / "src"

skip_build = bool(os.environ.get("ROGUE_SKIP_WEB_BUILD"))
if skip_build:
self.app.display_info("ROGUE_SKIP_WEB_BUILD set; skipping web build.")
Comment on lines +33 to +35

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Parse ROGUE_SKIP_WEB_BUILD explicitly instead of using bool(...).

bool(os.environ.get(...)) treats "0" and "false" as True, so a caller can accidentally skip the web build and omit the UI.

Suggested fix
-        skip_build = bool(os.environ.get("ROGUE_SKIP_WEB_BUILD"))
+        skip_build = os.environ.get("ROGUE_SKIP_WEB_BUILD", "").lower() in {
+            "1",
+            "true",
+            "yes",
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hatch_build.py` around lines 33 - 35, The code currently sets skip_build =
bool(os.environ.get("ROGUE_SKIP_WEB_BUILD")) which treats any non-empty string
(e.g. "0" or "false") as True; change this to parse the ROGUE_SKIP_WEB_BUILD
value explicitly (e.g. val = os.environ.get("ROGUE_SKIP_WEB_BUILD",
"").strip().lower(); skip_build = val in ("1","true","yes") or use
distutils.util.strtobool on val) so that "0", "false", "no" (and empty) evaluate
to False and "1", "true", "yes" evaluate to True, then keep the existing
self.app.display_info call when skip_build is True.


if not skip_build and web_dir.is_dir():
needs_build = not dist_index.is_file() or _is_stale(dist_index, src_dir)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Broaden the staleness check before reusing dist/.

Right now a wheel can reuse stale web assets when files outside packages/web/src change. pnpm build also depends on inputs like package.json, pnpm-lock.yaml, vite.config.*, index.html, and public/**, so local packaging can silently ship an outdated UI.

Suggested fix
-        src_dir = web_dir / "src"
+        build_inputs = (
+            web_dir / "src",
+            web_dir / "public",
+            web_dir / "index.html",
+            web_dir / "package.json",
+            web_dir / "pnpm-lock.yaml",
+            web_dir / "tsconfig.json",
+            web_dir / "vite.config.ts",
+        )

         skip_build = bool(os.environ.get("ROGUE_SKIP_WEB_BUILD"))
         if skip_build:
             self.app.display_info("ROGUE_SKIP_WEB_BUILD set; skipping web build.")

         if not skip_build and web_dir.is_dir():
-            needs_build = not dist_index.is_file() or _is_stale(dist_index, src_dir)
+            needs_build = not dist_index.is_file() or _is_stale(
+                dist_index, build_inputs
+            )
-def _is_stale(dist_index: Path, src_dir: Path) -> bool:
-    if not src_dir.is_dir():
-        return False
+def _is_stale(dist_index: Path, inputs: tuple[Path, ...]) -> bool:
     dist_mtime = dist_index.stat().st_mtime
-    for path in src_dir.rglob("*"):
-        if path.is_file() and path.stat().st_mtime > dist_mtime:
-            return True
+    for root in inputs:
+        if root.is_file() and root.stat().st_mtime > dist_mtime:
+            return True
+        if root.is_dir():
+            for path in root.rglob("*"):
+                if path.is_file() and path.stat().st_mtime > dist_mtime:
+                    return True
     return False

Also applies to: 87-94

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hatch_build.py` at line 38, The current staleness check that sets needs_build
(the expression using dist_index.is_file() or _is_stale(dist_index, src_dir))
misses other pnpm/vite inputs so UI assets can be stale; update the logic to
consider additional inputs (package.json, pnpm-lock.yaml, any vite.config.*
files, index.html, and the public/ tree) by extending _is_stale (or adding a
helper like _is_stale_with_inputs) to accept and check a list of paths/patterns
and use it wherever _is_stale is called (including the other occurrence around
the 87-94 block) so needs_build becomes: not dist_index.is_file() or
_is_stale(dist_index, src_dir, extra_inputs) and ensure the implementation
recursively checks mtimes for public/** and globs for vite.config.*.

if needs_build:
pnpm = shutil.which("pnpm")
if pnpm is None:
if dist_index.is_file():
self.app.display_warning(
"pnpm not found; reusing existing packages/web/dist "
"(may be stale).",
)
else:
self.app.display_warning(
"pnpm not found and packages/web/dist is missing. "
"The wheel will not include the web UI. "
"Install Node + pnpm and rebuild, or run "
"`cd packages/web && pnpm install && pnpm build` manually.",
)
else:
self.app.display_info("Building web SPA via pnpm...")
try:
subprocess.run( # noqa: S603
[pnpm, "install", "--frozen-lockfile"],
cwd=str(web_dir),
check=True,
)
subprocess.run( # noqa: S603
[pnpm, "run", "build"],
cwd=str(web_dir),
check=True,
)
except subprocess.CalledProcessError as e:
self.app.display_error(f"pnpm build failed: {e}")
sys.exit(1)

# Force-include the web dist only when it actually exists, so installs
# in environments without pnpm (and without a prebuilt dist) don't fail
# on a missing forced-include path. This replaces the static
# `[tool.hatch.build.targets.*.force-include]` tables in pyproject.toml.
if dist_dir.is_dir():
target = self.target_name
if target == "wheel":
build_data.setdefault("force_include", {})[str(dist_dir)] = (
"rogue/web_dist"
)
elif target == "sdist":
build_data.setdefault("force_include", {})[str(dist_dir)] = (
"packages/web/dist"
)


def _is_stale(dist_index: Path, src_dir: Path) -> bool:
if not src_dir.is_dir():
return False
dist_mtime = dist_index.stat().st_mtime
for path in src_dir.rglob("*"):
if path.is_file() and path.stat().st_mtime > dist_mtime:
return True
return False
49 changes: 30 additions & 19 deletions lefthook.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,31 +23,25 @@ pre-commit:
python -c "import yaml; yaml.safe_load(open('$file'))" || exit 1
done

# Python
- name: add-trailing-comma
glob: "*.py"
run: add-trailing-comma {staged_files}

- name: isort
glob: "*.py"
run: isort --profile black {staged_files}

- name: black
# Python — ruff replaces black + flake8 + isort + bandit + add-trailing-comma;
# ty replaces mypy. Both are Astral Rust binaries, an order of magnitude
# faster than the toolchain they superseded.
- name: ruff-format
glob: "*.py"
exclude: ".venv/"
run: black {staged_files}

- name: flake8
glob: "*.py"
run: flake8 {staged_files}
run: uv run ruff format {staged_files}

- name: mypy
- name: ruff-check
glob: "*.py"
run: mypy --config-file .mypy.ini {staged_files}
exclude: ".venv/"
run: uv run ruff check --fix {staged_files}

- name: bandit
- name: ty
glob: "*.py"
run: bandit -c .bandit.yaml -r {staged_files}
exclude: ".venv/"
# ty does whole-project inference; running on the full source roots is
# both faster and more accurate than a per-file invocation.
run: uv run ty check rogue sdks/python hatch_build.py

- name: uv lock check
glob: "pyproject.toml"
Expand All @@ -65,3 +59,20 @@ pre-commit:
- name: go vet
glob: "*.go"
run: (cd packages/tui && go vet ./...)

# Web (TypeScript / React)
# `root:` rebases {staged_files} so they're relative to packages/web,
# which is what prettier/eslint expect when their config lives there.
- name: prettier-web
root: "packages/web/"
glob: "*.{ts,tsx,js,jsx,json,css,md}"
run: pnpm exec prettier --write {staged_files}

- name: eslint-web
root: "packages/web/"
glob: "*.{ts,tsx,js,jsx}"
run: pnpm exec eslint --fix {staged_files}

- name: tsc-web
glob: "packages/web/**/*.{ts,tsx}"
run: (cd packages/web && pnpm exec tsc --noEmit)
Loading
Loading