From b83cd039adb0a4965f98e2efb949018a7597e484 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hu=E1=BB=B3nh=20Th=C6=B0=C6=A1ng?= <252359928+Huynhthuongg@users.noreply.github.com> Date: Wed, 3 Jun 2026 07:00:15 +0700 Subject: [PATCH 1/9] Remove binary release screenshot --- .github/workflows/ci.yml | 8 +++- .gitignore | 3 ++ README.md | 11 ++++- app/universal_compiler_agent/__init__.py | 2 +- app/universal_compiler_agent/cli.py | 22 +++++++-- app/universal_compiler_agent/server.py | 54 +++++++---------------- app/universal_compiler_agent/templates.py | 5 +-- docs/CHANGELOG.md | 7 +++ docs/SPECIFICATION.md | 4 +- pyproject.toml | 6 ++- scripts/check.sh | 4 ++ tests/test_server.py | 36 +++++++++++++++ 12 files changed, 109 insertions(+), 53 deletions(-) create mode 100755 scripts/check.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ffd6d0c..e150f07 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,12 +7,16 @@ on: jobs: test: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ['3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: ${{ matrix.python-version }} - run: python -m pip install --upgrade pip - run: python -m pip install -e '.[dev]' - run: ruff check . - - run: pytest + - run: pytest -q diff --git a/.gitignore b/.gitignore index 6b5b6d1..c2571d0 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ backup-*.tar.gz # Local configuration .env + +# Generated release screenshots (avoid binary files in Codex diffs) +docs/screenshots/*-release.png diff --git a/README.md b/README.md index ff843c9..dd1dadf 100644 --- a/README.md +++ b/README.md @@ -62,10 +62,17 @@ tests/ Unit tests ```bash python -m pip install -e '.[dev]' -ruff check . -pytest +./scripts/check.sh ``` +`./scripts/check.sh` runs Ruff and the full pytest suite so release checks match CI. + +## Current release + +- Version: 0.1.1 +- Release date: 2026-06-02 +- Release focus: CLI dry-run previews, safe output directory validation, dashboard route alignment, and test workflow hardening. + ## Security model - Never hardcode secrets in generated output. diff --git a/app/universal_compiler_agent/__init__.py b/app/universal_compiler_agent/__init__.py index 04fee82..adaab43 100644 --- a/app/universal_compiler_agent/__init__.py +++ b/app/universal_compiler_agent/__init__.py @@ -1,4 +1,4 @@ """Universal Project Compiler Agent package.""" __all__ = ["__version__"] -__version__ = "0.1.0" +__version__ = "0.1.1" diff --git a/app/universal_compiler_agent/cli.py b/app/universal_compiler_agent/cli.py index 41d8ca0..d2457ad 100644 --- a/app/universal_compiler_agent/cli.py +++ b/app/universal_compiler_agent/cli.py @@ -19,6 +19,14 @@ def _read_input(args: argparse.Namespace) -> str: return "Universal Project Compiler Agent" +def _safe_output_dir(value: str) -> Path: + output_dir = Path(value) + if output_dir.is_absolute() or ".." in output_dir.parts: + msg = "output_dir must be a safe relative path" + raise argparse.ArgumentTypeError(msg) + return output_dir + + def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description="Compile requirements into a runnable project scaffold." @@ -37,7 +45,15 @@ def build_parser() -> argparse.ArgumentParser: compile_cmd.add_argument("--text", help="Inline requirements text.") compile_cmd.add_argument("--name", help="Override generated project name.") compile_cmd.add_argument( - "--output-dir", default="generated", help="Directory that will receive output." + "--dry-run", + action="store_true", + help="Print the generated plan as JSON without writing files.", + ) + compile_cmd.add_argument( + "--output-dir", + default=Path("generated"), + type=_safe_output_dir, + help="Safe relative directory that will receive output.", ) return parser @@ -47,12 +63,12 @@ def main(argv: list[str] | None = None) -> int: args = parser.parse_args(argv) requirements = _read_input(args) - if args.command == "plan": + if args.command == "plan" or args.dry_run: plan = build_plan(requirements, args.name) print(json.dumps(asdict(plan), indent=2)) return 0 - result = compile_project(requirements, Path(args.output_dir), args.name) + result = compile_project(requirements, args.output_dir, args.name) print(f"Generated {result.file_count} files in {result.root}") return 0 diff --git a/app/universal_compiler_agent/server.py b/app/universal_compiler_agent/server.py index aff944b..1bf05b7 100644 --- a/app/universal_compiler_agent/server.py +++ b/app/universal_compiler_agent/server.py @@ -11,6 +11,9 @@ from .compiler import compile_project from .planner import build_plan +from .templates import INDEX_HTML + +APP_VERSION = "0.1.1" class PlanRequest(BaseModel): @@ -22,8 +25,15 @@ class CompileRequest(PlanRequest): output_dir: str = Field(default="generated", max_length=240) +def _safe_output_dir(value: str) -> Path: + output_dir = Path(value) + if output_dir.is_absolute() or ".." in output_dir.parts: + raise HTTPException(status_code=400, detail="output_dir must be a safe relative path") + return output_dir + + def create_app() -> FastAPI: - app = FastAPI(title="Universal Project Compiler Agent", version="0.1.0") + app = FastAPI(title="Universal Project Compiler Agent", version=APP_VERSION) @app.middleware("http") async def security_headers(request: Request, call_next): # type: ignore[no-untyped-def] @@ -36,38 +46,7 @@ async def security_headers(request: Request, call_next): # type: ignore[no-unty @app.get("/", response_class=HTMLResponse) def index() -> str: - return """ - - - - - - Universal Project Compiler Agent - - - -
-
Android-first • Termux-first • Production-ready
-

Compile requirements into runnable software projects.

-

Use POST /plan to analyze requirements or POST /compile to emit a secure, maintainable scaffold with docs, tests, and scripts.

-
-

CLI

upca compile --input-file spec.md

-

API

JSON endpoints for automation and future UI integrations.

-

Security

Path safety, secret redaction, and hardened HTTP headers.

-
-""" + return INDEX_HTML @app.get("/health") def health() -> dict[str, str]: @@ -80,10 +59,11 @@ def plan(request: PlanRequest) -> JSONResponse: @app.post("/compile") def compile_endpoint(request: CompileRequest) -> dict[str, object]: - output_dir = Path(request.output_dir) - if output_dir.is_absolute() or ".." in output_dir.parts: - raise HTTPException(status_code=400, detail="output_dir must be a safe relative path") - result = compile_project(request.requirements, output_dir, request.project_name) + result = compile_project( + request.requirements, + _safe_output_dir(request.output_dir), + request.project_name, + ) return {"root": str(result.root), "file_count": result.file_count, "slug": result.plan.slug} return app diff --git a/app/universal_compiler_agent/templates.py b/app/universal_compiler_agent/templates.py index 380cb8e..806a73e 100644 --- a/app/universal_compiler_agent/templates.py +++ b/app/universal_compiler_agent/templates.py @@ -10,7 +10,6 @@ Universal Project Compiler Agent - + + +
+
+

🌤️ Weather Dashboard

+

Get real-time weather information from around the world

+
+ +
+ +
+
+

Search Weather

+
+ +
+ + +
+ + + + +
+
+ + +
+
+

Saved Cities

+
+

No saved cities yet

+
+
+
+
+
+ + + + +""" + + +def get_index_html() -> str: + """Get the index HTML template.""" + return HTML_TEMPLATE diff --git a/weather_dashboard/weather_service.py b/weather_dashboard/weather_service.py new file mode 100644 index 0000000..d728b09 --- /dev/null +++ b/weather_dashboard/weather_service.py @@ -0,0 +1,117 @@ +"""Weather API service for fetching weather data.""" + +from __future__ import annotations + +import aiohttp +from datetime import datetime +from typing import Optional, List +import logging + +from .config import Settings +from .models import ( + WeatherData, + Coordinates, + WeatherMain, + WeatherDescription, + Wind, + SearchResult, +) + +logger = logging.getLogger(__name__) + + +class WeatherService: + """Service for fetching weather data from OpenWeatherMap API.""" + + def __init__(self, settings: Settings): + """Initialize weather service.""" + self.settings = settings + self.base_url = settings.openweather_base_url + self.api_key = settings.openweather_api_key + + async def get_current_weather( + self, latitude: float, longitude: float + ) -> WeatherData: + """Fetch current weather for given coordinates.""" + url = f"{self.base_url}/weather" + params = { + "lat": latitude, + "lon": longitude, + "appid": self.api_key, + "units": "metric", + } + + async with aiohttp.ClientSession() as session: + async with session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=10)) as response: + if response.status != 200: + error_data = await response.json() + raise ValueError(f"Weather API error: {error_data.get('message', 'Unknown error')}") + + data = await response.json() + return self._parse_weather_response(data) + + async def search_cities(self, query: str, limit: int = 5) -> List[SearchResult]: + """Search for cities by name.""" + url = f"{self.base_url}/../geo/1.0/direct" + params = { + "q": query, + "limit": limit, + "appid": self.api_key, + } + + async with aiohttp.ClientSession() as session: + async with session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=10)) as response: + if response.status != 200: + logger.error(f"City search failed with status {response.status}") + return [] + + data = await response.json() + return [ + SearchResult( + name=item["name"], + latitude=item["lat"], + longitude=item["lon"], + country=item.get("country", ""), + state=item.get("state", None), + ) + for item in data + ] + + def _parse_weather_response(self, data: dict) -> WeatherData: + """Parse OpenWeatherMap API response.""" + coords = data.get("coord", {}) + weather = data.get("weather", [{}])[0] + main = data.get("main", {}) + wind = data.get("wind", {}) + sys = data.get("sys", {}) + + return WeatherData( + city=data.get("name", "Unknown"), + country=sys.get("country", ""), + coordinates=Coordinates( + latitude=coords.get("lat", 0.0), + longitude=coords.get("lon", 0.0), + ), + weather=WeatherDescription( + main=weather.get("main", ""), + description=weather.get("description", ""), + icon=weather.get("icon", "01d"), + ), + main=WeatherMain( + temperature=main.get("temp", 0.0), + feels_like=main.get("feels_like", 0.0), + temp_min=main.get("temp_min", 0.0), + temp_max=main.get("temp_max", 0.0), + pressure=main.get("pressure", 0), + humidity=main.get("humidity", 0), + ), + wind=Wind( + speed=wind.get("speed", 0.0), + deg=wind.get("deg", 0), + gust=wind.get("gust", None), + ), + cloudiness=data.get("clouds", {}).get("all", 0), + sunrise=datetime.fromtimestamp(sys.get("sunrise", 0)), + sunset=datetime.fromtimestamp(sys.get("sunset", 0)), + timezone=data.get("timezone", 0), + ) From e97ee8be2c468a6af9f99a5bc6f9dd7d91ea00cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hu=E1=BB=B3nh=20Th=C6=B0=C6=A1ng?= <252359928+Huynhthuongg@users.noreply.github.com> Date: Sun, 7 Jun 2026 11:14:32 +0700 Subject: [PATCH 9/9] Update pyproject.toml Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a3a0300..73d16ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dev = [ upca = "universal_compiler_agent.cli:main" [tool.setuptools] -package-dir = {"": "app"} +package-dir = { "" = "app" } [tool.setuptools.packages.find] where = ["app"]