[RELEASE] Final version.
85
.gitignore
vendored
@@ -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
|
||||
|
||||
25
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.
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
Before Width: | Height: | Size: 137 KiB |
BIN
assets/app_logo.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
assets/app_logo_16.png
Normal file
|
After Width: | Height: | Size: 864 B |
BIN
assets/app_logo_32.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
assets/app_logo_48.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
@@ -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 <<DESK
|
||||
[Desktop Entry]
|
||||
Name=APIClient - Agent
|
||||
Comment=AI-first API testing client
|
||||
Exec=/opt/apiclient-agent/APIClient-Agent
|
||||
Icon=/opt/apiclient-agent/_internal/assets/app_logo.png
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Development;Network;
|
||||
Keywords=API;REST;HTTP;AI;
|
||||
DESK
|
||||
update-desktop-database /usr/share/applications 2>/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 <hello@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 ""
|
||||
|
||||
15
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()
|
||||
|
||||