diff --git a/.gitignore b/.gitignore index 65ab971..e76f743 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,94 @@ -venv/ +# ── Python ──────────────────────────────────────────────────────────────────── __pycache__/ -*.pyc -*.pyo +*.py[cod] +*$py.class *.pyd -.Python +*.pyo +*.so +*.egg *.egg-info/ +.eggs/ +pip-wheel-metadata/ +MANIFEST + +# ── Virtual environments ────────────────────────────────────────────────────── +venv/ +.venv/ +env/ +.env/ +ENV/ + +# ── Distribution / build ────────────────────────────────────────────────────── dist/ build/ *.spec + +# ── PyInstaller work files (keep *.spec only if custom; generated ones are noise) +# Uncomment if you commit a hand-crafted spec file: +# !APIClient-Agent.spec + +# ── Database & secrets ─────────────────────────────────────────────────────── *.db *.sqlite *.sqlite3 .env +.env.* +!.env.example +secrets.json +credentials.json + +# ── Logs ───────────────────────────────────────────────────────────────────── *.log +logs/ + +# ── OS artifacts ───────────────────────────────────────────────────────────── .DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db Thumbs.db +desktop.ini + +# ── IDE / editor ───────────────────────────────────────────────────────────── +.idea/ +.vscode/ +*.swp +*.swo +*~ +.project +.pydevproject +*.sublime-project +*.sublime-workspace + +# ── Testing & coverage ─────────────────────────────────────────────────────── +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +htmlcov/ +.coverage +.coverage.* +coverage.xml +*.cover +nosetests.xml +test-results/ + +# ── Jupyter ─────────────────────────────────────────────────────────────────── +.ipynb_checkpoints/ +*.ipynb + +# ── Packaging scratch ───────────────────────────────────────────────────────── +*.tar.gz +*.zip +*.deb +*.rpm +*.dmg +*.AppImage +*.exe +*.msi + +# ── Assets: generated logo PNGs are committed (app requires them at runtime). +# Only ignore temp/intermediate files produced during asset generation. +assets/*.webp +assets/app-ss.png diff --git a/LICENSE b/LICENSE index 3a47b40..c47a13e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 EKIKA.co +Copyright (c) 2025-2026 EKIKA.co Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -19,3 +19,26 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------------------- + +Third-Party Notices +------------------- + +This software bundles or depends on the following open-source packages. +Each package retains its own license. + + PyQt6 - GPL v3 / Commercial https://riverbankcomputing.com/software/pyqt + httpx - BSD 3-Clause https://github.com/encode/httpx + websockets - BSD 3-Clause https://github.com/python-websockets/websockets + anthropic-sdk - MIT https://github.com/anthropics/anthropic-sdk-python + PyYAML - MIT https://github.com/yaml/pyyaml + PyInstaller - GPL v2+ with bootloader exception + https://github.com/pyinstaller/pyinstaller + +Font licenses + Quicksand - Open Font License 1.1 https://fonts.google.com/specimen/Quicksand + Open Sans - Apache License 2.0 https://fonts.google.com/specimen/Open+Sans + +The full text of each third-party license is available in the respective +package source repository or distribution. diff --git a/app/ui/main_window.py b/app/ui/main_window.py index 958821c..c5df84c 100644 --- a/app/ui/main_window.py +++ b/app/ui/main_window.py @@ -59,21 +59,16 @@ class EnvBar(QWidget): layout.setContentsMargins(12, 0, 12, 0) layout.setSpacing(6) - # EKIKA logo - logo_path = os.path.join(_ASSETS_DIR, "ekika_logo.png") - if os.path.exists(logo_path): + # App logo icon + app_logo_path = os.path.join(_ASSETS_DIR, "app_logo_32.png") + if os.path.exists(app_logo_path): logo_lbl = QLabel() - logo_lbl.setObjectName("ekikaLogo") - pix = QPixmap(logo_path) - logo_lbl.setPixmap(pix.scaledToHeight(26, Qt.TransformationMode.SmoothTransformation)) - logo_lbl.setToolTip("EKIKA") + logo_lbl.setObjectName("appLogo") + pix = QPixmap(app_logo_path) + logo_lbl.setPixmap(pix.scaledToHeight(32, Qt.TransformationMode.SmoothTransformation)) + logo_lbl.setToolTip("APIClient - Agent") layout.addWidget(logo_lbl) - # Thin separator - sep = QLabel("|") - sep.setObjectName("brandSub") - sep.setFixedWidth(12) - sep.setAlignment(Qt.AlignmentFlag.AlignCenter) - layout.addWidget(sep) + layout.addSpacing(4) brand = QLabel("APIClient") brand.setObjectName("brandName") diff --git a/assets/app-ss.png b/assets/app-ss.png deleted file mode 100644 index d5b5a66..0000000 Binary files a/assets/app-ss.png and /dev/null differ diff --git a/assets/app_logo.png b/assets/app_logo.png new file mode 100644 index 0000000..df0347f Binary files /dev/null and b/assets/app_logo.png differ diff --git a/assets/app_logo_16.png b/assets/app_logo_16.png new file mode 100644 index 0000000..d6256cd Binary files /dev/null and b/assets/app_logo_16.png differ diff --git a/assets/app_logo_32.png b/assets/app_logo_32.png new file mode 100644 index 0000000..5c230bb Binary files /dev/null and b/assets/app_logo_32.png differ diff --git a/assets/app_logo_48.png b/assets/app_logo_48.png new file mode 100644 index 0000000..e9e188a Binary files /dev/null and b/assets/app_logo_48.png differ diff --git a/assets/ekika_logo.png b/assets/ekika_logo.png index 7a3f64b..fcaaaed 100644 Binary files a/assets/ekika_logo.png and b/assets/ekika_logo.png differ diff --git a/assets/ekika_logo.webp b/assets/ekika_logo.webp deleted file mode 100644 index 3a8df98..0000000 Binary files a/assets/ekika_logo.webp and /dev/null differ diff --git a/build_installer.sh b/build_installer.sh index 020edae..210a6f4 100755 --- a/build_installer.sh +++ b/build_installer.sh @@ -1,34 +1,258 @@ -#!/bin/bash -set -e +#!/usr/bin/env bash +# ============================================================================= +# build_installer.sh - Build APIClient-Agent standalone distribution +# +# Usage: +# ./build_installer.sh [--onefile] [--clean] [--deb] [--dmg] +# +# Options: +# --onefile Bundle into a single executable (larger, slower start) instead +# of the default one-directory layout (recommended for Linux/Mac) +# --clean Remove previous dist/ and build/ before building +# --deb Also build a .deb package after the build (requires fpm) +# --dmg Also build a .dmg image after the build (macOS only, requires +# create-dmg: brew install create-dmg) +# +# Requirements: +# Python 3.11+, a virtual environment at ./venv, and all pip deps installed. +# PyInstaller is installed automatically from requirements.txt. +# ============================================================================= +set -euo pipefail -echo "=== Building API Client installer ===" +APP_NAME="APIClient-Agent" +APP_VERSION="2.0.0" +APP_BUNDLE_ID="co.ekika.apiclient-agent" +MAIN_SCRIPT="main.py" +ICON_PNG="assets/app_logo.png" +ICON_ICO="assets/app_logo.ico" +ICON_ICNS="assets/app_logo.icns" -# Install deps -pip install -r requirements.txt +PYTHON="${PYTHON:-venv/bin/python}" +PIP="${PIP:-venv/bin/pip}" -# Build with PyInstaller -pyinstaller \ - --onedir \ - --windowed \ - --name "APIClient" \ - --add-data "app:app" \ - main.py +# ── Argument parsing ────────────────────────────────────────────────────────── +OPT_ONEFILE=false +OPT_CLEAN=false +OPT_DEB=false +OPT_DMG=false -echo "" -echo "=== Build complete ===" -echo "Executable: dist/APIClient/APIClient" -echo "" +for arg in "$@"; do + case $arg in + --onefile) OPT_ONEFILE=true ;; + --clean) OPT_CLEAN=true ;; + --deb) OPT_DEB=true ;; + --dmg) OPT_DMG=true ;; + *) + echo "Unknown option: $arg" + echo "Usage: $0 [--onefile] [--clean] [--deb] [--dmg]" + exit 1 + ;; + esac +done -# Optional: create .deb (requires fpm: gem install fpm) -if command -v fpm &> /dev/null; then - echo "Creating .deb package..." - fpm -s dir -t deb \ - -n api-client \ - -v 1.0.0 \ - --description "Postman-like API client" \ - dist/APIClient/=/opt/api-client \ - --after-install /dev/null - echo "Package: api-client_1.0.0_amd64.deb" -else - echo "Tip: install fpm (gem install fpm) to also generate a .deb package" +# ── Helpers ─────────────────────────────────────────────────────────────────── +info() { echo -e "\033[1;34m[INFO]\033[0m $*"; } +ok() { echo -e "\033[1;32m[ OK ]\033[0m $*"; } +warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; } +die() { echo -e "\033[1;31m[FAIL]\033[0m $*" >&2; exit 1; } + +# ── Platform detection ──────────────────────────────────────────────────────── +OS="$(uname -s)" +case "$OS" in + Linux*) PLATFORM=linux ;; + Darwin*) PLATFORM=macos ;; + MINGW*|CYGWIN*|MSYS*) PLATFORM=windows ;; + *) die "Unsupported platform: $OS" ;; +esac +info "Platform: $PLATFORM" + +# ── Preflight checks ────────────────────────────────────────────────────────── +[[ -f "$MAIN_SCRIPT" ]] || die "$MAIN_SCRIPT not found. Run from the project root." +[[ -d "venv" ]] || die "Virtual environment 'venv/' not found. Create it first: + python3 -m venv venv && venv/bin/pip install -r requirements.txt" +[[ -f "requirements.txt" ]] || die "requirements.txt not found." + +info "Python: $($PYTHON --version)" + +# ── Clean ───────────────────────────────────────────────────────────────────── +if $OPT_CLEAN; then + info "Cleaning previous build artifacts..." + rm -rf dist/ build/ "${APP_NAME}.spec" + ok "Cleaned." fi + +# ── Install / upgrade dependencies ─────────────────────────────────────────── +info "Installing/verifying dependencies..." +$PIP install -q -r requirements.txt +ok "Dependencies ready." + +# ── Prepare icon ───────────────────────────────────────────────────────────── +# PyInstaller needs .ico on Windows, .icns on macOS, .png on Linux. +ICON_ARG="" +if [[ "$PLATFORM" == "windows" ]]; then + if [[ -f "$ICON_ICO" ]]; then + ICON_ARG="--icon=$ICON_ICO" + else + warn ".ico icon not found at $ICON_ICO - building without icon." + fi +elif [[ "$PLATFORM" == "macos" ]]; then + if [[ -f "$ICON_ICNS" ]]; then + ICON_ARG="--icon=$ICON_ICNS" + elif [[ -f "$ICON_PNG" ]]; then + # Convert PNG -> ICNS using sips + iconutil if available + if command -v sips &>/dev/null && command -v iconutil &>/dev/null; then + info "Converting PNG to ICNS..." + ICONSET_DIR="/tmp/${APP_NAME}.iconset" + mkdir -p "$ICONSET_DIR" + for sz in 16 32 64 128 256 512; do + sips -z $sz $sz "$ICON_PNG" --out "$ICONSET_DIR/icon_${sz}x${sz}.png" &>/dev/null + sips -z $((sz*2)) $((sz*2)) "$ICON_PNG" --out "$ICONSET_DIR/icon_${sz}x${sz}@2x.png" &>/dev/null + done + iconutil -c icns "$ICONSET_DIR" -o "$ICON_ICNS" && ok "ICNS created." + ICON_ARG="--icon=$ICON_ICNS" + else + warn "sips/iconutil not available; building without .icns icon." + fi + fi +elif [[ "$PLATFORM" == "linux" ]]; then + [[ -f "$ICON_PNG" ]] && ICON_ARG="--icon=$ICON_PNG" || warn "PNG icon not found." +fi + +# ── PyInstaller arguments ───────────────────────────────────────────────────── +PYINSTALLER_ARGS=( + $([[ $OPT_ONEFILE == true ]] && echo "--onefile" || echo "--onedir") + "--windowed" + "--name" "$APP_NAME" + "--add-data" "app:app" + "--add-data" "assets:assets" + "--clean" + "--noconfirm" +) +[[ -n "$ICON_ARG" ]] && PYINSTALLER_ARGS+=("$ICON_ARG") + +# macOS bundle metadata +if [[ "$PLATFORM" == "macos" ]]; then + PYINSTALLER_ARGS+=( + "--osx-bundle-identifier" "$APP_BUNDLE_ID" + ) +fi + +# Hidden imports that PyInstaller may miss +PYINSTALLER_ARGS+=( + "--hidden-import" "PyQt6.QtSvg" + "--hidden-import" "PyQt6.QtNetwork" + "--hidden-import" "httpx._transports.default" + "--hidden-import" "websockets.legacy.client" + "--hidden-import" "anthropic" +) + +PYINSTALLER_ARGS+=("$MAIN_SCRIPT") + +# ── Build ───────────────────────────────────────────────────────────────────── +info "Running PyInstaller..." +$PYTHON -m PyInstaller "${PYINSTALLER_ARGS[@]}" + +# ── Verify output ───────────────────────────────────────────────────────────── +if [[ $OPT_ONEFILE == true ]]; then + BIN="dist/${APP_NAME}" + [[ "$PLATFORM" == "windows" ]] && BIN="${BIN}.exe" + [[ -f "$BIN" ]] || die "Build failed: expected $BIN not found." + ok "Executable: $BIN ($(du -sh "$BIN" | cut -f1))" +else + OUT_DIR="dist/${APP_NAME}" + [[ -d "$OUT_DIR" ]] || die "Build failed: expected $OUT_DIR not found." + ok "Distribution directory: $OUT_DIR ($(du -sh "$OUT_DIR" | cut -f1))" +fi + +# ── .deb package ───────────────────────────────────────────────────────────── +if $OPT_DEB; then + if [[ "$PLATFORM" != "linux" ]]; then + warn ".deb packaging is only supported on Linux. Skipping." + elif ! command -v fpm &>/dev/null; then + warn "fpm not found. Install it with: gem install fpm + Then re-run with --deb." + else + info "Building .deb package..." + DEB_NAME="apiclient-agent_${APP_VERSION}_amd64.deb" + + # Write a post-install script that creates a desktop entry + POSTINST=$(mktemp) + cat > "$POSTINST" <<'EOF' +#!/bin/bash +cat > /usr/share/applications/apiclient-agent.desktop </dev/null || true +EOF + chmod +x "$POSTINST" + + fpm \ + --input-type dir \ + --output-type deb \ + --name "apiclient-agent" \ + --version "$APP_VERSION" \ + --architecture amd64 \ + --maintainer "EKIKA.co " \ + --description "APIClient - Agent: AI-first API testing desktop client (Python/PyQt6)" \ + --url "https://git.ekika.co/EKIKA.co/APIClient-Agent" \ + --license "MIT" \ + --category "Development" \ + --after-install "$POSTINST" \ + --package "$DEB_NAME" \ + "dist/${APP_NAME}/=/opt/apiclient-agent" + + rm -f "$POSTINST" + [[ -f "$DEB_NAME" ]] && ok ".deb package: $DEB_NAME ($(du -sh "$DEB_NAME" | cut -f1))" \ + || warn ".deb build may have failed." + fi +fi + +# ── .dmg image (macOS) ─────────────────────────────────────────────────────── +if $OPT_DMG; then + if [[ "$PLATFORM" != "macos" ]]; then + warn ".dmg packaging is only supported on macOS. Skipping." + elif ! command -v create-dmg &>/dev/null; then + warn "create-dmg not found. Install it with: brew install create-dmg + Then re-run with --dmg." + else + info "Building .dmg image..." + DMG_NAME="${APP_NAME}-${APP_VERSION}.dmg" + APP_BUNDLE="dist/${APP_NAME}.app" + [[ -d "$APP_BUNDLE" ]] || die ".app bundle not found at $APP_BUNDLE" + + create-dmg \ + --volname "$APP_NAME $APP_VERSION" \ + --window-pos 200 120 \ + --window-size 600 400 \ + --icon-size 100 \ + --icon "${APP_NAME}.app" 175 190 \ + --hide-extension "${APP_NAME}.app" \ + --app-drop-link 425 190 \ + "$DMG_NAME" \ + "$APP_BUNDLE" + + [[ -f "$DMG_NAME" ]] && ok ".dmg image: $DMG_NAME ($(du -sh "$DMG_NAME" | cut -f1))" \ + || warn ".dmg build may have failed." + fi +fi + +# ── Summary ─────────────────────────────────────────────────────────────────── +echo "" +echo " ┌─────────────────────────────────────────────┐" +printf " │ %-43s│\n" "${APP_NAME} v${APP_VERSION} build complete" +printf " │ Platform : %-31s│\n" "$PLATFORM" +if [[ $OPT_ONEFILE == true ]]; then + printf " │ Output : %-31s│\n" "dist/${APP_NAME}" +else + printf " │ Output : %-31s│\n" "dist/${APP_NAME}/" +fi +echo " └─────────────────────────────────────────────┘" +echo "" diff --git a/main.py b/main.py index cbd4b1e..fefaa00 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,6 @@ -import sys +import sys, os from PyQt6.QtWidgets import QApplication +from PyQt6.QtGui import QIcon from app.core.fonts import load_fonts from app.ui.theme import apply from app.ui.main_window import MainWindow @@ -7,11 +8,23 @@ from app.ui.main_window import MainWindow APP_NAME = "APIClient - Agent" APP_VERSION = "2.0.0" +_ASSETS = os.path.join(os.path.dirname(__file__), "assets") + if __name__ == "__main__": app = QApplication(sys.argv) app.setApplicationName(APP_NAME) app.setApplicationVersion(APP_VERSION) app.setOrganizationName("EKIKA") + + # App icon (taskbar + window title bar) + icon = QIcon() + for size, fname in [(16, "app_logo_16.png"), (32, "app_logo_32.png"), + (48, "app_logo_48.png"), (256, "app_logo.png")]: + path = os.path.join(_ASSETS, fname) + if os.path.exists(path): + icon.addFile(path) + app.setWindowIcon(icon) + load_fonts() apply(app, dark=True) window = MainWindow()